diff --git a/cmd/admin-bucket-handlers.go b/cmd/admin-bucket-handlers.go new file mode 100644 index 000000000..77ffdc495 --- /dev/null +++ b/cmd/admin-bucket-handlers.go @@ -0,0 +1,249 @@ +/* + * 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 ( + "encoding/json" + "io" + "io/ioutil" + "net/http" + + "github.com/gorilla/mux" + "github.com/minio/minio/cmd/config" + "github.com/minio/minio/cmd/logger" + "github.com/minio/minio/pkg/auth" + "github.com/minio/minio/pkg/env" + iampolicy "github.com/minio/minio/pkg/iam/policy" + "github.com/minio/minio/pkg/madmin" +) + +const ( + bucketQuotaConfigFile = "quota.json" + bucketReplicationTargetsFile = "replication-targets.json" +) + +// PutBucketQuotaConfigHandler - PUT Bucket quota configuration. +// ---------- +// Places a quota configuration on the specified bucket. The quota +// specified in the quota configuration will be applied by default +// to enforce total quota for the specified bucket. +func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "PutBucketQuotaConfig") + + defer logger.AuditLog(w, r, "PutBucketQuotaConfig", mustGetClaimsFromToken(r)) + + objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SetBucketQuotaAdminAction) + if objectAPI == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + + vars := mux.Vars(r) + bucket := vars["bucket"] + + // Turn off quota commands if data usage info is unavailable. + if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOff { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminBucketQuotaDisabled), r.URL) + return + } + + if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil { + writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL) + return + } + + data, err := ioutil.ReadAll(r.Body) + if err != nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) + return + } + + if _, err = parseBucketQuota(bucket, data); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + if err = globalBucketMetadataSys.Update(bucket, bucketQuotaConfigFile, data); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + // Write success response. + writeSuccessResponseHeadersOnly(w) +} + +// GetBucketQuotaConfigHandler - gets bucket quota configuration +func (a adminAPIHandlers) GetBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "GetBucketQuotaConfig") + + defer logger.AuditLog(w, r, "GetBucketQuotaConfig", mustGetClaimsFromToken(r)) + + objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetBucketQuotaAdminAction) + if objectAPI == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + + vars := mux.Vars(r) + bucket := vars["bucket"] + if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil { + writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL) + return + } + + config, err := globalBucketMetadataSys.GetQuotaConfig(bucket) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + configData, err := json.Marshal(config) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + // Write success response. + writeSuccessResponseJSON(w, configData) +} + +// SetBucketReplicationTargetHandler - sets a replication target for bucket +func (a adminAPIHandlers) SetBucketReplicationTargetHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "SetBucketReplicationTarget") + + defer logger.AuditLog(w, r, "SetBucketReplicationTarget", mustGetClaimsFromToken(r)) + vars := mux.Vars(r) + bucket := vars["bucket"] + + // Get current object layer instance. + objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.SetBucketReplicationTargetAction) + if objectAPI == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + // Turn off replication if disk crawl is unavailable. + if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOff { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBucketReplicationDisabledError), r.URL) + return + } + + // Check if bucket exists. + if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil { + writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL) + return + } + + if versioned := globalBucketVersioningSys.Enabled(bucket); !versioned { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrReplicationBucketNeedsVersioningError), r.URL) + return + } + + cred, _, _, s3Err := validateAdminSignature(ctx, r, "") + if s3Err != ErrNone { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL) + return + } + + password := cred.SecretKey + + reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength)) + if err != nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL) + return + } + var target madmin.BucketReplicationTarget + if err = json.Unmarshal(reqBytes, &target); err != nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL) + return + } + target.Arn = globalBucketReplicationSys.getReplicationARN(target.URL()) + tgtBytes, err := json.Marshal(&target) + if err != nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL) + return + } + if err = globalBucketMetadataSys.Update(bucket, bucketReplicationTargetsFile, tgtBytes); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + if err = globalBucketReplicationSys.SetTarget(ctx, bucket, &target); err != nil { + writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL) + return + } + + // Write success response. + writeSuccessResponseHeadersOnly(w) +} + +// GetBucketReplicationTargetsHandler - gets bucket replication targets for a particular bucket +func (a adminAPIHandlers) GetBucketReplicationTargetsHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "GetBucketReplicationTarget") + + defer logger.AuditLog(w, r, "GetBucketReplicationTarget", mustGetClaimsFromToken(r)) + vars := mux.Vars(r) + bucket := vars["bucket"] + // Get current object layer instance. + objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetBucketReplicationTargetAction) + if objectAPI == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + + target, err := globalBucketMetadataSys.GetReplicationTargetConfig(bucket) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + + // remove secretKey from creds + var tgt madmin.BucketReplicationTarget + if !target.Empty() { + var creds auth.Credentials + creds.AccessKey = target.Credentials.AccessKey + tgt = madmin.BucketReplicationTarget{Endpoint: target.Endpoint, TargetBucket: target.TargetBucket, Credentials: &creds} + + } + data, err := json.Marshal(tgt) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + // Write success response. + writeSuccessResponseJSON(w, data) +} + +// GetBucketReplicationARNHandler - gets replication ARN for a particular remote +func (a adminAPIHandlers) GetBucketReplicationARNHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "GetBucketReplicationARN") + + defer logger.AuditLog(w, r, "GetBucketReplicationARN", mustGetClaimsFromToken(r)) + vars := mux.Vars(r) + rURL := vars["url"] + // Get current object layer instance. + objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetBucketReplicationTargetAction) + if objectAPI == nil { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) + return + } + data, err := json.Marshal(globalBucketReplicationSys.getARN(rURL)) + if err != nil { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + // Write success response. + writeSuccessResponseJSON(w, data) +} diff --git a/cmd/admin-quota-handlers.go b/cmd/admin-quota-handlers.go deleted file mode 100644 index 3a65d7112..000000000 --- a/cmd/admin-quota-handlers.go +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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 ( - "encoding/json" - "io/ioutil" - "net/http" - - "github.com/gorilla/mux" - "github.com/minio/minio/cmd/config" - "github.com/minio/minio/cmd/logger" - "github.com/minio/minio/pkg/env" - iampolicy "github.com/minio/minio/pkg/iam/policy" -) - -const ( - bucketQuotaConfigFile = "quota.json" -) - -// PutBucketQuotaConfigHandler - PUT Bucket quota configuration. -// ---------- -// Places a quota configuration on the specified bucket. The quota -// specified in the quota configuration will be applied by default -// to enforce total quota for the specified bucket. -func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "PutBucketQuotaConfig") - - defer logger.AuditLog(w, r, "PutBucketQuotaConfig", mustGetClaimsFromToken(r)) - - objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SetBucketQuotaAdminAction) - if objectAPI == nil { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) - return - } - - vars := mux.Vars(r) - bucket := vars["bucket"] - - // Turn off quota commands if data usage info is unavailable. - if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOff { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminBucketQuotaDisabled), r.URL) - return - } - - if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil { - writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL) - return - } - - data, err := ioutil.ReadAll(r.Body) - if err != nil { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) - return - } - - if _, err = parseBucketQuota(bucket, data); err != nil { - writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) - return - } - - if err = globalBucketMetadataSys.Update(bucket, bucketQuotaConfigFile, data); err != nil { - writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) - return - } - - // Write success response. - writeSuccessResponseHeadersOnly(w) -} - -// GetBucketQuotaConfigHandler - gets bucket quota configuration -func (a adminAPIHandlers) GetBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "GetBucketQuotaConfig") - - defer logger.AuditLog(w, r, "GetBucketQuotaConfig", mustGetClaimsFromToken(r)) - - objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetBucketQuotaAdminAction) - if objectAPI == nil { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL) - return - } - - vars := mux.Vars(r) - bucket := vars["bucket"] - if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil { - writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL) - return - } - - config, err := globalBucketMetadataSys.GetQuotaConfig(bucket) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - configData, err := json.Marshal(config) - if err != nil { - writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) - return - } - - // Write success response. - writeSuccessResponseJSON(w, configData) -} diff --git a/cmd/admin-router.go b/cmd/admin-router.go index 2f599fe03..cbcbdb5bb 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -171,8 +171,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(httpTraceHdrs(adminAPI.SetGroupStatus)).Queries("group", "{group:.*}").Queries("status", "{status:.*}") } - // Quota operations if globalIsDistErasure || globalIsErasure { + // Quota operations if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOn { // GetBucketQuotaConfig adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc( @@ -181,6 +181,16 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc( httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}") } + // Bucket replication operations + // GetBucketReplicationTargetHandler + adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-replication-target").HandlerFunc( + httpTraceHdrs(adminAPI.GetBucketReplicationTargetsHandler)).Queries("bucket", "{bucket:.*}") + // GetBucketReplicationARN Handler + adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-replication-arn").HandlerFunc( + httpTraceHdrs(adminAPI.GetBucketReplicationARNHandler)).Queries("url", "{url:.*}") + // SetBucketReplicationTargetHandler + adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-replication-target").HandlerFunc( + httpTraceHdrs(adminAPI.SetBucketReplicationTargetHandler)).Queries("bucket", "{bucket:.*}") } // -- Top APIs -- diff --git a/cmd/api-errors.go b/cmd/api-errors.go index 71f72ccff..39b445d3d 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -34,6 +34,8 @@ import ( "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/bucket/lifecycle" + "github.com/minio/minio/pkg/bucket/replication" + objectlock "github.com/minio/minio/pkg/bucket/object/lock" "github.com/minio/minio/pkg/bucket/policy" "github.com/minio/minio/pkg/bucket/versioning" @@ -104,6 +106,12 @@ const ( ErrNoSuchCORSConfiguration ErrNoSuchWebsiteConfiguration ErrReplicationConfigurationNotFoundError + ErrReplicationDestinationNotFoundError + ErrReplicationTargetNotFoundError + + ErrReplicationNeedsVersioningError + ErrReplicationBucketNeedsVersioningError + ErrBucketReplicationDisabledError ErrNoSuchKey ErrNoSuchUpload ErrNoSuchVersion @@ -812,6 +820,31 @@ var errorCodes = errorCodeMap{ Description: "The replication configuration was not found", HTTPStatusCode: http.StatusNotFound, }, + ErrReplicationDestinationNotFoundError: { + Code: "ReplicationDestinationNotFoundError", + Description: "The replication destination bucket does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrReplicationTargetNotFoundError: { + Code: "ReplicationTargetNotFoundError", + Description: "The replication target does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrReplicationNeedsVersioningError: { + Code: "InvalidRequest", + Description: "Versioning must be 'Enabled' on the bucket to apply a replication configuration", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrReplicationBucketNeedsVersioningError: { + Code: "InvalidRequest", + Description: "Versioning must be 'Enabled' on the bucket to add a replication target", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBucketReplicationDisabledError: { + Code: "XMinioAdminBucketReplicationDisabled", + Description: "Replication specified but disk usage crawl is disabled on MinIO server", + HTTPStatusCode: http.StatusBadRequest, + }, ErrNoSuchObjectLockConfiguration: { Code: "NoSuchObjectLockConfiguration", Description: "The specified object does not have a ObjectLock configuration", @@ -1837,6 +1870,12 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) { apiErr = ErrObjectLockConfigurationNotFound case BucketQuotaConfigNotFound: apiErr = ErrAdminNoSuchQuotaConfiguration + case BucketReplicationConfigNotFound: + apiErr = ErrReplicationConfigurationNotFoundError + case BucketReplicationDestinationNotFound: + apiErr = ErrReplicationDestinationNotFoundError + case BucketReplicationTargetNotFound: + apiErr = ErrReplicationTargetNotFoundError case BucketQuotaExceeded: apiErr = ErrAdminBucketQuotaExceeded case *event.ErrInvalidEventName: @@ -1941,6 +1980,12 @@ func toAPIError(ctx context.Context, err error) APIError { Description: e.Error(), HTTPStatusCode: http.StatusBadRequest, } + case replication.Error: + apiErr = APIError{ + Code: "MalformedXML", + Description: e.Error(), + HTTPStatusCode: http.StatusBadRequest, + } case tags.Error: apiErr = APIError{ Code: e.Code(), diff --git a/cmd/api-headers.go b/cmd/api-headers.go index 39274d344..293ce5169 100644 --- a/cmd/api-headers.go +++ b/cmd/api-headers.go @@ -157,7 +157,9 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp if objInfo.VersionID != "" { w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID} } - + if objInfo.ReplicationStatus.String() != "" { + w.Header()[xhttp.AmzBucketReplicationStatus] = []string{objInfo.ReplicationStatus.String()} + } if lc, err := globalLifecycleSys.Get(objInfo.Bucket); err == nil { ruleID, expiryTime := lc.PredictExpiryTime(lifecycle.ObjectOpts{ Name: objInfo.Name, diff --git a/cmd/api-router.go b/cmd/api-router.go index 1626c208d..6d01c6d5d 100644 --- a/cmd/api-router.go +++ b/cmd/api-router.go @@ -166,6 +166,21 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool) // GetBucketEncryption bucket.Methods(http.MethodGet).HandlerFunc( maxClients(collectAPIStats("getbucketencryption", httpTraceAll(api.GetBucketEncryptionHandler)))).Queries("encryption", "") + // GetBucketObjectLockConfig + bucket.Methods(http.MethodGet).HandlerFunc( + maxClients(collectAPIStats("getbucketobjectlockconfiguration", httpTraceAll(api.GetBucketObjectLockConfigHandler)))).Queries("object-lock", "") + // GetBucketReplicationConfig + bucket.Methods(http.MethodGet).HandlerFunc( + maxClients(collectAPIStats("getbucketreplicationconfiguration", httpTraceAll(api.GetBucketReplicationConfigHandler)))).Queries("replication", "") + + // GetBucketVersioning + bucket.Methods(http.MethodGet).HandlerFunc( + maxClients(collectAPIStats("getbucketversioning", httpTraceAll(api.GetBucketVersioningHandler)))).Queries("versioning", "") + // GetBucketNotification + bucket.Methods(http.MethodGet).HandlerFunc( + maxClients(collectAPIStats("getbucketnotification", httpTraceAll(api.GetBucketNotificationHandler)))).Queries("notification", "") + // ListenNotification + bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listennotification", httpTraceAll(api.ListenNotificationHandler))).Queries("events", "{events:.*}") // Dummy Bucket Calls // GetBucketACL -- this is a dummy call. @@ -192,9 +207,6 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool) // GetBucketLifecycleHandler - this is a dummy call. bucket.Methods(http.MethodGet).HandlerFunc( maxClients(collectAPIStats("getbucketlifecycle", httpTraceAll(api.GetBucketLifecycleHandler)))).Queries("lifecycle", "") - // GetBucketReplicationHandler - this is a dummy call. - bucket.Methods(http.MethodGet).HandlerFunc( - maxClients(collectAPIStats("getbucketreplication", httpTraceAll(api.GetBucketReplicationHandler)))).Queries("replication", "") // GetBucketTaggingHandler bucket.Methods(http.MethodGet).HandlerFunc( maxClients(collectAPIStats("getbuckettagging", httpTraceAll(api.GetBucketTaggingHandler)))).Queries("tagging", "") @@ -205,17 +217,6 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool) bucket.Methods(http.MethodDelete).HandlerFunc( maxClients(collectAPIStats("deletebuckettagging", httpTraceAll(api.DeleteBucketTaggingHandler)))).Queries("tagging", "") - // GetBucketObjectLockConfig - bucket.Methods(http.MethodGet).HandlerFunc( - maxClients(collectAPIStats("getbucketobjectlockconfiguration", httpTraceAll(api.GetBucketObjectLockConfigHandler)))).Queries("object-lock", "") - // GetBucketVersioning - bucket.Methods(http.MethodGet).HandlerFunc( - maxClients(collectAPIStats("getbucketversioning", httpTraceAll(api.GetBucketVersioningHandler)))).Queries("versioning", "") - // GetBucketNotification - bucket.Methods(http.MethodGet).HandlerFunc( - maxClients(collectAPIStats("getbucketnotification", httpTraceAll(api.GetBucketNotificationHandler)))).Queries("notification", "") - // ListenNotification - bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listennotification", httpTraceAll(api.ListenNotificationHandler))).Queries("events", "{events:.*}") // ListMultipartUploads bucket.Methods(http.MethodGet).HandlerFunc( maxClients(collectAPIStats("listmultipartuploads", httpTraceAll(api.ListMultipartUploadsHandler)))).Queries("uploads", "") @@ -234,6 +235,11 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool) // PutBucketLifecycle bucket.Methods(http.MethodPut).HandlerFunc( maxClients(collectAPIStats("putbucketlifecycle", httpTraceAll(api.PutBucketLifecycleHandler)))).Queries("lifecycle", "") + // PutBucketReplicationConfig + bucket.Methods(http.MethodPut).HandlerFunc( + maxClients(collectAPIStats("putbucketreplicationconfiguration", httpTraceAll(api.PutBucketReplicationConfigHandler)))).Queries("replication", "") + // GetObjectRetention + // PutBucketEncryption bucket.Methods(http.MethodPut).HandlerFunc( maxClients(collectAPIStats("putbucketencryption", httpTraceAll(api.PutBucketEncryptionHandler)))).Queries("encryption", "") @@ -269,6 +275,9 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool) // DeleteBucketPolicy bucket.Methods(http.MethodDelete).HandlerFunc( maxClients(collectAPIStats("deletebucketpolicy", httpTraceAll(api.DeleteBucketPolicyHandler)))).Queries("policy", "") + // DeleteBucketReplication + bucket.Methods(http.MethodDelete).HandlerFunc( + maxClients(collectAPIStats("deletebucketreplicationconfiguration", httpTraceAll(api.DeleteBucketReplicationConfigHandler)))).Queries("replication", "") // DeleteBucketLifecycle bucket.Methods(http.MethodDelete).HandlerFunc( maxClients(collectAPIStats("deletebucketlifecycle", httpTraceAll(api.DeleteBucketLifecycleHandler)))).Queries("lifecycle", "") diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go index bb861a9ae..335b601ff 100644 --- a/cmd/bucket-handlers.go +++ b/cmd/bucket-handlers.go @@ -32,12 +32,15 @@ import ( "github.com/minio/minio-go/v7/pkg/set" "github.com/minio/minio-go/v7/pkg/tags" + "github.com/minio/minio/cmd/config" "github.com/minio/minio/cmd/config/etcd/dns" "github.com/minio/minio/cmd/crypto" xhttp "github.com/minio/minio/cmd/http" "github.com/minio/minio/cmd/logger" objectlock "github.com/minio/minio/pkg/bucket/object/lock" "github.com/minio/minio/pkg/bucket/policy" + "github.com/minio/minio/pkg/bucket/replication" + "github.com/minio/minio/pkg/env" "github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/handlers" "github.com/minio/minio/pkg/hash" @@ -46,8 +49,9 @@ import ( ) const ( - objectLockConfig = "object-lock.xml" - bucketTaggingConfig = "tagging.xml" + objectLockConfig = "object-lock.xml" + bucketTaggingConfig = "tagging.xml" + bucketReplicationConfig = "replication.xml" ) // Check if there are buckets on server without corresponding entry in etcd backend and @@ -1219,3 +1223,143 @@ func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r // Write success response. writeSuccessResponseHeadersOnly(w) } + +// PutBucketReplicationConfigHandler - PUT Bucket replication configuration. +// ---------- +// Add a replication configuration on the specified bucket as specified in https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketReplication.html +func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "PutBucketReplicationConfig") + defer logger.AuditLog(w, r, "PutBucketReplicationConfig", mustGetClaimsFromToken(r)) + + vars := mux.Vars(r) + bucket := vars["bucket"] + objectAPI := api.ObjectAPI() + if objectAPI == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r)) + return + } + // Turn off replication if disk crawl is unavailable. + if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOff { + writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBucketReplicationDisabledError), r.URL) + return + } + if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) + return + } + // Check if bucket exists. + if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + if versioned := globalBucketVersioningSys.Enabled(bucket); !versioned { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNeedsVersioningError), r.URL, guessIsBrowserReq(r)) + return + } + replicationConfig, err := replication.ParseConfig(io.LimitReader(r.Body, r.ContentLength)) + if err != nil { + apiErr := errorCodes.ToAPIErr(ErrMalformedXML) + apiErr.Description = err.Error() + writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r)) + return + } + sameTarget, err := globalBucketReplicationSys.validateDestination(ctx, bucket, replicationConfig) + if err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + // Validate the received bucket replication config + if err = replicationConfig.Validate(bucket, sameTarget); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + configData, err := xml.Marshal(replicationConfig) + if err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + if err = globalBucketMetadataSys.Update(bucket, bucketReplicationConfig, configData); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + // Write success response. + writeSuccessResponseHeadersOnly(w) +} + +// GetBucketReplicationConfigHandler - GET Bucket replication configuration. +// ---------- +// Gets the replication configuration for a bucket. +func (api objectAPIHandlers) GetBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "GetBucketReplicationConfig") + + defer logger.AuditLog(w, r, "GetBucketReplicationConfig", mustGetClaimsFromToken(r)) + + vars := mux.Vars(r) + bucket := vars["bucket"] + + objectAPI := api.ObjectAPI() + if objectAPI == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r)) + return + } + + // check if user has permissions to perform this operation + if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) + return + } + // Check if bucket exists. + if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + config, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket) + if err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + configData, err := xml.Marshal(config) + if err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + // Write success response. + writeSuccessResponseXML(w, configData) +} + +// DeleteBucketReplicationConfigHandler - DELETE Bucket replication config. +// ---------- +func (api objectAPIHandlers) DeleteBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) { + ctx := newContext(r, w, "DeleteBucketReplicationConfig") + defer logger.AuditLog(w, r, "DeleteBucketReplicationConfig", mustGetClaimsFromToken(r)) + vars := mux.Vars(r) + bucket := vars["bucket"] + + objectAPI := api.ObjectAPI() + if objectAPI == nil { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r)) + return + } + + if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) + return + } + // Check if bucket exists. + if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + if err := globalBucketMetadataSys.Update(bucket, bucketReplicationConfig, nil); err != nil { + writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) + return + } + + // Write success response. + writeSuccessResponseHeadersOnly(w) +} diff --git a/cmd/bucket-metadata-sys.go b/cmd/bucket-metadata-sys.go index dbac1633b..9a94643b5 100644 --- a/cmd/bucket-metadata-sys.go +++ b/cmd/bucket-metadata-sys.go @@ -28,6 +28,7 @@ import ( "github.com/minio/minio/pkg/bucket/lifecycle" objectlock "github.com/minio/minio/pkg/bucket/object/lock" "github.com/minio/minio/pkg/bucket/policy" + "github.com/minio/minio/pkg/bucket/replication" "github.com/minio/minio/pkg/bucket/versioning" "github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/madmin" @@ -153,6 +154,10 @@ func (sys *BucketMetadataSys) Update(bucket string, configFile string, configDat meta.VersioningConfigXML = configData case bucketQuotaConfigFile: meta.QuotaConfigJSON = configData + case bucketReplicationConfig: + meta.ReplicationConfigXML = configData + case bucketReplicationTargetsFile: + meta.ReplicationTargetsConfigJSON = configData default: return fmt.Errorf("Unknown bucket %s metadata update requested %s", bucket, configFile) } @@ -318,7 +323,37 @@ func (sys *BucketMetadataSys) GetQuotaConfig(bucket string) (*madmin.BucketQuota return meta.quotaConfig, nil } -// GetConfig returns the current bucket metadata +// GetReplicationConfig returns configured bucket replication config +// The returned object may not be modified. +func (sys *BucketMetadataSys) GetReplicationConfig(ctx context.Context, bucket string) (*replication.Config, error) { + meta, err := sys.GetConfig(bucket) + if err != nil { + if errors.Is(err, errConfigNotFound) { + return nil, BucketReplicationConfigNotFound{Bucket: bucket} + } + return nil, err + } + + if meta.replicationConfig == nil { + return nil, BucketReplicationConfigNotFound{Bucket: bucket} + } + return meta.replicationConfig, nil +} + +// GetReplicationTargetConfig returns configured bucket replication target for this bucket +// The returned object may not be modified. +func (sys *BucketMetadataSys) GetReplicationTargetConfig(bucket string) (*madmin.BucketReplicationTarget, error) { + meta, err := sys.GetConfig(bucket) + if err != nil { + return nil, err + } + if meta.replicationTargetConfig == nil { + return nil, BucketReplicationTargetNotFound{Bucket: bucket} + } + return meta.replicationTargetConfig, nil +} + +// GetConfig returns a specific configuration from the bucket metadata. // The returned object may not be modified. func (sys *BucketMetadataSys) GetConfig(bucket string) (BucketMetadata, error) { objAPI := newObjectLayerWithoutSafeModeFn() diff --git a/cmd/bucket-metadata.go b/cmd/bucket-metadata.go index 118e20960..199a0d243 100644 --- a/cmd/bucket-metadata.go +++ b/cmd/bucket-metadata.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "encoding/binary" + "encoding/json" "encoding/xml" "errors" "fmt" @@ -32,6 +33,7 @@ import ( "github.com/minio/minio/pkg/bucket/lifecycle" objectlock "github.com/minio/minio/pkg/bucket/object/lock" "github.com/minio/minio/pkg/bucket/policy" + "github.com/minio/minio/pkg/bucket/replication" "github.com/minio/minio/pkg/bucket/versioning" "github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/madmin" @@ -59,27 +61,31 @@ var ( // bucketMetadataFormat refers to the format. // bucketMetadataVersion can be used to track a rolling upgrade of a field. type BucketMetadata struct { - Name string - Created time.Time - LockEnabled bool // legacy not used anymore. - PolicyConfigJSON []byte - NotificationConfigXML []byte - LifecycleConfigXML []byte - ObjectLockConfigXML []byte - VersioningConfigXML []byte - EncryptionConfigXML []byte - TaggingConfigXML []byte - QuotaConfigJSON []byte + Name string + Created time.Time + LockEnabled bool // legacy not used anymore. + PolicyConfigJSON []byte + NotificationConfigXML []byte + LifecycleConfigXML []byte + ObjectLockConfigXML []byte + VersioningConfigXML []byte + EncryptionConfigXML []byte + TaggingConfigXML []byte + QuotaConfigJSON []byte + ReplicationConfigXML []byte + ReplicationTargetsConfigJSON []byte // Unexported fields. Must be updated atomically. - policyConfig *policy.Policy - notificationConfig *event.Config - lifecycleConfig *lifecycle.Lifecycle - objectLockConfig *objectlock.Config - versioningConfig *versioning.Versioning - sseConfig *bucketsse.BucketSSEConfig - taggingConfig *tags.Tags - quotaConfig *madmin.BucketQuota + policyConfig *policy.Policy + notificationConfig *event.Config + lifecycleConfig *lifecycle.Lifecycle + objectLockConfig *objectlock.Config + versioningConfig *versioning.Versioning + sseConfig *bucketsse.BucketSSEConfig + taggingConfig *tags.Tags + quotaConfig *madmin.BucketQuota + replicationConfig *replication.Config + replicationTargetConfig *madmin.BucketReplicationTarget } // newBucketMetadata creates BucketMetadata with the supplied name and Created to Now. @@ -94,6 +100,7 @@ func newBucketMetadata(name string) BucketMetadata { versioningConfig: &versioning.Versioning{ XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/", }, + replicationTargetConfig: &madmin.BucketReplicationTarget{}, } } @@ -119,7 +126,6 @@ func (b *BucketMetadata) Load(ctx context.Context, api ObjectLayer, name string) default: return fmt.Errorf("loadBucketMetadata: unknown version: %d", binary.LittleEndian.Uint16(data[2:4])) } - // OK, parse data. _, err = b.UnmarshalMsg(data[4:]) return err @@ -136,7 +142,6 @@ func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket strin if err != errConfigNotFound { return b, err } - // Old bucket without bucket metadata. Hence we migrate existing settings. return b, b.convertLegacyConfigs(ctx, objectAPI) } @@ -213,6 +218,22 @@ func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLa } } + if len(b.ReplicationConfigXML) != 0 { + b.replicationConfig, err = replication.ParseConfig(bytes.NewReader(b.ReplicationConfigXML)) + if err != nil { + return err + } + } else { + b.replicationConfig = nil + } + + if len(b.ReplicationTargetsConfigJSON) != 0 { + if err = json.Unmarshal(b.ReplicationTargetsConfigJSON, b.replicationTargetConfig); err != nil { + return err + } + } else { + b.replicationTargetConfig = &madmin.BucketReplicationTarget{} + } return nil } @@ -225,6 +246,8 @@ func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI Obj bucketQuotaConfigFile, bucketSSEConfig, bucketTaggingConfig, + bucketReplicationConfig, + bucketReplicationTargetsFile, objectLockConfig, } @@ -281,6 +304,10 @@ func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI Obj b.VersioningConfigXML = enabledBucketVersioningConfig case bucketQuotaConfigFile: b.QuotaConfigJSON = configData + case bucketReplicationConfig: + b.ReplicationConfigXML = configData + case bucketReplicationTargetsFile: + b.ReplicationTargetsConfigJSON = configData } } @@ -315,7 +342,6 @@ func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error { if err != nil { return err } - configFile := path.Join(bucketConfigPrefix, b.Name, bucketMetadataFile) return saveConfig(ctx, api, configFile, data) } diff --git a/cmd/bucket-metadata_gen.go b/cmd/bucket-metadata_gen.go index a331be67c..cd78d0523 100644 --- a/cmd/bucket-metadata_gen.go +++ b/cmd/bucket-metadata_gen.go @@ -90,6 +90,18 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) { err = msgp.WrapError(err, "QuotaConfigJSON") return } + case "ReplicationConfigXML": + z.ReplicationConfigXML, err = dc.ReadBytes(z.ReplicationConfigXML) + if err != nil { + err = msgp.WrapError(err, "ReplicationConfigXML") + return + } + case "ReplicationTargetsConfigJSON": + z.ReplicationTargetsConfigJSON, err = dc.ReadBytes(z.ReplicationTargetsConfigJSON) + if err != nil { + err = msgp.WrapError(err, "ReplicationTargetsConfigJSON") + return + } default: err = dc.Skip() if err != nil { @@ -103,9 +115,9 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) { // EncodeMsg implements msgp.Encodable func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) { - // map header, size 11 + // map header, size 13 // write "Name" - err = en.Append(0x8b, 0xa4, 0x4e, 0x61, 0x6d, 0x65) + err = en.Append(0x8d, 0xa4, 0x4e, 0x61, 0x6d, 0x65) if err != nil { return } @@ -214,15 +226,35 @@ func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) { err = msgp.WrapError(err, "QuotaConfigJSON") return } + // write "ReplicationConfigXML" + err = en.Append(0xb4, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c) + if err != nil { + return + } + err = en.WriteBytes(z.ReplicationConfigXML) + if err != nil { + err = msgp.WrapError(err, "ReplicationConfigXML") + return + } + // write "ReplicationTargetsConfigJSON" + err = en.Append(0xbc, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e) + if err != nil { + return + } + err = en.WriteBytes(z.ReplicationTargetsConfigJSON) + if err != nil { + err = msgp.WrapError(err, "ReplicationTargetsConfigJSON") + return + } return } // MarshalMsg implements msgp.Marshaler func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) - // map header, size 11 + // map header, size 13 // string "Name" - o = append(o, 0x8b, 0xa4, 0x4e, 0x61, 0x6d, 0x65) + o = append(o, 0x8d, 0xa4, 0x4e, 0x61, 0x6d, 0x65) o = msgp.AppendString(o, z.Name) // string "Created" o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64) @@ -254,6 +286,12 @@ func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) { // string "QuotaConfigJSON" o = append(o, 0xaf, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e) o = msgp.AppendBytes(o, z.QuotaConfigJSON) + // string "ReplicationConfigXML" + o = append(o, 0xb4, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c) + o = msgp.AppendBytes(o, z.ReplicationConfigXML) + // string "ReplicationTargetsConfigJSON" + o = append(o, 0xbc, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e) + o = msgp.AppendBytes(o, z.ReplicationTargetsConfigJSON) return } @@ -341,6 +379,18 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "QuotaConfigJSON") return } + case "ReplicationConfigXML": + z.ReplicationConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.ReplicationConfigXML) + if err != nil { + err = msgp.WrapError(err, "ReplicationConfigXML") + return + } + case "ReplicationTargetsConfigJSON": + z.ReplicationTargetsConfigJSON, bts, err = msgp.ReadBytesBytes(bts, z.ReplicationTargetsConfigJSON) + if err != nil { + err = msgp.WrapError(err, "ReplicationTargetsConfigJSON") + return + } default: bts, err = msgp.Skip(bts) if err != nil { @@ -355,6 +405,6 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *BucketMetadata) Msgsize() (s int) { - s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 29 + msgp.BytesPrefixSize + len(z.ReplicationTargetsConfigJSON) return } diff --git a/cmd/bucket-replication.go b/cmd/bucket-replication.go new file mode 100644 index 000000000..e943c14af --- /dev/null +++ b/cmd/bucket-replication.go @@ -0,0 +1,350 @@ +/* + * 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/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} + } + 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()) +} diff --git a/cmd/data-crawler.go b/cmd/data-crawler.go index af35b5ab0..bd9560167 100644 --- a/cmd/data-crawler.go +++ b/cmd/data-crawler.go @@ -30,6 +30,7 @@ import ( "github.com/minio/minio/cmd/config" "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/bucket/lifecycle" + "github.com/minio/minio/pkg/bucket/replication" "github.com/minio/minio/pkg/color" "github.com/minio/minio/pkg/env" "github.com/minio/minio/pkg/event" @@ -314,7 +315,6 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo filter = nil } } - if _, ok := f.oldCache.Cache[thisHash.Key()]; filter != nil && ok { // If folder isn't in filter and we have data, skip it completely. if folder.name != dataUsageRoot && !filter.containsDir(folder.name) { @@ -637,3 +637,14 @@ func sleepDuration(d time.Duration, x float64) { time.Sleep(d) } } + +// healReplication will heal a scanned item that has failed replication. +func (i *crawlItem) healReplication(ctx context.Context, o ObjectLayer, meta actionMeta) { + if meta.oi.ReplicationStatus == replication.Pending || + meta.oi.ReplicationStatus == replication.Failed { + // if heal encounters a pending replication status, either replication + // has failed due to server shutdown or crawler and PutObject replication are in contention. + healPending := meta.oi.ReplicationStatus == replication.Pending + replicateObject(ctx, meta.oi.Bucket, meta.oi.Name, meta.oi.VersionID, o, nil, healPending) + } +} diff --git a/cmd/dummy-handlers.go b/cmd/dummy-handlers.go index 2d33e7182..7460db38d 100644 --- a/cmd/dummy-handlers.go +++ b/cmd/dummy-handlers.go @@ -160,38 +160,6 @@ func (api objectAPIHandlers) GetBucketLoggingHandler(w http.ResponseWriter, r *h writeSuccessResponseXML(w, []byte(loggingDefaultConfig)) } -// GetBucketReplicationHandler - GET bucket replication, a dummy api -func (api objectAPIHandlers) GetBucketReplicationHandler(w http.ResponseWriter, r *http.Request) { - ctx := newContext(r, w, "GetBucketReplication") - - defer logger.AuditLog(w, r, "GetBucketReplication", mustGetClaimsFromToken(r)) - - vars := mux.Vars(r) - bucket := vars["bucket"] - - objAPI := api.ObjectAPI() - if objAPI == nil { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r)) - return - } - - // Allow getBucketCors if policy action is set, since this is a dummy call - // we are simply re-purposing the bucketPolicyAction. - if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone { - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r)) - return - } - - // Validate if bucket exists, before proceeding further... - _, err := objAPI.GetBucketInfo(ctx, bucket) - if err != nil { - writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r)) - return - } - - writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationConfigurationNotFoundError), r.URL, guessIsBrowserReq(r)) -} - // DeleteBucketWebsiteHandler - DELETE bucket website, a dummy api func (api objectAPIHandlers) DeleteBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) { writeSuccessResponseHeadersOnly(w) diff --git a/cmd/erasure-metadata.go b/cmd/erasure-metadata.go index 661032586..2692a42a9 100644 --- a/cmd/erasure-metadata.go +++ b/cmd/erasure-metadata.go @@ -26,6 +26,7 @@ import ( 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/sync/errgroup" "github.com/minio/sha256-simd" ) @@ -131,6 +132,9 @@ func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo { // Add user tags to the object info objInfo.UserTags = fi.Metadata[xhttp.AmzObjectTagging] + // Add replication status to the object info + objInfo.ReplicationStatus = replication.StatusType(fi.Metadata[xhttp.AmzBucketReplicationStatus]) + // etag/md5Sum has already been extracted. We need to // remove to avoid it from appearing as part of // response headers. e.g, X-Minio-* or X-Amz-*. @@ -146,7 +150,6 @@ func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo { } else { objInfo.StorageClass = globalMinioDefaultStorageClass } - // Success. return objInfo } diff --git a/cmd/erasure-object.go b/cmd/erasure-object.go index d1c334353..40e720229 100644 --- a/cmd/erasure-object.go +++ b/cmd/erasure-object.go @@ -69,12 +69,10 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d if !srcInfo.metadataOnly { return oi, NotImplemented{} } - defer ObjectPathUpdated(path.Join(dstBucket, dstObject)) // Read metadata associated with the object from all disks. storageDisks := er.getDisks() - metaArr, errs := readAllFileInfo(ctx, storageDisks, srcBucket, srcObject, srcOpts.VersionID) // get Quorum for this object diff --git a/cmd/gateway/s3/gateway-s3.go b/cmd/gateway/s3/gateway-s3.go index d947a2f81..37db6e276 100644 --- a/cmd/gateway/s3/gateway-s3.go +++ b/cmd/gateway/s3/gateway-s3.go @@ -471,12 +471,10 @@ func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string, ServerSideEncryption: opts.ServerSideEncryption, UserTags: tagMap, } - ui, err := l.Client.PutObject(ctx, bucket, object, data, data.Size(), data.MD5Base64String(), data.SHA256HexString(), putOpts) if err != nil { return objInfo, minio.ErrorRespToObjectError(err, bucket, object) } - // On success, populate the key & metadata so they are present in the notification oi := miniogo.ObjectInfo{ ETag: ui.ETag, @@ -712,7 +710,6 @@ func (l *s3Objects) DeleteBucketPolicy(ctx context.Context, bucket string) error // GetObjectTags gets the tags set on the object func (l *s3Objects) GetObjectTags(ctx context.Context, bucket string, object string, opts minio.ObjectOptions) (*tags.Tags, error) { var err error - if _, err = l.GetObjectInfo(ctx, bucket, object, opts); err != nil { return nil, minio.ErrorRespToObjectError(err, bucket, object) } diff --git a/cmd/generic-handlers.go b/cmd/generic-handlers.go index 099617f97..209d866a2 100644 --- a/cmd/generic-handlers.go +++ b/cmd/generic-handlers.go @@ -406,7 +406,6 @@ var supportedDummyBucketAPIs = map[string][]string{ "website": {http.MethodGet, http.MethodDelete}, "logging": {http.MethodGet}, "accelerate": {http.MethodGet}, - "replication": {http.MethodGet}, "requestPayment": {http.MethodGet}, } @@ -418,7 +417,6 @@ var notImplementedBucketResourceNames = map[string]struct{}{ "logging": {}, "inventory": {}, "accelerate": {}, - "replication": {}, "requestPayment": {}, } diff --git a/cmd/globals.go b/cmd/globals.go index 179cf101d..7245e224c 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -154,9 +154,9 @@ var ( globalPolicySys *PolicySys globalIAMSys *IAMSys - globalLifecycleSys *LifecycleSys - globalBucketSSEConfigSys *BucketSSEConfigSys - + globalLifecycleSys *LifecycleSys + globalBucketSSEConfigSys *BucketSSEConfigSys + globalBucketReplicationSys *BucketReplicationSys // globalAPIConfig controls S3 API requests throttling, // healthcheck readiness deadlines and cors settings. globalAPIConfig apiConfig diff --git a/cmd/handler-utils.go b/cmd/handler-utils.go index 8ff9cd040..24241a098 100644 --- a/cmd/handler-utils.go +++ b/cmd/handler-utils.go @@ -77,6 +77,7 @@ var supportedHeaders = []string{ xhttp.AmzStorageClass, xhttp.AmzObjectTagging, "expires", + xhttp.AmzBucketReplicationStatus, // Add more supported headers here. } diff --git a/cmd/http/headers.go b/cmd/http/headers.go index d0103a603..c7ab9602d 100644 --- a/cmd/http/headers.go +++ b/cmd/http/headers.go @@ -81,7 +81,7 @@ const ( AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date" AmzObjectLockLegalHold = "X-Amz-Object-Lock-Legal-Hold" AmzObjectLockBypassGovernance = "X-Amz-Bypass-Governance-Retention" - + AmzBucketReplicationStatus = "X-Amz-Replication-Status" // Multipart parts count AmzMpPartsCount = "x-amz-mp-parts-count" diff --git a/cmd/object-api-datatypes.go b/cmd/object-api-datatypes.go index bf934adb6..3561f6bd5 100644 --- a/cmd/object-api-datatypes.go +++ b/cmd/object-api-datatypes.go @@ -22,6 +22,7 @@ import ( "time" humanize "github.com/dustin/go-humanize" + "github.com/minio/minio/pkg/bucket/replication" "github.com/minio/minio/pkg/hash" "github.com/minio/minio/pkg/madmin" ) @@ -181,6 +182,7 @@ type ObjectInfo struct { // Specify object storage class StorageClass string + ReplicationStatus replication.StatusType // User-Defined metadata UserDefined map[string]string diff --git a/cmd/object-api-errors.go b/cmd/object-api-errors.go index c9d73112c..6ba7b28f8 100644 --- a/cmd/object-api-errors.go +++ b/cmd/object-api-errors.go @@ -348,6 +348,27 @@ func (e BucketQuotaExceeded) Error() string { return "Bucket quota exceeded for bucket: " + e.Bucket } +// BucketReplicationConfigNotFound - no bucket replication config found +type BucketReplicationConfigNotFound GenericError + +func (e BucketReplicationConfigNotFound) Error() string { + return "The replication configuration was not found: " + e.Bucket +} + +// BucketReplicationDestinationNotFound bucket does not exist. +type BucketReplicationDestinationNotFound GenericError + +func (e BucketReplicationDestinationNotFound) Error() string { + return "Destination bucket does not exist: " + e.Bucket +} + +// BucketReplicationTargetNotFound replication target does not exist. +type BucketReplicationTargetNotFound GenericError + +func (e BucketReplicationTargetNotFound) Error() string { + return "Replication target not found: " + e.Bucket +} + /// Bucket related errors. // BucketNameInvalid - bucketname provided is invalid. diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index 426f3e59c..c416d656a 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -44,6 +44,7 @@ import ( "github.com/minio/minio/cmd/logger" objectlock "github.com/minio/minio/pkg/bucket/object/lock" "github.com/minio/minio/pkg/bucket/policy" + "github.com/minio/minio/pkg/bucket/replication" "github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/handlers" "github.com/minio/minio/pkg/hash" @@ -1153,7 +1154,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re if objTags != "" { srcInfo.UserDefined[xhttp.AmzObjectTagging] = objTags } - srcInfo.UserDefined = objectlock.FilterObjectLockMetadata(srcInfo.UserDefined, true, true) retPerms := isPutActionAllowed(getRequestAuthType(r), dstBucket, dstObject, r, iampolicy.PutObjectRetentionAction) holdPerms := isPutActionAllowed(getRequestAuthType(r), dstBucket, dstObject, r, iampolicy.PutObjectLegalHoldAction) @@ -1176,6 +1176,9 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) return } + if globalBucketReplicationSys.mustReplicate(ctx, r, dstBucket, dstObject, srcInfo.UserDefined, srcInfo.ReplicationStatus.String()) { + srcInfo.UserDefined[xhttp.AmzBucketReplicationStatus] = replication.Pending.String() + } // Store the preserved compression metadata. for k, v := range compressMetadata { @@ -1254,7 +1257,17 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re objInfo.ETag = getDecryptedETag(r.Header, objInfo, false) response := generateCopyObjectResponse(objInfo.ETag, objInfo.ModTime) encodedSuccessResponse := encodeResponse(response) - + if globalBucketReplicationSys.mustReplicate(ctx, r, dstBucket, dstObject, objInfo.UserDefined, objInfo.ReplicationStatus.String()) { + defer replicateObject(ctx, dstBucket, dstObject, objInfo.VersionID, objectAPI, &eventArgs{ + EventName: event.ObjectCreatedCopy, + BucketName: dstBucket, + Object: objInfo, + ReqParams: extractReqParams(r), + RespElements: extractRespElements(w), + UserAgent: r.UserAgent(), + Host: handlers.GetSourceIP(r), + }, false) + } setPutObjHeaders(w, objInfo, false) // We must not use the http.Header().Set method here because some (broken) // clients expect the x-amz-copy-source-version-id header key to be literally @@ -1497,7 +1510,15 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) return } - + if globalBucketReplicationSys.mustReplicate(ctx, r, bucket, object, metadata, "") { + metadata[xhttp.AmzBucketReplicationStatus] = string(replication.Pending) + } + if r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() { + if s3Err = isPutActionAllowed(getRequestAuthType(r), bucket, object, r, iampolicy.ReplicateObjectAction); s3Err != ErrNone { + writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) + return + } + } var objectEncryptionKey crypto.ObjectKey if objectAPI.IsEncryptionSupported() { if crypto.IsRequested(r.Header) && !HasSuffix(object, SlashSeparator) { // handle SSE requests @@ -1552,7 +1573,17 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req } } } - + if globalBucketReplicationSys.mustReplicate(ctx, r, bucket, object, metadata, "") { + defer replicateObject(ctx, bucket, object, objInfo.VersionID, objectAPI, &eventArgs{ + EventName: event.ObjectCreatedPut, + BucketName: bucket, + Object: objInfo, + ReqParams: extractReqParams(r), + RespElements: extractRespElements(w), + UserAgent: r.UserAgent(), + Host: handlers.GetSourceIP(r), + }, false) + } setPutObjHeaders(w, objInfo, false) writeSuccessResponseHeadersOnly(w) @@ -1664,7 +1695,9 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r)) return } - + if globalBucketReplicationSys.mustReplicate(ctx, r, bucket, object, metadata, "") { + metadata[xhttp.AmzBucketReplicationStatus] = string(replication.Pending) + } // We need to preserve the encryption headers set in EncryptRequest, // so we do not want to override them, copy them instead. for k, v := range encMetadata { @@ -2611,7 +2644,17 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite } setPutObjHeaders(w, objInfo, false) - + if globalBucketReplicationSys.mustReplicate(ctx, r, bucket, object, objInfo.UserDefined, objInfo.ReplicationStatus.String()) { + defer replicateObject(ctx, bucket, object, objInfo.VersionID, objectAPI, &eventArgs{ + EventName: event.ObjectCreatedCompleteMultipartUpload, + BucketName: bucket, + Object: objInfo, + ReqParams: extractReqParams(r), + RespElements: extractRespElements(w), + UserAgent: r.UserAgent(), + Host: handlers.GetSourceIP(r), + }, false) + } // Write success response. writeSuccessResponseXML(w, encodedSuccessResponse) diff --git a/cmd/server-main.go b/cmd/server-main.go index 86a4c5ff2..f1cf56596 100644 --- a/cmd/server-main.go +++ b/cmd/server-main.go @@ -168,6 +168,9 @@ func newAllSubsystems() { // Create new bucket versioning subsystem globalBucketVersioningSys = NewBucketVersioningSys() + + // Create new bucket replication subsytem + globalBucketReplicationSys = NewBucketReplicationSys() } func initSafeMode(ctx context.Context, newObject ObjectLayer) (err error) { @@ -337,6 +340,10 @@ func initAllSubsystems(ctx context.Context, newObject ObjectLayer) (err error) { return fmt.Errorf("Unable to initialize notification system: %w", err) } + // Initialize bucket replication sub-system. + if err = globalBucketReplicationSys.Init(GlobalContext, buckets, newObject); err != nil { + return fmt.Errorf("Unable to initialize bucket replication sub-system: %w", err) + } return nil } diff --git a/cmd/xl-storage.go b/cmd/xl-storage.go index 5cf931d2a..21c34f65c 100644 --- a/cmd/xl-storage.go +++ b/cmd/xl-storage.go @@ -414,6 +414,9 @@ func (s *xlStorage) CrawlAndGetDataUsage(ctx context.Context, cache dataUsageCac } } + for _, version := range fivs.Versions { + item.healReplication(ctx, objAPI, actionMeta{oi: version.ToObjectInfo(item.bucket, item.objectPath())}) + } return totalSize, nil }) diff --git a/docs/bucket/replication/DELETE_bucket_replication.png b/docs/bucket/replication/DELETE_bucket_replication.png new file mode 100644 index 000000000..64b17f3bc Binary files /dev/null and b/docs/bucket/replication/DELETE_bucket_replication.png differ diff --git a/docs/bucket/replication/HEAD_bucket_replication.png b/docs/bucket/replication/HEAD_bucket_replication.png new file mode 100644 index 000000000..77e1d54d6 Binary files /dev/null and b/docs/bucket/replication/HEAD_bucket_replication.png differ diff --git a/docs/bucket/replication/PUT_bucket_replication.png b/docs/bucket/replication/PUT_bucket_replication.png new file mode 100644 index 000000000..d662cc791 Binary files /dev/null and b/docs/bucket/replication/PUT_bucket_replication.png differ diff --git a/docs/bucket/replication/README.md b/docs/bucket/replication/README.md new file mode 100644 index 000000000..72addb892 --- /dev/null +++ b/docs/bucket/replication/README.md @@ -0,0 +1,35 @@ +# Bucket Replication Guide [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io) [![Docker Pulls](https://img.shields.io/docker/pulls/minio/minio.svg?maxAge=604800)](https://hub.docker.com/r/minio/minio/) + +Bucket replication is designed to replicate specific objects in buckets +that are configured for replication. + +To replicate objects in a bucket to a destination bucket on a target site either on the same cluster or a different cluster, start by creating version enabled buckets on both `source` and `dest` buckets. Next, the target site and destination bucket need to be configured on MinIO server by setting + +` +$ mc admin bucket replication set myminio/source https://accessKey:secretKey@replica-endpoint:9000/dest --path-style "auto" --api "s3v2" +` + +Note that the admin needs "s3:GetReplicationConfigurationAction" permission on source in addition to "s3:ReplicateObject" permission on destination side. + +Next, the replication ARN associated with the replica URL https://replica-endpoint:9000 can be fetched from the MinIO server by using the command +`mc admin bucket remote myminio/source https://replica-endpoint:9000` + +The replication configuration can now be added to the source bucket by applying the json file with replication configuration. The ReplicationArn is passed in as a json element in the configuration. + +The replication configuration follows [S3 Spec](https://docs.aws.amazon.com/AmazonS3/latest/dev/replication-add-config.html). Any +objects uploaded to the source bucket that meet replication criteria will now automatically be replicated by the MinIO server. Replication can be stopped at any time by disabling specific rules in the configuration or deleting the replication configuration. + +When an object is deleted from the source bucket, the replica will not be deleted as per S3 spec. +![delete](https://raw.githubusercontent.com/minio/minio/master/docs/bucket/replication/DELETE_bucket_replication.png) + +When object locking is used in conjunction with replication, both source and destination buckets needs to have object locking enabled. Similarly objects encrypted on the server side, will be replicated if destination also supports +encryption. + +Replication status can be seen in the metadata on the source and destination objects. On the source side, the `X-Amz-Replication-Status` changes from `PENDING` to `COMPLETE` or `FAILED` after replication attempt is made. On the destination side, a `X-Amz-Replication-Status` status of `REPLICA` indicates that the object +was replicated successfully. Any replication failures are automatically reattempted during a periodic disk usage crawl. +![put](https://raw.githubusercontent.com/minio/minio/master/docs/bucket/replication/PUT_bucket_replication.png) + +![head](https://raw.githubusercontent.com/minio/minio/master/docs/bucket/replication/HEAD_bucket_replication.png) + +## Additional notes +The [S3 Spec]([S3 Spec](https://docs.aws.amazon.com/AmazonS3/latest/dev/replication-add-config.html) has configuration such as IAM Role,AccessControlTranslation, Metrics and SourceSelectionCriteria which are not required for MinIO. diff --git a/go.mod b/go.mod index 1c12941e0..3c77e4dc0 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/Azure/azure-storage-blob-go v0.8.0 github.com/Azure/go-autorest/autorest/adal v0.9.0 // indirect github.com/Shopify/sarama v1.24.1 - github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect github.com/alecthomas/participle v0.2.1 github.com/aws/aws-sdk-go v1.20.21 github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2 @@ -26,7 +25,7 @@ require ( github.com/elazarl/go-bindata-assetfs v1.0.0 github.com/fatih/color v1.7.0 github.com/fatih/structs v1.1.0 - github.com/go-ole/go-ole v1.2.4 // indirect + github.com/go-ini/ini v1.57.0 // indirect github.com/go-sql-driver/mysql v1.5.0 github.com/gomodule/redigo v2.0.0+incompatible github.com/google/uuid v1.1.1 diff --git a/go.sum b/go.sum index 625d47c2f..37fee1587 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,14 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.39.0 h1:UgQP9na6OTfp4dsAiz/eFpFA1C6tPdH5wiRdi19tuMw= cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= +contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0= git.apache.org/thrift.git v0.13.0 h1:/3bz5WZ+sqYArk7MBBBbDufMxKKOA56/6JO6psDpUDY= git.apache.org/thrift.git v0.13.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/Azure/azure-pipeline-go v0.2.1 h1:OLBdZJ3yvOn2MezlWvbrBMTEUQC72zAftRZOMdj5HYo= github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= github.com/Azure/azure-storage-blob-go v0.8.0 h1:53qhf0Oxa0nOjgbDeeYPUeyiNmafAFEY95rZLK0Tj6o= github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= +github.com/Azure/go-autorest v11.7.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest/adal v0.9.0 h1:SigMbuFNuKgc1xcGhaeapbh+8fgsu+GxgDRFyg7f5lM= @@ -21,6 +23,7 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.24.1 h1:svn9vfN3R1Hz21WR2Gj0VW9ehaDGkiOS+VqlIcZOkMI= github.com/Shopify/sarama v1.24.1/go.mod h1:fGP8eQ6PugKEI0iUETYYtnP6d1pH/bdDMTel1X5ajsU= github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= @@ -47,7 +50,10 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheggaaa/pb v1.0.28 h1:kWGpdAcSp3MxMU9CCHOwz/8V0kCHN4+9yQm2MzWuI98= @@ -61,11 +67,19 @@ github.com/colinmarc/hdfs/v2 v2.1.1 h1:x0hw/m+o3UE20Scso/KCkvYNc9Di39TBlCfGMkJ1/ github.com/colinmarc/hdfs/v2 v2.1.1/go.mod h1:M3x+k8UKKmxtFu++uAZ0OtDU8jR3jnaZIAc6yK4Ue0c= github.com/coredns/coredns v1.4.0 h1:RubBkYmkByUqZWWkjRHvNLnUHgkRVqAWgSMmRFvpE1A= github.com/coredns/coredns v1.4.0/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0= +github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.12+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.0.0 h1:XJIw/+VlJ+87J+doOxznsAWIdmWuViOVhkQamW5YV28= github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -73,6 +87,7 @@ github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4= github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/djherbis/atime v1.0.0 h1:ySLvBAM0EvOGaX7TI4dAM5lWj+RdJUCKtGSEHN8SGBg= github.com/djherbis/atime v1.0.0/go.mod h1:5W+KBIuTwVGcqjIfaTwt+KSYX1o6uep8dtevevQP/f8= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -80,6 +95,8 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q= +github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= @@ -100,12 +117,18 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.4.1 h1:Wv2VwvNn73pAdFIVUQRXYDFp31lXKbqblIXo/Q5GPSg= github.com/frankban/quicktest v1.4.1/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ= +github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= +github.com/go-ini/ini v1.57.0 h1:Qwzj3wZQW+Plax5Ntj+GYe07DfGj1OH+aL1nMTMaNow= +github.com/go-ini/ini v1.57.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -121,6 +144,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekf github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191002201903-404acd9df4cc/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -160,18 +185,26 @@ github.com/googleapis/gax-go/v2 v2.0.4 h1:hU4mGcQI4DaAYW+IbTun+2qEZVFxK0ySjQLTbS github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.5-0.20200711200521-98cb6bf42e08 h1:kPna6oIGlRXWmg/jkKfxbpvsl+0DHYnw1qQwN+6+gyA= github.com/gorilla/mux v1.7.5-0.20200711200521-98cb6bf42e08/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.4/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= @@ -183,6 +216,7 @@ github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9 github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.9.1 h1:9PZfAcVEvez4yhLH2TBU64/h/z4xlFI80cWXRrxuKuM= github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= @@ -207,6 +241,7 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/raft v1.1.1-0.20190703171940-f639636d18e0/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8= github.com/hashicorp/raft v1.1.2 h1:oxEL5DDeurYxLd3UbcY/hccgSPhLLpiBZ1YxtWEq59c= github.com/hashicorp/raft v1.1.2/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8= github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea/go.mod h1:pNv7Wc3ycL6F5oOWn+tPGo2gWD4a5X+yp/ntwdKLjRk= @@ -240,6 +275,7 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.4/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.1/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= @@ -254,6 +290,7 @@ github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/readahead v1.3.1 h1:QqXNYvm+VvqYcbrRT4LojUciM0XrznFRIDrbHiJtu/0= github.com/klauspost/readahead v1.3.1/go.mod h1:AH9juHzNH7xqdqFHrMRSHeH2Ps+vFf+kblDqzPFiLJg= +github.com/klauspost/reedsolomon v1.9.7/go.mod h1:+8WD025Xpby8/kG5h/HDPIFhiiuGEtZOKw+5Y4drAD8= github.com/klauspost/reedsolomon v1.9.9 h1:qCL7LZlv17xMixl55nq2/Oa1Y86nfO8EqDfv2GHND54= github.com/klauspost/reedsolomon v1.9.9/go.mod h1:O7yFFHiQwDR6b2t63KPUpccPtNdp5ADgh1gg4fd12wo= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= @@ -266,6 +303,9 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kurin/blazer v0.5.4-0.20200327014341-8f90a40f8af7/go.mod h1:4FCXMUWo9DllR2Do4TtBd377ezyAJ51vB5uTBjt0pGU= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY= github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5 h1:0x4qcEHDpruK6ML/m/YSlFUUu0UpRD3I2PHsNCuGnyA= @@ -275,6 +315,7 @@ github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaa github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149 h1:HfxbT6/JcvIljmERptWhwa8XzP7H3T+Z2N26gTsaDaA= github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= +github.com/mattn/go-ieproxy v0.0.0-20190805055040-f9202b1cfdeb/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI= github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -283,14 +324,22 @@ github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.5 h1:jrGtp51JOKTWgvLFzfG6OtZOJcK2sEnzc/U+zw7TtbA= +github.com/mattn/go-runewidth v0.0.5/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI= github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/minio/cli v1.22.0 h1:VTQm7lmXm3quxO917X3p+el1l0Ca5X3S4PM2ruUYO68= github.com/minio/cli v1.22.0/go.mod h1:bYxnK0uS629N3Bq+AOZZ+6lwF77Sodk4+UL9vNuXhOY= +github.com/minio/gokrb5/v7 v7.2.5/go.mod h1:z6fE6twrvMN004M+KRTHnmtfpxsBIztP0PVsak0/4f8= +github.com/minio/hdfs/v3 v3.0.1/go.mod h1:6ALh9HsAwG9xAXdpdrZJcSY0vR6z3K+9XIz6Y9pQG/c= github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA= github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc= +github.com/minio/lsync v1.0.1/go.mod h1:tCFzfo0dlvdGl70IT4IAK/5Wtgb0/BrTmo/jE8pArKA= +github.com/minio/mc v0.0.0-20200624133159-bb3c0d45f2e6 h1:52MggKSmFdAf6hXH/hVr1zc1K4T3imDUZnqAsmXwfz4= +github.com/minio/mc v0.0.0-20200624133159-bb3c0d45f2e6/go.mod h1:rLRpqr0ZLHfahCNAwxfc5RkrJxGr4YdI45XNxjwlJ0A= github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= github.com/minio/minio-go/v7 v7.0.1 h1:sL2y4uuNUEi7AjvWjoGyDFQKFX2zA0DU2tGM9m3s5f8= @@ -311,6 +360,7 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mmcloughlin/avo v0.0.0-20200303042253-6df701fe672f/go.mod h1:L0u9qfRMLNBO97u6pPukRp6ncoQz0Q25W69fvtht3vA= github.com/mmcloughlin/avo v0.0.0-20200523190732-4439b6b2c061 h1:UCU8+cLbbvyxi0sQ9fSeoEhZgvrrD9HKMtX6Gmc1vk8= github.com/mmcloughlin/avo v0.0.0-20200523190732-4439b6b2c061/go.mod h1:wqKykBG2QzQDJEzvRkcS8x6MiSJkF52hXZsXcjaB3ls= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= @@ -324,28 +374,42 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/montanaflynn/stats v0.5.0 h1:2EkzeTSqBB4V4bJwWrt5gIIrZmpJBcoIRGS2kWLgzmk= github.com/montanaflynn/stats v0.5.0/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/gnatsd v1.4.1/go.mod h1:nqco77VO78hLCJpIcVfygDP2rPGfsEHkGTUk94uh5DQ= +github.com/nats-io/go-nats v1.7.2/go.mod h1:+t7RHT5ApZebkrQdnn6AhQJmhJJiKAvJUio1PiiCtj0= +github.com/nats-io/go-nats-streaming v0.4.4/go.mod h1:gfq4R3c9sKAINOpelo0gn/b9QDMBZnmrttcsNF+lqyo= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server v1.4.1 h1:Ul1oSOGNV/L8kjr4v6l2f9Yet6WY+LevH1/7cRZ/qyA= +github.com/nats-io/nats-server v1.4.1/go.mod h1:c8f/fHd2B6Hgms3LtCaI7y6pC4WD1f4SUxcCud5vhBc= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= github.com/nats-io/nats-server/v2 v2.1.7 h1:jCoQwDvRYJy3OpOTHeYfvIPLP46BMeDmH7XEJg/r42I= github.com/nats-io/nats-server/v2 v2.1.7/go.mod h1:rbRrRE/Iv93O/rUvZ9dh4NfT0Cm9HWjW/BqOWLGgYiE= +github.com/nats-io/nats-streaming-server v0.14.2/go.mod h1:RyqtDJZvMZO66YmyjIYdIvS69zu/wDAkyNWa8PIUa5c= github.com/nats-io/nats-streaming-server v0.18.0 h1:+RDozeN9scwCm0Wc2fYlvGcP144hvxvSOtxZ8FE21ME= github.com/nats-io/nats-streaming-server v0.18.0/go.mod h1:Y9Aiif2oANuoKazQrs4wXtF3jqt6p97ODQg68lR5TnY= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nats.go v1.10.0 h1:L8qnKaofSfNFbXg0C5F71LdjPRnmQwSsA4ukmkt1TvY= github.com/nats-io/nats.go v1.10.0/go.mod h1:AjGArbfyR50+afOUotNX2Xs5SYHf+CoOa5HH1eEl2HE= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3 h1:6JrEfig+HzTH85yxzhSVbjHRJv9cn0p6n3IngIcM5/k= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.4 h1:aEsHIssIk6ETN5m2/MD8Y4B2X7FfXrBAUdkyRvbVYzA= github.com/nats-io/nkeys v0.1.4/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nats-io/stan.go v0.4.5/go.mod h1:Ji7mK6gRZJSH1nc3ZJH6vi7zn/QnZhpR9Arm4iuzsUQ= github.com/nats-io/stan.go v0.7.0 h1:sMVHD9RkxPOl6PJfDVBQd+gbxWkApeYl6GrH+10msO4= github.com/nats-io/stan.go v0.7.0/go.mod h1:Ci6mUIpGQTjl++MqK2XzkWI/0vF+Bl72uScx7ejSYmU= github.com/ncw/directio v1.0.5 h1:JSUBhdjEvVaJvOoyPAbcW0fnd0tvRXD76wEfZ1KcQz4= github.com/ncw/directio v1.0.5/go.mod h1:rX/pKEYkOXBGOggmcyJeJGloCkleSvphPx2eV3t6ROk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nsqio/go-nsq v1.0.7 h1:O0pIZJYTf+x7cZBA0UMY8WxFG79lYTURmWzAAh48ljY= github.com/nsqio/go-nsq v1.0.7/go.mod h1:XP5zaUs3pqf+Q71EqUJs3HYfBIqfK6G83WQMdNN+Ito= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/getopt v0.0.0-20180729010549-6fdd0a2c7117/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= @@ -358,11 +422,15 @@ github.com/pierrec/lz4 v2.4.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.3.0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/xattr v0.4.1/go.mod h1:W2cGD0TBEus7MkUgv0tNZ9JutLtVO3cXu+IBRuHqnFs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.2-0.20190702141536-6ffe496ea953/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -370,21 +438,28 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20190704165056-9c2d0518ed81 h1:zQTtDd7fQiF9e80lbl+ShnD9/5NSq5r1EhcS8955ECg= +github.com/rcrowley/go-metrics v0.0.0-20190704165056-9c2d0518ed81/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -400,11 +475,16 @@ github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 h1:hBSHahWMEgzwRyS6dRpxY0XyjZsHyQ61s084wo5PJe0= +github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= @@ -418,6 +498,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/tidwall/gjson v1.3.5 h1:2oW9FBNu8qt9jy5URgrzsVx/T/KSn3qn/smJQ0crlDQ= github.com/tidwall/gjson v1.3.5/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= @@ -426,11 +508,17 @@ github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/sjson v1.0.4 h1:UcdIRXff12Lpnu3OLtZvnc03g4vH2suXDXhBwBqmzYg= github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y= +github.com/tinylib/msgp v1.1.1/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= @@ -445,32 +533,42 @@ github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd/v3 v3.3.0-rc.0.0.20200707003333-58bb8ae09f8e h1:HZQLoe71Q24wVyDrGBRcVuogx32U+cPlcm/WoSLUI6c= go.etcd.io/etcd/v3 v3.3.0-rc.0.0.20200707003333-58bb8ae09f8e/go.mod h1:UENlOa05tkNvLx9VnNziSerG4Ro74upGK6Apd4v6M/Y= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo= go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= +go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= golang.org/x/arch v0.0.0-20190909030613-46d78d1859ac/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= @@ -504,6 +602,8 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -515,6 +615,7 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181021155630-eda9bb28ed51/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -555,30 +656,37 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190914235951-31e00f45c22e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200425043458-8463f397d07c h1:iHhCR0b26amDCiiO+kBguKZom9aMF+NrFxh9zeKR/XU= golang.org/x/tools v0.0.0-20200425043458-8463f397d07c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200502202811-ed308ab3e770/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM= google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8 h1:x913Lq/RebkvUmRSdQ8MNb0GZKn+SR1ESfoetcQSeak= google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190513181449-d00d292a067c/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -597,8 +705,10 @@ gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUy gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -632,6 +742,7 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/pkg/bucket/policy/action.go b/pkg/bucket/policy/action.go index b6d18610e..277d70221 100644 --- a/pkg/bucket/policy/action.go +++ b/pkg/bucket/policy/action.go @@ -149,28 +149,49 @@ const ( // PutObjectVersionTaggingAction - PutObjectVersionTagging Rest API action. PutObjectVersionTaggingAction = "s3:PutObjectVersionTagging" + + // GetReplicationConfigurationAction - GetReplicationConfiguration REST API action + GetReplicationConfigurationAction = "s3:GetReplicationConfiguration" + // PutReplicationConfigurationAction - PutReplicationConfiguration REST API action + PutReplicationConfigurationAction = "s3:PutReplicationConfiguration" + + // ReplicateObjectAction - ReplicateObject REST API action + ReplicateObjectAction = "s3:ReplicateObject" + + // ReplicateDeleteAction - ReplicateDelete REST API action + ReplicateDeleteAction = "s3:ReplicateDelete" + + // ReplicateTagsAction - ReplicateTags REST API action + ReplicateTagsAction = "s3:ReplicateTags" + + // GetObjectVersionForReplicationAction - GetObjectVersionForReplication REST API action + GetObjectVersionForReplicationAction = "s3:GetObjectVersionForReplication" ) // List of all supported object actions. var supportedObjectActions = map[Action]struct{}{ - AbortMultipartUploadAction: {}, - DeleteObjectAction: {}, - GetObjectAction: {}, - ListMultipartUploadPartsAction: {}, - PutObjectAction: {}, - BypassGovernanceRetentionAction: {}, - PutObjectRetentionAction: {}, - GetObjectRetentionAction: {}, - PutObjectLegalHoldAction: {}, - GetObjectLegalHoldAction: {}, - GetObjectTaggingAction: {}, - PutObjectTaggingAction: {}, - DeleteObjectTaggingAction: {}, - GetObjectVersionAction: {}, - GetObjectVersionTaggingAction: {}, - DeleteObjectVersionAction: {}, - DeleteObjectVersionTaggingAction: {}, - PutObjectVersionTaggingAction: {}, + AbortMultipartUploadAction: {}, + DeleteObjectAction: {}, + GetObjectAction: {}, + ListMultipartUploadPartsAction: {}, + PutObjectAction: {}, + BypassGovernanceRetentionAction: {}, + PutObjectRetentionAction: {}, + GetObjectRetentionAction: {}, + PutObjectLegalHoldAction: {}, + GetObjectLegalHoldAction: {}, + GetObjectTaggingAction: {}, + PutObjectTaggingAction: {}, + DeleteObjectTaggingAction: {}, + GetObjectVersionAction: {}, + GetObjectVersionTaggingAction: {}, + DeleteObjectVersionAction: {}, + DeleteObjectVersionTaggingAction: {}, + PutObjectVersionTaggingAction: {}, + ReplicateObjectAction: {}, + ReplicateDeleteAction: {}, + ReplicateTagsAction: {}, + GetObjectVersionForReplicationAction: {}, } // isObjectAction - returns whether action is object type or not. @@ -224,6 +245,12 @@ var supportedActions = map[Action]struct{}{ GetBucketEncryptionAction: {}, PutBucketVersioningAction: {}, GetBucketVersioningAction: {}, + GetReplicationConfigurationAction: {}, + PutReplicationConfigurationAction: {}, + ReplicateObjectAction: {}, + ReplicateDeleteAction: {}, + ReplicateTagsAction: {}, + GetObjectVersionForReplicationAction: {}, } // IsValid - checks if action is valid or not. @@ -366,4 +393,10 @@ var actionConditionKeyMap = map[Action]condition.KeySet{ append([]condition.Key{ condition.S3VersionID, }, condition.CommonKeys...)...), + GetReplicationConfigurationAction: condition.NewKeySet(condition.CommonKeys...), + PutReplicationConfigurationAction: condition.NewKeySet(condition.CommonKeys...), + ReplicateObjectAction: condition.NewKeySet(condition.CommonKeys...), + ReplicateDeleteAction: condition.NewKeySet(condition.CommonKeys...), + ReplicateTagsAction: condition.NewKeySet(condition.CommonKeys...), + GetObjectVersionForReplicationAction: condition.NewKeySet(condition.CommonKeys...), } diff --git a/pkg/bucket/replication/and.go b/pkg/bucket/replication/and.go new file mode 100644 index 000000000..14059d1b0 --- /dev/null +++ b/pkg/bucket/replication/and.go @@ -0,0 +1,62 @@ +/* + * 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 replication + +import ( + "encoding/xml" +) + +// And - a tag to combine a prefix and multiple tags for replication configuration rule. +type And struct { + XMLName xml.Name `xml:"And" json:"And"` + Prefix string `xml:"Prefix,omitempty" json:"Prefix,omitempty"` + Tags []Tag `xml:"Tag,omitempty" json:"Tag,omitempty"` +} + +var errDuplicateTagKey = Errorf("Duplicate Tag Keys are not allowed") + +// isEmpty returns true if Tags field is null +func (a And) isEmpty() bool { + return len(a.Tags) == 0 && a.Prefix == "" +} + +// Validate - validates the And field +func (a And) Validate() error { + if a.ContainsDuplicateTag() { + return errDuplicateTagKey + } + for _, t := range a.Tags { + if err := t.Validate(); err != nil { + return err + } + } + return nil +} + +// ContainsDuplicateTag - returns true if duplicate keys are present in And +func (a And) ContainsDuplicateTag() bool { + x := make(map[string]struct{}, len(a.Tags)) + + for _, t := range a.Tags { + if _, has := x[t.Key]; has { + return true + } + x[t.Key] = struct{}{} + } + + return false +} diff --git a/pkg/bucket/replication/destination.go b/pkg/bucket/replication/destination.go new file mode 100644 index 000000000..3b63c1eb9 --- /dev/null +++ b/pkg/bucket/replication/destination.go @@ -0,0 +1,118 @@ +/* + * 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 replication + +import ( + "encoding/xml" + "fmt" + "strings" + + "github.com/minio/minio/pkg/wildcard" +) + +// DestinationARNPrefix - destination ARN prefix as per AWS S3 specification. +const DestinationARNPrefix = "arn:aws:s3:::" + +// Destination - destination in ReplicationConfiguration. +type Destination struct { + XMLName xml.Name `xml:"Destination" json:"Destination"` + Bucket string `xml:"Bucket" json:"Bucket"` + StorageClass string `xml:"StorageClass" json:"StorageClass"` + //EncryptionConfiguration TODO: not needed for MinIO +} + +func (d Destination) isValidStorageClass() bool { + if d.StorageClass == "" { + return true + } + return d.StorageClass == "STANDARD" || d.StorageClass == "REDUCED_REDUNDANCY" +} + +// IsValid - checks whether Destination is valid or not. +func (d Destination) IsValid() bool { + return d.Bucket != "" || !d.isValidStorageClass() +} + +func (d Destination) String() string { + return DestinationARNPrefix + d.Bucket +} + +// MarshalXML - encodes to XML data. +func (d Destination) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if err := e.EncodeToken(start); err != nil { + return err + } + if err := e.EncodeElement(d.String(), xml.StartElement{Name: xml.Name{Local: "Bucket"}}); err != nil { + return err + } + if d.StorageClass != "" { + if err := e.EncodeElement(d.StorageClass, xml.StartElement{Name: xml.Name{Local: "StorageClass"}}); err != nil { + return err + } + } + return e.EncodeToken(xml.EndElement{Name: start.Name}) +} + +// UnmarshalXML - decodes XML data. +func (d *Destination) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) (err error) { + // Make subtype to avoid recursive UnmarshalXML(). + type destination Destination + dest := destination{} + + if err := dec.DecodeElement(&dest, &start); err != nil { + return err + } + parsedDest, err := parseDestination(dest.Bucket) + if err != nil { + return err + } + if dest.StorageClass != "" { + switch dest.StorageClass { + case "STANDARD", "REDUCED_REDUNDANCY": + default: + return fmt.Errorf("unknown storage class %v", dest.StorageClass) + } + } + parsedDest.StorageClass = dest.StorageClass + *d = parsedDest + return nil +} + +// Validate - validates Resource is for given bucket or not. +func (d Destination) Validate(bucketName string) error { + if !d.IsValid() { + return Errorf("invalid destination") + } + + if !wildcard.Match(d.Bucket, bucketName) { + return Errorf("bucket name does not match") + } + return nil +} + +// parseDestination - parses string to Destination. +func parseDestination(s string) (Destination, error) { + if !strings.HasPrefix(s, DestinationARNPrefix) { + return Destination{}, Errorf("invalid destination '%v'", s) + } + + bucketName := strings.TrimPrefix(s, DestinationARNPrefix) + + return Destination{ + Bucket: bucketName, + }, nil +} diff --git a/pkg/bucket/replication/error.go b/pkg/bucket/replication/error.go new file mode 100644 index 000000000..59f937fa4 --- /dev/null +++ b/pkg/bucket/replication/error.go @@ -0,0 +1,44 @@ +/* + * 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 replication + +import ( + "fmt" +) + +// Error is the generic type for any error happening during tag +// parsing. +type Error struct { + err error +} + +// Errorf - formats according to a format specifier and returns +// the string as a value that satisfies error of type tagging.Error +func Errorf(format string, a ...interface{}) error { + return Error{err: fmt.Errorf(format, a...)} +} + +// Unwrap the internal error. +func (e Error) Unwrap() error { return e.err } + +// Error 'error' compatible method. +func (e Error) Error() string { + if e.err == nil { + return "replication: cause " + } + return e.err.Error() +} diff --git a/pkg/bucket/replication/filter.go b/pkg/bucket/replication/filter.go new file mode 100644 index 000000000..5da350cb1 --- /dev/null +++ b/pkg/bucket/replication/filter.go @@ -0,0 +1,120 @@ +/* + * 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 replication + +import ( + "encoding/xml" +) + +var ( + errInvalidFilter = Errorf("Filter must have exactly one of Prefix, Tag, or And specified") +) + +// Filter - a filter for a replication configuration Rule. +type Filter struct { + XMLName xml.Name `xml:"Filter" json:"Filter"` + Prefix string + And And + Tag Tag + // Caching tags, only once + cachedTags map[string]struct{} +} + +// IsEmpty returns true if filter is not set +func (f Filter) IsEmpty() bool { + return f.And.isEmpty() && f.Tag.IsEmpty() && f.Prefix == "" +} + +// MarshalXML - produces the xml representation of the Filter struct +// only one of Prefix, And and Tag should be present in the output. +func (f Filter) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if err := e.EncodeToken(start); err != nil { + return err + } + + switch { + case !f.And.isEmpty(): + if err := e.EncodeElement(f.And, xml.StartElement{Name: xml.Name{Local: "And"}}); err != nil { + return err + } + case !f.Tag.IsEmpty(): + if err := e.EncodeElement(f.Tag, xml.StartElement{Name: xml.Name{Local: "Tag"}}); err != nil { + return err + } + default: + // Always print Prefix field when both And & Tag are empty + if err := e.EncodeElement(f.Prefix, xml.StartElement{Name: xml.Name{Local: "Prefix"}}); err != nil { + return err + } + } + + return e.EncodeToken(xml.EndElement{Name: start.Name}) +} + +// Validate - validates the filter element +func (f Filter) Validate() error { + // A Filter must have exactly one of Prefix, Tag, or And specified. + if !f.And.isEmpty() { + if f.Prefix != "" { + return errInvalidFilter + } + if !f.Tag.IsEmpty() { + return errInvalidFilter + } + if err := f.And.Validate(); err != nil { + return err + } + } + if f.Prefix != "" { + if !f.Tag.IsEmpty() { + return errInvalidFilter + } + } + if !f.Tag.IsEmpty() { + if err := f.Tag.Validate(); err != nil { + return err + } + } + return nil +} + +// TestTags tests if the object tags satisfy the Filter tags requirement, +// it returns true if there is no tags in the underlying Filter. +func (f *Filter) TestTags(ttags []string) bool { + if f.cachedTags == nil { + tags := make(map[string]struct{}) + for _, t := range append(f.And.Tags, f.Tag) { + if !t.IsEmpty() { + tags[t.String()] = struct{}{} + } + } + f.cachedTags = tags + } + for ct := range f.cachedTags { + foundTag := false + for _, t := range ttags { + if ct == t { + foundTag = true + break + } + } + if !foundTag { + return false + } + } + return true +} diff --git a/pkg/bucket/replication/replication.go b/pkg/bucket/replication/replication.go new file mode 100644 index 000000000..f1c1ee440 --- /dev/null +++ b/pkg/bucket/replication/replication.go @@ -0,0 +1,206 @@ +/* + * 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 replication + +import ( + "encoding/xml" + "io" + "sort" + "strings" +) + +// StatusType of Replication for x-amz-replication-status header +type StatusType string + +const ( + // Pending - replication is pending. + Pending StatusType = "PENDING" + + // Complete - replication completed ok. + Complete StatusType = "COMPLETE" + + // Failed - replication failed. + Failed StatusType = "FAILED" + + // Replica - this is a replica. + Replica StatusType = "REPLICA" +) + +// String returns string representation of status +func (s StatusType) String() string { + return string(s) +} + +var ( + errReplicationTooManyRules = Errorf("Replication configuration allows a maximum of 1000 rules") + errReplicationNoRule = Errorf("Replication configuration should have at least one rule") + errReplicationUniquePriority = Errorf("Replication configuration has duplicate priority") + errReplicationDestinationMismatch = Errorf("The destination bucket must be same for all rules") + errReplicationArnMissing = Errorf("Replication Arn missing") +) + +// Config - replication configuration specified in +// https://docs.aws.amazon.com/AmazonS3/latest/dev/replication-add-config.html +type Config struct { + XMLName xml.Name `xml:"ReplicationConfiguration" json:"-"` + Rules []Rule `xml:"Rule" json:"Rules"` + // ReplicationArn is a MinIO only extension and optional for AWS + ReplicationArn string `xml:"ReplicationArn,omitempty" json:"ReplicationArn,omitempty"` +} + +// Maximum 2MiB size per replication config. +const maxReplicationConfigSize = 2 << 20 + +// ParseConfig parses ReplicationConfiguration from xml +func ParseConfig(reader io.Reader) (*Config, error) { + config := Config{} + if err := xml.NewDecoder(io.LimitReader(reader, maxReplicationConfigSize)).Decode(&config); err != nil { + return nil, err + } + return &config, nil +} + +// Validate - validates the replication configuration +func (c Config) Validate(bucket string, sameTarget bool) error { + // replication config can't have more than 1000 rules + if len(c.Rules) > 1000 { + return errReplicationTooManyRules + } + // replication config should have at least one rule + if len(c.Rules) == 0 { + return errReplicationNoRule + } + if c.ReplicationArn == "" { + return errReplicationArnMissing + } + // Validate all the rules in the replication config + targetMap := make(map[string]struct{}) + priorityMap := make(map[string]struct{}) + for _, r := range c.Rules { + if len(targetMap) == 0 { + targetMap[r.Destination.Bucket] = struct{}{} + } + if _, ok := targetMap[r.Destination.Bucket]; !ok { + return errReplicationDestinationMismatch + } + if err := r.Validate(bucket, sameTarget); err != nil { + return err + } + if _, ok := priorityMap[string(r.Priority)]; ok { + return errReplicationUniquePriority + } + priorityMap[string(r.Priority)] = struct{}{} + } + return nil +} + +// ObjectOpts provides information to deduce whether replication +// can be triggered on the resultant object. +type ObjectOpts struct { + Name string + UserTags string + VersionID string + IsLatest bool + DeleteMarker bool + SSEC bool +} + +// FilterActionableRules returns the rules actions that need to be executed +// after evaluating prefix/tag filtering +func (c Config) FilterActionableRules(obj ObjectOpts) []Rule { + if obj.Name == "" { + return nil + } + var rules []Rule + for _, rule := range c.Rules { + if rule.Status == Disabled { + continue + } + if !strings.HasPrefix(obj.Name, rule.Prefix()) { + continue + } + if rule.Filter.TestTags(strings.Split(obj.UserTags, "&")) { + rules = append(rules, rule) + } + } + sort.Slice(rules[:], func(i, j int) bool { + return rules[i].Priority > rules[j].Priority + }) + return rules +} + +// GetDestination returns destination bucket and storage class. +func (c Config) GetDestination() Destination { + for _, rule := range c.Rules { + if rule.Status == Disabled { + continue + } + return rule.Destination + } + return Destination{} +} + +// Replicate returns true if the object should be replicated. +func (c Config) Replicate(obj ObjectOpts) bool { + + for _, rule := range c.FilterActionableRules(obj) { + + if obj.DeleteMarker { + // Indicates whether MinIO will remove a delete marker. By default, delete markers + // are not replicated. + return false + } + if obj.SSEC { + return false + } + if obj.VersionID != "" && !obj.IsLatest { + return false + } + if rule.Status == Disabled { + continue + } + return true + } + return false +} + +// HasActiveRules - returns whether replication policy has active rules +// Optionally a prefix can be supplied. +// If recursive is specified the function will also return true if any level below the +// prefix has active rules. If no prefix is specified recursive is effectively true. +func (c Config) HasActiveRules(prefix string, recursive bool) bool { + if len(c.Rules) == 0 { + return false + } + for _, rule := range c.Rules { + if rule.Status == Disabled { + continue + } + if len(prefix) > 0 && len(rule.Filter.Prefix) > 0 { + // incoming prefix must be in rule prefix + if !recursive && !strings.HasPrefix(prefix, rule.Filter.Prefix) { + continue + } + // If recursive, we can skip this rule if it doesn't match the tested prefix. + if recursive && !strings.HasPrefix(rule.Filter.Prefix, prefix) { + continue + } + } + return true + } + return false +} diff --git a/pkg/bucket/replication/rule.go b/pkg/bucket/replication/rule.go new file mode 100644 index 000000000..c5615e405 --- /dev/null +++ b/pkg/bucket/replication/rule.go @@ -0,0 +1,156 @@ +/* + * 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 replication + +import ( + "bytes" + "encoding/xml" +) + +// Status represents Enabled/Disabled status +type Status string + +// Supported status types +const ( + Enabled Status = "Enabled" + Disabled Status = "Disabled" +) + +// DeleteMarkerReplication - whether delete markers are replicated - https://docs.aws.amazon.com/AmazonS3/latest/dev/replication-add-config.html +type DeleteMarkerReplication struct { + Status Status `xml:"Status"` // should be set to "Disabled" by default +} + +// IsEmpty returns true if DeleteMarkerReplication is not set +func (d DeleteMarkerReplication) IsEmpty() bool { + return len(d.Status) == 0 +} + +// Validate validates whether the status is disabled. +func (d DeleteMarkerReplication) Validate() error { + if d.IsEmpty() { + return errDeleteMarkerReplicationMissing + } + if d.Status != Disabled { + return errInvalidDeleteMarkerReplicationStatus + } + return nil +} + +// Rule - a rule for replication configuration. +type Rule struct { + XMLName xml.Name `xml:"Rule" json:"Rule"` + ID string `xml:"ID,omitempty" json:"ID,omitempty"` + Status Status `xml:"Status" json:"Status"` + Priority int `xml:"Priority" json:"Priority"` + DeleteMarkerReplication DeleteMarkerReplication `xml:"DeleteMarkerReplication" json:"DeleteMarkerReplication"` + Destination Destination `xml:"Destination" json:"Destination"` + Filter Filter `xml:"Filter" json:"Filter"` +} + +var ( + errInvalidRuleID = Errorf("ID must be less than 255 characters") + errEmptyRuleStatus = Errorf("Status should not be empty") + errInvalidRuleStatus = Errorf("Status must be set to either Enabled or Disabled") + errDeleteMarkerReplicationMissing = Errorf("DeleteMarkerReplication must be specified") + errPriorityMissing = Errorf("Priority must be specified") + errInvalidDeleteMarkerReplicationStatus = Errorf("Delete marker replication is currently not supported") + errDestinationSourceIdentical = Errorf("Destination bucket cannot be the same as the source bucket.") +) + +// validateID - checks if ID is valid or not. +func (r Rule) validateID() error { + // cannot be longer than 255 characters + if len(r.ID) > 255 { + return errInvalidRuleID + } + return nil +} + +// validateStatus - checks if status is valid or not. +func (r Rule) validateStatus() error { + // Status can't be empty + if len(r.Status) == 0 { + return errEmptyRuleStatus + } + + // Status must be one of Enabled or Disabled + if r.Status != Enabled && r.Status != Disabled { + return errInvalidRuleStatus + } + return nil +} + +func (r Rule) validateFilter() error { + if err := r.Filter.Validate(); err != nil { + return err + } + return nil +} + +// Prefix - a rule can either have prefix under or under +// . This method returns the prefix from the +// location where it is available +func (r Rule) Prefix() string { + if r.Filter.Prefix != "" { + return r.Filter.Prefix + } + return r.Filter.And.Prefix +} + +// Tags - a rule can either have tag under or under +// . This method returns all the tags from the +// rule in the format tag1=value1&tag2=value2 +func (r Rule) Tags() string { + if !r.Filter.Tag.IsEmpty() { + return r.Filter.Tag.String() + } + if len(r.Filter.And.Tags) != 0 { + var buf bytes.Buffer + for _, t := range r.Filter.And.Tags { + if buf.Len() > 0 { + buf.WriteString("&") + } + buf.WriteString(t.String()) + } + return buf.String() + } + return "" +} + +// Validate - validates the rule element +func (r Rule) Validate(bucket string, sameTarget bool) error { + if err := r.validateID(); err != nil { + return err + } + if err := r.validateStatus(); err != nil { + return err + } + if err := r.validateFilter(); err != nil { + return err + } + if err := r.DeleteMarkerReplication.Validate(); err != nil { + return err + } + if r.Priority <= 0 { + return errPriorityMissing + } + if r.Destination.Bucket == bucket && sameTarget { + return errDestinationSourceIdentical + } + return nil +} diff --git a/pkg/bucket/replication/tag.go b/pkg/bucket/replication/tag.go new file mode 100644 index 000000000..9a5b25a21 --- /dev/null +++ b/pkg/bucket/replication/tag.go @@ -0,0 +1,56 @@ +/* + * 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 replication + +import ( + "encoding/xml" + "unicode/utf8" +) + +// Tag - a tag for a replication configuration Rule filter. +type Tag struct { + XMLName xml.Name `xml:"Tag" json:"Tag"` + Key string `xml:"Key,omitempty" json:"Key,omitempty"` + Value string `xml:"Value,omitempty" json:"Value,omitempty"` +} + +var ( + errInvalidTagKey = Errorf("The TagKey you have provided is invalid") + errInvalidTagValue = Errorf("The TagValue you have provided is invalid") +) + +func (tag Tag) String() string { + return tag.Key + "=" + tag.Value +} + +// IsEmpty returns whether this tag is empty or not. +func (tag Tag) IsEmpty() bool { + return tag.Key == "" +} + +// Validate checks this tag. +func (tag Tag) Validate() error { + if len(tag.Key) == 0 || utf8.RuneCountInString(tag.Key) > 128 { + return errInvalidTagKey + } + + if utf8.RuneCountInString(tag.Value) > 256 { + return errInvalidTagValue + } + + return nil +} diff --git a/pkg/event/name.go b/pkg/event/name.go index d53313bb1..434661807 100644 --- a/pkg/event/name.go +++ b/pkg/event/name.go @@ -43,9 +43,9 @@ const ( ObjectRemovedAll ObjectRemovedDelete ObjectRemovedDeleteMarkerCreated - BucketCreated BucketRemoved + OperationReplicationFailed ) // Expand - returns expanded values of abbreviated event type. @@ -104,6 +104,8 @@ func (name Name) String() string { return "s3:ObjectRemoved:Delete" case ObjectRemovedDeleteMarkerCreated: return "s3:ObjectRemoved:DeleteMarkerCreated" + case OperationReplicationFailed: + return "s3:Replication:OperationFailedReplication" } return "" @@ -188,6 +190,8 @@ func ParseName(s string) (Name, error) { return ObjectRemovedDelete, nil case "s3:ObjectRemoved:DeleteMarkerCreated": return ObjectRemovedDeleteMarkerCreated, nil + case "s3:Replication:OperationFailedReplication": + return OperationReplicationFailed, nil default: return 0, &ErrInvalidEventName{s} } diff --git a/pkg/iam/policy/action.go b/pkg/iam/policy/action.go index 45122a9bf..4ae35fdff 100644 --- a/pkg/iam/policy/action.go +++ b/pkg/iam/policy/action.go @@ -158,6 +158,22 @@ const ( // GetBucketVersioningAction - GetBucketVersioning REST API action GetBucketVersioningAction = "s3:GetBucketVersioning" + // GetReplicationConfigurationAction - GetReplicationConfiguration REST API action + GetReplicationConfigurationAction = "s3:GetReplicationConfiguration" + // PutReplicationConfigurationAction - PutReplicationConfiguration REST API action + PutReplicationConfigurationAction = "s3:PutReplicationConfiguration" + + // ReplicateObjectAction - ReplicateObject REST API action + ReplicateObjectAction = "s3:ReplicateObject" + + // ReplicateDeleteAction - ReplicateDelete REST API action + ReplicateDeleteAction = "s3:ReplicateDelete" + + // ReplicateTagsAction - ReplicateTags REST API action + ReplicateTagsAction = "s3:ReplicateTags" + + // GetObjectVersionForReplicationAction - GetObjectVersionForReplication REST API action + GetObjectVersionForReplicationAction = "s3:GetObjectVersionForReplication" // AllActions - all API actions AllActions = "s3:*" @@ -208,30 +224,40 @@ var supportedActions = map[Action]struct{}{ GetBucketEncryptionAction: {}, PutBucketVersioningAction: {}, GetBucketVersioningAction: {}, + GetReplicationConfigurationAction: {}, + PutReplicationConfigurationAction: {}, + ReplicateObjectAction: {}, + ReplicateDeleteAction: {}, + ReplicateTagsAction: {}, + GetObjectVersionForReplicationAction: {}, AllActions: {}, } // List of all supported object actions. var supportedObjectActions = map[Action]struct{}{ - AllActions: {}, - AbortMultipartUploadAction: {}, - DeleteObjectAction: {}, - GetObjectAction: {}, - ListMultipartUploadPartsAction: {}, - PutObjectAction: {}, - BypassGovernanceRetentionAction: {}, - PutObjectRetentionAction: {}, - GetObjectRetentionAction: {}, - PutObjectLegalHoldAction: {}, - GetObjectLegalHoldAction: {}, - GetObjectTaggingAction: {}, - PutObjectTaggingAction: {}, - DeleteObjectTaggingAction: {}, - GetObjectVersionAction: {}, - GetObjectVersionTaggingAction: {}, - DeleteObjectVersionAction: {}, - DeleteObjectVersionTaggingAction: {}, - PutObjectVersionTaggingAction: {}, + AllActions: {}, + AbortMultipartUploadAction: {}, + DeleteObjectAction: {}, + GetObjectAction: {}, + ListMultipartUploadPartsAction: {}, + PutObjectAction: {}, + BypassGovernanceRetentionAction: {}, + PutObjectRetentionAction: {}, + GetObjectRetentionAction: {}, + PutObjectLegalHoldAction: {}, + GetObjectLegalHoldAction: {}, + GetObjectTaggingAction: {}, + PutObjectTaggingAction: {}, + DeleteObjectTaggingAction: {}, + GetObjectVersionAction: {}, + GetObjectVersionTaggingAction: {}, + DeleteObjectVersionAction: {}, + DeleteObjectVersionTaggingAction: {}, + PutObjectVersionTaggingAction: {}, + ReplicateObjectAction: {}, + ReplicateDeleteAction: {}, + ReplicateTagsAction: {}, + GetObjectVersionForReplicationAction: {}, } // isObjectAction - returns whether action is object type or not. @@ -360,4 +386,10 @@ var actionConditionKeyMap = map[Action]condition.KeySet{ append([]condition.Key{ condition.S3VersionID, }, condition.CommonKeys...)...), + GetReplicationConfigurationAction: condition.NewKeySet(condition.CommonKeys...), + PutReplicationConfigurationAction: condition.NewKeySet(condition.CommonKeys...), + ReplicateObjectAction: condition.NewKeySet(condition.CommonKeys...), + ReplicateDeleteAction: condition.NewKeySet(condition.CommonKeys...), + ReplicateTagsAction: condition.NewKeySet(condition.CommonKeys...), + GetObjectVersionForReplicationAction: condition.NewKeySet(condition.CommonKeys...), } diff --git a/pkg/iam/policy/admin-action.go b/pkg/iam/policy/admin-action.go index afcbe1c72..ee3d86cbd 100644 --- a/pkg/iam/policy/admin-action.go +++ b/pkg/iam/policy/admin-action.go @@ -108,46 +108,55 @@ const ( // GetBucketQuotaAdminAction - allow getting bucket quota GetBucketQuotaAdminAction = "admin:GetBucketQuota" + // Bucket Replication admin Actions + + // SetBucketReplicationTargetAction - allow setting bucket replication target + SetBucketReplicationTargetAction = "admin:SetBucketReplicationTarget" + // GetBucketReplicationTargetAction - allow getting bucket replication targets + GetBucketReplicationTargetAction = "admin:GetBucketReplicationTarget" + // AllAdminActions - provides all admin permissions AllAdminActions = "admin:*" ) // List of all supported admin actions. var supportedAdminActions = map[AdminAction]struct{}{ - HealAdminAction: {}, - StorageInfoAdminAction: {}, - DataUsageInfoAdminAction: {}, - TopLocksAdminAction: {}, - ProfilingAdminAction: {}, - TraceAdminAction: {}, - ConsoleLogAdminAction: {}, - KMSKeyStatusAdminAction: {}, - ServerInfoAdminAction: {}, - OBDInfoAdminAction: {}, - ServerUpdateAdminAction: {}, - ServiceRestartAdminAction: {}, - ServiceStopAdminAction: {}, - ConfigUpdateAdminAction: {}, - CreateUserAdminAction: {}, - DeleteUserAdminAction: {}, - ListUsersAdminAction: {}, - EnableUserAdminAction: {}, - DisableUserAdminAction: {}, - GetUserAdminAction: {}, - AddUserToGroupAdminAction: {}, - RemoveUserFromGroupAdminAction: {}, - GetGroupAdminAction: {}, - ListGroupsAdminAction: {}, - EnableGroupAdminAction: {}, - DisableGroupAdminAction: {}, - CreatePolicyAdminAction: {}, - DeletePolicyAdminAction: {}, - GetPolicyAdminAction: {}, - AttachPolicyAdminAction: {}, - ListUserPoliciesAdminAction: {}, - SetBucketQuotaAdminAction: {}, - GetBucketQuotaAdminAction: {}, - AllAdminActions: {}, + HealAdminAction: {}, + StorageInfoAdminAction: {}, + DataUsageInfoAdminAction: {}, + TopLocksAdminAction: {}, + ProfilingAdminAction: {}, + TraceAdminAction: {}, + ConsoleLogAdminAction: {}, + KMSKeyStatusAdminAction: {}, + ServerInfoAdminAction: {}, + OBDInfoAdminAction: {}, + ServerUpdateAdminAction: {}, + ServiceRestartAdminAction: {}, + ServiceStopAdminAction: {}, + ConfigUpdateAdminAction: {}, + CreateUserAdminAction: {}, + DeleteUserAdminAction: {}, + ListUsersAdminAction: {}, + EnableUserAdminAction: {}, + DisableUserAdminAction: {}, + GetUserAdminAction: {}, + AddUserToGroupAdminAction: {}, + RemoveUserFromGroupAdminAction: {}, + GetGroupAdminAction: {}, + ListGroupsAdminAction: {}, + EnableGroupAdminAction: {}, + DisableGroupAdminAction: {}, + CreatePolicyAdminAction: {}, + DeletePolicyAdminAction: {}, + GetPolicyAdminAction: {}, + AttachPolicyAdminAction: {}, + ListUserPoliciesAdminAction: {}, + SetBucketQuotaAdminAction: {}, + GetBucketQuotaAdminAction: {}, + SetBucketReplicationTargetAction: {}, + GetBucketReplicationTargetAction: {}, + AllAdminActions: {}, } // IsValid - checks if action is valid or not. @@ -158,37 +167,39 @@ func (action AdminAction) IsValid() bool { // adminActionConditionKeyMap - holds mapping of supported condition key for an action. var adminActionConditionKeyMap = map[Action]condition.KeySet{ - AllAdminActions: condition.NewKeySet(condition.AllSupportedAdminKeys...), - HealAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - StorageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ServerInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - DataUsageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - OBDInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - TopLocksAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ProfilingAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - TraceAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ConsoleLogAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - KMSKeyStatusAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ServerUpdateAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ServiceRestartAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ServiceStopAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ConfigUpdateAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - CreateUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - DeleteUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ListUsersAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - EnableUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - DisableUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - GetUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - AddUserToGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - RemoveUserFromGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ListGroupsAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - EnableGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - DisableGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - CreatePolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - DeletePolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - GetPolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - AttachPolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - ListUserPoliciesAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - SetBucketQuotaAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), - GetBucketQuotaAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + AllAdminActions: condition.NewKeySet(condition.AllSupportedAdminKeys...), + HealAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + StorageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ServerInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + DataUsageInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + OBDInfoAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + TopLocksAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ProfilingAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + TraceAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ConsoleLogAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + KMSKeyStatusAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ServerUpdateAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ServiceRestartAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ServiceStopAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ConfigUpdateAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + CreateUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + DeleteUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ListUsersAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + EnableUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + DisableUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + GetUserAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + AddUserToGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + RemoveUserFromGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ListGroupsAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + EnableGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + DisableGroupAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + CreatePolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + DeletePolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + GetPolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + AttachPolicyAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + ListUserPoliciesAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + SetBucketQuotaAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + GetBucketQuotaAdminAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + SetBucketReplicationTargetAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), + GetBucketReplicationTargetAction: condition.NewKeySet(condition.AllSupportedAdminKeys...), } diff --git a/pkg/madmin/examples/bucket-replication.go b/pkg/madmin/examples/bucket-replication.go new file mode 100644 index 000000000..85c76863c --- /dev/null +++ b/pkg/madmin/examples/bucket-replication.go @@ -0,0 +1,61 @@ +// +build ignore + +/* + * 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 main + +import ( + "context" + "log" + + "github.com/minio/minio/pkg/auth" + "github.com/minio/minio/pkg/madmin" +) + +func main() { + // Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY and my-bucketname are + // dummy values, please replace them with original values. + + // API requests are secure (HTTPS) if secure=true and insecure (HTTP) otherwise. + // New returns an MinIO Admin client object. + madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true) + if err != nil { + log.Fatalln(err) + } + ctx := context.Background() + creds, err := auth.CreateCredentials("access-key", "secret-key") + if err != nil { + log.Fatalln(err) + } + target := madmin.BucketReplicationTarget{Endpoint: "site2:9000", Credentials: creds, TargetBucket: "destbucket", IsSSL: false} + // Set bucket replication target + if err := madmClnt.SetBucketReplicationTarget(ctx, "srcbucket", &target); err != nil { + log.Fatalln(err) + } + // Get bucket replication target + target, err = madmClnt.GetBucketReplicationTarget(ctx, "srcbucket") + if err != nil { + log.Fatalln(err) + } + + // Remove bucket replication target + if err := madmClnt.SetBucketReplicationTarget(ctx, "srcbucket", nil); err != nil { + log.Fatalln(err) + } + +} diff --git a/pkg/madmin/replication-commands.go b/pkg/madmin/replication-commands.go new file mode 100644 index 000000000..61ede1d7a --- /dev/null +++ b/pkg/madmin/replication-commands.go @@ -0,0 +1,163 @@ +/* + * 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 madmin + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "github.com/minio/minio/pkg/auth" +) + +// BucketReplicationTarget represents the target bucket and site to be replicated to. +type BucketReplicationTarget struct { + Endpoint string `json:"endpoint"` + Credentials *auth.Credentials `json:"credentials"` + TargetBucket string `json:"targetbucket"` + IsSSL bool `json:"isssl"` + Path string `json:"path,omitempty"` + API string `json:"api,omitempty"` + Arn string `json:"arn,omitempty"` +} + +// URL returns replication target url +func (t BucketReplicationTarget) URL() string { + scheme := "http" + if t.IsSSL { + scheme = "https" + } + return fmt.Sprintf("%s://%s", scheme, t.Endpoint) +} + +// Empty returns true if struct is empty. +func (t BucketReplicationTarget) Empty() bool { + return t.String() == "" || t.Credentials == nil +} + +func (t *BucketReplicationTarget) String() string { + return fmt.Sprintf("%s %s", t.Endpoint, t.TargetBucket) +} + +// GetBucketReplicationTarget - gets replication target for this bucket +func (adm *AdminClient) GetBucketReplicationTarget(ctx context.Context, bucket string) (target BucketReplicationTarget, err error) { + queryValues := url.Values{} + queryValues.Set("bucket", bucket) + + reqData := requestData{ + relPath: adminAPIPrefix + "/get-bucket-replication-target", + queryValues: queryValues, + } + + // Execute GET on /minio/admin/v3/get-bucket-replication-target + resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) + + defer closeResponse(resp) + if err != nil { + return target, err + } + + if resp.StatusCode != http.StatusOK { + return target, httpRespToErrorResponse(resp) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return target, err + } + if err = json.Unmarshal(b, &target); err != nil { + return target, err + } + if target.Empty() { + return target, errors.New("No Replication target configured") + } + return target, nil +} + +// SetBucketReplicationTarget sets up a replication target for this bucket +func (adm *AdminClient) SetBucketReplicationTarget(ctx context.Context, bucket string, target *BucketReplicationTarget) error { + data, err := json.Marshal(target) + if err != nil { + return err + } + encData, err := EncryptData(adm.getSecretKey(), data) + if err != nil { + return err + } + queryValues := url.Values{} + queryValues.Set("bucket", bucket) + + reqData := requestData{ + relPath: adminAPIPrefix + "/set-bucket-replication-target", + queryValues: queryValues, + content: encData, + } + + // Execute PUT on /minio/admin/v3/set-bucket-replication-target to set a replication target for this bucket. + resp, err := adm.executeMethod(ctx, http.MethodPut, reqData) + + defer closeResponse(resp) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return httpRespToErrorResponse(resp) + } + + return nil +} + +// GetBucketReplicationARN - gets replication Arn for this remote +func (adm *AdminClient) GetBucketReplicationARN(ctx context.Context, rURL string) (arn string, err error) { + queryValues := url.Values{} + queryValues.Set("url", rURL) + + reqData := requestData{ + relPath: adminAPIPrefix + "/get-bucket-replication-arn", + queryValues: queryValues, + } + + // Execute GET on /minio/admin/v3/list-bucket-replication-arn + resp, err := adm.executeMethod(ctx, http.MethodGet, reqData) + + defer closeResponse(resp) + if err != nil { + return arn, err + } + + if resp.StatusCode != http.StatusOK { + return arn, httpRespToErrorResponse(resp) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return arn, err + } + if err = json.Unmarshal(b, &arn); err != nil { + return arn, err + } + if arn == "" { + return arn, fmt.Errorf("Missing Replication ARN") + } + return arn, nil +}