github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/replicate-add.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  	"net/url"
    24  	"path"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/dustin/go-humanize"
    30  	"github.com/fatih/color"
    31  	"github.com/minio/cli"
    32  	json "github.com/minio/colorjson"
    33  	"github.com/minio/madmin-go/v3"
    34  	"github.com/minio/mc/pkg/probe"
    35  	"github.com/minio/minio-go/v7/pkg/replication"
    36  	"github.com/minio/minio-go/v7/pkg/s3utils"
    37  	"github.com/minio/pkg/v2/console"
    38  )
    39  
    40  var replicateAddFlags = []cli.Flag{
    41  	cli.StringFlag{
    42  		Name:   "arn",
    43  		Usage:  "unique role ARN",
    44  		Hidden: true,
    45  	},
    46  	cli.StringFlag{
    47  		Name:  "id",
    48  		Usage: "id for the rule, should be a unique value",
    49  	},
    50  	cli.StringFlag{
    51  		Name:  "tags",
    52  		Usage: "format '<key1>=<value1>&<key2>=<value2>&<key3>=<value3>', multiple values allowed for multiple key/value pairs",
    53  	},
    54  	cli.StringFlag{
    55  		Name:  "storage-class",
    56  		Usage: `storage class for destination, valid values are either "STANDARD" or "REDUCED_REDUNDANCY"`,
    57  	},
    58  	cli.BoolFlag{
    59  		Name:  "disable",
    60  		Usage: "disable the rule",
    61  	},
    62  	cli.IntFlag{
    63  		Name:  "priority",
    64  		Usage: "priority of the rule, should be unique and is a required field",
    65  	},
    66  	cli.StringFlag{
    67  		Name:  "remote-bucket",
    68  		Usage: "remote bucket, should be a unique value for the configuration",
    69  	},
    70  	cli.StringFlag{
    71  		Name:  "replicate",
    72  		Value: `delete-marker,delete,existing-objects,metadata-sync`,
    73  		Usage: `comma separated list to enable replication of soft deletes, permanent deletes, existing objects and metadata sync`,
    74  	},
    75  	cli.StringFlag{
    76  		Name:  "path",
    77  		Value: "auto",
    78  		Usage: "bucket path lookup supported by the server. Valid options are ['auto', 'on', 'off']'",
    79  	},
    80  	cli.StringFlag{
    81  		Name:  "region",
    82  		Usage: "region of the destination bucket (optional)",
    83  	},
    84  	cli.StringFlag{
    85  		Name:  "bandwidth",
    86  		Usage: "set bandwidth limit in bits per second (K,B,G,T for metric and Ki,Bi,Gi,Ti for IEC units)",
    87  	},
    88  	cli.BoolFlag{
    89  		Name:  "sync",
    90  		Usage: "enable synchronous replication for this target. default is async",
    91  	},
    92  	cli.UintFlag{
    93  		Name:  "healthcheck-seconds",
    94  		Usage: "health check interval in seconds",
    95  		Value: 60,
    96  	},
    97  	cli.BoolFlag{
    98  		Name:  "disable-proxy",
    99  		Usage: "disable proxying in active-active replication. If unset, default behavior is to proxy",
   100  	},
   101  }
   102  
   103  var replicateAddCmd = cli.Command{
   104  	Name:         "add",
   105  	Usage:        "add a server side replication configuration rule",
   106  	Action:       mainReplicateAdd,
   107  	OnUsageError: onUsageError,
   108  	Before:       setGlobalsFromContext,
   109  	Flags:        append(globalFlags, replicateAddFlags...),
   110  	CustomHelpTemplate: `NAME:
   111    {{.HelpName}} - {{.Usage}}
   112  
   113  USAGE:
   114    {{.HelpName}} TARGET
   115  
   116  FLAGS:
   117    {{range .VisibleFlags}}{{.}}
   118    {{end}}
   119  EXAMPLES:
   120    1. Add replication configuration rule on bucket "sourcebucket" for alias "sourceminio" with alias "targetminio" to replicate all operations in an active-active replication setup.
   121       {{.Prompt}} {{.HelpName}} sourceminio/sourcebucket --remote-bucket targetminio/targetbucket \
   122           --priority 1 
   123  
   124    2. Add replication configuration rule on bucket "mybucket" for alias "myminio" to replicate all operations in an active-active replication setup.
   125       {{.Prompt}} {{.HelpName}} myminio/mybucket --remote-bucket https://foobar:foo12345@minio.siteb.example.com/targetbucket \
   126           --priority 1 
   127  
   128    3. Add replication configuration rule on bucket "mybucket" for alias "myminio" to replicate all objects with tags
   129       "key1=value1, key2=value2" to targetbucket synchronously with bandwidth set to 2 gigabits per second. 
   130       {{.Prompt}} {{.HelpName}} myminio/mybucket --remote-bucket https://foobar:foo12345@minio.siteb.example.com/targetbucket  \
   131           --tags "key1=value1&key2=value2" --bandwidth "2G" --sync \
   132           --priority 1
   133  
   134    4. Disable a replication configuration rule on bucket "mybucket" for alias "myminio".
   135       {{.Prompt}} {{.HelpName}} myminio/mybucket --remote-bucket https://foobar:foo12345@minio.siteb.example.com/targetbucket  \
   136           --tags "key1=value1&key2=value2" \
   137           --priority 1 --disable
   138  
   139    5. Add replication configuration rule with existing object replication, delete marker replication and versioned deletes
   140       enabled on bucket "mybucket" for alias "myminio".
   141       {{.Prompt}} {{.HelpName}} myminio/mybucket --remote-bucket https://foobar:foo12345@minio.siteb.example.com/targetbucket  \
   142           --replicate "existing-objects,delete,delete-marker" \
   143           --priority 1
   144  `,
   145  }
   146  
   147  // checkReplicateAddSyntax - validate all the passed arguments
   148  func checkReplicateAddSyntax(ctx *cli.Context) {
   149  	if len(ctx.Args()) != 1 {
   150  		showCommandHelpAndExit(ctx, 1) // last argument is exit code
   151  	}
   152  	if ctx.String("remote-bucket") == "" {
   153  		fatal(errDummy().Trace(), "--remote-bucket flag needs to be specified.")
   154  	}
   155  }
   156  
   157  type replicateAddMessage struct {
   158  	Op     string `json:"op"`
   159  	Status string `json:"status"`
   160  	URL    string `json:"url"`
   161  	ID     string `json:"id"`
   162  }
   163  
   164  const (
   165  	enableStatus  = "enable"
   166  	disableStatus = "disable"
   167  )
   168  
   169  func (l replicateAddMessage) JSON() string {
   170  	l.Status = "success"
   171  	jsonMessageBytes, e := json.MarshalIndent(l, "", " ")
   172  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   173  	return string(jsonMessageBytes)
   174  }
   175  
   176  func (l replicateAddMessage) String() string {
   177  	if l.ID != "" {
   178  		return console.Colorize("replicateAddMessage", "Replication configuration rule with ID `"+l.ID+"` applied to "+l.URL+".")
   179  	}
   180  	return console.Colorize("replicateAddMessage", "Replication configuration rule applied to "+l.URL+" successfully.")
   181  }
   182  
   183  func extractCredentialURL(argURL string) (accessKey, secretKey string, u *url.URL) {
   184  	var parsedURL string
   185  	if strings.HasPrefix(argURL, "http://") || strings.HasPrefix(argURL, "https://") {
   186  		if hostKeyTokens.MatchString(argURL) {
   187  			fatalIf(errInvalidArgument().Trace(argURL), "temporary tokens are not allowed for remote targets")
   188  		}
   189  		if hostKeys.MatchString(argURL) {
   190  			parts := hostKeys.FindStringSubmatch(argURL)
   191  			if len(parts) != 5 {
   192  				fatalIf(errInvalidArgument().Trace(argURL), "unsupported remote target format, please check --help")
   193  			}
   194  			accessKey = parts[2]
   195  			secretKey = parts[3]
   196  			parsedURL = fmt.Sprintf("%s%s", parts[1], parts[4])
   197  		}
   198  	} else {
   199  		var alias string
   200  		var aliasCfg *aliasConfigV10
   201  		// get alias config by alias url
   202  		alias, parsedURL, aliasCfg = mustExpandAlias(argURL)
   203  		if aliasCfg == nil {
   204  			fatalIf(errInvalidAliasedURL(alias).Trace(argURL), "No such alias `"+alias+"` found.")
   205  			return
   206  		}
   207  		accessKey, secretKey = aliasCfg.AccessKey, aliasCfg.SecretKey
   208  	}
   209  	var e error
   210  	if parsedURL == "" {
   211  		fatalIf(errInvalidArgument().Trace(argURL), "no valid credentials were detected")
   212  	}
   213  	u, e = url.Parse(parsedURL)
   214  	if e != nil {
   215  		fatalIf(errInvalidArgument().Trace(parsedURL), "unsupported URL format %v", e)
   216  	}
   217  
   218  	return accessKey, secretKey, u
   219  }
   220  
   221  // fetchRemoteTarget - returns the dest bucket, dest endpoint, access and secret key
   222  func fetchRemoteTarget(cli *cli.Context) (bktTarget *madmin.BucketTarget) {
   223  	if !cli.IsSet("remote-bucket") {
   224  		fatalIf(probe.NewError(fmt.Errorf("missing Remote target configuration")), "unable to parse remote target")
   225  	}
   226  	p := cli.String("path")
   227  	if !isValidPath(p) {
   228  		fatalIf(errInvalidArgument().Trace(p),
   229  			"unrecognized bucket path style. Valid options are `[on, off, auto]`.")
   230  	}
   231  
   232  	tgtURL := cli.String("remote-bucket")
   233  	accessKey, secretKey, u := extractCredentialURL(tgtURL)
   234  	var tgtBucket string
   235  	if u.Path != "" {
   236  		tgtBucket = path.Clean(u.Path[1:])
   237  	}
   238  	fatalIf(probe.NewError(s3utils.CheckValidBucketName(tgtBucket)).Trace(tgtURL), "invalid target bucket")
   239  
   240  	bandwidthStr := cli.String("bandwidth")
   241  	bandwidth, e := getBandwidthInBytes(bandwidthStr)
   242  	fatalIf(probe.NewError(e).Trace(bandwidthStr), "invalid bandwidth value")
   243  
   244  	console.SetColor(cred, color.New(color.FgYellow, color.Italic))
   245  	creds := &madmin.Credentials{AccessKey: accessKey, SecretKey: secretKey}
   246  	disableproxy := cli.Bool("disable-proxy")
   247  	bktTarget = &madmin.BucketTarget{
   248  		TargetBucket:        tgtBucket,
   249  		Secure:              u.Scheme == "https",
   250  		Credentials:         creds,
   251  		Endpoint:            u.Host,
   252  		Path:                p,
   253  		API:                 "s3v4",
   254  		Type:                madmin.ServiceType("replication"),
   255  		Region:              cli.String("region"),
   256  		BandwidthLimit:      int64(bandwidth),
   257  		ReplicationSync:     cli.Bool("sync"),
   258  		DisableProxy:        disableproxy,
   259  		HealthCheckDuration: time.Duration(cli.Uint("healthcheck-seconds")) * time.Second,
   260  	}
   261  	return bktTarget
   262  }
   263  
   264  func getBandwidthInBytes(bandwidthStr string) (bandwidth uint64, err error) {
   265  	if bandwidthStr != "" {
   266  		bandwidth, err = humanize.ParseBytes(bandwidthStr)
   267  		if err != nil {
   268  			return
   269  		}
   270  		bandwidth = bandwidth / 8
   271  	}
   272  	return
   273  }
   274  
   275  func mainReplicateAdd(cliCtx *cli.Context) error {
   276  	ctx, cancelReplicateAdd := context.WithCancel(globalContext)
   277  	defer cancelReplicateAdd()
   278  
   279  	console.SetColor("replicateAddMessage", color.New(color.FgGreen))
   280  
   281  	checkReplicateAddSyntax(cliCtx)
   282  
   283  	// Get the alias parameter from cli
   284  	args := cliCtx.Args()
   285  	aliasedURL := args.Get(0)
   286  
   287  	// Create a new Client
   288  	client, err := newClient(aliasedURL)
   289  	fatalIf(err, "unable to initialize connection.")
   290  
   291  	var sourceBucket string
   292  	switch c := client.(type) {
   293  	case *S3Client:
   294  		sourceBucket, _ = c.url2BucketAndObject()
   295  	default:
   296  		fatalIf(err.Trace(args...), "replication is not supported for filesystem")
   297  	}
   298  	// Create a new MinIO Admin Client
   299  	admclient, cerr := newAdminClient(aliasedURL)
   300  	fatalIf(cerr, "unable to initialize admin connection.")
   301  
   302  	bktTarget := fetchRemoteTarget(cliCtx)
   303  	arn, e := admclient.SetRemoteTarget(globalContext, sourceBucket, bktTarget)
   304  	fatalIf(probe.NewError(e).Trace(args...), "unable to configure remote target")
   305  
   306  	rcfg, err := client.GetReplication(ctx)
   307  	fatalIf(err.Trace(args...), "unable to fetch replication configuration")
   308  
   309  	ruleStatus := enableStatus
   310  	if cliCtx.Bool(disableStatus) {
   311  		ruleStatus = disableStatus
   312  	}
   313  	dmReplicateStatus := disableStatus
   314  	deleteReplicationStatus := disableStatus
   315  	replicaSync := enableStatus
   316  	existingReplicationStatus := disableStatus
   317  	replSlice := strings.Split(cliCtx.String("replicate"), ",")
   318  	for _, opt := range replSlice {
   319  		switch strings.TrimSpace(strings.ToLower(opt)) {
   320  		case "delete-marker":
   321  			dmReplicateStatus = enableStatus
   322  		case "delete":
   323  			deleteReplicationStatus = enableStatus
   324  		case "metadata-sync", "replica-metadata-sync":
   325  			replicaSync = enableStatus
   326  		case "existing-objects":
   327  			existingReplicationStatus = enableStatus
   328  		default:
   329  			fatalIf(probe.NewError(fmt.Errorf("invalid value for --replicate flag %s", cliCtx.String("replicate"))),
   330  				`--replicate flag takes one or more comma separated string with values "delete", "delete-marker", "metadata-sync", "existing-objects" or "" to disable these settings`)
   331  		}
   332  	}
   333  
   334  	opts := replication.Options{
   335  		TagString:               cliCtx.String("tags"),
   336  		StorageClass:            cliCtx.String("storage-class"),
   337  		Priority:                strconv.Itoa(cliCtx.Int("priority")),
   338  		RuleStatus:              ruleStatus,
   339  		ID:                      cliCtx.String("id"),
   340  		DestBucket:              arn,
   341  		Op:                      replication.AddOption,
   342  		ReplicateDeleteMarkers:  dmReplicateStatus,
   343  		ReplicateDeletes:        deleteReplicationStatus,
   344  		ReplicaSync:             replicaSync,
   345  		ExistingObjectReplicate: existingReplicationStatus,
   346  	}
   347  	fatalIf(client.SetReplication(ctx, &rcfg, opts), "unable to add replication rule")
   348  
   349  	printMsg(replicateAddMessage{
   350  		Op:  cliCtx.Command.Name,
   351  		URL: aliasedURL,
   352  		ID:  opts.ID,
   353  	})
   354  	return nil
   355  }