github.com/minio/madmin-go/v3@v3.0.51/remote-target-commands.go (about) 1 // 2 // Copyright (c) 2015-2022 MinIO, Inc. 3 // 4 // This file is part of MinIO Object Storage stack 5 // 6 // This program is free software: you can redistribute it and/or modify 7 // it under the terms of the GNU Affero General Public License as 8 // published by the Free Software Foundation, either version 3 of the 9 // License, or (at your option) any later version. 10 // 11 // This program is distributed in the hope that it will be useful, 12 // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 // GNU Affero General Public License for more details. 15 // 16 // You should have received a copy of the GNU Affero General Public License 17 // along with this program. If not, see <http://www.gnu.org/licenses/>. 18 // 19 20 package madmin 21 22 import ( 23 "context" 24 "encoding/json" 25 "fmt" 26 "io" 27 "net/http" 28 "net/url" 29 "strings" 30 "time" 31 ) 32 33 // ServiceType represents service type 34 type ServiceType string 35 36 const ( 37 // ReplicationService specifies replication service 38 ReplicationService ServiceType = "replication" 39 ) 40 41 // IsValid returns true if ARN type represents replication 42 func (t ServiceType) IsValid() bool { 43 return t == ReplicationService 44 } 45 46 // ARN is a struct to define arn. 47 type ARN struct { 48 Type ServiceType 49 ID string 50 Region string 51 Bucket string 52 } 53 54 // Empty returns true if arn struct is empty 55 func (a ARN) Empty() bool { 56 return !a.Type.IsValid() 57 } 58 59 func (a ARN) String() string { 60 return fmt.Sprintf("arn:minio:%s:%s:%s:%s", a.Type, a.Region, a.ID, a.Bucket) 61 } 62 63 // ParseARN return ARN struct from string in arn format. 64 func ParseARN(s string) (*ARN, error) { 65 // ARN must be in the format of arn:minio:<Type>:<REGION>:<ID>:<remote-bucket> 66 if !strings.HasPrefix(s, "arn:minio:") { 67 return nil, fmt.Errorf("Invalid ARN %s", s) 68 } 69 70 tokens := strings.Split(s, ":") 71 if len(tokens) != 6 { 72 return nil, fmt.Errorf("Invalid ARN %s", s) 73 } 74 75 if tokens[4] == "" || tokens[5] == "" { 76 return nil, fmt.Errorf("Invalid ARN %s", s) 77 } 78 79 return &ARN{ 80 Type: ServiceType(tokens[2]), 81 Region: tokens[3], 82 ID: tokens[4], 83 Bucket: tokens[5], 84 }, nil 85 } 86 87 // BucketTarget represents the target bucket and site association. 88 type BucketTarget struct { 89 SourceBucket string `json:"sourcebucket"` 90 Endpoint string `json:"endpoint"` 91 Credentials *Credentials `json:"credentials"` 92 TargetBucket string `json:"targetbucket"` 93 Secure bool `json:"secure"` 94 Path string `json:"path,omitempty"` 95 API string `json:"api,omitempty"` 96 Arn string `json:"arn,omitempty"` 97 Type ServiceType `json:"type"` 98 Region string `json:"region,omitempty"` 99 BandwidthLimit int64 `json:"bandwidthlimit,omitempty"` 100 ReplicationSync bool `json:"replicationSync"` 101 StorageClass string `json:"storageclass,omitempty"` 102 HealthCheckDuration time.Duration `json:"healthCheckDuration,omitempty"` 103 DisableProxy bool `json:"disableProxy"` 104 ResetBeforeDate time.Time `json:"resetBeforeDate,omitempty"` 105 ResetID string `json:"resetID,omitempty"` 106 TotalDowntime time.Duration `json:"totalDowntime"` 107 LastOnline time.Time `json:"lastOnline"` 108 Online bool `json:"isOnline"` 109 Latency LatencyStat `json:"latency"` 110 DeploymentID string `json:"deploymentID,omitempty"` 111 } 112 113 // Clone returns shallow clone of BucketTarget without secret key in credentials 114 func (t *BucketTarget) Clone() BucketTarget { 115 return BucketTarget{ 116 SourceBucket: t.SourceBucket, 117 Endpoint: t.Endpoint, 118 TargetBucket: t.TargetBucket, 119 Credentials: &Credentials{AccessKey: t.Credentials.AccessKey}, 120 Secure: t.Secure, 121 Path: t.Path, 122 API: t.API, 123 Arn: t.Arn, 124 Type: t.Type, 125 Region: t.Region, 126 BandwidthLimit: t.BandwidthLimit, 127 ReplicationSync: t.ReplicationSync, 128 StorageClass: t.StorageClass, // target storage class 129 HealthCheckDuration: t.HealthCheckDuration, 130 DisableProxy: t.DisableProxy, 131 ResetBeforeDate: t.ResetBeforeDate, 132 ResetID: t.ResetID, 133 TotalDowntime: t.TotalDowntime, 134 LastOnline: t.LastOnline, 135 Online: t.Online, 136 Latency: t.Latency, 137 DeploymentID: t.DeploymentID, 138 } 139 } 140 141 // URL returns target url 142 func (t BucketTarget) URL() *url.URL { 143 scheme := "http" 144 if t.Secure { 145 scheme = "https" 146 } 147 return &url.URL{ 148 Scheme: scheme, 149 Host: t.Endpoint, 150 } 151 } 152 153 // Empty returns true if struct is empty. 154 func (t BucketTarget) Empty() bool { 155 return t.String() == "" || t.Credentials == nil 156 } 157 158 func (t *BucketTarget) String() string { 159 return fmt.Sprintf("%s %s", t.Endpoint, t.TargetBucket) 160 } 161 162 // BucketTargets represents a slice of bucket targets by type and endpoint 163 type BucketTargets struct { 164 Targets []BucketTarget 165 } 166 167 // Empty returns true if struct is empty. 168 func (t BucketTargets) Empty() bool { 169 if len(t.Targets) == 0 { 170 return true 171 } 172 empty := true 173 for _, t := range t.Targets { 174 if !t.Empty() { 175 return false 176 } 177 } 178 return empty 179 } 180 181 // ListRemoteTargets - gets target(s) for this bucket 182 func (adm *AdminClient) ListRemoteTargets(ctx context.Context, bucket, arnType string) (targets []BucketTarget, err error) { 183 queryValues := url.Values{} 184 queryValues.Set("bucket", bucket) 185 queryValues.Set("type", arnType) 186 187 reqData := requestData{ 188 relPath: adminAPIPrefix + "/list-remote-targets", 189 queryValues: queryValues, 190 } 191 192 // Execute GET on /minio/admin/v3/list-remote-targets 193 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 194 195 defer closeResponse(resp) 196 if err != nil { 197 return targets, err 198 } 199 200 if resp.StatusCode != http.StatusOK { 201 return targets, httpRespToErrorResponse(resp) 202 } 203 204 b, err := io.ReadAll(resp.Body) 205 if err != nil { 206 return targets, err 207 } 208 if err = json.Unmarshal(b, &targets); err != nil { 209 return targets, err 210 } 211 return targets, nil 212 } 213 214 // SetRemoteTarget sets up a remote target for this bucket 215 func (adm *AdminClient) SetRemoteTarget(ctx context.Context, bucket string, target *BucketTarget) (string, error) { 216 data, err := json.Marshal(target) 217 if err != nil { 218 return "", err 219 } 220 encData, err := EncryptData(adm.getSecretKey(), data) 221 if err != nil { 222 return "", err 223 } 224 queryValues := url.Values{} 225 queryValues.Set("bucket", bucket) 226 227 reqData := requestData{ 228 relPath: adminAPIPrefix + "/set-remote-target", 229 queryValues: queryValues, 230 content: encData, 231 } 232 233 // Execute PUT on /minio/admin/v3/set-remote-target to set a target for this bucket of specific arn type. 234 resp, err := adm.executeMethod(ctx, http.MethodPut, reqData) 235 236 defer closeResponse(resp) 237 if err != nil { 238 return "", err 239 } 240 241 if resp.StatusCode != http.StatusOK { 242 return "", httpRespToErrorResponse(resp) 243 } 244 b, err := io.ReadAll(resp.Body) 245 if err != nil { 246 return "", err 247 } 248 var arn string 249 if err = json.Unmarshal(b, &arn); err != nil { 250 return "", err 251 } 252 return arn, nil 253 } 254 255 // TargetUpdateType - type of update on the remote target 256 type TargetUpdateType int 257 258 const ( 259 // CredentialsUpdateType update creds 260 CredentialsUpdateType TargetUpdateType = 1 + iota 261 // SyncUpdateType update synchronous replication setting 262 SyncUpdateType 263 // ProxyUpdateType update proxy setting 264 ProxyUpdateType 265 // BandwidthLimitUpdateType update bandwidth limit 266 BandwidthLimitUpdateType 267 // HealthCheckDurationUpdateType update health check duration 268 HealthCheckDurationUpdateType 269 // PathUpdateType update Path 270 PathUpdateType 271 // ResetUpdateType sets ResetBeforeDate and ResetID on a bucket target 272 ResetUpdateType 273 ) 274 275 // GetTargetUpdateOps returns a slice of update operations being 276 // performed with `mc admin bucket remote edit` 277 func GetTargetUpdateOps(values url.Values) []TargetUpdateType { 278 var ops []TargetUpdateType 279 if values.Get("update") != "true" { 280 return ops 281 } 282 if values.Get("creds") == "true" { 283 ops = append(ops, CredentialsUpdateType) 284 } 285 if values.Get("sync") == "true" { 286 ops = append(ops, SyncUpdateType) 287 } 288 if values.Get("proxy") == "true" { 289 ops = append(ops, ProxyUpdateType) 290 } 291 if values.Get("healthcheck") == "true" { 292 ops = append(ops, HealthCheckDurationUpdateType) 293 } 294 if values.Get("bandwidth") == "true" { 295 ops = append(ops, BandwidthLimitUpdateType) 296 } 297 if values.Get("path") == "true" { 298 ops = append(ops, PathUpdateType) 299 } 300 return ops 301 } 302 303 // UpdateRemoteTarget updates credentials for a remote bucket target 304 func (adm *AdminClient) UpdateRemoteTarget(ctx context.Context, target *BucketTarget, ops ...TargetUpdateType) (string, error) { 305 if target == nil { 306 return "", fmt.Errorf("target cannot be nil") 307 } 308 data, err := json.Marshal(target) 309 if err != nil { 310 return "", err 311 } 312 encData, err := EncryptData(adm.getSecretKey(), data) 313 if err != nil { 314 return "", err 315 } 316 queryValues := url.Values{} 317 queryValues.Set("bucket", target.SourceBucket) 318 queryValues.Set("update", "true") 319 320 for _, op := range ops { 321 switch op { 322 case CredentialsUpdateType: 323 queryValues.Set("creds", "true") 324 case SyncUpdateType: 325 queryValues.Set("sync", "true") 326 case ProxyUpdateType: 327 queryValues.Set("proxy", "true") 328 case BandwidthLimitUpdateType: 329 queryValues.Set("bandwidth", "true") 330 case HealthCheckDurationUpdateType: 331 queryValues.Set("healthcheck", "true") 332 case PathUpdateType: 333 queryValues.Set("path", "true") 334 } 335 } 336 337 reqData := requestData{ 338 relPath: adminAPIPrefix + "/set-remote-target", 339 queryValues: queryValues, 340 content: encData, 341 } 342 343 // Execute PUT on /minio/admin/v3/set-remote-target to set a target for this bucket of specific arn type. 344 resp, err := adm.executeMethod(ctx, http.MethodPut, reqData) 345 346 defer closeResponse(resp) 347 if err != nil { 348 return "", err 349 } 350 351 if resp.StatusCode != http.StatusOK { 352 return "", httpRespToErrorResponse(resp) 353 } 354 b, err := io.ReadAll(resp.Body) 355 if err != nil { 356 return "", err 357 } 358 var arn string 359 if err = json.Unmarshal(b, &arn); err != nil { 360 return "", err 361 } 362 return arn, nil 363 } 364 365 // RemoveRemoteTarget removes a remote target associated with particular ARN for this bucket 366 func (adm *AdminClient) RemoveRemoteTarget(ctx context.Context, bucket, arn string) error { 367 queryValues := url.Values{} 368 queryValues.Set("bucket", bucket) 369 queryValues.Set("arn", arn) 370 371 reqData := requestData{ 372 relPath: adminAPIPrefix + "/remove-remote-target", 373 queryValues: queryValues, 374 } 375 376 // Execute PUT on /minio/admin/v3/remove-remote-target to remove a target for this bucket 377 // with specific ARN 378 resp, err := adm.executeMethod(ctx, http.MethodDelete, reqData) 379 defer closeResponse(resp) 380 if err != nil { 381 return err 382 } 383 384 if resp.StatusCode != http.StatusNoContent { 385 return httpRespToErrorResponse(resp) 386 } 387 return nil 388 }