github.com/minio/madmin-go/v2@v2.2.1/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/ioutil" 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:"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 } 110 111 // Clone returns shallow clone of BucketTarget without secret key in credentials 112 func (t *BucketTarget) Clone() BucketTarget { 113 return BucketTarget{ 114 SourceBucket: t.SourceBucket, 115 Endpoint: t.Endpoint, 116 TargetBucket: t.TargetBucket, 117 Credentials: &Credentials{AccessKey: t.Credentials.AccessKey}, 118 Secure: t.Secure, 119 Path: t.Path, 120 API: t.API, 121 Arn: t.Arn, 122 Type: t.Type, 123 Region: t.Region, 124 BandwidthLimit: t.BandwidthLimit, 125 ReplicationSync: t.ReplicationSync, 126 StorageClass: t.StorageClass, // target storage class 127 HealthCheckDuration: t.HealthCheckDuration, 128 DisableProxy: t.DisableProxy, 129 ResetBeforeDate: t.ResetBeforeDate, 130 ResetID: t.ResetID, 131 TotalDowntime: t.TotalDowntime, 132 LastOnline: t.LastOnline, 133 Online: t.Online, 134 } 135 } 136 137 // URL returns target url 138 func (t BucketTarget) URL() *url.URL { 139 scheme := "http" 140 if t.Secure { 141 scheme = "https" 142 } 143 return &url.URL{ 144 Scheme: scheme, 145 Host: t.Endpoint, 146 } 147 } 148 149 // Empty returns true if struct is empty. 150 func (t BucketTarget) Empty() bool { 151 return t.String() == "" || t.Credentials == nil 152 } 153 154 func (t *BucketTarget) String() string { 155 return fmt.Sprintf("%s %s", t.Endpoint, t.TargetBucket) 156 } 157 158 // BucketTargets represents a slice of bucket targets by type and endpoint 159 type BucketTargets struct { 160 Targets []BucketTarget 161 } 162 163 // Empty returns true if struct is empty. 164 func (t BucketTargets) Empty() bool { 165 if len(t.Targets) == 0 { 166 return true 167 } 168 empty := true 169 for _, t := range t.Targets { 170 if !t.Empty() { 171 return false 172 } 173 } 174 return empty 175 } 176 177 // ListRemoteTargets - gets target(s) for this bucket 178 func (adm *AdminClient) ListRemoteTargets(ctx context.Context, bucket, arnType string) (targets []BucketTarget, err error) { 179 queryValues := url.Values{} 180 queryValues.Set("bucket", bucket) 181 queryValues.Set("type", arnType) 182 183 reqData := requestData{ 184 relPath: adminAPIPrefix + "/list-remote-targets", 185 queryValues: queryValues, 186 } 187 188 // Execute GET on /minio/admin/v3/list-remote-targets 189 resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) 190 191 defer closeResponse(resp) 192 if err != nil { 193 return targets, err 194 } 195 196 if resp.StatusCode != http.StatusOK { 197 return targets, httpRespToErrorResponse(resp) 198 } 199 200 b, err := ioutil.ReadAll(resp.Body) 201 if err != nil { 202 return targets, err 203 } 204 if err = json.Unmarshal(b, &targets); err != nil { 205 return targets, err 206 } 207 return targets, nil 208 } 209 210 // SetRemoteTarget sets up a remote target for this bucket 211 func (adm *AdminClient) SetRemoteTarget(ctx context.Context, bucket string, target *BucketTarget) (string, error) { 212 data, err := json.Marshal(target) 213 if err != nil { 214 return "", err 215 } 216 encData, err := EncryptData(adm.getSecretKey(), data) 217 if err != nil { 218 return "", err 219 } 220 queryValues := url.Values{} 221 queryValues.Set("bucket", bucket) 222 223 reqData := requestData{ 224 relPath: adminAPIPrefix + "/set-remote-target", 225 queryValues: queryValues, 226 content: encData, 227 } 228 229 // Execute PUT on /minio/admin/v3/set-remote-target to set a target for this bucket of specific arn type. 230 resp, err := adm.executeMethod(ctx, http.MethodPut, reqData) 231 232 defer closeResponse(resp) 233 if err != nil { 234 return "", err 235 } 236 237 if resp.StatusCode != http.StatusOK { 238 return "", httpRespToErrorResponse(resp) 239 } 240 b, err := ioutil.ReadAll(resp.Body) 241 if err != nil { 242 return "", err 243 } 244 var arn string 245 if err = json.Unmarshal(b, &arn); err != nil { 246 return "", err 247 } 248 return arn, nil 249 } 250 251 // TargetUpdateType - type of update on the remote target 252 type TargetUpdateType int 253 254 const ( 255 // CredentialsUpdateType update creds 256 CredentialsUpdateType TargetUpdateType = 1 + iota 257 // SyncUpdateType update synchronous replication setting 258 SyncUpdateType 259 // ProxyUpdateType update proxy setting 260 ProxyUpdateType 261 // BandwidthLimitUpdateType update bandwidth limit 262 BandwidthLimitUpdateType 263 // HealthCheckDurationUpdateType update health check duration 264 HealthCheckDurationUpdateType 265 // PathUpdateType update Path 266 PathUpdateType 267 // ResetUpdateType sets ResetBeforeDate and ResetID on a bucket target 268 ResetUpdateType 269 ) 270 271 // GetTargetUpdateOps returns a slice of update operations being 272 // performed with `mc admin bucket remote edit` 273 func GetTargetUpdateOps(values url.Values) []TargetUpdateType { 274 var ops []TargetUpdateType 275 if values.Get("update") != "true" { 276 return ops 277 } 278 if values.Get("creds") == "true" { 279 ops = append(ops, CredentialsUpdateType) 280 } 281 if values.Get("sync") == "true" { 282 ops = append(ops, SyncUpdateType) 283 } 284 if values.Get("proxy") == "true" { 285 ops = append(ops, ProxyUpdateType) 286 } 287 if values.Get("healthcheck") == "true" { 288 ops = append(ops, HealthCheckDurationUpdateType) 289 } 290 if values.Get("bandwidth") == "true" { 291 ops = append(ops, BandwidthLimitUpdateType) 292 } 293 if values.Get("path") == "true" { 294 ops = append(ops, PathUpdateType) 295 } 296 return ops 297 } 298 299 // UpdateRemoteTarget updates credentials for a remote bucket target 300 func (adm *AdminClient) UpdateRemoteTarget(ctx context.Context, target *BucketTarget, ops ...TargetUpdateType) (string, error) { 301 if target == nil { 302 return "", fmt.Errorf("target cannot be nil") 303 } 304 data, err := json.Marshal(target) 305 if err != nil { 306 return "", err 307 } 308 encData, err := EncryptData(adm.getSecretKey(), data) 309 if err != nil { 310 return "", err 311 } 312 queryValues := url.Values{} 313 queryValues.Set("bucket", target.SourceBucket) 314 queryValues.Set("update", "true") 315 316 for _, op := range ops { 317 switch op { 318 case CredentialsUpdateType: 319 queryValues.Set("creds", "true") 320 case SyncUpdateType: 321 queryValues.Set("sync", "true") 322 case ProxyUpdateType: 323 queryValues.Set("proxy", "true") 324 case BandwidthLimitUpdateType: 325 queryValues.Set("bandwidth", "true") 326 case HealthCheckDurationUpdateType: 327 queryValues.Set("healthcheck", "true") 328 case PathUpdateType: 329 queryValues.Set("path", "true") 330 } 331 } 332 333 reqData := requestData{ 334 relPath: adminAPIPrefix + "/set-remote-target", 335 queryValues: queryValues, 336 content: encData, 337 } 338 339 // Execute PUT on /minio/admin/v3/set-remote-target to set a target for this bucket of specific arn type. 340 resp, err := adm.executeMethod(ctx, http.MethodPut, reqData) 341 342 defer closeResponse(resp) 343 if err != nil { 344 return "", err 345 } 346 347 if resp.StatusCode != http.StatusOK { 348 return "", httpRespToErrorResponse(resp) 349 } 350 b, err := ioutil.ReadAll(resp.Body) 351 if err != nil { 352 return "", err 353 } 354 var arn string 355 if err = json.Unmarshal(b, &arn); err != nil { 356 return "", err 357 } 358 return arn, nil 359 } 360 361 // RemoveRemoteTarget removes a remote target associated with particular ARN for this bucket 362 func (adm *AdminClient) RemoveRemoteTarget(ctx context.Context, bucket, arn string) error { 363 queryValues := url.Values{} 364 queryValues.Set("bucket", bucket) 365 queryValues.Set("arn", arn) 366 367 reqData := requestData{ 368 relPath: adminAPIPrefix + "/remove-remote-target", 369 queryValues: queryValues, 370 } 371 372 // Execute PUT on /minio/admin/v3/remove-remote-target to remove a target for this bucket 373 // with specific ARN 374 resp, err := adm.executeMethod(ctx, http.MethodDelete, reqData) 375 defer closeResponse(resp) 376 if err != nil { 377 return err 378 } 379 380 if resp.StatusCode != http.StatusNoContent { 381 return httpRespToErrorResponse(resp) 382 } 383 return nil 384 }