/* * MinIO Cloud Storage, (C) 2020 MinIO, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cmd import ( "context" "fmt" "net/http" "strings" "sync" "time" miniogo "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/encrypt" "github.com/minio/minio-go/v7/pkg/tags" "github.com/minio/minio/cmd/crypto" xhttp "github.com/minio/minio/cmd/http" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/bucket/replication" "github.com/minio/minio/pkg/bucket/versioning" "github.com/minio/minio/pkg/event" iampolicy "github.com/minio/minio/pkg/iam/policy" "github.com/minio/minio/pkg/madmin" ) // BucketReplicationSys represents replication subsystem type BucketReplicationSys struct { sync.RWMutex targetsMap map[string]*miniogo.Core targetsARNMap map[string]string } // GetConfig - gets replication config associated to a given bucket name. func (sys *BucketReplicationSys) GetConfig(ctx context.Context, bucketName string) (rc *replication.Config, err error) { if globalIsGateway { objAPI := newObjectLayerWithoutSafeModeFn() if objAPI == nil { return nil, errServerNotInitialized } return nil, BucketReplicationConfigNotFound{Bucket: bucketName} } return globalBucketMetadataSys.GetReplicationConfig(ctx, bucketName) } // SetTarget - sets a new minio-go client replication target for this bucket. func (sys *BucketReplicationSys) SetTarget(ctx context.Context, bucket string, tgt *madmin.BucketReplicationTarget) error { if globalIsGateway { return nil } // delete replication targets that were removed if tgt.Empty() { sys.Lock() if currTgt, ok := sys.targetsMap[bucket]; ok { delete(sys.targetsARNMap, currTgt.EndpointURL().String()) } delete(sys.targetsMap, bucket) sys.Unlock() return nil } clnt, err := getReplicationTargetClient(tgt) if err != nil { return BucketReplicationTargetNotFound{Bucket: tgt.TargetBucket} } ok, err := clnt.BucketExists(ctx, tgt.TargetBucket) if err != nil { return err } if !ok { return BucketReplicationDestinationNotFound{Bucket: tgt.TargetBucket} } vcfg, err := clnt.GetBucketVersioning(ctx, tgt.TargetBucket) if err != nil || vcfg.Status != string(versioning.Enabled) { return BucketReplicationTargetNotVersioned{Bucket: tgt.TargetBucket} } sys.Lock() sys.targetsMap[bucket] = clnt sys.targetsARNMap[tgt.URL()] = tgt.Arn sys.Unlock() return nil } // GetTargetClient returns minio-go client for target instance func (sys *BucketReplicationSys) GetTargetClient(ctx context.Context, bucket string) *miniogo.Core { var clnt *miniogo.Core sys.RLock() if c, ok := sys.targetsMap[bucket]; ok { clnt = c } sys.RUnlock() return clnt } // validateDestination returns error if replication destination bucket missing or not configured // It also returns true if replication destination is same as this server. func (sys *BucketReplicationSys) validateDestination(ctx context.Context, bucket string, rCfg *replication.Config) (bool, error) { clnt := sys.GetTargetClient(ctx, bucket) if clnt == nil { return false, BucketReplicationTargetNotFound{Bucket: bucket} } if found, _ := clnt.BucketExists(ctx, rCfg.GetDestination().Bucket); !found { return false, BucketReplicationDestinationNotFound{Bucket: rCfg.GetDestination().Bucket} } // validate replication ARN against target endpoint for k, v := range sys.targetsARNMap { if v == rCfg.ReplicationArn { if k == clnt.EndpointURL().String() { sameTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort) return sameTarget, nil } } } return false, BucketReplicationTargetNotFound{Bucket: bucket} } // NewBucketReplicationSys - creates new replication system. func NewBucketReplicationSys() *BucketReplicationSys { return &BucketReplicationSys{ targetsMap: make(map[string]*miniogo.Core), targetsARNMap: make(map[string]string), } } // Init initializes the bucket replication subsystem for buckets with replication config func (sys *BucketReplicationSys) Init(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) error { if objAPI == nil { return errServerNotInitialized } // In gateway mode, replication is not supported. if globalIsGateway { return nil } // Load bucket replication targets once during boot. sys.load(ctx, buckets, objAPI) return nil } // create minio-go clients for buckets having replication targets func (sys *BucketReplicationSys) load(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) { for _, bucket := range buckets { tgt, err := globalBucketMetadataSys.GetReplicationTargetConfig(bucket.Name) if err != nil { continue } if tgt == nil || tgt.Empty() { continue } tgtClient, err := getReplicationTargetClient(tgt) if err != nil { continue } sys.Lock() sys.targetsMap[bucket.Name] = tgtClient sys.targetsARNMap[tgt.URL()] = tgt.Arn sys.Unlock() } } // GetARN returns the ARN associated with replication target URL func (sys *BucketReplicationSys) getARN(endpoint string) string { return sys.targetsARNMap[endpoint] } // getReplicationTargetInstanceTransport contains a singleton roundtripper. var getReplicationTargetInstanceTransport http.RoundTripper var getReplicationTargetInstanceTransportOnce sync.Once // Returns a minio-go Client configured to access remote host described in replication target config. var getReplicationTargetClient = func(tcfg *madmin.BucketReplicationTarget) (*miniogo.Core, error) { config := tcfg.Credentials // if Signature version '4' use NewV4 directly. creds := credentials.NewStaticV4(config.AccessKey, config.SecretKey, "") // if Signature version '2' use NewV2 directly. if strings.ToUpper(tcfg.API) == "S3V2" { creds = credentials.NewStaticV2(config.AccessKey, config.SecretKey, "") } getReplicationTargetInstanceTransportOnce.Do(func() { getReplicationTargetInstanceTransport = NewGatewayHTTPTransport() }) core, err := miniogo.NewCore(tcfg.Endpoint, &miniogo.Options{ Creds: creds, Secure: tcfg.IsSSL, Transport: getReplicationTargetInstanceTransport, }) return core, err } // mustReplicate returns true if object meets replication criteria. func (sys *BucketReplicationSys) mustReplicate(ctx context.Context, r *http.Request, bucket, object string, meta map[string]string, replStatus string) bool { if globalIsGateway { return false } if rs, ok := meta[xhttp.AmzBucketReplicationStatus]; ok { replStatus = rs } if replication.StatusType(replStatus) == replication.Replica { return false } if s3Err := isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.GetReplicationConfigurationAction); s3Err != ErrNone { return false } cfg, err := globalBucketReplicationSys.GetConfig(ctx, bucket) if err != nil { return false } opts := replication.ObjectOpts{ Name: object, SSEC: crypto.SSEC.IsEncrypted(meta), } tagStr, ok := meta[xhttp.AmzObjectTagging] if ok { opts.UserTags = tagStr } return cfg.Replicate(opts) } func putReplicationOpts(dest replication.Destination, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions) { meta := make(map[string]string) for k, v := range objInfo.UserDefined { if k == xhttp.AmzBucketReplicationStatus { continue } meta[k] = v } tag, err := tags.ParseObjectTags(objInfo.UserTags) if err != nil { return } putOpts = miniogo.PutObjectOptions{ UserMetadata: meta, UserTags: tag.ToMap(), ContentType: objInfo.ContentType, ContentEncoding: objInfo.ContentEncoding, StorageClass: dest.StorageClass, ReplicationVersionID: objInfo.VersionID, ReplicationStatus: miniogo.ReplicationStatusReplica, ReplicationMTime: objInfo.ModTime, } if mode, ok := objInfo.UserDefined[xhttp.AmzObjectLockMode]; ok { rmode := miniogo.RetentionMode(mode) putOpts.Mode = rmode } if retainDateStr, ok := objInfo.UserDefined[xhttp.AmzObjectLockRetainUntilDate]; ok { rdate, err := time.Parse(time.RFC3339, retainDateStr) if err != nil { return } putOpts.RetainUntilDate = rdate } if lhold, ok := objInfo.UserDefined[xhttp.AmzObjectLockLegalHold]; ok { putOpts.LegalHold = miniogo.LegalHoldStatus(lhold) } if crypto.S3.IsEncrypted(objInfo.UserDefined) { putOpts.ServerSideEncryption = encrypt.NewSSE() } return } // replicateObject replicates the specified version of the object to destination bucket // The source object is then updated to reflect the replication status. func replicateObject(ctx context.Context, bucket, object, versionID string, objectAPI ObjectLayer, eventArg *eventArgs, healPending bool) { cfg, err := globalBucketReplicationSys.GetConfig(ctx, bucket) if err != nil { logger.LogIf(ctx, err) return } tgt := globalBucketReplicationSys.GetTargetClient(ctx, bucket) if tgt == nil { return } gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, readLock, ObjectOptions{}) if err != nil { return } defer gr.Close() objInfo := gr.ObjInfo size, err := objInfo.GetActualSize() if err != nil { logger.LogIf(ctx, err) return } dest := cfg.GetDestination() if dest.Bucket == "" { return } // In the rare event that replication is in pending state either due to // server shut down/crash before replication completed or healing and PutObject // race - do an additional stat to see if the version ID exists if healPending { _, err := tgt.StatObject(ctx, dest.Bucket, object, miniogo.StatObjectOptions{VersionID: objInfo.VersionID}) if err == nil { // object with same VersionID already exists, replication kicked off by // PutObject might have completed. return } } putOpts := putReplicationOpts(dest, objInfo) replicationStatus := replication.Complete _, err = tgt.PutObject(ctx, dest.Bucket, object, gr, size, "", "", putOpts) if err != nil { replicationStatus = replication.Failed // Notify replication failure event. if eventArg == nil { eventArg = &eventArgs{ BucketName: bucket, Object: objInfo, Host: "Internal: [Replication]", } } eventArg.EventName = event.OperationReplicationFailed eventArg.Object.UserDefined[xhttp.AmzBucketReplicationStatus] = replicationStatus.String() sendEvent(*eventArg) } objInfo.UserDefined[xhttp.AmzBucketReplicationStatus] = replicationStatus.String() if objInfo.UserTags != "" { objInfo.UserDefined[xhttp.AmzObjectTagging] = objInfo.UserTags } objInfo.metadataOnly = true // Perform only metadata updates. if _, err = objectAPI.CopyObject(ctx, bucket, object, bucket, object, objInfo, ObjectOptions{ VersionID: objInfo.VersionID, }, ObjectOptions{VersionID: objInfo.VersionID}); err != nil { logger.LogIf(ctx, err) } } // getReplicationARN gets existing ARN for an endpoint or generates a new one. func (sys *BucketReplicationSys) getReplicationARN(endpoint string) string { arn, ok := sys.targetsARNMap[endpoint] if ok { return arn } return fmt.Sprintf("arn:minio:s3::%s:*", mustGetUUID()) }