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 }