github.com/minio/console@v1.3.0/api/user_buckets_lifecycle.go (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2021 MinIO, Inc. 3 // 4 // This program is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Affero General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // This program is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17 package api 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "strconv" 24 "strings" 25 "time" 26 27 "github.com/minio/minio-go/v7" 28 29 "github.com/rs/xid" 30 31 "github.com/minio/mc/cmd/ilm" 32 33 "github.com/minio/minio-go/v7/pkg/lifecycle" 34 35 "github.com/go-openapi/runtime/middleware" 36 "github.com/minio/console/api/operations" 37 bucketApi "github.com/minio/console/api/operations/bucket" 38 "github.com/minio/console/models" 39 ) 40 41 type MultiLifecycleResult struct { 42 BucketName string 43 Error string 44 } 45 46 func registerBucketsLifecycleHandlers(api *operations.ConsoleAPI) { 47 api.BucketGetBucketLifecycleHandler = bucketApi.GetBucketLifecycleHandlerFunc(func(params bucketApi.GetBucketLifecycleParams, session *models.Principal) middleware.Responder { 48 listBucketLifecycleResponse, err := getBucketLifecycleResponse(session, params) 49 if err != nil { 50 return bucketApi.NewGetBucketLifecycleDefault(err.Code).WithPayload(err.APIError) 51 } 52 return bucketApi.NewGetBucketLifecycleOK().WithPayload(listBucketLifecycleResponse) 53 }) 54 api.BucketAddBucketLifecycleHandler = bucketApi.AddBucketLifecycleHandlerFunc(func(params bucketApi.AddBucketLifecycleParams, session *models.Principal) middleware.Responder { 55 err := getAddBucketLifecycleResponse(session, params) 56 if err != nil { 57 return bucketApi.NewAddBucketLifecycleDefault(err.Code).WithPayload(err.APIError) 58 } 59 return bucketApi.NewAddBucketLifecycleCreated() 60 }) 61 api.BucketUpdateBucketLifecycleHandler = bucketApi.UpdateBucketLifecycleHandlerFunc(func(params bucketApi.UpdateBucketLifecycleParams, session *models.Principal) middleware.Responder { 62 err := getEditBucketLifecycleRule(session, params) 63 if err != nil { 64 return bucketApi.NewUpdateBucketLifecycleDefault(err.Code).WithPayload(err.APIError) 65 } 66 67 return bucketApi.NewUpdateBucketLifecycleOK() 68 }) 69 api.BucketDeleteBucketLifecycleRuleHandler = bucketApi.DeleteBucketLifecycleRuleHandlerFunc(func(params bucketApi.DeleteBucketLifecycleRuleParams, session *models.Principal) middleware.Responder { 70 err := getDeleteBucketLifecycleRule(session, params) 71 if err != nil { 72 return bucketApi.NewDeleteBucketLifecycleRuleDefault(err.Code).WithPayload(err.APIError) 73 } 74 75 return bucketApi.NewDeleteBucketLifecycleRuleNoContent() 76 }) 77 api.BucketAddMultiBucketLifecycleHandler = bucketApi.AddMultiBucketLifecycleHandlerFunc(func(params bucketApi.AddMultiBucketLifecycleParams, session *models.Principal) middleware.Responder { 78 multiBucketResponse, err := getAddMultiBucketLifecycleResponse(session, params) 79 if err != nil { 80 bucketApi.NewAddMultiBucketLifecycleDefault(err.Code).WithPayload(err.APIError) 81 } 82 83 return bucketApi.NewAddMultiBucketLifecycleOK().WithPayload(multiBucketResponse) 84 }) 85 } 86 87 // getBucketLifecycle() gets lifecycle lists for a bucket from MinIO API and returns their implementations 88 func getBucketLifecycle(ctx context.Context, client MinioClient, bucketName string) (*models.BucketLifecycleResponse, error) { 89 lifecycleList, err := client.getLifecycleRules(ctx, bucketName) 90 if err != nil { 91 return nil, err 92 } 93 var rules []*models.ObjectBucketLifecycle 94 95 for _, rule := range lifecycleList.Rules { 96 97 var tags []*models.LifecycleTag 98 99 for _, tagData := range rule.RuleFilter.And.Tags { 100 tags = append(tags, &models.LifecycleTag{ 101 Key: tagData.Key, 102 Value: tagData.Value, 103 }) 104 } 105 106 rulePrefix := rule.RuleFilter.And.Prefix 107 108 if rulePrefix == "" { 109 rulePrefix = rule.RuleFilter.Prefix 110 } 111 112 rules = append(rules, &models.ObjectBucketLifecycle{ 113 ID: rule.ID, 114 Status: rule.Status, 115 Prefix: rulePrefix, 116 Expiration: &models.ExpirationResponse{ 117 Date: rule.Expiration.Date.Format(time.RFC3339), 118 Days: int64(rule.Expiration.Days), 119 DeleteMarker: rule.Expiration.DeleteMarker.IsEnabled(), 120 DeleteAll: bool(rule.Expiration.DeleteAll), 121 NoncurrentExpirationDays: int64(rule.NoncurrentVersionExpiration.NoncurrentDays), 122 NewerNoncurrentExpirationVersions: int64(rule.NoncurrentVersionExpiration.NewerNoncurrentVersions), 123 }, 124 Transition: &models.TransitionResponse{ 125 Date: rule.Transition.Date.Format(time.RFC3339), 126 Days: int64(rule.Transition.Days), 127 StorageClass: rule.Transition.StorageClass, 128 NoncurrentStorageClass: rule.NoncurrentVersionTransition.StorageClass, 129 NoncurrentTransitionDays: int64(rule.NoncurrentVersionTransition.NoncurrentDays), 130 }, 131 Tags: tags, 132 }) 133 } 134 135 // serialize output 136 lifecycleBucketsResponse := &models.BucketLifecycleResponse{ 137 Lifecycle: rules, 138 } 139 140 return lifecycleBucketsResponse, nil 141 } 142 143 // getBucketLifecycleResponse performs getBucketLifecycle() and serializes it to the handler's output 144 func getBucketLifecycleResponse(session *models.Principal, params bucketApi.GetBucketLifecycleParams) (*models.BucketLifecycleResponse, *CodedAPIError) { 145 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 146 defer cancel() 147 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 148 if err != nil { 149 return nil, ErrorWithContext(ctx, err) 150 } 151 // create a minioClient interface implementation 152 // defining the client to be used 153 minioClient := minioClient{client: mClient} 154 155 bucketEvents, err := getBucketLifecycle(ctx, minioClient, params.BucketName) 156 if err != nil { 157 return nil, ErrorWithContext(ctx, ErrBucketLifeCycleNotConfigured, err) 158 } 159 return bucketEvents, nil 160 } 161 162 // addBucketLifecycle gets lifecycle lists for a bucket from MinIO API and returns their implementations 163 func addBucketLifecycle(ctx context.Context, client MinioClient, params bucketApi.AddBucketLifecycleParams) error { 164 // Configuration that is already set. 165 lfcCfg, err := client.getLifecycleRules(ctx, params.BucketName) 166 if err != nil { 167 if e := err; minio.ToErrorResponse(e).Code == "NoSuchLifecycleConfiguration" { 168 lfcCfg = lifecycle.NewConfiguration() 169 } else { 170 return err 171 } 172 } 173 174 id := xid.New().String() 175 176 opts := ilm.LifecycleOptions{} 177 178 // Verify if transition rule is requested 179 switch params.Body.Type { 180 case models.AddBucketLifecycleTypeTransition: 181 if params.Body.TransitionDays == 0 && params.Body.NoncurrentversionTransitionDays == 0 { 182 return errors.New("you must provide a value for transition days or date") 183 } 184 185 status := !params.Body.Disable 186 opts = ilm.LifecycleOptions{ 187 ID: id, 188 Prefix: ¶ms.Body.Prefix, 189 Status: &status, 190 Tags: ¶ms.Body.Tags, 191 ExpiredObjectDeleteMarker: ¶ms.Body.ExpiredObjectDeleteMarker, 192 ExpiredObjectAllversions: ¶ms.Body.ExpiredObjectDeleteAll, 193 } 194 195 if params.Body.NoncurrentversionTransitionDays > 0 { 196 noncurrentVersionTransitionDays := int(params.Body.NoncurrentversionTransitionDays) 197 noncurrentVersionTransitionStorageClass := strings.ToUpper(params.Body.NoncurrentversionTransitionStorageClass) 198 opts.NoncurrentVersionTransitionDays = &noncurrentVersionTransitionDays 199 opts.NoncurrentVersionTransitionStorageClass = &noncurrentVersionTransitionStorageClass 200 } else if params.Body.TransitionDays > 0 { 201 tdays := strconv.Itoa(int(params.Body.TransitionDays)) 202 sclass := strings.ToUpper(params.Body.StorageClass) 203 opts.TransitionDays = &tdays 204 opts.StorageClass = &sclass 205 } 206 207 case models.AddBucketLifecycleTypeExpiry: 208 // Verify if expiry items are set 209 if params.Body.NoncurrentversionTransitionDays != 0 { 210 return errors.New("non current version Transition Days cannot be set when expiry is being configured") 211 } 212 213 if params.Body.NoncurrentversionTransitionStorageClass != "" { 214 return errors.New("non current version Transition Storage Class cannot be set when expiry is being configured") 215 } 216 217 status := !params.Body.Disable 218 opts = ilm.LifecycleOptions{ 219 ID: id, 220 Prefix: ¶ms.Body.Prefix, 221 Status: &status, 222 Tags: ¶ms.Body.Tags, 223 ExpiredObjectDeleteMarker: ¶ms.Body.ExpiredObjectDeleteMarker, 224 ExpiredObjectAllversions: ¶ms.Body.ExpiredObjectDeleteAll, 225 } 226 227 if params.Body.NewerNoncurrentversionExpirationVersions > 0 { 228 versions := int(params.Body.NewerNoncurrentversionExpirationVersions) 229 opts.NewerNoncurrentExpirationVersions = &versions 230 } 231 switch { 232 case params.Body.NoncurrentversionExpirationDays > 0: 233 days := int(params.Body.NoncurrentversionExpirationDays) 234 opts.NoncurrentVersionExpirationDays = &days 235 case params.Body.ExpiryDays > 0: 236 days := strconv.Itoa(int(params.Body.ExpiryDays)) 237 opts.ExpiryDays = &days 238 } 239 default: 240 // Non set, we return errors 241 return errors.New("no valid lifecycle configuration requested") 242 } 243 244 newRule, merr := opts.ToILMRule() 245 if merr != nil { 246 return merr.ToGoError() 247 } 248 249 lfcCfg.Rules = append(lfcCfg.Rules, newRule) 250 251 return client.setBucketLifecycle(ctx, params.BucketName, lfcCfg) 252 } 253 254 // getAddBucketLifecycleResponse returns the response of adding a bucket lifecycle response 255 func getAddBucketLifecycleResponse(session *models.Principal, params bucketApi.AddBucketLifecycleParams) *CodedAPIError { 256 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 257 defer cancel() 258 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 259 if err != nil { 260 return ErrorWithContext(ctx, err) 261 } 262 // create a minioClient interface implementation 263 // defining the client to be used 264 minioClient := minioClient{client: mClient} 265 266 err = addBucketLifecycle(ctx, minioClient, params) 267 if err != nil { 268 return ErrorWithContext(ctx, err) 269 } 270 271 return nil 272 } 273 274 // editBucketLifecycle gets lifecycle lists for a bucket from MinIO API and updates the selected lifecycle rule 275 func editBucketLifecycle(ctx context.Context, client MinioClient, params bucketApi.UpdateBucketLifecycleParams) error { 276 // Configuration that is already set. 277 lfcCfg, err := client.getLifecycleRules(ctx, params.BucketName) 278 if err != nil { 279 if e := err; minio.ToErrorResponse(e).Code == "NoSuchLifecycleConfiguration" { 280 lfcCfg = lifecycle.NewConfiguration() 281 } else { 282 return err 283 } 284 } 285 286 id := params.LifecycleID 287 288 opts := ilm.LifecycleOptions{} 289 290 // Verify if transition items are set 291 switch *params.Body.Type { 292 case models.UpdateBucketLifecycleTypeTransition: 293 if params.Body.TransitionDays == 0 && params.Body.NoncurrentversionTransitionDays == 0 { 294 return errors.New("you must select transition days or non-current transition days configuration") 295 } 296 297 status := !params.Body.Disable 298 opts = ilm.LifecycleOptions{ 299 ID: id, 300 Prefix: ¶ms.Body.Prefix, 301 Status: &status, 302 Tags: ¶ms.Body.Tags, 303 ExpiredObjectDeleteMarker: ¶ms.Body.ExpiredObjectDeleteMarker, 304 ExpiredObjectAllversions: ¶ms.Body.ExpiredObjectDeleteAll, 305 } 306 307 if params.Body.NoncurrentversionTransitionDays > 0 { 308 noncurrentVersionTransitionDays := int(params.Body.NoncurrentversionTransitionDays) 309 noncurrentVersionTransitionStorageClass := strings.ToUpper(params.Body.NoncurrentversionTransitionStorageClass) 310 opts.NoncurrentVersionTransitionDays = &noncurrentVersionTransitionDays 311 opts.NoncurrentVersionTransitionStorageClass = &noncurrentVersionTransitionStorageClass 312 313 } else { 314 tdays := strconv.Itoa(int(params.Body.TransitionDays)) 315 sclass := strings.ToUpper(params.Body.StorageClass) 316 opts.TransitionDays = &tdays 317 opts.StorageClass = &sclass 318 } 319 case models.UpdateBucketLifecycleTypeExpiry: // Verify if expiry configuration is set 320 if params.Body.NoncurrentversionTransitionDays != 0 { 321 return errors.New("non current version Transition Days cannot be set when expiry is being configured") 322 } 323 324 if params.Body.NoncurrentversionTransitionStorageClass != "" { 325 return errors.New("non current version Transition Storage Class cannot be set when expiry is being configured") 326 } 327 328 status := !params.Body.Disable 329 opts = ilm.LifecycleOptions{ 330 ID: id, 331 Prefix: ¶ms.Body.Prefix, 332 Status: &status, 333 Tags: ¶ms.Body.Tags, 334 ExpiredObjectDeleteMarker: ¶ms.Body.ExpiredObjectDeleteMarker, 335 ExpiredObjectAllversions: ¶ms.Body.ExpiredObjectDeleteAll, 336 } 337 338 if params.Body.NoncurrentversionExpirationDays > 0 { 339 days := int(params.Body.NoncurrentversionExpirationDays) 340 opts.NoncurrentVersionExpirationDays = &days 341 } else { 342 days := strconv.Itoa(int(params.Body.ExpiryDays)) 343 opts.ExpiryDays = &days 344 } 345 default: 346 // Non set, we return errors 347 return errors.New("no valid configuration requested") 348 } 349 350 var rule *lifecycle.Rule 351 for i := range lfcCfg.Rules { 352 if lfcCfg.Rules[i].ID == opts.ID { 353 rule = &lfcCfg.Rules[i] 354 break 355 } 356 } 357 if rule == nil { 358 return errors.New("unable to find the matching rule to update") 359 } 360 361 err2 := ilm.ApplyRuleFields(rule, opts) 362 if err2.ToGoError() != nil { 363 return fmt.Errorf("Unable to generate new lifecycle rule: %v", err2.ToGoError()) 364 } 365 366 return client.setBucketLifecycle(ctx, params.BucketName, lfcCfg) 367 } 368 369 // getEditBucketLifecycleRule returns the response of bucket lifecycle tier edit 370 func getEditBucketLifecycleRule(session *models.Principal, params bucketApi.UpdateBucketLifecycleParams) *CodedAPIError { 371 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 372 defer cancel() 373 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 374 if err != nil { 375 return ErrorWithContext(ctx, err) 376 } 377 // create a minioClient interface implementation 378 // defining the client to be used 379 minioClient := minioClient{client: mClient} 380 381 err = editBucketLifecycle(ctx, minioClient, params) 382 if err != nil { 383 return ErrorWithContext(ctx, err) 384 } 385 386 return nil 387 } 388 389 // deleteBucketLifecycle deletes lifecycle rule by passing an empty rule to a selected ID 390 func deleteBucketLifecycle(ctx context.Context, client MinioClient, params bucketApi.DeleteBucketLifecycleRuleParams) error { 391 // Configuration that is already set. 392 lfcCfg, err := client.getLifecycleRules(ctx, params.BucketName) 393 if err != nil { 394 if e := err; minio.ToErrorResponse(e).Code == "NoSuchLifecycleConfiguration" { 395 lfcCfg = lifecycle.NewConfiguration() 396 } else { 397 return err 398 } 399 } 400 401 if len(lfcCfg.Rules) == 0 { 402 return errors.New("no rules available to delete") 403 } 404 405 var newRules []lifecycle.Rule 406 407 for _, rule := range lfcCfg.Rules { 408 if rule.ID != params.LifecycleID { 409 newRules = append(newRules, rule) 410 } 411 } 412 413 if len(newRules) == len(lfcCfg.Rules) && len(lfcCfg.Rules) > 0 { 414 // rule doesn't exist 415 return fmt.Errorf("lifecycle rule for id '%s' doesn't exist", params.LifecycleID) 416 } 417 418 lfcCfg.Rules = newRules 419 420 return client.setBucketLifecycle(ctx, params.BucketName, lfcCfg) 421 } 422 423 // getDeleteBucketLifecycleRule returns the response of bucket lifecycle tier delete 424 func getDeleteBucketLifecycleRule(session *models.Principal, params bucketApi.DeleteBucketLifecycleRuleParams) *CodedAPIError { 425 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 426 defer cancel() 427 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 428 if err != nil { 429 return ErrorWithContext(ctx, err) 430 } 431 // create a minioClient interface implementation 432 // defining the client to be used 433 minioClient := minioClient{client: mClient} 434 435 err = deleteBucketLifecycle(ctx, minioClient, params) 436 if err != nil { 437 return ErrorWithContext(ctx, err) 438 } 439 440 return nil 441 } 442 443 // addMultiBucketLifecycle creates multibuckets lifecycle assignments 444 func addMultiBucketLifecycle(ctx context.Context, client MinioClient, params bucketApi.AddMultiBucketLifecycleParams) []MultiLifecycleResult { 445 bucketsRelation := params.Body.Buckets 446 447 // Parallel Lifecycle rules set 448 449 parallelLifecycleBucket := func(bucketName string) chan MultiLifecycleResult { 450 remoteProc := make(chan MultiLifecycleResult) 451 452 lifecycleParams := models.AddBucketLifecycle{ 453 Type: *params.Body.Type, 454 StorageClass: params.Body.StorageClass, 455 TransitionDays: params.Body.TransitionDays, 456 Prefix: params.Body.Prefix, 457 NoncurrentversionTransitionDays: params.Body.NoncurrentversionTransitionDays, 458 NoncurrentversionTransitionStorageClass: params.Body.NoncurrentversionTransitionStorageClass, 459 NoncurrentversionExpirationDays: params.Body.NoncurrentversionExpirationDays, 460 Tags: params.Body.Tags, 461 ExpiryDays: params.Body.ExpiryDays, 462 Disable: false, 463 ExpiredObjectDeleteMarker: params.Body.ExpiredObjectDeleteMarker, 464 ExpiredObjectDeleteAll: params.Body.ExpiredObjectDeleteMarker, 465 } 466 467 go func() { 468 defer close(remoteProc) 469 470 lifecycleParams := bucketApi.AddBucketLifecycleParams{ 471 BucketName: bucketName, 472 Body: &lifecycleParams, 473 } 474 475 // We add lifecycle rule & expect a response 476 err := addBucketLifecycle(ctx, client, lifecycleParams) 477 478 errorReturn := "" 479 480 if err != nil { 481 errorReturn = err.Error() 482 } 483 484 retParams := MultiLifecycleResult{ 485 BucketName: bucketName, 486 Error: errorReturn, 487 } 488 489 remoteProc <- retParams 490 }() 491 return remoteProc 492 } 493 494 var lifecycleManagement []chan MultiLifecycleResult 495 496 for _, bucketName := range bucketsRelation { 497 rBucket := parallelLifecycleBucket(bucketName) 498 lifecycleManagement = append(lifecycleManagement, rBucket) 499 } 500 501 var resultsList []MultiLifecycleResult 502 for _, result := range lifecycleManagement { 503 res := <-result 504 resultsList = append(resultsList, res) 505 } 506 507 return resultsList 508 } 509 510 // getAddMultiBucketLifecycleResponse returns the response of multibucket lifecycle assignment 511 func getAddMultiBucketLifecycleResponse(session *models.Principal, params bucketApi.AddMultiBucketLifecycleParams) (*models.MultiLifecycleResult, *CodedAPIError) { 512 ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) 513 defer cancel() 514 mClient, err := newMinioClient(session, getClientIP(params.HTTPRequest)) 515 if err != nil { 516 return nil, ErrorWithContext(ctx, err) 517 } 518 // create a minioClient interface implementation 519 // defining the client to be used 520 minioClient := minioClient{client: mClient} 521 522 multiCycleResult := addMultiBucketLifecycle(ctx, minioClient, params) 523 524 var returnList []*models.MulticycleResultItem 525 526 for _, resultItem := range multiCycleResult { 527 multicycleRS := models.MulticycleResultItem{ 528 BucketName: resultItem.BucketName, 529 Error: resultItem.Error, 530 } 531 532 returnList = append(returnList, &multicycleRS) 533 } 534 535 finalResult := models.MultiLifecycleResult{Results: returnList} 536 537 return &finalResult, nil 538 }