github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/batch-expire.go (about) 1 // Copyright (c) 2015-2023 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 "bytes" 22 "context" 23 "encoding/json" 24 "errors" 25 "fmt" 26 "io" 27 "net/http" 28 "runtime" 29 "strconv" 30 "time" 31 32 "github.com/minio/minio-go/v7/pkg/tags" 33 "github.com/minio/minio/internal/bucket/versioning" 34 xhttp "github.com/minio/minio/internal/http" 35 xioutil "github.com/minio/minio/internal/ioutil" 36 "github.com/minio/minio/internal/logger" 37 "github.com/minio/pkg/v2/env" 38 "github.com/minio/pkg/v2/wildcard" 39 "github.com/minio/pkg/v2/workers" 40 "gopkg.in/yaml.v3" 41 ) 42 43 // expire: # Expire objects that match a condition 44 // apiVersion: v1 45 // bucket: mybucket # Bucket where this batch job will expire matching objects from 46 // prefix: myprefix # (Optional) Prefix under which this job will expire objects matching the rules below. 47 // rules: 48 // - type: object # regular objects with zero or more older versions 49 // name: NAME # match object names that satisfy the wildcard expression. 50 // olderThan: 70h # match objects older than this value 51 // createdBefore: "2006-01-02T15:04:05.00Z" # match objects created before "date" 52 // tags: 53 // - key: name 54 // value: pick* # match objects with tag 'name', all values starting with 'pick' 55 // metadata: 56 // - key: content-type 57 // value: image/* # match objects with 'content-type', all values starting with 'image/' 58 // size: 59 // lessThan: "10MiB" # match objects with size less than this value (e.g. 10MiB) 60 // greaterThan: 1MiB # match objects with size greater than this value (e.g. 1MiB) 61 // purge: 62 // # retainVersions: 0 # (default) delete all versions of the object. This option is the fastest. 63 // # retainVersions: 5 # keep the latest 5 versions of the object. 64 // 65 // - type: deleted # objects with delete marker as their latest version 66 // name: NAME # match object names that satisfy the wildcard expression. 67 // olderThan: 10h # match objects older than this value (e.g. 7d10h31s) 68 // createdBefore: "2006-01-02T15:04:05.00Z" # match objects created before "date" 69 // purge: 70 // # retainVersions: 0 # (default) delete all versions of the object. This option is the fastest. 71 // # retainVersions: 5 # keep the latest 5 versions of the object including delete markers. 72 // 73 // notify: 74 // endpoint: https://notify.endpoint # notification endpoint to receive job completion status 75 // token: Bearer xxxxx # optional authentication token for the notification endpoint 76 // 77 // retry: 78 // attempts: 10 # number of retries for the job before giving up 79 // delay: 500ms # least amount of delay between each retry 80 81 //go:generate msgp -file $GOFILE 82 83 // BatchJobExpirePurge type accepts non-negative versions to be retained 84 type BatchJobExpirePurge struct { 85 line, col int 86 RetainVersions int `yaml:"retainVersions" json:"retainVersions"` 87 } 88 89 var _ yaml.Unmarshaler = &BatchJobExpirePurge{} 90 91 // UnmarshalYAML - BatchJobExpirePurge extends unmarshal to extract line, col 92 func (p *BatchJobExpirePurge) UnmarshalYAML(val *yaml.Node) error { 93 type purge BatchJobExpirePurge 94 var tmp purge 95 err := val.Decode(&tmp) 96 if err != nil { 97 return err 98 } 99 100 *p = BatchJobExpirePurge(tmp) 101 p.line, p.col = val.Line, val.Column 102 return nil 103 } 104 105 // Validate returns nil if value is valid, ie > 0. 106 func (p BatchJobExpirePurge) Validate() error { 107 if p.RetainVersions < 0 { 108 return BatchJobYamlErr{ 109 line: p.line, 110 col: p.col, 111 msg: "retainVersions must be >= 0", 112 } 113 } 114 return nil 115 } 116 117 // BatchJobExpireFilter holds all the filters currently supported for batch replication 118 type BatchJobExpireFilter struct { 119 line, col int 120 OlderThan time.Duration `yaml:"olderThan,omitempty" json:"olderThan"` 121 CreatedBefore *time.Time `yaml:"createdBefore,omitempty" json:"createdBefore"` 122 Tags []BatchJobKV `yaml:"tags,omitempty" json:"tags"` 123 Metadata []BatchJobKV `yaml:"metadata,omitempty" json:"metadata"` 124 Size BatchJobSizeFilter `yaml:"size" json:"size"` 125 Type string `yaml:"type" json:"type"` 126 Name string `yaml:"name" json:"name"` 127 Purge BatchJobExpirePurge `yaml:"purge" json:"purge"` 128 } 129 130 var _ yaml.Unmarshaler = &BatchJobExpireFilter{} 131 132 // UnmarshalYAML - BatchJobExpireFilter extends unmarshal to extract line, col 133 // information 134 func (ef *BatchJobExpireFilter) UnmarshalYAML(value *yaml.Node) error { 135 type expFilter BatchJobExpireFilter 136 var tmp expFilter 137 err := value.Decode(&tmp) 138 if err != nil { 139 return err 140 } 141 *ef = BatchJobExpireFilter(tmp) 142 ef.line, ef.col = value.Line, value.Column 143 return err 144 } 145 146 // Matches returns true if obj matches the filter conditions specified in ef. 147 func (ef BatchJobExpireFilter) Matches(obj ObjectInfo, now time.Time) bool { 148 switch ef.Type { 149 case BatchJobExpireObject: 150 if obj.DeleteMarker { 151 return false 152 } 153 case BatchJobExpireDeleted: 154 if !obj.DeleteMarker { 155 return false 156 } 157 default: 158 // we should never come here, Validate should have caught this. 159 logger.LogOnceIf(context.Background(), fmt.Errorf("invalid filter type: %s", ef.Type), ef.Type) 160 return false 161 } 162 163 if len(ef.Name) > 0 && !wildcard.Match(ef.Name, obj.Name) { 164 return false 165 } 166 if ef.OlderThan > 0 && now.Sub(obj.ModTime) <= ef.OlderThan { 167 return false 168 } 169 170 if ef.CreatedBefore != nil && !obj.ModTime.Before(*ef.CreatedBefore) { 171 return false 172 } 173 174 if len(ef.Tags) > 0 && !obj.DeleteMarker { 175 // Only parse object tags if tags filter is specified. 176 var tagMap map[string]string 177 if len(obj.UserTags) != 0 { 178 t, err := tags.ParseObjectTags(obj.UserTags) 179 if err != nil { 180 return false 181 } 182 tagMap = t.ToMap() 183 } 184 185 for _, kv := range ef.Tags { 186 // Object (version) must match all tags specified in 187 // the filter 188 var match bool 189 for t, v := range tagMap { 190 if kv.Match(BatchJobKV{Key: t, Value: v}) { 191 match = true 192 } 193 } 194 if !match { 195 return false 196 } 197 } 198 199 } 200 if len(ef.Metadata) > 0 && !obj.DeleteMarker { 201 for _, kv := range ef.Metadata { 202 // Object (version) must match all x-amz-meta and 203 // standard metadata headers 204 // specified in the filter 205 var match bool 206 for k, v := range obj.UserDefined { 207 if !stringsHasPrefixFold(k, "x-amz-meta-") && !isStandardHeader(k) { 208 continue 209 } 210 // We only need to match x-amz-meta or standardHeaders 211 if kv.Match(BatchJobKV{Key: k, Value: v}) { 212 match = true 213 } 214 } 215 if !match { 216 return false 217 } 218 } 219 } 220 221 return ef.Size.InRange(obj.Size) 222 } 223 224 const ( 225 // BatchJobExpireObject - object type 226 BatchJobExpireObject string = "object" 227 // BatchJobExpireDeleted - delete marker type 228 BatchJobExpireDeleted string = "deleted" 229 ) 230 231 // Validate returns nil if ef has valid fields, validation error otherwise. 232 func (ef BatchJobExpireFilter) Validate() error { 233 switch ef.Type { 234 case BatchJobExpireObject: 235 case BatchJobExpireDeleted: 236 if len(ef.Tags) > 0 || len(ef.Metadata) > 0 { 237 return BatchJobYamlErr{ 238 line: ef.line, 239 col: ef.col, 240 msg: "delete type filter can't have tags or metadata", 241 } 242 } 243 default: 244 return BatchJobYamlErr{ 245 line: ef.line, 246 col: ef.col, 247 msg: "invalid batch-expire type", 248 } 249 } 250 251 for _, tag := range ef.Tags { 252 if err := tag.Validate(); err != nil { 253 return err 254 } 255 } 256 257 for _, meta := range ef.Metadata { 258 if err := meta.Validate(); err != nil { 259 return err 260 } 261 } 262 if err := ef.Purge.Validate(); err != nil { 263 return err 264 } 265 if err := ef.Size.Validate(); err != nil { 266 return err 267 } 268 if ef.CreatedBefore != nil && !ef.CreatedBefore.Before(time.Now()) { 269 return BatchJobYamlErr{ 270 line: ef.line, 271 col: ef.col, 272 msg: "CreatedBefore is in the future", 273 } 274 } 275 return nil 276 } 277 278 // BatchJobExpire represents configuration parameters for a batch expiration 279 // job typically supplied in yaml form 280 type BatchJobExpire struct { 281 line, col int 282 APIVersion string `yaml:"apiVersion" json:"apiVersion"` 283 Bucket string `yaml:"bucket" json:"bucket"` 284 Prefix string `yaml:"prefix" json:"prefix"` 285 NotificationCfg BatchJobNotification `yaml:"notify" json:"notify"` 286 Retry BatchJobRetry `yaml:"retry" json:"retry"` 287 Rules []BatchJobExpireFilter `yaml:"rules" json:"rules"` 288 } 289 290 var _ yaml.Unmarshaler = &BatchJobExpire{} 291 292 // UnmarshalYAML - BatchJobExpire extends default unmarshal to extract line, col information. 293 func (r *BatchJobExpire) UnmarshalYAML(val *yaml.Node) error { 294 type expireJob BatchJobExpire 295 var tmp expireJob 296 err := val.Decode(&tmp) 297 if err != nil { 298 return err 299 } 300 301 *r = BatchJobExpire(tmp) 302 r.line, r.col = val.Line, val.Column 303 return nil 304 } 305 306 // Notify notifies notification endpoint if configured regarding job failure or success. 307 func (r BatchJobExpire) Notify(ctx context.Context, body io.Reader) error { 308 if r.NotificationCfg.Endpoint == "" { 309 return nil 310 } 311 312 ctx, cancel := context.WithTimeout(ctx, 10*time.Second) 313 defer cancel() 314 315 req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.NotificationCfg.Endpoint, body) 316 if err != nil { 317 return err 318 } 319 320 if r.NotificationCfg.Token != "" { 321 req.Header.Set("Authorization", r.NotificationCfg.Token) 322 } 323 324 clnt := http.Client{Transport: getRemoteInstanceTransport} 325 resp, err := clnt.Do(req) 326 if err != nil { 327 return err 328 } 329 330 xhttp.DrainBody(resp.Body) 331 if resp.StatusCode != http.StatusOK { 332 return errors.New(resp.Status) 333 } 334 335 return nil 336 } 337 338 // Expire expires object versions which have already matched supplied filter conditions 339 func (r *BatchJobExpire) Expire(ctx context.Context, api ObjectLayer, vc *versioning.Versioning, objsToDel []ObjectToDelete) []error { 340 opts := ObjectOptions{ 341 PrefixEnabledFn: vc.PrefixEnabled, 342 VersionSuspended: vc.Suspended(), 343 } 344 _, errs := api.DeleteObjects(ctx, r.Bucket, objsToDel, opts) 345 return errs 346 } 347 348 const ( 349 batchExpireName = "batch-expire.bin" 350 batchExpireFormat = 1 351 batchExpireVersionV1 = 1 352 batchExpireVersion = batchExpireVersionV1 353 batchExpireAPIVersion = "v1" 354 batchExpireJobDefaultRetries = 3 355 batchExpireJobDefaultRetryDelay = 250 * time.Millisecond 356 ) 357 358 type objInfoCache map[string]*ObjectInfo 359 360 func newObjInfoCache() objInfoCache { 361 return objInfoCache(make(map[string]*ObjectInfo)) 362 } 363 364 func (oiCache objInfoCache) Add(toDel ObjectToDelete, oi *ObjectInfo) { 365 oiCache[fmt.Sprintf("%s-%s", toDel.ObjectName, toDel.VersionID)] = oi 366 } 367 368 func (oiCache objInfoCache) Get(toDel ObjectToDelete) (*ObjectInfo, bool) { 369 oi, ok := oiCache[fmt.Sprintf("%s-%s", toDel.ObjectName, toDel.VersionID)] 370 return oi, ok 371 } 372 373 func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo, job BatchJobRequest, api ObjectLayer, wk *workers.Workers, expireCh <-chan []expireObjInfo) { 374 vc, _ := globalBucketVersioningSys.Get(r.Bucket) 375 retryAttempts := r.Retry.Attempts 376 delay := job.Expire.Retry.Delay 377 if delay == 0 { 378 delay = batchExpireJobDefaultRetryDelay 379 } 380 381 var i int 382 for toExpire := range expireCh { 383 select { 384 case <-ctx.Done(): 385 return 386 default: 387 } 388 if i > 0 { 389 if wait := globalBatchConfig.ExpirationWait(); wait > 0 { 390 time.Sleep(wait) 391 } 392 } 393 i++ 394 wk.Take() 395 go func(toExpire []expireObjInfo) { 396 defer wk.Give() 397 398 toExpireAll := make([]ObjectInfo, 0, len(toExpire)) 399 toDel := make([]ObjectToDelete, 0, len(toExpire)) 400 oiCache := newObjInfoCache() 401 for _, exp := range toExpire { 402 if exp.ExpireAll { 403 toExpireAll = append(toExpireAll, exp.ObjectInfo) 404 continue 405 } 406 // Cache ObjectInfo value via pointers for 407 // subsequent use to track objects which 408 // couldn't be deleted. 409 od := ObjectToDelete{ 410 ObjectV: ObjectV{ 411 ObjectName: exp.Name, 412 VersionID: exp.VersionID, 413 }, 414 } 415 toDel = append(toDel, od) 416 oiCache.Add(od, &exp.ObjectInfo) 417 } 418 419 var done bool 420 // DeleteObject(deletePrefix: true) to expire all versions of an object 421 for _, exp := range toExpireAll { 422 var success bool 423 for attempts := 1; attempts <= retryAttempts; attempts++ { 424 select { 425 case <-ctx.Done(): 426 done = true 427 default: 428 } 429 stopFn := globalBatchJobsMetrics.trace(batchJobMetricExpire, ri.JobID, attempts) 430 _, err := api.DeleteObject(ctx, exp.Bucket, encodeDirObject(exp.Name), ObjectOptions{ 431 DeletePrefix: true, 432 DeletePrefixObject: true, // use prefix delete on exact object (this is an optimization to avoid fan-out calls) 433 }) 434 if err != nil { 435 stopFn(exp, err) 436 logger.LogIf(ctx, fmt.Errorf("Failed to expire %s/%s versionID=%s due to %v (attempts=%d)", toExpire[i].Bucket, toExpire[i].Name, toExpire[i].VersionID, err, attempts)) 437 } else { 438 stopFn(exp, err) 439 success = true 440 break 441 } 442 } 443 ri.trackMultipleObjectVersions(r.Bucket, exp, success) 444 if done { 445 break 446 } 447 } 448 449 if done { 450 return 451 } 452 453 // DeleteMultiple objects 454 toDelCopy := make([]ObjectToDelete, len(toDel)) 455 for attempts := 1; attempts <= retryAttempts; attempts++ { 456 select { 457 case <-ctx.Done(): 458 return 459 default: 460 } 461 462 stopFn := globalBatchJobsMetrics.trace(batchJobMetricExpire, ri.JobID, attempts) 463 // Copying toDel to select from objects whose 464 // deletion failed 465 copy(toDelCopy, toDel) 466 var failed int 467 errs := r.Expire(ctx, api, vc, toDel) 468 // reslice toDel in preparation for next retry 469 // attempt 470 toDel = toDel[:0] 471 for i, err := range errs { 472 if err != nil { 473 stopFn(toDelCopy[i], err) 474 logger.LogIf(ctx, fmt.Errorf("Failed to expire %s/%s versionID=%s due to %v (attempts=%d)", ri.Bucket, toDelCopy[i].ObjectName, toDelCopy[i].VersionID, err, attempts)) 475 failed++ 476 if attempts == retryAttempts { // all retry attempts failed, record failure 477 if oi, ok := oiCache.Get(toDelCopy[i]); ok { 478 ri.trackCurrentBucketObject(r.Bucket, *oi, false) 479 } 480 } else { 481 toDel = append(toDel, toDelCopy[i]) 482 } 483 } else { 484 stopFn(toDelCopy[i], nil) 485 if oi, ok := oiCache.Get(toDelCopy[i]); ok { 486 ri.trackCurrentBucketObject(r.Bucket, *oi, true) 487 } 488 } 489 } 490 491 globalBatchJobsMetrics.save(ri.JobID, ri) 492 493 if failed == 0 { 494 break 495 } 496 497 // Add a delay between retry attempts 498 if attempts < retryAttempts { 499 time.Sleep(delay) 500 } 501 } 502 }(toExpire) 503 } 504 } 505 506 type expireObjInfo struct { 507 ObjectInfo 508 ExpireAll bool 509 } 510 511 // Start the batch expiration job, resumes if there was a pending job via "job.ID" 512 func (r *BatchJobExpire) Start(ctx context.Context, api ObjectLayer, job BatchJobRequest) error { 513 ri := &batchJobInfo{ 514 JobID: job.ID, 515 JobType: string(job.Type()), 516 StartTime: job.Started, 517 } 518 if err := ri.load(ctx, api, job); err != nil { 519 return err 520 } 521 522 globalBatchJobsMetrics.save(job.ID, ri) 523 lastObject := ri.Object 524 525 now := time.Now().UTC() 526 527 workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_EXPIRATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2))) 528 if err != nil { 529 return err 530 } 531 532 wk, err := workers.New(workerSize) 533 if err != nil { 534 // invalid worker size. 535 return err 536 } 537 538 ctx, cancel := context.WithCancel(ctx) 539 defer cancel() 540 541 results := make(chan ObjectInfo, workerSize) 542 if err := api.Walk(ctx, r.Bucket, r.Prefix, results, WalkOptions{ 543 Marker: lastObject, 544 LatestOnly: false, // we need to visit all versions of the object to implement purge: retainVersions 545 VersionsSort: WalkVersionsSortDesc, 546 }); err != nil { 547 // Do not need to retry if we can't list objects on source. 548 return err 549 } 550 551 // Goroutine to periodically save batch-expire job's in-memory state 552 saverQuitCh := make(chan struct{}) 553 go func() { 554 saveTicker := time.NewTicker(10 * time.Second) 555 defer saveTicker.Stop() 556 for { 557 select { 558 case <-saveTicker.C: 559 // persist in-memory state to disk after every 10secs. 560 logger.LogIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job)) 561 562 case <-ctx.Done(): 563 // persist in-memory state immediately before exiting due to context cancellation. 564 logger.LogIf(ctx, ri.updateAfter(ctx, api, 0, job)) 565 return 566 567 case <-saverQuitCh: 568 // persist in-memory state immediately to disk. 569 logger.LogIf(ctx, ri.updateAfter(ctx, api, 0, job)) 570 return 571 } 572 } 573 }() 574 575 expireCh := make(chan []expireObjInfo, workerSize) 576 expireDoneCh := make(chan struct{}) 577 go func() { 578 defer close(expireDoneCh) 579 batchObjsForDelete(ctx, r, ri, job, api, wk, expireCh) 580 }() 581 582 var ( 583 prevObj ObjectInfo 584 matchedFilter BatchJobExpireFilter 585 versionsCount int 586 toDel []expireObjInfo 587 ) 588 for result := range results { 589 // Apply filter to find the matching rule to apply expiry 590 // actions accordingly. 591 // nolint:gocritic 592 if result.IsLatest { 593 // send down filtered entries to be deleted using 594 // DeleteObjects method 595 if len(toDel) > 10 { // batch up to 10 objects/versions to be expired simultaneously. 596 xfer := make([]expireObjInfo, len(toDel)) 597 copy(xfer, toDel) 598 599 var done bool 600 select { 601 case <-ctx.Done(): 602 done = true 603 case expireCh <- xfer: 604 toDel = toDel[:0] // resetting toDel 605 } 606 if done { 607 break 608 } 609 } 610 var match BatchJobExpireFilter 611 var found bool 612 for _, rule := range r.Rules { 613 if rule.Matches(result, now) { 614 match = rule 615 found = true 616 break 617 } 618 } 619 if !found { 620 continue 621 } 622 623 prevObj = result 624 matchedFilter = match 625 versionsCount = 1 626 // Include the latest version 627 if matchedFilter.Purge.RetainVersions == 0 { 628 toDel = append(toDel, expireObjInfo{ 629 ObjectInfo: result, 630 ExpireAll: true, 631 }) 632 continue 633 } 634 } else if prevObj.Name == result.Name { 635 if matchedFilter.Purge.RetainVersions == 0 { 636 continue // including latest version in toDel suffices, skipping other versions 637 } 638 versionsCount++ 639 } else { 640 continue 641 } 642 643 if versionsCount <= matchedFilter.Purge.RetainVersions { 644 continue // retain versions 645 } 646 toDel = append(toDel, expireObjInfo{ 647 ObjectInfo: result, 648 }) 649 } 650 // Send any remaining objects downstream 651 if len(toDel) > 0 { 652 select { 653 case <-ctx.Done(): 654 case expireCh <- toDel: 655 } 656 } 657 xioutil.SafeClose(expireCh) 658 659 <-expireDoneCh // waits for the expire goroutine to complete 660 wk.Wait() // waits for all expire workers to retire 661 662 ri.Complete = ri.ObjectsFailed == 0 663 ri.Failed = ri.ObjectsFailed > 0 664 globalBatchJobsMetrics.save(job.ID, ri) 665 666 // Close the saverQuitCh - this also triggers saving in-memory state 667 // immediately one last time before we exit this method. 668 xioutil.SafeClose(saverQuitCh) 669 670 // Notify expire jobs final status to the configured endpoint 671 buf, _ := json.Marshal(ri) 672 if err := r.Notify(context.Background(), bytes.NewReader(buf)); err != nil { 673 logger.LogIf(context.Background(), fmt.Errorf("unable to notify %v", err)) 674 } 675 676 return nil 677 } 678 679 //msgp:ignore batchExpireJobError 680 type batchExpireJobError struct { 681 Code string 682 Description string 683 HTTPStatusCode int 684 } 685 686 func (e batchExpireJobError) Error() string { 687 return e.Description 688 } 689 690 // maxBatchRules maximum number of rules a batch-expiry job supports 691 const maxBatchRules = 50 692 693 // Validate validates the job definition input 694 func (r *BatchJobExpire) Validate(ctx context.Context, job BatchJobRequest, o ObjectLayer) error { 695 if r == nil { 696 return nil 697 } 698 699 if r.APIVersion != batchExpireAPIVersion { 700 return batchExpireJobError{ 701 Code: "InvalidArgument", 702 Description: "Unsupported batch expire API version", 703 HTTPStatusCode: http.StatusBadRequest, 704 } 705 } 706 707 if r.Bucket == "" { 708 return batchExpireJobError{ 709 Code: "InvalidArgument", 710 Description: "Bucket argument missing", 711 HTTPStatusCode: http.StatusBadRequest, 712 } 713 } 714 715 if _, err := o.GetBucketInfo(ctx, r.Bucket, BucketOptions{}); err != nil { 716 if isErrBucketNotFound(err) { 717 return batchExpireJobError{ 718 Code: "NoSuchSourceBucket", 719 Description: "The specified source bucket does not exist", 720 HTTPStatusCode: http.StatusNotFound, 721 } 722 } 723 return err 724 } 725 726 if len(r.Rules) > maxBatchRules { 727 return batchExpireJobError{ 728 Code: "InvalidArgument", 729 Description: "Too many rules. Batch expire job can't have more than 100 rules", 730 HTTPStatusCode: http.StatusBadRequest, 731 } 732 } 733 734 for _, rule := range r.Rules { 735 if err := rule.Validate(); err != nil { 736 return batchExpireJobError{ 737 Code: "InvalidArgument", 738 Description: fmt.Sprintf("Invalid batch expire rule: %s", err), 739 HTTPStatusCode: http.StatusBadRequest, 740 } 741 } 742 } 743 744 if err := r.Retry.Validate(); err != nil { 745 return batchExpireJobError{ 746 Code: "InvalidArgument", 747 Description: fmt.Sprintf("Invalid batch expire retry configuration: %s", err), 748 HTTPStatusCode: http.StatusBadRequest, 749 } 750 } 751 return nil 752 }