storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/bucket-targets.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2020 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package cmd 18 19 import ( 20 "context" 21 "crypto/sha256" 22 "encoding/hex" 23 "encoding/json" 24 "net/http" 25 "strings" 26 "sync" 27 "sync/atomic" 28 "time" 29 30 minio "github.com/minio/minio-go/v7" 31 miniogo "github.com/minio/minio-go/v7" 32 "github.com/minio/minio-go/v7/pkg/credentials" 33 34 "storj.io/minio/cmd/crypto" 35 "storj.io/minio/cmd/logger" 36 "storj.io/minio/pkg/bucket/versioning" 37 "storj.io/minio/pkg/madmin" 38 ) 39 40 const ( 41 defaultHealthCheckDuration = 100 * time.Second 42 ) 43 44 // BucketTargetSys represents bucket targets subsystem 45 type BucketTargetSys struct { 46 sync.RWMutex 47 arnRemotesMap map[string]*TargetClient 48 targetsMap map[string][]madmin.BucketTarget 49 } 50 51 // ListTargets lists bucket targets across tenant or for individual bucket, and returns 52 // results filtered by arnType 53 func (sys *BucketTargetSys) ListTargets(ctx context.Context, bucket, arnType string) (targets []madmin.BucketTarget) { 54 if bucket != "" { 55 if ts, err := sys.ListBucketTargets(ctx, bucket); err == nil { 56 for _, t := range ts.Targets { 57 if string(t.Type) == arnType || arnType == "" { 58 targets = append(targets, t.Clone()) 59 } 60 } 61 } 62 return targets 63 } 64 sys.RLock() 65 defer sys.RUnlock() 66 for _, tgts := range sys.targetsMap { 67 for _, t := range tgts { 68 if string(t.Type) == arnType || arnType == "" { 69 targets = append(targets, t.Clone()) 70 } 71 } 72 } 73 return 74 } 75 76 // ListBucketTargets - gets list of bucket targets for this bucket. 77 func (sys *BucketTargetSys) ListBucketTargets(ctx context.Context, bucket string) (*madmin.BucketTargets, error) { 78 sys.RLock() 79 defer sys.RUnlock() 80 81 tgts, ok := sys.targetsMap[bucket] 82 if ok { 83 return &madmin.BucketTargets{Targets: tgts}, nil 84 } 85 return nil, BucketRemoteTargetNotFound{Bucket: bucket} 86 } 87 88 // SetTarget - sets a new minio-go client target for this bucket. 89 func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *madmin.BucketTarget, update bool) error { 90 if GlobalIsGateway { 91 return nil 92 } 93 if !tgt.Type.IsValid() && !update { 94 return BucketRemoteArnTypeInvalid{Bucket: bucket} 95 } 96 clnt, err := sys.getRemoteTargetClient(tgt) 97 if err != nil { 98 return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket} 99 } 100 // validate if target credentials are ok 101 if _, err = clnt.BucketExists(ctx, tgt.TargetBucket); err != nil { 102 if minio.ToErrorResponse(err).Code == "NoSuchBucket" { 103 return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket} 104 } 105 return BucketRemoteConnectionErr{Bucket: tgt.TargetBucket, Err: err} 106 } 107 if tgt.Type == madmin.ReplicationService { 108 if !globalIsErasure { 109 return NotImplemented{Message: "Replication is not implemented in " + getMinioMode()} 110 } 111 if !globalBucketVersioningSys.Enabled(bucket) { 112 return BucketReplicationSourceNotVersioned{Bucket: bucket} 113 } 114 vcfg, err := clnt.GetBucketVersioning(ctx, tgt.TargetBucket) 115 if err != nil { 116 return BucketRemoteConnectionErr{Bucket: tgt.TargetBucket, Err: err} 117 } 118 if vcfg.Status != string(versioning.Enabled) { 119 return BucketRemoteTargetNotVersioned{Bucket: tgt.TargetBucket} 120 } 121 if tgt.ReplicationSync && tgt.BandwidthLimit > 0 { 122 return NotImplemented{Message: "Synchronous replication does not support bandwidth limits"} 123 } 124 } 125 if tgt.Type == madmin.ILMService { 126 if globalBucketVersioningSys.Enabled(bucket) { 127 vcfg, err := clnt.GetBucketVersioning(ctx, tgt.TargetBucket) 128 if err != nil { 129 if minio.ToErrorResponse(err).Code == "NoSuchBucket" { 130 return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket} 131 } 132 return BucketRemoteConnectionErr{Bucket: tgt.TargetBucket, Err: err} 133 } 134 if vcfg.Status != string(versioning.Enabled) { 135 return BucketRemoteTargetNotVersioned{Bucket: tgt.TargetBucket} 136 } 137 } 138 } 139 sys.Lock() 140 defer sys.Unlock() 141 142 tgts := sys.targetsMap[bucket] 143 newtgts := make([]madmin.BucketTarget, len(tgts)) 144 labels := make(map[string]struct{}, len(tgts)) 145 found := false 146 for idx, t := range tgts { 147 labels[t.Label] = struct{}{} 148 if t.Type == tgt.Type { 149 if t.Arn == tgt.Arn && !update { 150 return BucketRemoteAlreadyExists{Bucket: t.TargetBucket} 151 } 152 if t.Label == tgt.Label && !update { 153 return BucketRemoteLabelInUse{Bucket: t.TargetBucket} 154 } 155 newtgts[idx] = *tgt 156 found = true 157 continue 158 } 159 newtgts[idx] = t 160 } 161 if _, ok := labels[tgt.Label]; ok && !update { 162 return BucketRemoteLabelInUse{Bucket: tgt.TargetBucket} 163 } 164 if !found && !update { 165 newtgts = append(newtgts, *tgt) 166 } 167 168 sys.targetsMap[bucket] = newtgts 169 sys.arnRemotesMap[tgt.Arn] = clnt 170 return nil 171 } 172 173 // RemoveTarget - removes a remote bucket target for this source bucket. 174 func (sys *BucketTargetSys) RemoveTarget(ctx context.Context, bucket, arnStr string) error { 175 if GlobalIsGateway { 176 return nil 177 } 178 if arnStr == "" { 179 return BucketRemoteArnInvalid{Bucket: bucket} 180 } 181 arn, err := madmin.ParseARN(arnStr) 182 if err != nil { 183 return BucketRemoteArnInvalid{Bucket: bucket} 184 } 185 if arn.Type == madmin.ReplicationService { 186 if !globalIsErasure { 187 return NotImplemented{Message: "Replication is not implemented in " + getMinioMode()} 188 } 189 // reject removal of remote target if replication configuration is present 190 rcfg, err := getReplicationConfig(ctx, bucket) 191 if err == nil && rcfg.RoleArn == arnStr { 192 if _, ok := sys.arnRemotesMap[arnStr]; ok { 193 return BucketRemoteRemoveDisallowed{Bucket: bucket} 194 } 195 } 196 } 197 if arn.Type == madmin.ILMService { 198 // reject removal of remote target if lifecycle transition uses this arn 199 config, err := globalBucketMetadataSys.GetLifecycleConfig(bucket) 200 if err == nil && transitionSCInUse(ctx, config, bucket, arnStr) { 201 if _, ok := sys.arnRemotesMap[arnStr]; ok { 202 return BucketRemoteRemoveDisallowed{Bucket: bucket} 203 } 204 } 205 } 206 207 // delete ARN type from list of matching targets 208 sys.Lock() 209 defer sys.Unlock() 210 found := false 211 tgts, ok := sys.targetsMap[bucket] 212 if !ok { 213 return BucketRemoteTargetNotFound{Bucket: bucket} 214 } 215 targets := make([]madmin.BucketTarget, 0, len(tgts)) 216 for _, tgt := range tgts { 217 if tgt.Arn != arnStr { 218 targets = append(targets, tgt) 219 continue 220 } 221 found = true 222 } 223 if !found { 224 return BucketRemoteTargetNotFound{Bucket: bucket} 225 } 226 sys.targetsMap[bucket] = targets 227 delete(sys.arnRemotesMap, arnStr) 228 return nil 229 } 230 231 // GetRemoteTargetClient returns minio-go client for replication target instance 232 func (sys *BucketTargetSys) GetRemoteTargetClient(ctx context.Context, arn string) *TargetClient { 233 sys.RLock() 234 defer sys.RUnlock() 235 return sys.arnRemotesMap[arn] 236 } 237 238 // GetRemoteTargetWithLabel returns bucket target given a target label 239 func (sys *BucketTargetSys) GetRemoteTargetWithLabel(ctx context.Context, bucket, targetLabel string) *madmin.BucketTarget { 240 sys.RLock() 241 defer sys.RUnlock() 242 for _, t := range sys.targetsMap[bucket] { 243 if strings.ToUpper(t.Label) == strings.ToUpper(targetLabel) { 244 tgt := t.Clone() 245 return &tgt 246 } 247 } 248 return nil 249 } 250 251 // GetRemoteArnWithLabel returns bucket target's ARN given its target label 252 func (sys *BucketTargetSys) GetRemoteArnWithLabel(ctx context.Context, bucket, tgtLabel string) *madmin.ARN { 253 tgt := sys.GetRemoteTargetWithLabel(ctx, bucket, tgtLabel) 254 if tgt == nil { 255 return nil 256 } 257 arn, err := madmin.ParseARN(tgt.Arn) 258 if err != nil { 259 return nil 260 } 261 return arn 262 } 263 264 // GetRemoteLabelWithArn returns a bucket target's label given its ARN 265 func (sys *BucketTargetSys) GetRemoteLabelWithArn(ctx context.Context, bucket, arnStr string) string { 266 sys.RLock() 267 defer sys.RUnlock() 268 for _, t := range sys.targetsMap[bucket] { 269 if t.Arn == arnStr { 270 return t.Label 271 } 272 } 273 return "" 274 } 275 276 // NewBucketTargetSys - creates new replication system. 277 func NewBucketTargetSys() *BucketTargetSys { 278 return &BucketTargetSys{ 279 arnRemotesMap: make(map[string]*TargetClient), 280 targetsMap: make(map[string][]madmin.BucketTarget), 281 } 282 } 283 284 // Init initializes the bucket targets subsystem for buckets which have targets configured. 285 func (sys *BucketTargetSys) Init(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) error { 286 if objAPI == nil { 287 return errServerNotInitialized 288 } 289 290 // In gateway mode, bucket targets is not supported. 291 if GlobalIsGateway { 292 return nil 293 } 294 295 // Load bucket targets once during boot in background. 296 go sys.load(ctx, buckets, objAPI) 297 return nil 298 } 299 300 // UpdateAllTargets updates target to reflect metadata updates 301 func (sys *BucketTargetSys) UpdateAllTargets(bucket string, tgts *madmin.BucketTargets) { 302 if sys == nil { 303 return 304 } 305 sys.Lock() 306 defer sys.Unlock() 307 if tgts == nil || tgts.Empty() { 308 // remove target and arn association 309 if tgts, ok := sys.targetsMap[bucket]; ok { 310 for _, t := range tgts { 311 delete(sys.arnRemotesMap, t.Arn) 312 } 313 } 314 delete(sys.targetsMap, bucket) 315 return 316 } 317 318 if len(tgts.Targets) > 0 { 319 sys.targetsMap[bucket] = tgts.Targets 320 } 321 for _, tgt := range tgts.Targets { 322 tgtClient, err := sys.getRemoteTargetClient(&tgt) 323 if err != nil { 324 continue 325 } 326 sys.arnRemotesMap[tgt.Arn] = tgtClient 327 } 328 sys.targetsMap[bucket] = tgts.Targets 329 } 330 331 // create minio-go clients for buckets having remote targets 332 func (sys *BucketTargetSys) load(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) { 333 for _, bucket := range buckets { 334 cfg, err := globalBucketMetadataSys.GetBucketTargetsConfig(bucket.Name) 335 if err != nil { 336 logger.LogIf(ctx, err) 337 continue 338 } 339 if cfg == nil || cfg.Empty() { 340 continue 341 } 342 if len(cfg.Targets) > 0 { 343 sys.targetsMap[bucket.Name] = cfg.Targets 344 } 345 for _, tgt := range cfg.Targets { 346 tgtClient, err := sys.getRemoteTargetClient(&tgt) 347 if err != nil { 348 logger.LogIf(ctx, err) 349 continue 350 } 351 sys.arnRemotesMap[tgt.Arn] = tgtClient 352 } 353 sys.targetsMap[bucket.Name] = cfg.Targets 354 } 355 } 356 357 // getRemoteTargetInstanceTransport contains a singleton roundtripper. 358 var getRemoteTargetInstanceTransport http.RoundTripper 359 var getRemoteTargetInstanceTransportOnce sync.Once 360 361 // Returns a minio-go Client configured to access remote host described in replication target config. 362 func (sys *BucketTargetSys) getRemoteTargetClient(tcfg *madmin.BucketTarget) (*TargetClient, error) { 363 config := tcfg.Credentials 364 creds := credentials.NewStaticV4(config.AccessKey, config.SecretKey, "") 365 366 getRemoteTargetInstanceTransportOnce.Do(func() { 367 getRemoteTargetInstanceTransport = NewRemoteTargetHTTPTransport() 368 }) 369 api, err := minio.New(tcfg.Endpoint, &miniogo.Options{ 370 Creds: creds, 371 Secure: tcfg.Secure, 372 Region: tcfg.Region, 373 Transport: getRemoteTargetInstanceTransport, 374 }) 375 if err != nil { 376 return nil, err 377 } 378 hcDuration := defaultHealthCheckDuration 379 if tcfg.HealthCheckDuration >= 1 { // require minimum health check duration of 1 sec. 380 hcDuration = tcfg.HealthCheckDuration 381 } 382 tc := &TargetClient{ 383 Client: api, 384 healthCheckDuration: hcDuration, 385 bucket: tcfg.TargetBucket, 386 replicateSync: tcfg.ReplicationSync, 387 } 388 go tc.healthCheck() 389 return tc, nil 390 } 391 392 // getRemoteARN gets existing ARN for an endpoint or generates a new one. 393 func (sys *BucketTargetSys) getRemoteARN(bucket string, target *madmin.BucketTarget) string { 394 if target == nil { 395 return "" 396 } 397 tgts := sys.targetsMap[bucket] 398 for _, tgt := range tgts { 399 if tgt.Type == target.Type && tgt.TargetBucket == target.TargetBucket && target.URL().String() == tgt.URL().String() { 400 return tgt.Arn 401 } 402 } 403 if !madmin.ServiceType(target.Type).IsValid() { 404 return "" 405 } 406 return generateARN(target) 407 } 408 409 // generate ARN that is unique to this target type 410 func generateARN(t *madmin.BucketTarget) string { 411 hash := sha256.New() 412 hash.Write([]byte(t.Type)) 413 hash.Write([]byte(t.Region)) 414 hash.Write([]byte(t.TargetBucket)) 415 hashSum := hex.EncodeToString(hash.Sum(nil)) 416 arn := madmin.ARN{ 417 Type: t.Type, 418 ID: hashSum, 419 Region: t.Region, 420 Bucket: t.TargetBucket, 421 } 422 return arn.String() 423 } 424 425 // Returns parsed target config. If KMS is configured, remote target is decrypted 426 func parseBucketTargetConfig(bucket string, cdata, cmetadata []byte) (*madmin.BucketTargets, error) { 427 var ( 428 data []byte 429 err error 430 t madmin.BucketTargets 431 meta map[string]string 432 ) 433 if len(cdata) == 0 { 434 return nil, nil 435 } 436 data = cdata 437 if len(cmetadata) != 0 { 438 if err := json.Unmarshal(cmetadata, &meta); err != nil { 439 return nil, err 440 } 441 if crypto.S3.IsEncrypted(meta) { 442 if data, err = decryptBucketMetadata(cdata, bucket, meta, crypto.Context{ 443 bucket: bucket, 444 bucketTargetsFile: bucketTargetsFile, 445 }); err != nil { 446 return nil, err 447 } 448 } 449 } 450 451 if err = json.Unmarshal(data, &t); err != nil { 452 return nil, err 453 } 454 return &t, nil 455 } 456 457 // TargetClient is the struct for remote target client. 458 type TargetClient struct { 459 *miniogo.Client 460 up int32 461 healthCheckDuration time.Duration 462 bucket string // remote bucket target 463 replicateSync bool 464 } 465 466 func (tc *TargetClient) isOffline() bool { 467 return atomic.LoadInt32(&tc.up) == 0 468 } 469 470 func (tc *TargetClient) healthCheck() { 471 for { 472 _, err := tc.BucketExists(GlobalContext, tc.bucket) 473 if err != nil { 474 atomic.StoreInt32(&tc.up, 0) 475 time.Sleep(tc.healthCheckDuration) 476 continue 477 } 478 atomic.StoreInt32(&tc.up, 1) 479 time.Sleep(tc.healthCheckDuration) 480 } 481 }