github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/retention-common.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  	"strconv"
    24  	"time"
    25  
    26  	json "github.com/minio/colorjson"
    27  	"github.com/minio/mc/pkg/probe"
    28  	"github.com/minio/minio-go/v7"
    29  	"github.com/minio/pkg/v2/console"
    30  )
    31  
    32  // Structured message depending on the type of console.
    33  type retentionCmdMessage struct {
    34  	Op        lockOpType          `json:"op"`
    35  	Mode      minio.RetentionMode `json:"mode"`
    36  	Validity  string              `json:"validity"`
    37  	URLPath   string              `json:"urlpath"`
    38  	VersionID string              `json:"versionID"`
    39  	Status    string              `json:"status"`
    40  	Err       error               `json:"error"`
    41  }
    42  
    43  // Colorized message for console printing.
    44  func (m retentionCmdMessage) String() string {
    45  	var color, msg string
    46  	ed := ""
    47  	if m.Op == lockOpClear {
    48  		ed = "ed"
    49  	}
    50  
    51  	if m.Err != nil {
    52  		color = "RetentionFailure"
    53  		msg = fmt.Sprintf("Unable to %s object retention on `%s`: %s", m.Op, m.URLPath, m.Err)
    54  	} else {
    55  		color = "RetentionSuccess"
    56  		msg = fmt.Sprintf("Object retention successfully %s%s for `%s`", m.Op, ed, m.URLPath)
    57  	}
    58  	if m.VersionID != "" {
    59  		msg += fmt.Sprintf(" (version-id=%s)", m.VersionID)
    60  	}
    61  	msg += "."
    62  	return console.Colorize(color, msg)
    63  }
    64  
    65  // JSON'ified message for scripting.
    66  func (m retentionCmdMessage) JSON() string {
    67  	if m.Err != nil {
    68  		m.Status = "failure"
    69  	}
    70  	msgBytes, e := json.MarshalIndent(m, "", " ")
    71  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
    72  	return string(msgBytes)
    73  }
    74  
    75  type lockOpType string
    76  
    77  const (
    78  	lockOpInfo  = "info"
    79  	lockOpClear = "clear"
    80  	lockOpSet   = "set"
    81  )
    82  
    83  // Structured message depending on the type of console.
    84  type retentionBucketMessage struct {
    85  	Op       lockOpType          `json:"op"`
    86  	Enabled  string              `json:"enabled"`
    87  	Mode     minio.RetentionMode `json:"mode"`
    88  	Validity string              `json:"validity"`
    89  	Status   string              `json:"status"`
    90  }
    91  
    92  // Colorized message for console printing.
    93  func (m retentionBucketMessage) String() string {
    94  	if m.Op == lockOpClear {
    95  		return console.Colorize("RetentionSuccess", "Object lock configuration cleared successfully.")
    96  	}
    97  	// info/set command
    98  	if !m.Mode.IsValid() {
    99  		return console.Colorize("RetentionNotFound", "Object locking is not enabled.")
   100  	}
   101  	return console.Colorize("RetentionSuccess", fmt.Sprintf("Object locking '%s' is configured for %s.",
   102  		console.Colorize("Mode", m.Mode), console.Colorize("Validity", m.Validity)))
   103  }
   104  
   105  // JSON'ified message for scripting.
   106  func (m retentionBucketMessage) JSON() string {
   107  	msgBytes, e := json.MarshalIndent(m, "", " ")
   108  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   109  	return string(msgBytes)
   110  }
   111  
   112  func getRetainUntilDate(validity uint64, unit minio.ValidityUnit) (string, *probe.Error) {
   113  	if validity == 0 {
   114  		return "", probe.NewError(fmt.Errorf("invalid validity '%v'", validity))
   115  	}
   116  	t := UTCNow()
   117  	if unit == minio.Years {
   118  		t = t.AddDate(int(validity), 0, 0)
   119  	} else {
   120  		t = t.AddDate(0, 0, int(validity))
   121  	}
   122  	timeStr := t.Format(time.RFC3339)
   123  
   124  	return timeStr, nil
   125  }
   126  
   127  func setRetentionSingle(ctx context.Context, op lockOpType, alias, url, versionID string, mode minio.RetentionMode, retainUntil time.Time, bypassGovernance bool) *probe.Error {
   128  	newClnt, err := newClientFromAlias(alias, url)
   129  	if err != nil {
   130  		return err
   131  	}
   132  
   133  	msg := retentionCmdMessage{
   134  		Op:        op,
   135  		Mode:      mode,
   136  		URLPath:   urlJoinPath(alias, url),
   137  		VersionID: versionID,
   138  	}
   139  
   140  	err = newClnt.PutObjectRetention(ctx, versionID, mode, retainUntil, bypassGovernance)
   141  	if err != nil {
   142  		msg.Err = err.ToGoError()
   143  		msg.Status = "failure"
   144  	} else {
   145  		msg.Status = "success"
   146  	}
   147  
   148  	printMsg(msg)
   149  	return err
   150  }
   151  
   152  func parseRetentionValidity(validityStr string) (uint64, minio.ValidityUnit, *probe.Error) {
   153  	unitStr := string(validityStr[len(validityStr)-1])
   154  	validityStr = validityStr[:len(validityStr)-1]
   155  	validity, e := strconv.ParseUint(validityStr, 10, 64)
   156  	if e != nil {
   157  		return 0, "", probe.NewError(e).Trace(validityStr)
   158  	}
   159  
   160  	var unit minio.ValidityUnit
   161  	switch unitStr {
   162  	case "d", "D":
   163  		unit = minio.Days
   164  	case "y", "Y":
   165  		unit = minio.Years
   166  	default:
   167  		return 0, "", errInvalidArgument().Trace(unitStr)
   168  	}
   169  
   170  	return validity, unit, nil
   171  }
   172  
   173  func fatalIfBucketLockNotSupported(ctx context.Context, aliasedURL string) {
   174  	_, err := getBucketLockStatus(ctx, aliasedURL)
   175  	if err != nil {
   176  		fatalIf(errDummy().Trace(), "Remote bucket `%s` does not support locking", aliasedURL)
   177  	}
   178  }
   179  
   180  // Apply Retention for one object/version or many objects within a given prefix.
   181  func applyRetention(ctx context.Context, op lockOpType, target, versionID string, timeRef time.Time, withOlderVersions, isRecursive bool,
   182  	mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit, bypassGovernance bool,
   183  ) error {
   184  	clnt, err := newClient(target)
   185  	if err != nil {
   186  		fatalIf(err.Trace(), "Unable to parse the provided url.")
   187  	}
   188  
   189  	// Quit early if urlStr does not point to an S3 server
   190  	switch clnt.(type) {
   191  	case *S3Client:
   192  	default:
   193  		fatal(errDummy().Trace(), "Retention is supported only for S3 servers.")
   194  	}
   195  
   196  	var until time.Time
   197  	if mode != "" {
   198  		timeStr, err := getRetainUntilDate(validity, unit)
   199  		if err != nil {
   200  			return err.ToGoError()
   201  		}
   202  		var e error
   203  		until, e = time.Parse(time.RFC3339, timeStr)
   204  		if e != nil {
   205  			return e
   206  		}
   207  	}
   208  
   209  	alias, urlStr, _ := mustExpandAlias(target)
   210  	if versionID != "" || !isRecursive && !withOlderVersions {
   211  		err := setRetentionSingle(ctx, op, alias, urlStr, versionID, mode, until, bypassGovernance)
   212  		fatalIf(err.Trace(), "Unable to set retention on `%s`", target)
   213  		return nil
   214  	}
   215  
   216  	lstOptions := ListOptions{Recursive: isRecursive, ShowDir: DirNone}
   217  	if !timeRef.IsZero() {
   218  		lstOptions.WithOlderVersions = withOlderVersions
   219  		lstOptions.WithDeleteMarkers = true
   220  		lstOptions.TimeRef = timeRef
   221  	}
   222  
   223  	var cErr error
   224  	var atLeastOneRetentionApplied bool
   225  
   226  	for content := range clnt.List(ctx, lstOptions) {
   227  		if content.Err != nil {
   228  			errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.")
   229  			cErr = exitStatus(globalErrorExitStatus) // Set the exit status.
   230  			continue
   231  		}
   232  
   233  		// The spec does not allow setting retention on delete marker
   234  		if content.IsDeleteMarker {
   235  			continue
   236  		}
   237  
   238  		if !isRecursive && alias+getKey(content) != getStandardizedURL(target) {
   239  			break
   240  		}
   241  
   242  		err := setRetentionSingle(ctx, op, alias, content.URL.String(), content.VersionID, mode, until, bypassGovernance)
   243  		if err != nil {
   244  			errorIf(err.Trace(clnt.GetURL().String()), "Invalid URL")
   245  			continue
   246  		}
   247  
   248  		atLeastOneRetentionApplied = true
   249  	}
   250  
   251  	if !atLeastOneRetentionApplied {
   252  		errorIf(errDummy().Trace(clnt.GetURL().String()), "Unable to find any object/version to "+string(op)+" its retention.")
   253  		cErr = exitStatus(globalErrorExitStatus) // Set the exit status.
   254  	}
   255  
   256  	return cErr
   257  }
   258  
   259  // applyBucketLock - set object lock configuration.
   260  func applyBucketLock(op lockOpType, urlStr string, mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit) error {
   261  	client, err := newClient(urlStr)
   262  	if err != nil {
   263  		fatalIf(err.Trace(), "Unable to parse the provided url.")
   264  	}
   265  
   266  	ctx, cancelLock := context.WithCancel(globalContext)
   267  	defer cancelLock()
   268  	if op == lockOpClear || mode != "" {
   269  		err = client.SetObjectLockConfig(ctx, mode, validity, unit)
   270  		fatalIf(err, "Unable to apply bucket lock configuration.")
   271  	} else {
   272  		_, mode, validity, unit, err = client.GetObjectLockConfig(ctx)
   273  		fatalIf(err, "Unable to apply bucket lock configuration.")
   274  	}
   275  
   276  	printMsg(retentionBucketMessage{
   277  		Op:       op,
   278  		Enabled:  "Enabled",
   279  		Mode:     mode,
   280  		Validity: fmt.Sprintf("%d%s", validity, unit),
   281  		Status:   "success",
   282  	})
   283  
   284  	return nil
   285  }
   286  
   287  // showBucketLock - show object lock configuration.
   288  func showBucketLock(urlStr string) error {
   289  	client, err := newClient(urlStr)
   290  	if err != nil {
   291  		fatalIf(err.Trace(), "Unable to parse the provided url.")
   292  	}
   293  
   294  	ctx, cancelLock := context.WithCancel(globalContext)
   295  	defer cancelLock()
   296  
   297  	status, mode, validity, unit, err := client.GetObjectLockConfig(ctx)
   298  	fatalIf(err, "Unable to get bucket lock configuration.")
   299  
   300  	printMsg(retentionBucketMessage{
   301  		Op:       lockOpInfo,
   302  		Enabled:  status,
   303  		Mode:     mode,
   304  		Validity: fmt.Sprintf("%d%s", validity, unit),
   305  		Status:   "success",
   306  	})
   307  
   308  	return nil
   309  }