github.com/minio/mc@v0.0.0-20240507152021-646712d5e5fb/cmd/replicate-update.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  	"path"
    24  	"strconv"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/fatih/color"
    29  	"github.com/minio/cli"
    30  	json "github.com/minio/colorjson"
    31  	"github.com/minio/madmin-go/v3"
    32  	"github.com/minio/mc/pkg/probe"
    33  	"github.com/minio/minio-go/v7/pkg/replication"
    34  	"github.com/minio/minio-go/v7/pkg/s3utils"
    35  	"github.com/minio/pkg/v2/console"
    36  )
    37  
    38  var replicateUpdateFlags = []cli.Flag{
    39  	cli.StringFlag{
    40  		Name:  "id",
    41  		Usage: "id for the rule, should be a unique value",
    42  	},
    43  	cli.StringFlag{
    44  		Name:  "tags",
    45  		Usage: "format '<key1>=<value1>&<key2>=<value2>&<key3>=<value3>', multiple values allowed for multiple key/value pairs",
    46  	},
    47  	cli.StringFlag{
    48  		Name:  "storage-class",
    49  		Usage: `storage class for destination, valid values are ['STANDARD', 'REDUCED_REDUNDANCY']`,
    50  	},
    51  	cli.StringFlag{
    52  		Name:  "state",
    53  		Usage: "change rule status, valid values are ['enable', 'disable']",
    54  	},
    55  	cli.IntFlag{
    56  		Name:  "priority",
    57  		Usage: "priority of the rule, should be unique and is a required field",
    58  	},
    59  	cli.StringFlag{
    60  		Name:  "remote-bucket",
    61  		Usage: "destination bucket, should be a unique value for the configuration",
    62  	},
    63  	cli.StringFlag{
    64  		Name:  "replicate",
    65  		Usage: `comma separated list to enable replication of soft deletes, permanent deletes, existing objects and metadata sync. Valid options are "delete-marker","delete","existing-objects","metadata-sync" and ""'`,
    66  	},
    67  	cli.StringFlag{
    68  		Name:  "sync",
    69  		Usage: "enable synchronous replication for this target, valid values are ['enable', 'disable'].",
    70  		Value: "disable",
    71  	},
    72  	cli.StringFlag{
    73  		Name:  "proxy",
    74  		Usage: "enable proxying in active-active replication, valid values are ['enable', 'disable']",
    75  		Value: "enable",
    76  	},
    77  	cli.StringFlag{
    78  		Name:  "bandwidth",
    79  		Usage: "Set bandwidth limit in bits per second (K,B,G,T for metric and Ki,Bi,Gi,Ti for IEC units)",
    80  	},
    81  	cli.UintFlag{
    82  		Name:  "healthcheck-seconds",
    83  		Usage: "health check duration in seconds",
    84  		Value: 60,
    85  	},
    86  	cli.StringFlag{
    87  		Name:  "path",
    88  		Value: "auto",
    89  		Usage: "bucket path lookup supported by the server, valid options are ['on', 'off', 'auto']",
    90  	},
    91  }
    92  
    93  var replicateUpdateCmd = cli.Command{
    94  	Name:          "update",
    95  	Aliases:       []string{"edit"},
    96  	HiddenAliases: true,
    97  	Usage:         "modify an existing server side replication configuration rule",
    98  	Action:        mainReplicateUpdate,
    99  	OnUsageError:  onUsageError,
   100  	Before:        setGlobalsFromContext,
   101  	Flags:         append(globalFlags, replicateUpdateFlags...),
   102  	CustomHelpTemplate: `NAME:
   103    {{.HelpName}} - {{.Usage}}
   104  
   105  USAGE:
   106    {{.HelpName}} TARGET --id=RULE-ID [FLAGS]
   107  
   108  FLAGS:
   109    {{range .VisibleFlags}}{{.}}
   110    {{end}}
   111  EXAMPLES:
   112    1. Change priority of rule with rule ID "bsibgh8t874dnjst8hkg" on bucket "mybucket" for alias "myminio".
   113       {{.Prompt}} {{.HelpName}} myminio/mybucket --id "bsibgh8t874dnjst8hkg"  --priority 3
   114  
   115    2. Disable a replication configuration rule with rule ID "bsibgh8t874dnjst8hkg" on target myminio/bucket
   116       {{.Prompt}} {{.HelpName}} myminio/mybucket --id "bsibgh8t874dnjst8hkg" --state disable
   117  
   118    3. Set tags and storage class on a replication configuration with rule ID "kMYD.491" on target myminio/bucket/prefix.
   119       {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kMYD.491" --tags "key1=value1&key2=value2" \
   120  				  --storage-class "STANDARD" --priority 2
   121    4. Clear tags for replication configuration rule with ID "kMYD.491" on a target myminio/bucket.
   122       {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kMYD.491" --tags ""
   123  
   124    5. Enable delete marker replication on a replication configuration rule with ID "kxYD.491" on a target myminio/bucket.
   125       {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kxYD.491" --replicate "delete-marker"
   126  
   127    6. Disable delete marker and versioned delete replication on a replication configuration rule with ID "kxYD.491" on a target myminio/bucket.
   128       {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kxYD.491" --replicate ""
   129  
   130    7. Enable existing object replication on a configuration rule with ID "kxYD.491" on a target myminio/bucket. Rule previously had enabled delete marker and versioned delete replication.
   131       {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kxYD.491" --replicate "existing-objects,delete-marker,delete"
   132  
   133    8. Edit credentials for remote target with replication rule ID kxYD.491
   134       {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kxYD.491" --remote-bucket  https://foobar:newpassword@minio.siteb.example.com/targetbucket
   135    
   136    9. Edit credentials with alias "targetminio" for remote target with replication rule ID kxYD.491
   137       {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kxYD.491" --remote-bucket  targetminio/targetbucket
   138  
   139    10. Disable proxying and enable synchronous replication for remote target of bucket mybucket with rule ID kxYD.492
   140       {{.Prompt}} {{.HelpName}} myminio/mybucket --id "kxYD.492" --remote-bucket https://foobar:newpassword@minio.siteb.example.com/targetbucket \
   141           --sync "enable" --proxy "disable"
   142  `,
   143  }
   144  
   145  // checkReplicateUpdateSyntax - validate all the passed arguments
   146  func checkReplicateUpdateSyntax(ctx *cli.Context) {
   147  	if len(ctx.Args()) != 1 {
   148  		showCommandHelpAndExit(ctx, 1) // last argument is exit code
   149  	}
   150  }
   151  
   152  // modifyRemoteTarget - modifies the dest credentials or updates sync , disable-proxy settings
   153  func modifyRemoteTarget(cli *cli.Context, targets []madmin.BucketTarget, arnStr string) (*madmin.BucketTarget, []madmin.TargetUpdateType) {
   154  	args := cli.Args()
   155  	foundIdx := -1
   156  	for i, t := range targets {
   157  		if t.Arn == arnStr {
   158  			arn, e := madmin.ParseARN(arnStr)
   159  			if e != nil {
   160  				fatalIf(errInvalidArgument().Trace(args...), "Malformed ARN `"+arnStr+"` in replication config")
   161  			}
   162  			if arn.Bucket != t.TargetBucket {
   163  				fatalIf(errInvalidArgument().Trace(args...), "Expected remote bucket %s, got %s for rule id %s", t.TargetBucket, arn.Bucket, cli.String("id"))
   164  			}
   165  			foundIdx = i
   166  			break
   167  		}
   168  	}
   169  	if foundIdx < 0 {
   170  		fatalIf(errInvalidArgument().Trace(args...), "`"+arnStr+"` not found in replication config")
   171  	}
   172  	var ops []madmin.TargetUpdateType
   173  	bktTarget := targets[foundIdx].Clone()
   174  	if cli.IsSet("sync") {
   175  		syncState := strings.ToLower(cli.String("sync"))
   176  		switch syncState {
   177  		case "enable", "disable":
   178  			bktTarget.ReplicationSync = syncState == "enable"
   179  			ops = append(ops, madmin.SyncUpdateType)
   180  		default:
   181  			fatalIf(errInvalidArgument().Trace(args...), "--sync can be either [enable|disable]")
   182  		}
   183  	}
   184  	if cli.IsSet("proxy") {
   185  		proxyState := strings.ToLower(cli.String("proxy"))
   186  		switch proxyState {
   187  		case "enable", "disable":
   188  			bktTarget.DisableProxy = proxyState == "disable"
   189  			ops = append(ops, madmin.ProxyUpdateType)
   190  
   191  		default:
   192  			fatalIf(errInvalidArgument().Trace(args...), "--proxy can be either [enable|disable]")
   193  		}
   194  	}
   195  
   196  	if len(args) == 1 {
   197  		_, sourceBucket := url2Alias(args[0])
   198  
   199  		tgtURL := cli.String("remote-bucket")
   200  		accessKey, secretKey, u := extractCredentialURL(tgtURL)
   201  		var tgtBucket string
   202  		if u.Path != "" {
   203  			tgtBucket = path.Clean(u.Path[1:])
   204  		}
   205  		fatalIf(probe.NewError(s3utils.CheckValidBucketName(tgtBucket)).Trace(tgtURL), "invalid target bucket")
   206  
   207  		secure := u.Scheme == "https"
   208  		host := u.Host
   209  		if u.Port() == "" {
   210  			port := 80
   211  			if secure {
   212  				port = 443
   213  			}
   214  			host = host + ":" + strconv.Itoa(port)
   215  		}
   216  		console.SetColor(cred, color.New(color.FgYellow, color.Italic))
   217  		creds := &madmin.Credentials{AccessKey: accessKey, SecretKey: secretKey}
   218  		if tgtBucket != bktTarget.TargetBucket {
   219  			fatalIf(errInvalidArgument().Trace(args...), "configured remote target bucket `"+tgtBucket+"` does not match "+bktTarget.TargetBucket+"` for this ARN `"+bktTarget.Arn+"`")
   220  		}
   221  		if sourceBucket != bktTarget.SourceBucket {
   222  			fatalIf(errInvalidArgument().Trace(args...), "configured source bucket `"+sourceBucket+"` does not match "+bktTarget.SourceBucket+"` for this ARN `"+bktTarget.Arn+"`")
   223  		}
   224  		bktTarget.TargetBucket = tgtBucket
   225  		bktTarget.Secure = secure
   226  		bktTarget.Credentials = creds
   227  		bktTarget.Endpoint = host
   228  		ops = append(ops, madmin.CredentialsUpdateType)
   229  	}
   230  	if cli.IsSet("bandwidth") {
   231  		bandwidthStr := cli.String("bandwidth")
   232  		bandwidth, e := getBandwidthInBytes(bandwidthStr)
   233  		fatalIf(probe.NewError(e).Trace(bandwidthStr), "invalid bandwidth value")
   234  
   235  		bktTarget.BandwidthLimit = int64(bandwidth)
   236  		ops = append(ops, madmin.BandwidthLimitUpdateType)
   237  	}
   238  	if cli.IsSet("healthcheck-seconds") {
   239  		bktTarget.HealthCheckDuration = time.Duration(cli.Uint("healthcheck-seconds")) * time.Second
   240  		ops = append(ops, madmin.HealthCheckDurationUpdateType)
   241  	}
   242  	if cli.IsSet("path") {
   243  		bktTarget.Path = cli.String("path")
   244  		ops = append(ops, madmin.PathUpdateType)
   245  	}
   246  	return &bktTarget, ops
   247  }
   248  
   249  type replicateUpdateMessage struct {
   250  	Op     string `json:"op"`
   251  	Status string `json:"status"`
   252  	URL    string `json:"url"`
   253  	ID     string `json:"id"`
   254  }
   255  
   256  func (l replicateUpdateMessage) JSON() string {
   257  	l.Status = "success"
   258  	jsonMessageBytes, e := json.MarshalIndent(l, "", " ")
   259  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   260  	return string(jsonMessageBytes)
   261  }
   262  
   263  func (l replicateUpdateMessage) String() string {
   264  	if l.ID != "" {
   265  		return console.Colorize("replicateUpdateMessage", "Replication configuration rule with ID `"+l.ID+"` applied to "+l.URL+".")
   266  	}
   267  	return console.Colorize("replicateUpdateMessage", "Replication configuration rule applied to "+l.URL+" successfully.")
   268  }
   269  
   270  func mainReplicateUpdate(cliCtx *cli.Context) error {
   271  	ctx, cancelReplicateUpdate := context.WithCancel(globalContext)
   272  	defer cancelReplicateUpdate()
   273  
   274  	console.SetColor("replicateUpdateMessage", color.New(color.FgGreen))
   275  
   276  	checkReplicateUpdateSyntax(cliCtx)
   277  
   278  	// Get the alias parameter from cli
   279  	args := cliCtx.Args()
   280  	aliasedURL := args.Get(0)
   281  	// Create a new Client
   282  	client, err := newClient(aliasedURL)
   283  	fatalIf(err, "unable to initialize connection.")
   284  	rcfg, err := client.GetReplication(ctx)
   285  	fatalIf(err.Trace(args...), "unable to get replication configuration")
   286  
   287  	if !cliCtx.IsSet("id") {
   288  		fatalIf(errInvalidArgument(), "--id is a required flag")
   289  	}
   290  	var state string
   291  	if cliCtx.IsSet("state") {
   292  		state = strings.ToLower(cliCtx.String("state"))
   293  		if state != "enable" && state != "disable" {
   294  			fatalIf(err.Trace(args...), "--state can be either `enable` or `disable`")
   295  		}
   296  	}
   297  	var sourceBucket string
   298  	switch c := client.(type) {
   299  	case *S3Client:
   300  		sourceBucket, _ = c.url2BucketAndObject()
   301  	default:
   302  		fatalIf(err.Trace(args...), "replication is not supported for filesystem")
   303  	}
   304  	// Create a new MinIO Admin Client
   305  	admClient, err := newAdminClient(aliasedURL)
   306  	fatalIf(err, "unable to initialize admin connection.")
   307  
   308  	targets, e := admClient.ListRemoteTargets(globalContext, sourceBucket, "")
   309  	fatalIf(probe.NewError(e).Trace(args...), "unable to fetch remote target.")
   310  
   311  	var arn string
   312  	for _, rule := range rcfg.Rules {
   313  		if rule.ID == cliCtx.String("id") {
   314  			arn = rule.Destination.Bucket
   315  			if rcfg.Role != "" {
   316  				arn = rcfg.Role
   317  			}
   318  			break
   319  		}
   320  	}
   321  	if cliCtx.IsSet("remote-bucket") {
   322  		bktTarget, ops := modifyRemoteTarget(cliCtx, targets, arn)
   323  		_, e = admClient.UpdateRemoteTarget(globalContext, bktTarget, ops...)
   324  		if e != nil {
   325  			fatalIf(probe.NewError(e).Trace(args...), "Unable to update remote target `"+bktTarget.Endpoint+"` from `"+bktTarget.SourceBucket+"` -> `"+bktTarget.TargetBucket+"`")
   326  		}
   327  	} else {
   328  		if cliCtx.IsSet("sync") || cliCtx.IsSet("bandwidth") || cliCtx.IsSet("proxy") || cliCtx.IsSet("healthcheck-seconds") || cliCtx.IsSet("path") {
   329  			fatalIf(errInvalidArgument().Trace(args...), "--remote-bucket is a required flag`")
   330  		}
   331  	}
   332  
   333  	var vDeleteReplicate, dmReplicate, replicasync, existingReplState string
   334  	if cliCtx.IsSet("replicate") {
   335  		replSlice := strings.Split(cliCtx.String("replicate"), ",")
   336  		vDeleteReplicate = disableStatus
   337  		dmReplicate = disableStatus
   338  		replicasync = disableStatus
   339  		existingReplState = disableStatus
   340  
   341  		for _, opt := range replSlice {
   342  			switch strings.TrimSpace(strings.ToLower(opt)) {
   343  			case "delete-marker":
   344  				dmReplicate = enableStatus
   345  			case "delete":
   346  				vDeleteReplicate = enableStatus
   347  			case "metadata-sync", "replica-metadata-sync":
   348  				replicasync = enableStatus
   349  			case "existing-objects":
   350  				existingReplState = enableStatus
   351  			default:
   352  				if opt != "" {
   353  					fatalIf(probe.NewError(fmt.Errorf("invalid value for --replicate flag %s", cliCtx.String("replicate"))),
   354  						`--replicate flag takes one or more comma separated string with values "delete", "delete-marker", "metadata-sync", "existing-objects" or "" to disable these settings`)
   355  				}
   356  			}
   357  		}
   358  	}
   359  
   360  	opts := replication.Options{
   361  		TagString:    cliCtx.String("tags"),
   362  		RoleArn:      cliCtx.String("arn"),
   363  		StorageClass: cliCtx.String("storage-class"),
   364  		RuleStatus:   state,
   365  		ID:           cliCtx.String("id"),
   366  		Op:           replication.SetOption,
   367  		DestBucket:   arn,
   368  		IsSCSet:      cliCtx.IsSet("storage-class"),
   369  		IsTagSet:     cliCtx.IsSet("tags"),
   370  	}
   371  
   372  	if cliCtx.IsSet("priority") {
   373  		opts.Priority = strconv.Itoa(cliCtx.Int("priority"))
   374  	}
   375  	if cliCtx.IsSet("replicate") {
   376  		opts.ReplicateDeletes = vDeleteReplicate
   377  		opts.ReplicateDeleteMarkers = dmReplicate
   378  		opts.ReplicaSync = replicasync
   379  		opts.ExistingObjectReplicate = existingReplState
   380  	}
   381  
   382  	fatalIf(client.SetReplication(ctx, &rcfg, opts), "unable to modify replication rule")
   383  	printMsg(replicateUpdateMessage{
   384  		Op:  cliCtx.Command.Name,
   385  		URL: aliasedURL,
   386  		ID:  opts.ID,
   387  	})
   388  	return nil
   389  }