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  }