2021-04-18 15:41:13 -04:00
// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
2019-07-19 16:20:33 -04:00
package lifecycle
import (
"encoding/xml"
2021-03-30 02:52:30 -04:00
"fmt"
2019-07-19 16:20:33 -04:00
"io"
2021-07-20 20:36:55 -04:00
"net/http"
2019-07-19 16:20:33 -04:00
"strings"
2019-08-09 13:02:41 -04:00
"time"
2021-07-20 20:36:55 -04:00
xhttp "github.com/minio/minio/internal/http"
2019-07-19 16:20:33 -04:00
)
var (
2021-11-19 20:54:10 -05:00
errLifecycleTooManyRules = Errorf ( "Lifecycle configuration allows a maximum of 1000 rules" )
errLifecycleNoRule = Errorf ( "Lifecycle configuration should have at least one rule" )
errLifecycleDuplicateID = Errorf ( "Lifecycle configuration has rule with the same ID. Rule ID must be unique." )
errXMLNotWellFormed = Errorf ( "The XML you provided was not well-formed or did not validate against our published schema" )
errLifecycleInvalidNoncurrentExpiration = Errorf ( "Exactly one of NoncurrentDays (positive integer) or MaxNoncurrentVersions should be specified in a NoncurrentExpiration rule." )
2019-07-19 16:20:33 -04:00
)
2020-11-12 15:12:09 -05:00
const (
// TransitionComplete marks completed transition
TransitionComplete = "complete"
// TransitionPending - transition is yet to be attempted
TransitionPending = "pending"
)
2019-08-09 13:02:41 -04:00
// Action represents a delete action or other transition
// actions that will be implemented later.
type Action int
2020-06-12 13:28:21 -04:00
//go:generate stringer -type Action $GOFILE
2019-08-09 13:02:41 -04:00
const (
2021-03-05 17:15:53 -05:00
// NoneAction means no action required after evaluating lifecycle rules
2019-08-09 13:02:41 -04:00
NoneAction Action = iota
2020-11-12 15:12:09 -05:00
// DeleteAction means the object needs to be removed after evaluating lifecycle rules
2019-08-09 13:02:41 -04:00
DeleteAction
2020-07-04 23:56:02 -04:00
// DeleteVersionAction deletes a particular version
DeleteVersionAction
2020-11-12 15:12:09 -05:00
// TransitionAction transitions a particular object after evaluating lifecycle transition rules
TransitionAction
2021-11-16 12:28:29 -05:00
// TransitionVersionAction transitions a particular object version after evaluating lifecycle transition rules
2020-11-12 15:12:09 -05:00
TransitionVersionAction
// DeleteRestoredAction means the temporarily restored object needs to be removed after evaluating lifecycle rules
DeleteRestoredAction
// DeleteRestoredVersionAction deletes a particular version that was temporarily restored
DeleteRestoredVersionAction
2021-10-02 12:31:05 -04:00
// ActionCount must be the last action and shouldn't be used as a regular action.
ActionCount
2019-08-09 13:02:41 -04:00
)
2019-07-19 16:20:33 -04:00
// Lifecycle - Configuration for bucket lifecycle.
type Lifecycle struct {
XMLName xml . Name ` xml:"LifecycleConfiguration" `
Rules [ ] Rule ` xml:"Rule" `
}
2021-03-19 01:18:35 -04:00
// UnmarshalXML - decodes XML data.
func ( lc * Lifecycle ) UnmarshalXML ( d * xml . Decoder , start xml . StartElement ) ( err error ) {
switch start . Name . Local {
case "LifecycleConfiguration" , "BucketLifecycleConfiguration" :
default :
2021-03-30 02:52:30 -04:00
return xml . UnmarshalError ( fmt . Sprintf ( "expected element type <LifecycleConfiguration>/<BucketLifecycleConfiguration> but have <%s>" ,
start . Name . Local ) )
2021-03-19 01:18:35 -04:00
}
for {
// Read tokens from the XML document in a stream.
t , err := d . Token ( )
if err != nil {
if err == io . EOF {
break
}
return err
}
switch se := t . ( type ) {
case xml . StartElement :
switch se . Name . Local {
case "Rule" :
var r Rule
if err = d . DecodeElement ( & r , & se ) ; err != nil {
return err
}
lc . Rules = append ( lc . Rules , r )
default :
2021-03-30 02:52:30 -04:00
return xml . UnmarshalError ( fmt . Sprintf ( "expected element type <Rule> but have <%s>" , se . Name . Local ) )
2021-03-19 01:18:35 -04:00
}
}
}
return nil
}
2020-06-12 13:28:21 -04:00
// HasActiveRules - returns whether policy has active rules for.
// Optionally a prefix can be supplied.
// If recursive is specified the function will also return true if any level below the
// prefix has active rules. If no prefix is specified recursive is effectively true.
func ( lc Lifecycle ) HasActiveRules ( prefix string , recursive bool ) bool {
if len ( lc . Rules ) == 0 {
return false
}
for _ , rule := range lc . Rules {
if rule . Status == Disabled {
continue
}
2020-07-17 12:48:00 -04:00
2021-02-04 14:26:02 -05:00
if len ( prefix ) > 0 && len ( rule . GetPrefix ( ) ) > 0 {
2020-07-17 12:48:00 -04:00
if ! recursive {
// If not recursive, incoming prefix must be in rule prefix
2021-02-04 14:26:02 -05:00
if ! strings . HasPrefix ( prefix , rule . GetPrefix ( ) ) {
2020-07-17 12:48:00 -04:00
continue
}
2020-06-12 13:28:21 -04:00
}
2020-07-17 12:48:00 -04:00
if recursive {
// If recursive, we can skip this rule if it doesn't match the tested prefix.
2021-02-04 14:26:02 -05:00
if ! strings . HasPrefix ( prefix , rule . GetPrefix ( ) ) && ! strings . HasPrefix ( rule . GetPrefix ( ) , prefix ) {
2020-07-17 12:48:00 -04:00
continue
}
2020-06-12 13:28:21 -04:00
}
}
if rule . NoncurrentVersionExpiration . NoncurrentDays > 0 {
return true
}
2021-11-19 20:54:10 -05:00
if rule . NoncurrentVersionExpiration . MaxNoncurrentVersions > 0 {
return true
}
2021-10-01 14:58:17 -04:00
if ! rule . NoncurrentVersionTransition . IsNull ( ) {
2020-06-12 13:28:21 -04:00
return true
}
2020-11-12 15:12:09 -05:00
if rule . Expiration . IsNull ( ) && rule . Transition . IsNull ( ) {
2020-06-12 13:28:21 -04:00
continue
}
2020-11-12 15:12:09 -05:00
if ! rule . Expiration . IsDateNull ( ) && rule . Expiration . Date . Before ( time . Now ( ) ) {
return true
}
2021-10-01 14:58:17 -04:00
if ! rule . Expiration . IsDaysNull ( ) {
return true
}
2020-11-12 15:12:09 -05:00
if ! rule . Transition . IsDateNull ( ) && rule . Transition . Date . Before ( time . Now ( ) ) {
return true
}
2021-10-01 14:58:17 -04:00
if ! rule . Transition . IsNull ( ) { // this allows for Transition.Days to be zero.
2020-11-12 15:12:09 -05:00
return true
2020-06-12 13:28:21 -04:00
}
2021-10-01 14:58:17 -04:00
2020-06-12 13:28:21 -04:00
}
return false
2019-07-19 16:20:33 -04:00
}
// ParseLifecycleConfig - parses data in given reader to Lifecycle.
func ParseLifecycleConfig ( reader io . Reader ) ( * Lifecycle , error ) {
var lc Lifecycle
if err := xml . NewDecoder ( reader ) . Decode ( & lc ) ; err != nil {
return nil , err
}
return & lc , nil
}
// Validate - validates the lifecycle configuration
func ( lc Lifecycle ) Validate ( ) error {
// Lifecycle config can't have more than 1000 rules
if len ( lc . Rules ) > 1000 {
return errLifecycleTooManyRules
}
// Lifecycle config should have at least one rule
if len ( lc . Rules ) == 0 {
return errLifecycleNoRule
}
2021-10-01 14:58:17 -04:00
2019-07-19 16:20:33 -04:00
// Validate all the rules in the lifecycle config
for _ , r := range lc . Rules {
if err := r . Validate ( ) ; err != nil {
return err
}
}
2020-07-16 10:39:41 -04:00
// Make sure Rule ID is unique
2019-07-19 16:20:33 -04:00
for i := range lc . Rules {
if i == len ( lc . Rules ) - 1 {
break
}
otherRules := lc . Rules [ i + 1 : ]
for _ , otherRule := range otherRules {
2020-07-16 10:39:41 -04:00
if lc . Rules [ i ] . ID == otherRule . ID {
return errLifecycleDuplicateID
2019-07-19 16:20:33 -04:00
}
}
}
return nil
}
2019-08-09 13:02:41 -04:00
2020-05-28 13:55:48 -04:00
// FilterActionableRules returns the rules actions that need to be executed
// after evaluating prefix/tag filtering
2020-06-12 23:04:01 -04:00
func ( lc Lifecycle ) FilterActionableRules ( obj ObjectOpts ) [ ] Rule {
if obj . Name == "" {
2020-05-28 13:55:48 -04:00
return nil
2020-02-25 10:52:28 -05:00
}
2020-05-28 13:55:48 -04:00
var rules [ ] Rule
2019-08-09 13:02:41 -04:00
for _ , rule := range lc . Rules {
2020-02-25 10:52:28 -05:00
if rule . Status == Disabled {
2019-08-09 13:02:41 -04:00
continue
}
2021-02-04 14:26:02 -05:00
if ! strings . HasPrefix ( obj . Name , rule . GetPrefix ( ) ) {
2020-06-05 13:30:10 -04:00
continue
}
2020-06-12 23:04:01 -04:00
// Indicates whether MinIO will remove a delete marker with no
// noncurrent versions. If set to true, the delete marker will
// be expired; if set to false the policy takes no action. This
// cannot be specified with Days or Date in a Lifecycle
// Expiration Policy.
2020-08-25 15:38:59 -04:00
if rule . Expiration . DeleteMarker . val {
2020-06-12 23:04:01 -04:00
rules = append ( rules , rule )
continue
}
// The NoncurrentVersionExpiration action requests MinIO to expire
2020-11-12 15:12:09 -05:00
// noncurrent versions of objects x days after the objects become
2020-06-12 23:04:01 -04:00
// noncurrent.
if ! rule . NoncurrentVersionExpiration . IsDaysNull ( ) {
rules = append ( rules , rule )
continue
}
2021-11-19 20:54:10 -05:00
if rule . NoncurrentVersionExpiration . MaxNoncurrentVersions > 0 {
rules = append ( rules , rule )
continue
}
2020-11-12 15:12:09 -05:00
// The NoncurrentVersionTransition action requests MinIO to transition
// noncurrent versions of objects x days after the objects become
// noncurrent.
2021-10-01 14:58:17 -04:00
if ! rule . NoncurrentVersionTransition . IsNull ( ) {
2020-11-12 15:12:09 -05:00
rules = append ( rules , rule )
continue
}
2020-06-12 23:04:01 -04:00
if rule . Filter . TestTags ( strings . Split ( obj . UserTags , "&" ) ) {
2020-06-05 13:30:10 -04:00
rules = append ( rules , rule )
2019-08-09 13:02:41 -04:00
}
2020-11-12 15:12:09 -05:00
if ! rule . Transition . IsNull ( ) {
rules = append ( rules , rule )
}
2019-08-09 13:02:41 -04:00
}
2020-05-28 13:55:48 -04:00
return rules
2019-08-09 13:02:41 -04:00
}
2020-06-12 23:04:01 -04:00
// ObjectOpts provides information to deduce the lifecycle actions
// which can be triggered on the resultant object.
type ObjectOpts struct {
2021-10-01 14:58:17 -04:00
Name string
UserTags string
ModTime time . Time
VersionID string
IsLatest bool
DeleteMarker bool
NumVersions int
SuccessorModTime time . Time
TransitionStatus string
RestoreOngoing bool
RestoreExpires time . Time
2020-06-12 23:04:01 -04:00
}
2021-03-05 18:29:27 -05:00
// ExpiredObjectDeleteMarker returns true if an object version referred to by o
// is the only version remaining and is a delete marker. It returns false
// otherwise.
func ( o ObjectOpts ) ExpiredObjectDeleteMarker ( ) bool {
return o . DeleteMarker && o . NumVersions == 1
}
2019-08-09 13:02:41 -04:00
// ComputeAction returns the action to perform by evaluating all lifecycle rules
// against the object name and its modification time.
2020-06-12 23:04:01 -04:00
func ( lc Lifecycle ) ComputeAction ( obj ObjectOpts ) Action {
var action = NoneAction
if obj . ModTime . IsZero ( ) {
return action
2020-02-25 10:52:28 -05:00
}
2020-06-12 23:04:01 -04:00
for _ , rule := range lc . FilterActionableRules ( obj ) {
2021-08-05 14:21:21 -04:00
if obj . ExpiredObjectDeleteMarker ( ) {
if rule . Expiration . DeleteMarker . val {
// Indicates whether MinIO will remove a delete marker with no noncurrent versions.
// Only latest marker is removed. If set to true, the delete marker will be expired;
// if set to false the policy takes no action. This cannot be specified with Days or
// Date in a Lifecycle Expiration Policy.
return DeleteVersionAction
}
if ! rule . Expiration . IsDaysNull ( ) {
// Specifying the Days tag will automatically perform ExpiredObjectDeleteMarker cleanup
// once delete markers are old enough to satisfy the age criteria.
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-configuration-examples.html
if time . Now ( ) . After ( ExpectedExpiryTime ( obj . ModTime , int ( rule . Expiration . Days ) ) ) {
return DeleteVersionAction
}
}
2020-06-12 23:04:01 -04:00
}
if ! rule . NoncurrentVersionExpiration . IsDaysNull ( ) {
2020-09-09 21:11:24 -04:00
if obj . VersionID != "" && ! obj . IsLatest && ! obj . SuccessorModTime . IsZero ( ) {
// Non current versions should be deleted if their age exceeds non current days configuration
// https://docs.aws.amazon.com/AmazonS3/latest/dev/intro-lifecycle-rules.html#intro-lifecycle-rules-actions
2020-11-12 15:12:09 -05:00
if time . Now ( ) . After ( ExpectedExpiryTime ( obj . SuccessorModTime , int ( rule . NoncurrentVersionExpiration . NoncurrentDays ) ) ) {
2020-07-04 23:56:02 -04:00
return DeleteVersionAction
2020-06-12 23:04:01 -04:00
}
}
}
2021-02-10 11:51:34 -05:00
2021-10-01 14:58:17 -04:00
if ! rule . NoncurrentVersionTransition . IsNull ( ) {
2021-03-05 18:29:27 -05:00
if obj . VersionID != "" && ! obj . IsLatest && ! obj . SuccessorModTime . IsZero ( ) && ! obj . DeleteMarker && obj . TransitionStatus != TransitionComplete {
2021-04-19 13:30:42 -04:00
// Non current versions should be transitioned if their age exceeds non current days configuration
2020-11-12 15:12:09 -05:00
// https://docs.aws.amazon.com/AmazonS3/latest/dev/intro-lifecycle-rules.html#intro-lifecycle-rules-actions
2021-10-18 20:24:30 -04:00
if due , ok := rule . NoncurrentVersionTransition . NextDue ( obj ) ; ok && time . Now ( ) . UTC ( ) . After ( due ) {
2020-11-12 15:12:09 -05:00
return TransitionVersionAction
}
}
}
2020-06-12 23:04:01 -04:00
2020-07-18 18:43:13 -04:00
// Remove the object or simply add a delete marker (once) in a versioned bucket
if obj . VersionID == "" || obj . IsLatest && ! obj . DeleteMarker {
2020-06-12 23:04:01 -04:00
switch {
case ! rule . Expiration . IsDateNull ( ) :
if time . Now ( ) . UTC ( ) . After ( rule . Expiration . Date . Time ) {
2021-03-05 18:29:27 -05:00
return DeleteAction
2020-06-12 23:04:01 -04:00
}
case ! rule . Expiration . IsDaysNull ( ) :
2020-11-12 15:12:09 -05:00
if time . Now ( ) . UTC ( ) . After ( ExpectedExpiryTime ( obj . ModTime , int ( rule . Expiration . Days ) ) ) {
2021-03-05 18:29:27 -05:00
return DeleteAction
2020-06-12 23:04:01 -04:00
}
}
2021-03-05 18:29:27 -05:00
if obj . TransitionStatus != TransitionComplete {
2021-10-01 14:58:17 -04:00
if due , ok := rule . Transition . NextDue ( obj ) ; ok {
if time . Now ( ) . UTC ( ) . After ( due ) {
2021-03-05 18:29:27 -05:00
action = TransitionAction
2020-11-12 15:12:09 -05:00
}
2021-04-19 13:30:42 -04:00
}
if ! obj . RestoreExpires . IsZero ( ) && time . Now ( ) . After ( obj . RestoreExpires ) {
if obj . VersionID != "" {
action = DeleteRestoredVersionAction
} else {
action = DeleteRestoredAction
}
2020-11-12 15:12:09 -05:00
}
}
2021-03-05 18:29:27 -05:00
if ! obj . RestoreExpires . IsZero ( ) && time . Now ( ) . After ( obj . RestoreExpires ) {
if obj . VersionID != "" {
action = DeleteRestoredVersionAction
} else {
action = DeleteRestoredAction
}
}
2020-06-12 23:04:01 -04:00
}
2019-08-09 13:02:41 -04:00
}
2020-06-12 23:04:01 -04:00
return action
2019-08-09 13:02:41 -04:00
}
2020-05-21 17:12:52 -04:00
2020-11-12 15:12:09 -05:00
// ExpectedExpiryTime calculates the expiry, transition or restore date/time based on a object modtime.
// The expected transition or restore time is always a midnight time following the the object
// modification time plus the number of transition/restore days.
2020-05-21 17:12:52 -04:00
// e.g. If the object modtime is `Thu May 21 13:42:50 GMT 2020` and the object should
2020-11-12 15:12:09 -05:00
// transition in 1 day, then the expected transition time is `Fri, 23 May 2020 00:00:00 GMT`
func ExpectedExpiryTime ( modTime time . Time , days int ) time . Time {
2020-05-21 17:12:52 -04:00
t := modTime . UTC ( ) . Add ( time . Duration ( days + 1 ) * 24 * time . Hour )
return t . Truncate ( 24 * time . Hour )
}
// PredictExpiryTime returns the expiry date/time of a given object
2021-04-19 13:30:42 -04:00
// after evaluating the current lifecycle document.
2020-06-12 23:04:01 -04:00
func ( lc Lifecycle ) PredictExpiryTime ( obj ObjectOpts ) ( string , time . Time ) {
if obj . DeleteMarker {
// We don't need to send any x-amz-expiration for delete marker.
return "" , time . Time { }
}
2020-05-28 13:55:48 -04:00
var finalExpiryDate time . Time
var finalExpiryRuleID string
// Iterate over all actionable rules and find the earliest
// expiration date and its associated rule ID.
2020-06-12 23:04:01 -04:00
for _ , rule := range lc . FilterActionableRules ( obj ) {
if ! rule . NoncurrentVersionExpiration . IsDaysNull ( ) && ! obj . IsLatest && obj . VersionID != "" {
2021-02-17 17:51:29 -05:00
return rule . ID , ExpectedExpiryTime ( obj . SuccessorModTime , int ( rule . NoncurrentVersionExpiration . NoncurrentDays ) )
2020-06-12 23:04:01 -04:00
}
2020-05-28 13:55:48 -04:00
if ! rule . Expiration . IsDateNull ( ) {
if finalExpiryDate . IsZero ( ) || finalExpiryDate . After ( rule . Expiration . Date . Time ) {
finalExpiryRuleID = rule . ID
finalExpiryDate = rule . Expiration . Date . Time
}
}
if ! rule . Expiration . IsDaysNull ( ) {
2020-11-12 15:12:09 -05:00
expectedExpiry := ExpectedExpiryTime ( obj . ModTime , int ( rule . Expiration . Days ) )
2020-05-28 13:55:48 -04:00
if finalExpiryDate . IsZero ( ) || finalExpiryDate . After ( expectedExpiry ) {
finalExpiryRuleID = rule . ID
finalExpiryDate = expectedExpiry
}
}
2020-05-21 17:12:52 -04:00
}
2020-05-28 13:55:48 -04:00
return finalExpiryRuleID , finalExpiryDate
2020-05-21 17:12:52 -04:00
}
2021-04-19 13:30:42 -04:00
// PredictTransitionTime returns the transition date/time of a given object
// after evaluating the current lifecycle document.
func ( lc Lifecycle ) PredictTransitionTime ( obj ObjectOpts ) ( string , time . Time ) {
if obj . DeleteMarker {
// We don't need to send any x-minio-transition for delete marker.
return "" , time . Time { }
}
if obj . TransitionStatus == TransitionComplete {
return "" , time . Time { }
}
// Iterate over all actionable rules and find the earliest
// transition date and its associated rule ID.
2021-07-20 20:36:55 -04:00
var finalTransitionDate time . Time
var finalTransitionRuleID string
2021-04-19 13:30:42 -04:00
for _ , rule := range lc . FilterActionableRules ( obj ) {
2021-07-20 20:36:55 -04:00
if due , ok := rule . Transition . NextDue ( obj ) ; ok {
if finalTransitionDate . IsZero ( ) || finalTransitionDate . After ( due ) {
2021-04-19 13:30:42 -04:00
finalTransitionRuleID = rule . ID
2021-07-20 20:36:55 -04:00
finalTransitionDate = due
2021-04-19 13:30:42 -04:00
}
2021-07-20 20:36:55 -04:00
}
if due , ok := rule . NoncurrentVersionTransition . NextDue ( obj ) ; ok {
if finalTransitionDate . IsZero ( ) || finalTransitionDate . After ( due ) {
2021-04-19 13:30:42 -04:00
finalTransitionRuleID = rule . ID
2021-07-20 20:36:55 -04:00
finalTransitionDate = due
2021-04-19 13:30:42 -04:00
}
}
}
return finalTransitionRuleID , finalTransitionDate
}
2021-07-20 20:36:55 -04:00
// SetPredictionHeaders sets time to expiry and transition headers on w for a
// given obj.
func ( lc Lifecycle ) SetPredictionHeaders ( w http . ResponseWriter , obj ObjectOpts ) {
if ruleID , expiry := lc . PredictExpiryTime ( obj ) ; ! expiry . IsZero ( ) {
w . Header ( ) [ xhttp . AmzExpiration ] = [ ] string {
fmt . Sprintf ( ` expiry-date="%s", rule-id="%s" ` , expiry . Format ( http . TimeFormat ) , ruleID ) ,
}
}
if ruleID , transition := lc . PredictTransitionTime ( obj ) ; ! transition . IsZero ( ) {
w . Header ( ) [ xhttp . MinIOTransition ] = [ ] string {
fmt . Sprintf ( ` transition-date="%s", rule-id="%s" ` , transition . Format ( http . TimeFormat ) , ruleID ) ,
}
}
}
2021-07-21 22:12:44 -04:00
// TransitionTier returns remote tier that applies to obj per ILM rules.
func ( lc Lifecycle ) TransitionTier ( obj ObjectOpts ) string {
for _ , rule := range lc . FilterActionableRules ( obj ) {
if obj . IsLatest && rule . Transition . StorageClass != "" {
return rule . Transition . StorageClass
}
if ! obj . IsLatest && rule . NoncurrentVersionTransition . StorageClass != "" {
return rule . NoncurrentVersionTransition . StorageClass
}
}
return ""
}
2021-11-19 20:54:10 -05:00
// NoncurrentVersionsExpirationLimit returns the minimum limit on number of
// noncurrent versions across rules.
func ( lc Lifecycle ) NoncurrentVersionsExpirationLimit ( obj ObjectOpts ) int {
var lim int
for _ , rule := range lc . FilterActionableRules ( obj ) {
if rule . NoncurrentVersionExpiration . MaxNoncurrentVersions == 0 {
continue
}
if lim == 0 || lim > rule . NoncurrentVersionExpiration . MaxNoncurrentVersions {
lim = rule . NoncurrentVersionExpiration . MaxNoncurrentVersions
}
}
return lim
}
// HasMaxNoncurrentVersions returns true if there exists a rule with
// MaxNoncurrentVersions limit set.
func ( lc Lifecycle ) HasMaxNoncurrentVersions ( ) bool {
for _ , rule := range lc . Rules {
if rule . Status == Disabled {
continue
}
if rule . NoncurrentVersionExpiration . MaxNoncurrentVersions > 0 {
return true
}
}
return false
}