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  }