github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/retention-info.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 cmd
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/fatih/color"
    27  	"github.com/minio/cli"
    28  	json "github.com/minio/colorjson"
    29  	"github.com/minio/mc/pkg/probe"
    30  	"github.com/minio/minio-go/v7"
    31  	"github.com/minio/pkg/v2/console"
    32  )
    33  
    34  var retentionInfoFlags = []cli.Flag{
    35  	cli.BoolFlag{
    36  		Name:  "recursive, r",
    37  		Usage: "show retention info recursively",
    38  	},
    39  	cli.StringFlag{
    40  		Name:  "version-id, vid",
    41  		Usage: "show retention info of specific object version",
    42  	},
    43  	cli.StringFlag{
    44  		Name:  "rewind",
    45  		Usage: "roll back object(s) to current version at specified time",
    46  	},
    47  	cli.BoolFlag{
    48  		Name:  "versions",
    49  		Usage: "show retention info on object(s) and all its versions",
    50  	},
    51  	cli.BoolFlag{
    52  		Name:  "default",
    53  		Usage: "show bucket default retention mode",
    54  	},
    55  }
    56  
    57  var retentionInfoCmd = cli.Command{
    58  	Name:         "info",
    59  	Usage:        "show retention settings on object(s)",
    60  	Action:       mainRetentionInfo,
    61  	OnUsageError: onUsageError,
    62  	Before:       setGlobalsFromContext,
    63  	Flags:        append(retentionInfoFlags, globalFlags...),
    64  	CustomHelpTemplate: `NAME:
    65    {{.HelpName}} - {{.Usage}}
    66  
    67  USAGE:
    68    {{.HelpName}} [FLAGS] [governance | compliance] VALIDITY TARGET
    69  
    70  FLAGS:
    71    {{range .VisibleFlags}}{{.}}
    72    {{end}}
    73  
    74  EXAMPLES:
    75    1. Show object retention for a specific object
    76       $ {{.HelpName}} myminio/mybucket/prefix/obj.csv
    77  
    78    2. Show object retention for recursively for all objects at a given prefix
    79       $ {{.HelpName}} myminio/mybucket/prefix --recursive
    80  
    81    3. Show object retention to a specific version of a specific object
    82       $ {{.HelpName}} myminio/mybucket/prefix/obj.csv --version-id "3Jr2x6fqlBUsVzbvPihBO3HgNpgZgAnp"
    83  
    84    4. Show object retention for recursively for all versions of all objects under prefix
    85       $ {{.HelpName}} myminio/mybucket/prefix --recursive --versions
    86  
    87    5. Show default lock retention configuration for a bucket
    88       $ {{.HelpName}} myminio/mybucket/ --default
    89  `,
    90  }
    91  
    92  func parseInfoRetentionArgs(cliCtx *cli.Context) (target, versionID string, recursive bool, timeRef time.Time, withVersions, defaultMode bool) {
    93  	args := cliCtx.Args()
    94  
    95  	if len(args) != 1 {
    96  		showCommandHelpAndExit(cliCtx, 1)
    97  	}
    98  
    99  	target = args[0]
   100  	if target == "" {
   101  		fatalIf(errInvalidArgument().Trace(), "invalid target url '%v'", target)
   102  	}
   103  
   104  	versionID = cliCtx.String("version-id")
   105  	timeRef = parseRewindFlag(cliCtx.String("rewind"))
   106  	withVersions = cliCtx.Bool("versions")
   107  	recursive = cliCtx.Bool("recursive")
   108  	defaultMode = cliCtx.Bool("default")
   109  
   110  	if defaultMode && (versionID != "" || !timeRef.IsZero() || withVersions || recursive) {
   111  		fatalIf(errDummy(), "--default flag cannot be specified with any of --version-id, --rewind, --versions, --recursive.")
   112  	}
   113  
   114  	return
   115  }
   116  
   117  // Structured message depending on the type of console.
   118  type retentionInfoMessage struct {
   119  	Mode      minio.RetentionMode `json:"mode"`
   120  	Until     time.Time           `json:"until"`
   121  	URLPath   string              `json:"urlpath"`
   122  	VersionID string              `json:"versionID"`
   123  	Status    string              `json:"status"`
   124  	Err       error               `json:"error"`
   125  }
   126  
   127  type retentionInfoMessageList retentionInfoMessage
   128  
   129  func (m *retentionInfoMessageList) SetErr(e error) {
   130  	m.Err = e
   131  }
   132  
   133  func (m *retentionInfoMessageList) SetStatus(status string) {
   134  	m.Status = status
   135  }
   136  
   137  func (m *retentionInfoMessageList) SetMode(mode minio.RetentionMode) {
   138  	m.Mode = mode
   139  }
   140  
   141  func (m *retentionInfoMessageList) SetUntil(until time.Time) {
   142  	m.Until = until
   143  }
   144  
   145  // Colorized message for console printing.
   146  func (m retentionInfoMessageList) String() string {
   147  	if m.Err != nil {
   148  		return console.Colorize("RetentionFailure", fmt.Sprintf("Unable to get get object retention on `%s`: %s", m.URLPath, m.Err))
   149  	}
   150  
   151  	var msg string
   152  	var retentionField string
   153  
   154  	if m.Mode == "" {
   155  		retentionField += console.Colorize("RetentionNotFound", "NO RETENTION")
   156  	} else {
   157  		exp := ""
   158  		if m.Mode == minio.Governance {
   159  			now := time.Now()
   160  			if now.After(m.Until) {
   161  				exp = "EXPIRED"
   162  			}
   163  		}
   164  		retentionField += console.Colorize("RetentionSuccess", m.Mode.String()) + " " + console.Colorize("RetentionExpired", exp)
   165  	}
   166  
   167  	msg += "[ " + centerText(retentionField, 18) + " ]  "
   168  
   169  	if m.VersionID != "" {
   170  		msg += console.Colorize("RetentionVersionID", m.VersionID+"  ")
   171  	}
   172  
   173  	msg += m.URLPath
   174  	return msg
   175  }
   176  
   177  // JSON'ified message for scripting.
   178  func (m retentionInfoMessageList) JSON() string {
   179  	if m.Err != nil {
   180  		m.Status = "failure"
   181  	}
   182  	msgBytes, e := json.MarshalIndent(m, "", " ")
   183  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   184  	return string(msgBytes)
   185  }
   186  
   187  type retentionInfoMessageRecord retentionInfoMessage
   188  
   189  func (m *retentionInfoMessageRecord) SetErr(e error) {
   190  	m.Err = e
   191  }
   192  
   193  func (m *retentionInfoMessageRecord) SetStatus(status string) {
   194  	m.Status = status
   195  }
   196  
   197  func (m *retentionInfoMessageRecord) SetMode(mode minio.RetentionMode) {
   198  	m.Mode = mode
   199  }
   200  
   201  func (m *retentionInfoMessageRecord) SetUntil(until time.Time) {
   202  	m.Until = until
   203  }
   204  
   205  // Colorized message for console printing.
   206  func (m retentionInfoMessageRecord) String() string {
   207  	if m.Err != nil {
   208  		return console.Colorize("RetentionFailure", fmt.Sprintf("Unable to get object retention on `%s`: %s", m.URLPath, m.Err))
   209  	}
   210  
   211  	var msg strings.Builder
   212  	fmt.Fprintf(&msg, "Name    : %s\n", console.Colorize("RetentionSuccess", m.URLPath))
   213  
   214  	if m.VersionID != "" {
   215  		fmt.Fprintf(&msg, "Version : %s\n", console.Colorize("RetentionSuccess", m.VersionID))
   216  	}
   217  
   218  	fmt.Fprintf(&msg, "Mode    : ")
   219  	if m.Mode == "" {
   220  		fmt.Fprint(&msg, console.Colorize("RetentionNotFound", "NO RETENTION"))
   221  	} else {
   222  		fmt.Fprint(&msg, console.Colorize("RetentionSuccess", m.Mode))
   223  		if !m.Until.IsZero() {
   224  			msg.WriteString(", ")
   225  			exp := ""
   226  			now := time.Now()
   227  			if now.After(m.Until) {
   228  				prettyDuration := timeDurationToHumanizedDuration(now.Sub(m.Until)).StringShort()
   229  				exp = console.Colorize("RetentionExpired", "expired "+prettyDuration+" ago")
   230  			} else {
   231  				prettyDuration := timeDurationToHumanizedDuration(m.Until.Sub(now)).StringShort()
   232  				exp = console.Colorize("RetentionSuccess", "expiring in "+prettyDuration)
   233  			}
   234  			fmt.Fprint(&msg, exp)
   235  		}
   236  	}
   237  	fmt.Fprint(&msg, "\n")
   238  	return msg.String()
   239  }
   240  
   241  // JSON'ified message for scripting.
   242  func (m retentionInfoMessageRecord) JSON() string {
   243  	if m.Err != nil {
   244  		m.Status = "failure"
   245  	}
   246  	msgBytes, e := json.MarshalIndent(m, "", " ")
   247  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   248  	return string(msgBytes)
   249  }
   250  
   251  type retentionInfoMsg interface {
   252  	message
   253  	SetErr(error)
   254  	SetStatus(string)
   255  	SetMode(minio.RetentionMode)
   256  	SetUntil(time.Time)
   257  }
   258  
   259  // Show retention info for a single object or version
   260  func infoRetentionSingle(ctx context.Context, alias, url, versionID string, listStyle bool) *probe.Error {
   261  	newClnt, err := newClientFromAlias(alias, url)
   262  	if err != nil {
   263  		return err
   264  	}
   265  
   266  	var msg retentionInfoMsg
   267  
   268  	if listStyle {
   269  		msg = &retentionInfoMessageList{
   270  			URLPath:   urlJoinPath(alias, url),
   271  			VersionID: versionID,
   272  		}
   273  	} else {
   274  		msg = &retentionInfoMessageRecord{
   275  			URLPath:   urlJoinPath(alias, url),
   276  			VersionID: versionID,
   277  		}
   278  	}
   279  
   280  	mode, until, err := newClnt.GetObjectRetention(ctx, versionID)
   281  	if err != nil {
   282  		errResp := minio.ToErrorResponse(err.ToGoError())
   283  		if errResp.Code != "NoSuchObjectLockConfiguration" {
   284  			if _, ok := err.ToGoError().(ObjectNameEmpty); !ok {
   285  				msg.SetErr(err.ToGoError())
   286  				msg.SetStatus("failure")
   287  				printMsg(msg)
   288  			}
   289  			return err
   290  		}
   291  		err = nil
   292  	}
   293  
   294  	msg.SetStatus("success")
   295  	msg.SetMode(mode)
   296  	msg.SetUntil(until)
   297  
   298  	printMsg(msg)
   299  	return err
   300  }
   301  
   302  // Get Retention for one object/version or many objects within a given prefix.
   303  func getRetention(ctx context.Context, target, versionID string, timeRef time.Time, withOlderVersions, isRecursive bool) error {
   304  	clnt, err := newClient(target)
   305  	if err != nil {
   306  		fatalIf(err.Trace(), "Unable to parse the provided url.")
   307  	}
   308  
   309  	// Quit early if urlStr does not point to an S3 server
   310  	switch clnt.(type) {
   311  	case *S3Client:
   312  	default:
   313  		fatal(errDummy().Trace(), "Retention is supported only for S3 servers.")
   314  	}
   315  
   316  	alias, urlStr, _ := mustExpandAlias(target)
   317  	if versionID != "" || !isRecursive && !withOlderVersions {
   318  		err := infoRetentionSingle(ctx, alias, urlStr, versionID, false)
   319  		if err != nil {
   320  			if _, ok := err.ToGoError().(ObjectNameEmpty); ok {
   321  				return showBucketLock(target)
   322  			}
   323  			return exitStatus(globalErrorExitStatus)
   324  		}
   325  		return nil
   326  	}
   327  
   328  	lstOptions := ListOptions{Recursive: isRecursive, ShowDir: DirNone}
   329  	if !timeRef.IsZero() {
   330  		lstOptions.WithOlderVersions = withOlderVersions
   331  		lstOptions.WithDeleteMarkers = true
   332  		lstOptions.TimeRef = timeRef
   333  	}
   334  
   335  	var cErr error
   336  	var atLeastOneObjectOrVersionFound bool
   337  
   338  	for content := range clnt.List(ctx, lstOptions) {
   339  		if content.Err != nil {
   340  			errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.")
   341  			cErr = exitStatus(globalErrorExitStatus) // Set the exit status.
   342  			continue
   343  		}
   344  		// The spec does not allow setting retention on delete marker
   345  		if content.IsDeleteMarker {
   346  			continue
   347  		}
   348  
   349  		if !isRecursive && alias+getKey(content) != getStandardizedURL(target) {
   350  			break
   351  		}
   352  
   353  		err := infoRetentionSingle(ctx, alias, content.URL.String(), content.VersionID, true)
   354  		if err != nil {
   355  			errorIf(err.Trace(clnt.GetURL().String()), "Invalid URL")
   356  			cErr = exitStatus(globalErrorExitStatus)
   357  			continue
   358  		}
   359  
   360  		atLeastOneObjectOrVersionFound = true
   361  	}
   362  
   363  	if !atLeastOneObjectOrVersionFound {
   364  		errorIf(errDummy().Trace(clnt.GetURL().String()), "Unable to find any object/version to show its retention.")
   365  		cErr = exitStatus(globalErrorExitStatus) // Set the exit status.
   366  	}
   367  
   368  	return cErr
   369  }
   370  
   371  // main for retention info command.
   372  func mainRetentionInfo(cliCtx *cli.Context) error {
   373  	ctx, cancelSetRetention := context.WithCancel(globalContext)
   374  	defer cancelSetRetention()
   375  
   376  	console.SetColor("RetentionSuccess", color.New(color.FgGreen, color.Bold))
   377  	console.SetColor("RetentionNotFound", color.New(color.FgYellow))
   378  	console.SetColor("RetentionVersionID", color.New(color.FgGreen))
   379  	console.SetColor("RetentionExpired", color.New(color.FgRed, color.Bold))
   380  	console.SetColor("RetentionFailure", color.New(color.FgYellow))
   381  
   382  	target, versionID, recursive, rewind, withVersions, bucketMode := parseInfoRetentionArgs(cliCtx)
   383  
   384  	fatalIfBucketLockNotSupported(ctx, target)
   385  
   386  	if bucketMode {
   387  		return showBucketLock(target)
   388  	}
   389  
   390  	if withVersions && rewind.IsZero() {
   391  		rewind = time.Now().UTC()
   392  	}
   393  
   394  	return getRetention(ctx, target, versionID, rewind, withVersions, recursive)
   395  }