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 }