github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/ilm/options.go (about) 1 // Copyright (c) 2015-2022 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 ilm 19 20 import ( 21 "errors" 22 "fmt" 23 "math" 24 "strconv" 25 "strings" 26 27 "github.com/dustin/go-humanize" 28 "github.com/minio/cli" 29 "github.com/minio/mc/pkg/probe" 30 "github.com/minio/minio-go/v7/pkg/lifecycle" 31 "github.com/rs/xid" 32 ) 33 34 const defaultILMDateFormat string = "2006-01-02" 35 36 // RemoveILMRule - Remove the ILM rule (with ilmID) from the configuration in XML that is provided. 37 func RemoveILMRule(lfcCfg *lifecycle.Configuration, ilmID string) (*lifecycle.Configuration, *probe.Error) { 38 if lfcCfg == nil { 39 return lfcCfg, probe.NewError(fmt.Errorf("lifecycle configuration not set")) 40 } 41 if len(lfcCfg.Rules) == 0 { 42 return lfcCfg, probe.NewError(fmt.Errorf("lifecycle configuration not set")) 43 } 44 n := 0 45 for _, rule := range lfcCfg.Rules { 46 if rule.ID != ilmID { 47 lfcCfg.Rules[n] = rule 48 n++ 49 } 50 } 51 if n == len(lfcCfg.Rules) && len(lfcCfg.Rules) > 0 { 52 // if there was no filtering then rules will be of same length, means we didn't find 53 // our ilm id return an error here. 54 return lfcCfg, probe.NewError(fmt.Errorf("lifecycle rule for id '%s' not found", ilmID)) 55 } 56 lfcCfg.Rules = lfcCfg.Rules[:n] 57 return lfcCfg, nil 58 } 59 60 // LifecycleOptions is structure to encapsulate 61 type LifecycleOptions struct { 62 ID string 63 64 Status *bool 65 66 Prefix *string 67 Tags *string 68 ObjectSizeLessThan *int64 69 ObjectSizeGreaterThan *int64 70 ExpiryDate *string 71 ExpiryDays *string 72 TransitionDate *string 73 TransitionDays *string 74 StorageClass *string 75 76 ExpiredObjectDeleteMarker *bool 77 NoncurrentVersionExpirationDays *int 78 NewerNoncurrentExpirationVersions *int 79 NoncurrentVersionTransitionDays *int 80 NewerNoncurrentTransitionVersions *int 81 NoncurrentVersionTransitionStorageClass *string 82 ExpiredObjectAllversions *bool 83 } 84 85 // Filter returns lifecycle.Filter appropriate for opts 86 func (opts LifecycleOptions) Filter() lifecycle.Filter { 87 var f lifecycle.Filter 88 var tags []lifecycle.Tag 89 var predCount int 90 if opts.Tags != nil { 91 tags = extractILMTags(*opts.Tags) 92 predCount += len(tags) 93 } 94 var prefix string 95 if opts.Prefix != nil { 96 prefix = *opts.Prefix 97 predCount++ 98 } 99 100 var szLt, szGt int64 101 if opts.ObjectSizeLessThan != nil { 102 szLt = *opts.ObjectSizeLessThan 103 predCount++ 104 } 105 106 if opts.ObjectSizeGreaterThan != nil { 107 szGt = *opts.ObjectSizeGreaterThan 108 predCount++ 109 } 110 111 if predCount >= 2 { 112 f.And = lifecycle.And{ 113 Tags: tags, 114 Prefix: prefix, 115 ObjectSizeLessThan: szLt, 116 ObjectSizeGreaterThan: szGt, 117 } 118 } else { 119 // In a valid lifecycle rule filter at most one of the 120 // following will only be set. 121 f.Prefix = prefix 122 f.ObjectSizeGreaterThan = szGt 123 f.ObjectSizeLessThan = szLt 124 if len(tags) >= 1 { 125 f.Tag = tags[0] 126 } 127 } 128 129 return f 130 } 131 132 // ToILMRule creates lifecycle.Configuration based on LifecycleOptions 133 func (opts LifecycleOptions) ToILMRule() (lifecycle.Rule, *probe.Error) { 134 var ( 135 id, status string 136 137 nonCurrentVersionExpirationDays lifecycle.ExpirationDays 138 newerNonCurrentExpirationVersions int 139 nonCurrentVersionTransitionDays lifecycle.ExpirationDays 140 newerNonCurrentTransitionVersions int 141 nonCurrentVersionTransitionStorageClass string 142 ) 143 144 id = opts.ID 145 status = func() string { 146 if opts.Status != nil && !*opts.Status { 147 return "Disabled" 148 } 149 // Generating a new ILM rule without explicit status is enabled 150 return "Enabled" 151 }() 152 153 expiry, err := parseExpiry(opts.ExpiryDate, opts.ExpiryDays, opts.ExpiredObjectDeleteMarker, opts.ExpiredObjectAllversions) 154 if err != nil { 155 return lifecycle.Rule{}, err 156 } 157 158 transition, err := parseTransition(opts.StorageClass, opts.TransitionDate, opts.TransitionDays) 159 if err != nil { 160 return lifecycle.Rule{}, err 161 } 162 163 if opts.NoncurrentVersionExpirationDays != nil { 164 nonCurrentVersionExpirationDays = lifecycle.ExpirationDays(*opts.NoncurrentVersionExpirationDays) 165 } 166 if opts.NewerNoncurrentExpirationVersions != nil { 167 newerNonCurrentExpirationVersions = *opts.NewerNoncurrentExpirationVersions 168 } 169 if opts.NoncurrentVersionTransitionDays != nil { 170 nonCurrentVersionTransitionDays = lifecycle.ExpirationDays(*opts.NoncurrentVersionTransitionDays) 171 } 172 if opts.NewerNoncurrentTransitionVersions != nil { 173 newerNonCurrentTransitionVersions = *opts.NewerNoncurrentTransitionVersions 174 } 175 if opts.NoncurrentVersionTransitionStorageClass != nil { 176 nonCurrentVersionTransitionStorageClass = *opts.NoncurrentVersionTransitionStorageClass 177 } 178 179 newRule := lifecycle.Rule{ 180 ID: id, 181 RuleFilter: opts.Filter(), 182 Status: status, 183 Expiration: expiry, 184 Transition: transition, 185 NoncurrentVersionExpiration: lifecycle.NoncurrentVersionExpiration{ 186 NoncurrentDays: nonCurrentVersionExpirationDays, 187 NewerNoncurrentVersions: newerNonCurrentExpirationVersions, 188 }, 189 NoncurrentVersionTransition: lifecycle.NoncurrentVersionTransition{ 190 NoncurrentDays: nonCurrentVersionTransitionDays, 191 NewerNoncurrentVersions: newerNonCurrentTransitionVersions, 192 StorageClass: nonCurrentVersionTransitionStorageClass, 193 }, 194 } 195 196 if err := validateILMRule(newRule); err != nil { 197 return lifecycle.Rule{}, err 198 } 199 200 return newRule, nil 201 } 202 203 func strPtr(s string) *string { 204 ptr := s 205 return &ptr 206 } 207 208 func intPtr(i int) *int { 209 ptr := i 210 return &ptr 211 } 212 213 func int64Ptr(i int64) *int64 { 214 return &i 215 } 216 217 func boolPtr(b bool) *bool { 218 ptr := b 219 return &ptr 220 } 221 222 // GetLifecycleOptions create LifeCycleOptions based on cli inputs 223 func GetLifecycleOptions(ctx *cli.Context) (LifecycleOptions, *probe.Error) { 224 var ( 225 id string 226 227 status *bool 228 229 prefix *string 230 tags *string 231 sizeLt *int64 232 sizeGt *int64 233 expiryDate *string 234 expiryDays *string 235 transitionDate *string 236 transitionDays *string 237 tier *string 238 239 expiredObjectDeleteMarker *bool 240 noncurrentVersionExpirationDays *int 241 newerNoncurrentExpirationVersions *int 242 noncurrentVersionTransitionDays *int 243 newerNoncurrentTransitionVersions *int 244 noncurrentTier *string 245 expiredObjectAllversions *bool 246 ) 247 248 id = ctx.String("id") 249 if id == "" { 250 id = xid.New().String() 251 } 252 253 switch { 254 case ctx.IsSet("disable"): 255 status = boolPtr(!ctx.Bool("disable")) 256 case ctx.IsSet("enable"): 257 status = boolPtr(ctx.Bool("enable")) 258 } 259 260 if ctx.IsSet("prefix") { 261 prefix = strPtr(ctx.String("prefix")) 262 } else { 263 // Calculating the prefix for the aliased URL is deprecated in Aug 2022 264 // split the first arg i.e. path into alias, bucket and prefix 265 result := strings.SplitN(ctx.Args().First(), "/", 3) 266 // get the prefix from path 267 if len(result) > 2 { 268 p := result[len(result)-1] 269 if len(p) > 0 { 270 prefix = &p 271 } 272 } 273 } 274 275 if ctx.IsSet("size-lt") { 276 szStr := ctx.String("size-lt") 277 szLt, err := humanize.ParseBytes(szStr) 278 if err != nil || szLt > math.MaxInt64 { 279 return LifecycleOptions{}, probe.NewError(fmt.Errorf("size-lt value %s is invalid", szStr)) 280 } 281 282 sizeLt = int64Ptr(int64(szLt)) 283 } 284 if ctx.IsSet("size-gt") { 285 szStr := ctx.String("size-gt") 286 szGt, err := humanize.ParseBytes(szStr) 287 if err != nil || szGt > math.MaxInt64 { 288 return LifecycleOptions{}, probe.NewError(fmt.Errorf("size-gt value %s is invalid", szStr)) 289 } 290 sizeGt = int64Ptr(int64(szGt)) 291 } 292 293 // For backward-compatibility 294 if ctx.IsSet("storage-class") { 295 tier = strPtr(strings.ToUpper(ctx.String("storage-class"))) 296 } 297 if ctx.IsSet("noncurrentversion-transition-storage-class") { 298 noncurrentTier = strPtr(strings.ToUpper(ctx.String("noncurrentversion-transition-storage-class"))) 299 } 300 if ctx.IsSet("tier") { 301 tier = strPtr(strings.ToUpper(ctx.String("tier"))) 302 } 303 if f := "transition-tier"; ctx.IsSet(f) { 304 tier = strPtr(strings.ToUpper(ctx.String(f))) 305 } 306 if ctx.IsSet("noncurrentversion-tier") { 307 noncurrentTier = strPtr(strings.ToUpper(ctx.String("noncurrentversion-tier"))) 308 } 309 if f := "noncurrent-transition-tier"; ctx.IsSet(f) { 310 noncurrentTier = strPtr(strings.ToUpper(ctx.String(f))) 311 } 312 if tier != nil && !ctx.IsSet("transition-days") && !ctx.IsSet("transition-date") { 313 return LifecycleOptions{}, probe.NewError(errors.New("transition-date or transition-days must be set")) 314 } 315 if noncurrentTier != nil && !ctx.IsSet("noncurrentversion-transition-days") && !ctx.IsSet("noncurrent-transition-days") { 316 return LifecycleOptions{}, probe.NewError(errors.New("noncurrentversion-transition-days must be set")) 317 } 318 // for MinIO transition storage-class is same as label defined on 319 // `mc admin bucket remote add --service ilm --label` command 320 if ctx.IsSet("tags") { 321 tags = strPtr(ctx.String("tags")) 322 } 323 if ctx.IsSet("expiry-date") { 324 expiryDate = strPtr(ctx.String("expiry-date")) 325 } 326 if ctx.IsSet("expiry-days") { 327 expiryDays = strPtr(ctx.String("expiry-days")) 328 } 329 if f := "expire-days"; ctx.IsSet(f) { 330 expiryDays = strPtr(ctx.String(f)) 331 } 332 if ctx.IsSet("transition-date") { 333 transitionDate = strPtr(ctx.String("transition-date")) 334 } 335 if ctx.IsSet("transition-days") { 336 transitionDays = strPtr(ctx.String("transition-days")) 337 } 338 if ctx.IsSet("expired-object-delete-marker") { 339 expiredObjectDeleteMarker = boolPtr(ctx.Bool("expired-object-delete-marker")) 340 } 341 if f := "expire-delete-marker"; ctx.IsSet(f) { 342 expiredObjectDeleteMarker = boolPtr(ctx.Bool(f)) 343 } 344 if ctx.IsSet("noncurrentversion-expiration-days") { 345 noncurrentVersionExpirationDays = intPtr(ctx.Int("noncurrentversion-expiration-days")) 346 } 347 if f := "noncurrent-expire-days"; ctx.IsSet(f) { 348 ndaysStr := ctx.String(f) 349 ndays, err := strconv.Atoi(ndaysStr) 350 if err != nil { 351 return LifecycleOptions{}, probe.NewError(fmt.Errorf("failed to parse %s: %v", f, err)) 352 } 353 noncurrentVersionExpirationDays = &ndays 354 } 355 if ctx.IsSet("newer-noncurrentversions-expiration") { 356 newerNoncurrentExpirationVersions = intPtr(ctx.Int("newer-noncurrentversions-expiration")) 357 } 358 if f := "noncurrent-expire-newer"; ctx.IsSet(f) { 359 newerNoncurrentExpirationVersions = intPtr(ctx.Int(f)) 360 } 361 if ctx.IsSet("noncurrentversion-transition-days") { 362 noncurrentVersionTransitionDays = intPtr(ctx.Int("noncurrentversion-transition-days")) 363 } 364 if f := "noncurrent-transition-days"; ctx.IsSet(f) { 365 noncurrentVersionTransitionDays = intPtr(ctx.Int(f)) 366 } 367 if ctx.IsSet("newer-noncurrentversions-transition") { 368 newerNoncurrentTransitionVersions = intPtr(ctx.Int("newer-noncurrentversions-transition")) 369 } 370 if f := "noncurrent-transition-newer"; ctx.IsSet(f) { 371 newerNoncurrentTransitionVersions = intPtr(ctx.Int(f)) 372 } 373 if ctx.IsSet("expire-all-object-versions") { 374 expiredObjectAllversions = boolPtr(ctx.Bool("expire-all-object-versions")) 375 } 376 377 return LifecycleOptions{ 378 ID: id, 379 Status: status, 380 Prefix: prefix, 381 Tags: tags, 382 ObjectSizeLessThan: sizeLt, 383 ObjectSizeGreaterThan: sizeGt, 384 ExpiryDate: expiryDate, 385 ExpiryDays: expiryDays, 386 TransitionDate: transitionDate, 387 TransitionDays: transitionDays, 388 StorageClass: tier, 389 ExpiredObjectDeleteMarker: expiredObjectDeleteMarker, 390 NoncurrentVersionExpirationDays: noncurrentVersionExpirationDays, 391 NewerNoncurrentExpirationVersions: newerNoncurrentExpirationVersions, 392 NoncurrentVersionTransitionDays: noncurrentVersionTransitionDays, 393 NewerNoncurrentTransitionVersions: newerNoncurrentTransitionVersions, 394 NoncurrentVersionTransitionStorageClass: noncurrentTier, 395 ExpiredObjectAllversions: expiredObjectAllversions, 396 }, nil 397 } 398 399 // ApplyRuleFields applies non nil fields of LifcycleOptions to the existing lifecycle rule 400 func ApplyRuleFields(dest *lifecycle.Rule, opts LifecycleOptions) *probe.Error { 401 // If src has tags, it should override the destination 402 if opts.Tags != nil { 403 dest.RuleFilter.And.Tags = extractILMTags(*opts.Tags) 404 } 405 406 // since prefix is a part of command args, it is always present in the src rule and 407 // it should be always set to the destination. 408 if opts.Prefix != nil { 409 if dest.RuleFilter.And.Tags != nil { 410 dest.RuleFilter.And.Prefix = *opts.Prefix 411 } else { 412 dest.RuleFilter.Prefix = *opts.Prefix 413 } 414 } 415 416 // only one of expiration day, date or transition day, date is expected 417 if opts.ExpiryDate != nil { 418 date, err := parseExpiryDate(*opts.ExpiryDate) 419 if err != nil { 420 return err 421 } 422 dest.Expiration.Date = date 423 // reset everything else 424 dest.Expiration.Days = 0 425 dest.Expiration.DeleteMarker = false 426 } else if opts.ExpiryDays != nil { 427 days, err := parseExpiryDays(*opts.ExpiryDays) 428 if err != nil { 429 return err 430 } 431 dest.Expiration.Days = days 432 // reset everything else 433 dest.Expiration.Date = lifecycle.ExpirationDate{} 434 } else if opts.ExpiredObjectDeleteMarker != nil { 435 dest.Expiration.DeleteMarker = lifecycle.ExpireDeleteMarker(*opts.ExpiredObjectDeleteMarker) 436 dest.Expiration.Days = 0 437 dest.Expiration.Date = lifecycle.ExpirationDate{} 438 } else if opts.ExpiredObjectAllversions != nil { 439 dest.Expiration.DeleteAll = lifecycle.ExpirationBoolean(*opts.ExpiredObjectAllversions) 440 } 441 442 if opts.TransitionDate != nil { 443 date, err := parseTransitionDate(*opts.TransitionDate) 444 if err != nil { 445 return err 446 } 447 dest.Transition.Date = date 448 // reset everything else 449 dest.Transition.Days = 0 450 } else if opts.TransitionDays != nil { 451 days, err := parseTransitionDays(*opts.TransitionDays) 452 if err != nil { 453 return err 454 } 455 dest.Transition.Days = days 456 // reset everything else 457 dest.Transition.Date = lifecycle.ExpirationDate{} 458 } 459 460 if opts.NoncurrentVersionExpirationDays != nil { 461 dest.NoncurrentVersionExpiration.NoncurrentDays = lifecycle.ExpirationDays(*opts.NoncurrentVersionExpirationDays) 462 } 463 464 if opts.NewerNoncurrentExpirationVersions != nil { 465 dest.NoncurrentVersionExpiration.NewerNoncurrentVersions = *opts.NewerNoncurrentExpirationVersions 466 } 467 468 if opts.NoncurrentVersionTransitionDays != nil { 469 dest.NoncurrentVersionTransition.NoncurrentDays = lifecycle.ExpirationDays(*opts.NoncurrentVersionTransitionDays) 470 } 471 472 if opts.NewerNoncurrentTransitionVersions != nil { 473 dest.NoncurrentVersionTransition.NewerNoncurrentVersions = *opts.NewerNoncurrentTransitionVersions 474 } 475 476 if opts.NoncurrentVersionTransitionStorageClass != nil { 477 dest.NoncurrentVersionTransition.StorageClass = *opts.NoncurrentVersionTransitionStorageClass 478 } 479 480 if opts.StorageClass != nil { 481 dest.Transition.StorageClass = *opts.StorageClass 482 } 483 484 // Updated the status 485 if opts.Status != nil { 486 dest.Status = func() string { 487 if *opts.Status { 488 return "Enabled" 489 } 490 return "Disabled" 491 }() 492 } 493 494 return nil 495 }