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 }