mirror of
https://github.com/pgsty/minio.git
synced 2026-11-04 14:52:02 +03:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9a4ad7904 | |||
| e60b67d246 | |||
| 9004d69c6f | |||
| 8856a2d77b | |||
| 54a061bdda | |||
| 65b4b100a8 | |||
| 7cc9286e0f | |||
| 2f25639ea0 | |||
| 2070c215a2 | |||
| 94b98222c2 | |||
| 9c605ad153 | |||
| b7c7e59dac | |||
| 767c1436d3 | |||
| 699cf6ff45 | |||
| 9201870f6c | |||
| 6722f58668 | |||
| 7b9b7cef11 | |||
| 7d4fce09dc | |||
| 2075501d86 | |||
| bd099f5e71 | |||
| baf257adcb | |||
| 4fd1986885 | |||
| e1afac9439 | |||
| 580d9db85e | |||
| 42e2fd35d8 | |||
| 1a40c7c27c | |||
| f3bec41eb9 | |||
| 825634d24e | |||
| cb097e6b0a | |||
| 1cfb03fb74 | |||
| f293df647c | |||
| 10522438b7 | |||
| e2e5bd6f19 | |||
| cd7a0a9757 | |||
| b3eda248a3 | |||
| 95b51c48be | |||
| 486888f595 | |||
| 17ab8145b5 | |||
| b3ebc69034 | |||
| 761dde2f1b | |||
| 2bb6a3f4d0 | |||
| e83e947ca3 | |||
| 73733a8fb9 | |||
| ce6c23a360 | |||
| 99d8e6a30f | |||
| 2fa1d8ac48 | |||
| ca7e425ce8 | |||
| 8b9a19eef1 | |||
| 7f629df4d5 | |||
| 98ddc3596c | |||
| 5d23be6242 | |||
| d15d3a524b | |||
| 1e1d9acb1b |
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.5
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6
|
||||
|
||||
ARG RELEASE
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.5
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.5
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
|
||||
@@ -18,15 +18,31 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/minio/kes"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||
"github.com/minio/minio/internal/bucket/versioning"
|
||||
"github.com/minio/minio/internal/event"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
)
|
||||
|
||||
@@ -76,15 +92,17 @@ func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *
|
||||
return
|
||||
}
|
||||
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data); err != nil {
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
bucketMeta := madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeQuotaConfig,
|
||||
Bucket: bucket,
|
||||
Quota: data,
|
||||
Type: madmin.SRBucketMetaTypeQuotaConfig,
|
||||
Bucket: bucket,
|
||||
Quota: data,
|
||||
UpdatedAt: updatedAt,
|
||||
}
|
||||
if quotaConfig.Quota == 0 {
|
||||
bucketMeta.Quota = nil
|
||||
@@ -251,7 +269,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
||||
return
|
||||
}
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -346,7 +364,7 @@ func (a adminAPIHandlers) RemoveRemoteTargetHandler(w http.ResponseWriter, r *ht
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
||||
return
|
||||
}
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -354,3 +372,698 @@ func (a adminAPIHandlers) RemoveRemoteTargetHandler(w http.ResponseWriter, r *ht
|
||||
// Write success response.
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
// ExportBucketMetadataHandler - exports all bucket metadata as a zipped file
|
||||
func (a adminAPIHandlers) ExportBucketMetadataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ExportBucketMetadata")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
bucket := pathClean(r.Form.Get("bucket"))
|
||||
if !globalIsErasure {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
// Get current object layer instance.
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ExportBucketMetadataAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
var (
|
||||
buckets []BucketInfo
|
||||
err error
|
||||
)
|
||||
if bucket != "" {
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
buckets = append(buckets, BucketInfo{Name: bucket})
|
||||
} else {
|
||||
buckets, err = objectAPI.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize a zip writer which will provide a zipped content
|
||||
// of bucket metadata
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
rawDataFn := func(r io.Reader, filename string, sz int) error {
|
||||
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
||||
name: filename,
|
||||
size: int64(sz),
|
||||
mode: 0o600,
|
||||
modTime: time.Now(),
|
||||
isDir: false,
|
||||
sys: nil,
|
||||
})
|
||||
if zerr != nil {
|
||||
logger.LogIf(ctx, zerr)
|
||||
return nil
|
||||
}
|
||||
header.Method = zip.Deflate
|
||||
zwriter, zerr := zipWriter.CreateHeader(header)
|
||||
if zerr != nil {
|
||||
logger.LogIf(ctx, zerr)
|
||||
return nil
|
||||
}
|
||||
if _, err := io.Copy(zwriter, r); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
cfgFiles := []string{
|
||||
bucketPolicyConfig,
|
||||
bucketNotificationConfig,
|
||||
bucketLifecycleConfig,
|
||||
bucketSSEConfig,
|
||||
bucketTaggingConfig,
|
||||
bucketQuotaConfigFile,
|
||||
objectLockConfig,
|
||||
bucketVersioningConfig,
|
||||
bucketReplicationConfig,
|
||||
bucketTargetsFile,
|
||||
}
|
||||
for _, bi := range buckets {
|
||||
for _, cfgFile := range cfgFiles {
|
||||
cfgPath := pathJoin(bi.Name, cfgFile)
|
||||
bucket := bi.Name
|
||||
switch cfgFile {
|
||||
case bucketNotificationConfig:
|
||||
config, err := globalBucketMetadataSys.GetNotificationConfig(bucket)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
case bucketLifecycleConfig:
|
||||
config, err := globalBucketMetadataSys.GetLifecycleConfig(bucket)
|
||||
if err != nil {
|
||||
if errors.Is(err, BucketLifecycleNotFound{Bucket: bucket}) {
|
||||
continue
|
||||
}
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
case bucketQuotaConfigFile:
|
||||
config, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket)
|
||||
if err != nil {
|
||||
if errors.Is(err, BucketQuotaConfigNotFound{Bucket: bucket}) {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
case bucketSSEConfig:
|
||||
config, _, err := globalBucketMetadataSys.GetSSEConfig(bucket)
|
||||
if err != nil {
|
||||
if errors.Is(err, BucketSSEConfigNotFound{Bucket: bucket}) {
|
||||
continue
|
||||
}
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
case bucketTaggingConfig:
|
||||
config, _, err := globalBucketMetadataSys.GetTaggingConfig(bucket)
|
||||
if err != nil {
|
||||
if errors.Is(err, BucketTaggingNotFound{Bucket: bucket}) {
|
||||
continue
|
||||
}
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
case objectLockConfig:
|
||||
config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
|
||||
if err != nil {
|
||||
if errors.Is(err, BucketObjectLockConfigNotFound{Bucket: bucket}) {
|
||||
continue
|
||||
}
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
case bucketVersioningConfig:
|
||||
config, _, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
// ignore empty versioning configs
|
||||
if config.Status != versioning.Enabled && config.Status != versioning.Suspended {
|
||||
continue
|
||||
}
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
case bucketReplicationConfig:
|
||||
config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
||||
if err != nil {
|
||||
if errors.Is(err, BucketReplicationConfigNotFound{Bucket: bucket}) {
|
||||
continue
|
||||
}
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
case bucketTargetsFile:
|
||||
config, err := globalBucketMetadataSys.GetBucketTargetsConfig(bucket)
|
||||
if err != nil {
|
||||
if errors.Is(err, BucketRemoteTargetNotFound{Bucket: bucket}) {
|
||||
continue
|
||||
}
|
||||
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ImportBucketMetadataHandler - imports all bucket metadata from a zipped file and overwrite bucket metadata config
|
||||
// There are some caveats regarding the following:
|
||||
// 1. object lock config - object lock should have been specified at time of bucket creation. Only default retention settings are imported here.
|
||||
// 2. Replication config - is omitted from import as remote target credentials are not available from exported data for security reasons.
|
||||
// 3. lifecycle config - if transition rules are present, tier name needs to have been defined.
|
||||
func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ImportBucketMetadata")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
bucket := pathClean(r.Form.Get("bucket"))
|
||||
|
||||
if !globalIsErasure {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
// Get current object layer instance.
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ImportBucketMetadataAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
reader := bytes.NewReader(data)
|
||||
zr, err := zip.NewReader(reader, int64(len(data)))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
bucketMap := make(map[string]struct{}, 1)
|
||||
|
||||
// import object lock config if any - order of import matters here.
|
||||
for _, file := range zr.File {
|
||||
slc := strings.Split(file.Name, slashSeparator)
|
||||
if len(slc) != 2 { // expecting bucket/configfile in the zipfile
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
b, fileName := slc[0], slc[1]
|
||||
if bucket == "" { // use bucket requested in query parameters if specified. Otherwise default bucket name to directory name within zip
|
||||
bucket = b
|
||||
}
|
||||
switch fileName {
|
||||
case objectLockConfig:
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
config, err := objectlock.ParseObjectLockConfig(reader)
|
||||
if err != nil {
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||
apiErr.Description = err.Error()
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
if _, ok := bucketMap[bucket]; !ok {
|
||||
opts := BucketOptions{
|
||||
LockEnabled: config.ObjectLockEnabled == "Enabled",
|
||||
}
|
||||
err = objectAPI.MakeBucketWithLocation(ctx, bucket, opts)
|
||||
if err != nil {
|
||||
if _, ok := err.(BucketExists); !ok {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
bucketMap[bucket] = struct{}{}
|
||||
}
|
||||
|
||||
// Deny object locking configuration settings on existing buckets without object lock enabled.
|
||||
if _, _, err = globalBucketMetadataSys.GetObjectLockConfig(bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, objectLockConfig, configData)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Call site replication hook.
|
||||
//
|
||||
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||
// errors.
|
||||
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
||||
Bucket: bucket,
|
||||
ObjectLockConfig: &cfgStr,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// import versioning metadata
|
||||
for _, file := range zr.File {
|
||||
slc := strings.Split(file.Name, slashSeparator)
|
||||
if len(slc) != 2 { // expecting bucket/configfile in the zipfile
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
b, fileName := slc[0], slc[1]
|
||||
if bucket == "" { // use bucket requested in query parameters if specified. Otherwise default bucket name to directory name within zip
|
||||
bucket = b
|
||||
}
|
||||
switch fileName {
|
||||
case bucketVersioningConfig:
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
v, err := versioning.ParseConfig(io.LimitReader(reader, maxBucketVersioningConfigSize))
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
if _, ok := bucketMap[bucket]; !ok {
|
||||
err = objectAPI.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
if err != nil {
|
||||
if _, ok := err.(BucketExists); !ok {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
bucketMap[bucket] = struct{}{}
|
||||
}
|
||||
|
||||
if globalSiteReplicationSys.isEnabled() && v.Suspended() {
|
||||
writeErrorResponse(ctx, w, APIError{
|
||||
Code: "InvalidBucketState",
|
||||
Description: "Cluster replication is enabled for this site, so the versioning state cannot be changed.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
}, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled && v.Suspended() {
|
||||
writeErrorResponse(ctx, w, APIError{
|
||||
Code: "InvalidBucketState",
|
||||
Description: "An Object Lock configuration is present on this bucket, so the versioning state cannot be changed.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
}, r.URL)
|
||||
return
|
||||
}
|
||||
if _, err := getReplicationConfig(ctx, bucket); err == nil && v.Suspended() {
|
||||
writeErrorResponse(ctx, w, APIError{
|
||||
Code: "InvalidBucketState",
|
||||
Description: "A replication configuration is present on this bucket, so the versioning state cannot be changed.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
}, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := xml.Marshal(v)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, configData); err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range zr.File {
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, ""), r.URL)
|
||||
return
|
||||
}
|
||||
sz := file.FileInfo().Size()
|
||||
slc := strings.Split(file.Name, slashSeparator)
|
||||
if len(slc) != 2 { // expecting bucket/configfile in the zipfile
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
b, fileName := slc[0], slc[1]
|
||||
if bucket == "" { // use bucket requested in query parameters if specified. Otherwise default bucket name to directory name within zip
|
||||
bucket = b
|
||||
}
|
||||
// create bucket if it does not exist yet.
|
||||
if _, ok := bucketMap[bucket]; !ok {
|
||||
err = objectAPI.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
if err != nil {
|
||||
if _, ok := err.(BucketExists); !ok {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
bucketMap[bucket] = struct{}{}
|
||||
}
|
||||
switch fileName {
|
||||
case bucketNotificationConfig:
|
||||
config, err := event.ParseConfig(io.LimitReader(reader, sz), globalSite.Region, globalNotificationSys.targetList)
|
||||
if err != nil {
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||
if event.IsEventError(err) {
|
||||
apiErr = importError(ctx, err, file.Name, bucket)
|
||||
}
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketNotificationConfig, configData); err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
rulesMap := config.ToRulesMap()
|
||||
globalNotificationSys.AddRulesMap(bucket, rulesMap)
|
||||
case bucketPolicyConfig:
|
||||
// Error out if Content-Length is beyond allowed size.
|
||||
if sz > maxBucketPolicySize {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPolicyTooLarge), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
bucketPolicyBytes, err := ioutil.ReadAll(io.LimitReader(reader, sz))
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
bucketPolicy, err := policy.ParseConfig(bytes.NewReader(bucketPolicyBytes), bucket)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Version in policy must not be empty
|
||||
if bucketPolicy.Version == "" {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPolicy), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := json.Marshal(bucketPolicy)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, configData)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
// Call site replication hook.
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypePolicy,
|
||||
Bucket: bucket,
|
||||
Policy: bucketPolicyBytes,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
case bucketLifecycleConfig:
|
||||
bucketLifecycle, err := lifecycle.ParseLifecycleConfig(io.LimitReader(reader, sz))
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the received bucket policy document
|
||||
if err = bucketLifecycle.Validate(); err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the transition storage ARNs
|
||||
if err = validateTransitionTier(bucketLifecycle); err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := xml.Marshal(bucketLifecycle)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketLifecycleConfig, configData); err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
case bucketSSEConfig:
|
||||
// Parse bucket encryption xml
|
||||
encConfig, err := validateBucketSSEConfig(io.LimitReader(reader, maxBucketSSEConfigSize))
|
||||
if err != nil {
|
||||
apiErr := APIError{
|
||||
Code: "MalformedXML",
|
||||
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrMalformedXML].Description, err),
|
||||
HTTPStatusCode: errorCodes[ErrMalformedXML].HTTPStatusCode,
|
||||
}
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Return error if KMS is not initialized
|
||||
if GlobalKMS == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
||||
return
|
||||
}
|
||||
kmsKey := encConfig.KeyID()
|
||||
if kmsKey != "" {
|
||||
kmsContext := kms.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
|
||||
_, err := GlobalKMS.GenerateKey(kmsKey, kmsContext)
|
||||
if err != nil {
|
||||
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||
writeErrorResponse(ctx, w, importError(ctx, errKMSKeyNotFound, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
configData, err := xml.Marshal(encConfig)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Store the bucket encryption configuration in the object layer
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, configData)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Call site replication hook.
|
||||
//
|
||||
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||
// errors.
|
||||
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeSSEConfig,
|
||||
Bucket: bucket,
|
||||
SSEConfig: &cfgStr,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
case bucketTaggingConfig:
|
||||
tags, err := tags.ParseBucketXML(io.LimitReader(reader, sz))
|
||||
if err != nil {
|
||||
apiErr := errorCodes.ToAPIErrWithErr(ErrMalformedXML, fmt.Errorf("error importing %s with %w", file.Name, err))
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := xml.Marshal(tags)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, configData)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
// Call site replication hook.
|
||||
//
|
||||
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||
// errors.
|
||||
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeTags,
|
||||
Bucket: bucket,
|
||||
Tags: &cfgStr,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
case bucketQuotaConfigFile:
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
quotaConfig, err := parseBucketQuota(bucket, data)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if quotaConfig.Type == "fifo" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
bucketMeta := madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeQuotaConfig,
|
||||
Bucket: bucket,
|
||||
Quota: data,
|
||||
UpdatedAt: updatedAt,
|
||||
}
|
||||
if quotaConfig.Quota == 0 {
|
||||
bucketMeta.Quota = nil
|
||||
}
|
||||
|
||||
// Call site replication hook.
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, bucketMeta); err != nil {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/kes"
|
||||
@@ -96,6 +97,12 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
||||
}
|
||||
default:
|
||||
switch {
|
||||
case errors.Is(err, errTooManyPolicies):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminInvalidRequest",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errDecommissionAlreadyRunning):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioDecommissionNotAllowed",
|
||||
@@ -217,3 +224,27 @@ func toAdminAPIErrCode(ctx context.Context, err error) APIErrorCode {
|
||||
return toAPIErrorCode(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// wraps export error for more context
|
||||
func exportError(ctx context.Context, err error, fname, entity string) APIError {
|
||||
if entity == "" {
|
||||
return toAPIError(ctx, fmt.Errorf("error exporting %s with: %w", fname, err))
|
||||
}
|
||||
return toAPIError(ctx, fmt.Errorf("error exporting %s from %s with: %w", entity, fname, err))
|
||||
}
|
||||
|
||||
// wraps import error for more context
|
||||
func importError(ctx context.Context, err error, fname, entity string) APIError {
|
||||
if entity == "" {
|
||||
return toAPIError(ctx, fmt.Errorf("error importing %s with: %w", fname, err))
|
||||
}
|
||||
return toAPIError(ctx, fmt.Errorf("error importing %s from %s with: %w", entity, fname, err))
|
||||
}
|
||||
|
||||
// wraps import error for more context
|
||||
func importErrorWithAPIErr(ctx context.Context, apiErr APIErrorCode, err error, fname, entity string) APIError {
|
||||
if entity == "" {
|
||||
return errorCodes.ToAPIErrWithErr(apiErr, fmt.Errorf("error importing %s with: %w", fname, err))
|
||||
}
|
||||
return errorCodes.ToAPIErrWithErr(apiErr, fmt.Errorf("error importing %s from %s with: %w", entity, fname, err))
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ func (a adminAPIHandlers) SRPeerReplicateBucketItem(w http.ResponseWriter, r *ht
|
||||
err = errSRInvalidRequest(errInvalidArgument)
|
||||
case madmin.SRBucketMetaTypePolicy:
|
||||
if item.Policy == nil {
|
||||
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, nil)
|
||||
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, nil, item.UpdatedAt)
|
||||
} else {
|
||||
bktPolicy, berr := policy.ParseConfig(bytes.NewReader(item.Policy), item.Bucket)
|
||||
if berr != nil {
|
||||
@@ -222,33 +222,33 @@ func (a adminAPIHandlers) SRPeerReplicateBucketItem(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
if bktPolicy.IsEmpty() {
|
||||
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, nil)
|
||||
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, nil, item.UpdatedAt)
|
||||
} else {
|
||||
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, bktPolicy)
|
||||
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, bktPolicy, item.UpdatedAt)
|
||||
}
|
||||
}
|
||||
case madmin.SRBucketMetaTypeQuotaConfig:
|
||||
if item.Quota == nil {
|
||||
err = globalSiteReplicationSys.PeerBucketQuotaConfigHandler(ctx, item.Bucket, nil)
|
||||
err = globalSiteReplicationSys.PeerBucketQuotaConfigHandler(ctx, item.Bucket, nil, item.UpdatedAt)
|
||||
} else {
|
||||
quotaConfig, err := parseBucketQuota(item.Bucket, item.Quota)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if err = globalSiteReplicationSys.PeerBucketQuotaConfigHandler(ctx, item.Bucket, quotaConfig); err != nil {
|
||||
if err = globalSiteReplicationSys.PeerBucketQuotaConfigHandler(ctx, item.Bucket, quotaConfig, item.UpdatedAt); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
case madmin.SRBucketMetaTypeVersionConfig:
|
||||
err = globalSiteReplicationSys.PeerBucketVersioningHandler(ctx, item.Bucket, item.Versioning)
|
||||
err = globalSiteReplicationSys.PeerBucketVersioningHandler(ctx, item.Bucket, item.Versioning, item.UpdatedAt)
|
||||
case madmin.SRBucketMetaTypeTags:
|
||||
err = globalSiteReplicationSys.PeerBucketTaggingHandler(ctx, item.Bucket, item.Tags)
|
||||
err = globalSiteReplicationSys.PeerBucketTaggingHandler(ctx, item.Bucket, item.Tags, item.UpdatedAt)
|
||||
case madmin.SRBucketMetaTypeObjectLockConfig:
|
||||
err = globalSiteReplicationSys.PeerBucketObjectLockConfigHandler(ctx, item.Bucket, item.ObjectLockConfig)
|
||||
err = globalSiteReplicationSys.PeerBucketObjectLockConfigHandler(ctx, item.Bucket, item.ObjectLockConfig, item.UpdatedAt)
|
||||
case madmin.SRBucketMetaTypeSSEConfig:
|
||||
err = globalSiteReplicationSys.PeerBucketSSEConfigHandler(ctx, item.Bucket, item.SSEConfig)
|
||||
err = globalSiteReplicationSys.PeerBucketSSEConfigHandler(ctx, item.Bucket, item.SSEConfig, item.UpdatedAt)
|
||||
}
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
|
||||
+678
-1
@@ -24,9 +24,12 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config/dns"
|
||||
@@ -629,7 +632,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
opts.claims[k] = v
|
||||
}
|
||||
} else {
|
||||
// Need permission if we are creating a service acccount for a
|
||||
// Need permission if we are creating a service account for a
|
||||
// user <> to the request sender
|
||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: requestorUser,
|
||||
@@ -1512,3 +1515,677 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
allPoliciesFile = "policies.json"
|
||||
allUsersFile = "users.json"
|
||||
allGroupsFile = "groups.json"
|
||||
allSvcAcctsFile = "svcaccts.json"
|
||||
userPolicyMappingsFile = "user_mappings.json"
|
||||
groupPolicyMappingsFile = "group_mappings.json"
|
||||
stsUserPolicyMappingsFile = "stsuser_mappings.json"
|
||||
stsGroupPolicyMappingsFile = "stsgroup_mappings.json"
|
||||
)
|
||||
|
||||
// ExportIAMHandler - exports all iam info as a zipped file
|
||||
func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ExportIAM")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ExportIAMAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
// Initialize a zip writer which will provide a zipped content
|
||||
// of bucket metadata
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
rawDataFn := func(r io.Reader, filename string, sz int) error {
|
||||
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
||||
name: filename,
|
||||
size: int64(sz),
|
||||
mode: 0o600,
|
||||
modTime: time.Now(),
|
||||
isDir: false,
|
||||
sys: nil,
|
||||
})
|
||||
if zerr != nil {
|
||||
logger.LogIf(ctx, zerr)
|
||||
return nil
|
||||
}
|
||||
header.Method = zip.Deflate
|
||||
zwriter, zerr := zipWriter.CreateHeader(header)
|
||||
if zerr != nil {
|
||||
logger.LogIf(ctx, zerr)
|
||||
return nil
|
||||
}
|
||||
if _, err := io.Copy(zwriter, r); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
iamFiles := []string{
|
||||
allPoliciesFile,
|
||||
allUsersFile,
|
||||
allGroupsFile,
|
||||
allSvcAcctsFile,
|
||||
userPolicyMappingsFile,
|
||||
groupPolicyMappingsFile,
|
||||
stsUserPolicyMappingsFile,
|
||||
stsGroupPolicyMappingsFile,
|
||||
}
|
||||
for _, iamFile := range iamFiles {
|
||||
switch iamFile {
|
||||
case allPoliciesFile:
|
||||
allPolicies, err := globalIAMSys.ListPolicies(ctx, "")
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
policiesData, err := json.Marshal(allPolicies)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
if err = rawDataFn(bytes.NewReader(policiesData), iamFile, len(policiesData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
case allUsersFile:
|
||||
userCreds := make(map[string]auth.Credentials)
|
||||
globalIAMSys.store.rlock()
|
||||
err := globalIAMSys.store.loadUsers(ctx, regUser, userCreds)
|
||||
globalIAMSys.store.runlock()
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
userAccounts := make(map[string]madmin.AddOrUpdateUserReq)
|
||||
for u, cred := range userCreds {
|
||||
status := madmin.AccountDisabled
|
||||
if cred.IsValid() {
|
||||
status = madmin.AccountEnabled
|
||||
}
|
||||
userAccounts[u] = madmin.AddOrUpdateUserReq{
|
||||
SecretKey: cred.SecretKey,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
userData, err := json.Marshal(userAccounts)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = rawDataFn(bytes.NewReader(userData), iamFile, len(userData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
case allGroupsFile:
|
||||
groups := make(map[string]GroupInfo)
|
||||
globalIAMSys.store.rlock()
|
||||
err := globalIAMSys.store.loadGroups(ctx, groups)
|
||||
globalIAMSys.store.runlock()
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
groupData, err := json.Marshal(groups)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = rawDataFn(bytes.NewReader(groupData), iamFile, len(groupData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
case allSvcAcctsFile:
|
||||
serviceAccounts := make(map[string]auth.Credentials)
|
||||
globalIAMSys.store.rlock()
|
||||
err := globalIAMSys.store.loadUsers(ctx, svcUser, serviceAccounts)
|
||||
globalIAMSys.store.runlock()
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
svcAccts := make(map[string]madmin.SRSvcAccCreate)
|
||||
for user, acc := range serviceAccounts {
|
||||
if user == siteReplicatorSvcAcc {
|
||||
// skip the site replicate svc account as it should be created automatically if
|
||||
// site replication is enabled.
|
||||
continue
|
||||
}
|
||||
claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.AccessKey)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
_, policy, err := globalIAMSys.GetServiceAccount(ctx, acc.AccessKey)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var policyJSON []byte
|
||||
if policy != nil {
|
||||
policyJSON, err = json.Marshal(policy)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
svcAccts[user] = madmin.SRSvcAccCreate{
|
||||
Parent: acc.ParentUser,
|
||||
AccessKey: user,
|
||||
SecretKey: acc.SecretKey,
|
||||
Groups: acc.Groups,
|
||||
Claims: claims,
|
||||
SessionPolicy: json.RawMessage(policyJSON),
|
||||
Status: acc.Status,
|
||||
}
|
||||
}
|
||||
|
||||
svcAccData, err := json.Marshal(svcAccts)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = rawDataFn(bytes.NewReader(svcAccData), iamFile, len(svcAccData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
case userPolicyMappingsFile:
|
||||
userPolicyMap := make(map[string]MappedPolicy)
|
||||
globalIAMSys.store.rlock()
|
||||
err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, false, userPolicyMap)
|
||||
globalIAMSys.store.runlock()
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
userPolData, err := json.Marshal(userPolicyMap)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = rawDataFn(bytes.NewReader(userPolData), iamFile, len(userPolData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
case groupPolicyMappingsFile:
|
||||
groupPolicyMap := make(map[string]MappedPolicy)
|
||||
globalIAMSys.store.rlock()
|
||||
err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, true, groupPolicyMap)
|
||||
globalIAMSys.store.runlock()
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
grpPolData, err := json.Marshal(groupPolicyMap)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = rawDataFn(bytes.NewReader(grpPolData), iamFile, len(grpPolData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
case stsUserPolicyMappingsFile:
|
||||
userPolicyMap := make(map[string]MappedPolicy)
|
||||
globalIAMSys.store.rlock()
|
||||
err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, false, userPolicyMap)
|
||||
globalIAMSys.store.runlock()
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
userPolData, err := json.Marshal(userPolicyMap)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
if err = rawDataFn(bytes.NewReader(userPolData), iamFile, len(userPolData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
case stsGroupPolicyMappingsFile:
|
||||
groupPolicyMap := make(map[string]MappedPolicy)
|
||||
globalIAMSys.store.rlock()
|
||||
err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, true, groupPolicyMap)
|
||||
globalIAMSys.store.runlock()
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
grpPolData, err := json.Marshal(groupPolicyMap)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
if err = rawDataFn(bytes.NewReader(grpPolData), iamFile, len(grpPolData)); err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ImportIAM - imports all IAM info into MinIO
|
||||
func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ImportIAM")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
reader := bytes.NewReader(data)
|
||||
zr, err := zip.NewReader(reader, int64(len(data)))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
// import policies first
|
||||
{
|
||||
f, err := zr.Open(allPoliciesFile)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allPoliciesFile, ""), r.URL)
|
||||
return
|
||||
default:
|
||||
defer f.Close()
|
||||
var allPolicies map[string]iampolicy.Policy
|
||||
data, err = ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allPoliciesFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(data, &allPolicies)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allPoliciesFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
for policyName, policy := range allPolicies {
|
||||
if policy.IsEmpty() {
|
||||
err = globalIAMSys.DeletePolicy(ctx, policyName, true)
|
||||
} else {
|
||||
err = globalIAMSys.SetPolicy(ctx, policyName, policy)
|
||||
}
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, allPoliciesFile, policyName), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// import users
|
||||
{
|
||||
f, err := zr.Open(allUsersFile)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allUsersFile, ""), r.URL)
|
||||
return
|
||||
default:
|
||||
defer f.Close()
|
||||
var userAccts map[string]madmin.AddOrUpdateUserReq
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allUsersFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(data, &userAccts)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allUsersFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
for accessKey, ureq := range userAccts {
|
||||
// Not allowed to add a user with same access key as root credential
|
||||
if owner && accessKey == cred.AccessKey {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
userCred, exists := globalIAMSys.GetUser(ctx, accessKey)
|
||||
if exists && (userCred.IsTemp() || userCred.IsServiceAccount()) {
|
||||
// Updating STS credential is not allowed, and this API does not
|
||||
// support updating service accounts.
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if (cred.IsTemp() || cred.IsServiceAccount()) && cred.ParentUser == accessKey {
|
||||
// Incoming access key matches parent user then we should
|
||||
// reject password change requests.
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if accessKey has beginning and end space characters, this only applies to new users.
|
||||
if !exists && hasSpaceBE(accessKey) {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
checkDenyOnly := false
|
||||
if accessKey == cred.AccessKey {
|
||||
// Check that there is no explicit deny - otherwise it's allowed
|
||||
// to change one's own password.
|
||||
checkDenyOnly = true
|
||||
}
|
||||
|
||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
Action: iampolicy.CreateUserAdminAction,
|
||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
||||
IsOwner: owner,
|
||||
Claims: claims,
|
||||
DenyOnly: checkDenyOnly,
|
||||
}) {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allUsersFile, accessKey), r.URL)
|
||||
return
|
||||
}
|
||||
if err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, toAdminAPIErrCode(ctx, err), err, allUsersFile, accessKey), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// import groups
|
||||
{
|
||||
f, err := zr.Open(allGroupsFile)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allGroupsFile, ""), r.URL)
|
||||
return
|
||||
default:
|
||||
defer f.Close()
|
||||
var grpInfos map[string]GroupInfo
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allGroupsFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(data, &grpInfos); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allGroupsFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
for group, grpInfo := range grpInfos {
|
||||
// Check if group already exists
|
||||
if _, gerr := globalIAMSys.GetGroupDescription(group); gerr != nil {
|
||||
// If group does not exist, then check if the group has beginning and end space characters
|
||||
// we will reject such group names.
|
||||
if errors.Is(gerr, errNoSuchGroup) && hasSpaceBE(group) {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, err, allGroupsFile, group), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
if gerr := globalIAMSys.AddUsersToGroup(ctx, group, grpInfo.Members); gerr != nil {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, allGroupsFile, group), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// import service accounts
|
||||
{
|
||||
f, err := zr.Open(allSvcAcctsFile)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allSvcAcctsFile, ""), r.URL)
|
||||
return
|
||||
default:
|
||||
defer f.Close()
|
||||
var serviceAcctReqs map[string]madmin.SRSvcAccCreate
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, allSvcAcctsFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(data, &serviceAcctReqs); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, allSvcAcctsFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
for user, svcAcctReq := range serviceAcctReqs {
|
||||
var sp *iampolicy.Policy
|
||||
var err error
|
||||
if len(svcAcctReq.SessionPolicy) > 0 {
|
||||
sp, err = iampolicy.ParseConfig(bytes.NewReader(svcAcctReq.SessionPolicy))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
// service account access key cannot have space characters beginning and end of the string.
|
||||
if hasSpaceBE(svcAcctReq.AccessKey) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: svcAcctReq.AccessKey,
|
||||
Groups: svcAcctReq.Groups,
|
||||
Action: iampolicy.CreateServiceAccountAdminAction,
|
||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
||||
IsOwner: owner,
|
||||
Claims: claims,
|
||||
}) {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allSvcAcctsFile, user), r.URL)
|
||||
return
|
||||
}
|
||||
updateReq := true
|
||||
_, _, err = globalIAMSys.GetServiceAccount(ctx, svcAcctReq.AccessKey)
|
||||
if err != nil {
|
||||
if !errors.Is(err, errNoSuchServiceAccount) {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
|
||||
return
|
||||
}
|
||||
updateReq = false
|
||||
}
|
||||
if updateReq {
|
||||
opts := updateServiceAccountOpts{
|
||||
secretKey: svcAcctReq.SecretKey,
|
||||
status: svcAcctReq.Status,
|
||||
sessionPolicy: sp,
|
||||
}
|
||||
err = globalIAMSys.UpdateServiceAccount(ctx, svcAcctReq.AccessKey, opts)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
opts := newServiceAccountOpts{
|
||||
accessKey: user,
|
||||
secretKey: svcAcctReq.SecretKey,
|
||||
sessionPolicy: sp,
|
||||
claims: svcAcctReq.Claims,
|
||||
}
|
||||
|
||||
// In case of LDAP we need to resolve the targetUser to a DN and
|
||||
// query their groups:
|
||||
if globalLDAPConfig.Enabled {
|
||||
opts.claims[ldapUserN] = svcAcctReq.AccessKey // simple username
|
||||
targetUser, _, err := globalLDAPConfig.LookupUserDN(svcAcctReq.AccessKey)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
|
||||
return
|
||||
}
|
||||
opts.claims[ldapUser] = targetUser // username DN
|
||||
}
|
||||
|
||||
if _, err = globalIAMSys.NewServiceAccount(ctx, svcAcctReq.Parent, svcAcctReq.Groups, opts); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// import user policy mappings
|
||||
{
|
||||
f, err := zr.Open(userPolicyMappingsFile)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, userPolicyMappingsFile, ""), r.URL)
|
||||
return
|
||||
default:
|
||||
defer f.Close()
|
||||
var userPolicyMap map[string]MappedPolicy
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, userPolicyMappingsFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(data, &userPolicyMap); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, userPolicyMappingsFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
for u, pm := range userPolicyMap {
|
||||
// disallow setting policy mapping if user is a temporary user
|
||||
ok, _, err := globalIAMSys.IsTempUser(u)
|
||||
if err != nil && err != errNoSuchUser {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, userPolicyMappingsFile, u), r.URL)
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, errIAMActionNotAllowed, userPolicyMappingsFile, u), r.URL)
|
||||
return
|
||||
}
|
||||
if err := globalIAMSys.PolicyDBSet(ctx, u, pm.Policies, false); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, userPolicyMappingsFile, u), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// import group policy mappings
|
||||
{
|
||||
f, err := zr.Open(groupPolicyMappingsFile)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, groupPolicyMappingsFile, ""), r.URL)
|
||||
return
|
||||
default:
|
||||
defer f.Close()
|
||||
var grpPolicyMap map[string]MappedPolicy
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, groupPolicyMappingsFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(data, &grpPolicyMap); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, groupPolicyMappingsFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
for g, pm := range grpPolicyMap {
|
||||
if err := globalIAMSys.PolicyDBSet(ctx, g, pm.Policies, true); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, groupPolicyMappingsFile, g), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// import sts user policy mappings
|
||||
{
|
||||
f, err := zr.Open(stsUserPolicyMappingsFile)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsUserPolicyMappingsFile, ""), r.URL)
|
||||
return
|
||||
default:
|
||||
defer f.Close()
|
||||
var userPolicyMap map[string]MappedPolicy
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsUserPolicyMappingsFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(data, &userPolicyMap); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, stsUserPolicyMappingsFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
for u, pm := range userPolicyMap {
|
||||
// disallow setting policy mapping if user is a temporary user
|
||||
ok, _, err := globalIAMSys.IsTempUser(u)
|
||||
if err != nil && err != errNoSuchUser {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, stsUserPolicyMappingsFile, u), r.URL)
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, errIAMActionNotAllowed, stsUserPolicyMappingsFile, u), r.URL)
|
||||
return
|
||||
}
|
||||
if err := globalIAMSys.PolicyDBSet(ctx, u, pm.Policies, false); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, stsUserPolicyMappingsFile, u), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// import sts group policy mappings
|
||||
{
|
||||
f, err := zr.Open(stsGroupPolicyMappingsFile)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsGroupPolicyMappingsFile, ""), r.URL)
|
||||
return
|
||||
default:
|
||||
defer f.Close()
|
||||
var grpPolicyMap map[string]MappedPolicy
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrInvalidRequest, err, stsGroupPolicyMappingsFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(data, &grpPolicyMap); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminConfigBadJSON, err, stsGroupPolicyMappingsFile, ""), r.URL)
|
||||
return
|
||||
}
|
||||
for g, pm := range grpPolicyMap {
|
||||
if err := globalIAMSys.PolicyDBSet(ctx, g, pm.Policies, true); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, stsGroupPolicyMappingsFile, g), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-23
@@ -91,8 +91,11 @@ type allHealState struct {
|
||||
sync.RWMutex
|
||||
|
||||
// map of heal path to heal sequence
|
||||
healSeqMap map[string]*healSequence // Indexed by endpoint
|
||||
healLocalDisks map[Endpoint]struct{}
|
||||
healSeqMap map[string]*healSequence // Indexed by endpoint
|
||||
// keep track of the healing status of disks in the memory
|
||||
// false: the disk needs to be healed but no healing routine is started
|
||||
// true: the disk is currently healing
|
||||
healLocalDisks map[Endpoint]bool
|
||||
healStatus map[string]healingTracker // Indexed by disk ID
|
||||
}
|
||||
|
||||
@@ -100,7 +103,7 @@ type allHealState struct {
|
||||
func newHealState(cleanup bool) *allHealState {
|
||||
hstate := &allHealState{
|
||||
healSeqMap: make(map[string]*healSequence),
|
||||
healLocalDisks: map[Endpoint]struct{}{},
|
||||
healLocalDisks: make(map[Endpoint]bool),
|
||||
healStatus: make(map[string]healingTracker),
|
||||
}
|
||||
if cleanup {
|
||||
@@ -109,13 +112,6 @@ func newHealState(cleanup bool) *allHealState {
|
||||
return hstate
|
||||
}
|
||||
|
||||
func (ahs *allHealState) healDriveCount() int {
|
||||
ahs.RLock()
|
||||
defer ahs.RUnlock()
|
||||
|
||||
return len(ahs.healLocalDisks)
|
||||
}
|
||||
|
||||
func (ahs *allHealState) popHealLocalDisks(healLocalDisks ...Endpoint) {
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
@@ -165,23 +161,34 @@ func (ahs *allHealState) getLocalHealingDisks() map[string]madmin.HealingDisk {
|
||||
return dst
|
||||
}
|
||||
|
||||
// getHealLocalDiskEndpoints() returns the list of disks that need
|
||||
// to be healed but there is no healing routine in progress on them.
|
||||
func (ahs *allHealState) getHealLocalDiskEndpoints() Endpoints {
|
||||
ahs.RLock()
|
||||
defer ahs.RUnlock()
|
||||
|
||||
var endpoints Endpoints
|
||||
for ep := range ahs.healLocalDisks {
|
||||
endpoints = append(endpoints, ep)
|
||||
for ep, healing := range ahs.healLocalDisks {
|
||||
if !healing {
|
||||
endpoints = append(endpoints, ep)
|
||||
}
|
||||
}
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func (ahs *allHealState) markDiskForHealing(ep Endpoint) {
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
|
||||
ahs.healLocalDisks[ep] = true
|
||||
}
|
||||
|
||||
func (ahs *allHealState) pushHealLocalDisks(healLocalDisks ...Endpoint) {
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
|
||||
for _, ep := range healLocalDisks {
|
||||
ahs.healLocalDisks[ep] = struct{}{}
|
||||
ahs.healLocalDisks[ep] = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,16 +811,6 @@ func (h *healSequence) healMinioSysMeta(objAPI ObjectLayer, metaPrefix string) f
|
||||
}
|
||||
}
|
||||
|
||||
// healDiskFormat - heals format.json, return value indicates if a
|
||||
// failure error occurred.
|
||||
func (h *healSequence) healDiskFormat() error {
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
|
||||
return h.queueHealTask(healSource{bucket: SlashSeparator}, madmin.HealItemMetadata)
|
||||
}
|
||||
|
||||
// healBuckets - check for all buckets heal or just particular bucket.
|
||||
func (h *healSequence) healBuckets(objAPI ObjectLayer, bucketsOnly bool) error {
|
||||
if h.isQuitting() {
|
||||
|
||||
@@ -170,6 +170,12 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
// Set Group Status
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetGroupStatus))).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
|
||||
|
||||
// Export IAM info to zipped file
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/export-iam").HandlerFunc(httpTraceHdrs(adminAPI.ExportIAM))
|
||||
|
||||
// Import IAM info
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-iam").HandlerFunc(httpTraceHdrs(adminAPI.ImportIAM))
|
||||
|
||||
// GetBucketQuotaConfig
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
|
||||
@@ -188,6 +194,14 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.RemoveRemoteTargetHandler))).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
|
||||
|
||||
// Bucket migration operations
|
||||
// ExportBucketMetaHandler
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/export-bucket-metadata").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.ExportBucketMetadataHandler)))
|
||||
// ImportBucketMetaHandler
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-bucket-metadata").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.ImportBucketMetadataHandler)))
|
||||
|
||||
// Remote Tier management operations
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.AddTierHandler)))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.EditTierHandler)))
|
||||
|
||||
+1
-1
@@ -2231,7 +2231,7 @@ func toAPIError(ctx context.Context, err error) APIError {
|
||||
}
|
||||
case crypto.Error:
|
||||
apiErr = APIError{
|
||||
Code: "XMinIOEncryptionError",
|
||||
Code: "XMinioEncryptionError",
|
||||
Description: e.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
|
||||
@@ -500,11 +500,18 @@ func isSupportedS3AuthType(aType authType) bool {
|
||||
func setAuthHandler(h http.Handler) http.Handler {
|
||||
// handler for validating incoming authorization headers.
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tc, ok := r.Context().Value(contextTraceReqKey).(*traceCtxt)
|
||||
|
||||
aType := getRequestAuthType(r)
|
||||
if aType == authTypeSigned || aType == authTypeSignedV2 || aType == authTypeStreamingSigned {
|
||||
// Verify if date headers are set, if not reject the request
|
||||
amzDate, errCode := parseAmzDateHeader(r)
|
||||
if errCode != ErrNone {
|
||||
if ok {
|
||||
tc.funcName = "handler.Auth"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
// All our internal APIs are sensitive towards Date
|
||||
// header, for all requests where Date header is not
|
||||
// present we will reject such clients.
|
||||
@@ -516,6 +523,11 @@ func setAuthHandler(h http.Handler) http.Handler {
|
||||
// or in the future, reject request otherwise.
|
||||
curTime := UTCNow()
|
||||
if curTime.Sub(amzDate) > globalMaxSkewTime || amzDate.Sub(curTime) > globalMaxSkewTime {
|
||||
if ok {
|
||||
tc.funcName = "handler.Auth"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrRequestTimeTooSkewed), r.URL)
|
||||
atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1)
|
||||
return
|
||||
@@ -525,6 +537,12 @@ func setAuthHandler(h http.Handler) http.Handler {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if ok {
|
||||
tc.funcName = "handler.Auth"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL)
|
||||
atomic.AddUint64(&globalHTTPStats.rejectedRequestsAuth, 1)
|
||||
})
|
||||
|
||||
+121
-147
@@ -26,15 +26,12 @@ import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/color"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/console"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -258,26 +255,9 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
|
||||
|
||||
initBackgroundHealing(ctx, objAPI) // start quick background healing
|
||||
|
||||
bgSeq := mustGetHealSequence(ctx)
|
||||
|
||||
globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...)
|
||||
|
||||
if drivesToHeal := globalBackgroundHealState.healDriveCount(); drivesToHeal > 0 {
|
||||
logger.Info(fmt.Sprintf("Found drives to heal %d, waiting until %s to heal the content - use 'mc admin heal alias/ --verbose' to check the status",
|
||||
drivesToHeal, defaultMonitorNewDiskInterval))
|
||||
|
||||
// Heal any disk format and metadata early, if possible.
|
||||
// Start with format healing
|
||||
if err := bgSeq.healDiskFormat(); err != nil {
|
||||
if newObjectLayerFn() != nil {
|
||||
// log only in situations, when object layer
|
||||
// has fully initialized.
|
||||
logger.LogIf(bgSeq.ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go monitorLocalDisksAndHeal(ctx, z, bgSeq)
|
||||
go monitorLocalDisksAndHeal(ctx, z)
|
||||
}
|
||||
|
||||
func getLocalDisksToHeal() (disksToHeal Endpoints) {
|
||||
@@ -299,10 +279,108 @@ func getLocalDisksToHeal() (disksToHeal Endpoints) {
|
||||
return disksToHeal
|
||||
}
|
||||
|
||||
var newDiskHealingTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
||||
|
||||
func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint) error {
|
||||
logger.Info(fmt.Sprintf("Proceeding to heal '%s' - 'mc admin heal alias/ --verbose' to check the status.", endpoint))
|
||||
|
||||
disk, format, err := connectEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error: %w, %s", err, endpoint)
|
||||
}
|
||||
|
||||
poolIdx := globalEndpoints.GetLocalPoolIdx(disk.Endpoint())
|
||||
if poolIdx < 0 {
|
||||
return fmt.Errorf("unexpected pool index (%d) found in %s", poolIdx, disk.Endpoint())
|
||||
}
|
||||
|
||||
// Calculate the set index where the current endpoint belongs
|
||||
z.serverPools[poolIdx].erasureDisksMu.RLock()
|
||||
setIdx, _, err := findDiskIndex(z.serverPools[poolIdx].format, format)
|
||||
z.serverPools[poolIdx].erasureDisksMu.RUnlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if setIdx < 0 {
|
||||
return fmt.Errorf("unexpected set index (%d) found in %s", setIdx, disk.Endpoint())
|
||||
}
|
||||
|
||||
// Prevent parallel erasure set healing
|
||||
locker := z.NewNSLock(minioMetaBucket, fmt.Sprintf("new-disk-healing/%s/%d/%d", endpoint, poolIdx, setIdx))
|
||||
lkctx, err := locker.GetLock(ctx, newDiskHealingTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer locker.Unlock(lkctx.Cancel)
|
||||
|
||||
buckets, _ := z.ListBuckets(ctx)
|
||||
|
||||
// Buckets data are dispersed in multiple zones/sets, make
|
||||
// sure to heal all bucket metadata configuration.
|
||||
buckets = append(buckets, BucketInfo{
|
||||
Name: pathJoin(minioMetaBucket, minioConfigPrefix),
|
||||
}, BucketInfo{
|
||||
Name: pathJoin(minioMetaBucket, bucketMetaPrefix),
|
||||
})
|
||||
|
||||
// Heal latest buckets first.
|
||||
sort.Slice(buckets, func(i, j int) bool {
|
||||
a, b := strings.HasPrefix(buckets[i].Name, minioMetaBucket), strings.HasPrefix(buckets[j].Name, minioMetaBucket)
|
||||
if a != b {
|
||||
return a
|
||||
}
|
||||
return buckets[i].Created.After(buckets[j].Created)
|
||||
})
|
||||
|
||||
if serverDebugLog {
|
||||
logger.Info("Healing disk '%v' on %s pool", disk, humanize.Ordinal(poolIdx+1))
|
||||
}
|
||||
|
||||
// Load healing tracker in this disk
|
||||
tracker, err := loadHealingTracker(ctx, disk)
|
||||
if err != nil {
|
||||
// So someone changed the drives underneath, healing tracker missing.
|
||||
logger.LogIf(ctx, fmt.Errorf("Healing tracker missing on '%s', disk was swapped again on %s pool: %w",
|
||||
disk, humanize.Ordinal(poolIdx+1), err))
|
||||
tracker = newHealingTracker(disk)
|
||||
}
|
||||
|
||||
// Load bucket totals
|
||||
cache := dataUsageCache{}
|
||||
if err := cache.load(ctx, z.serverPools[poolIdx].sets[setIdx], dataUsageCacheName); err == nil {
|
||||
dataUsageInfo := cache.dui(dataUsageRoot, nil)
|
||||
tracker.ObjectsTotalCount = dataUsageInfo.ObjectsTotalCount
|
||||
tracker.ObjectsTotalSize = dataUsageInfo.ObjectsTotalSize
|
||||
}
|
||||
|
||||
tracker.PoolIndex, tracker.SetIndex, tracker.DiskIndex = disk.GetDiskLoc()
|
||||
tracker.setQueuedBuckets(buckets)
|
||||
if err := tracker.save(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start or resume healing of this erasure set
|
||||
err = z.serverPools[poolIdx].sets[setIdx].healErasureSet(ctx, tracker.QueuedBuckets, tracker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Healing disk '%s' is complete (healed: %d, failed: %d).", disk, tracker.ItemsHealed, tracker.ItemsFailed)
|
||||
|
||||
if serverDebugLog {
|
||||
tracker.printTo(os.Stdout)
|
||||
logger.Info("\n")
|
||||
}
|
||||
|
||||
logger.LogIf(ctx, tracker.delete(ctx))
|
||||
return nil
|
||||
}
|
||||
|
||||
// monitorLocalDisksAndHeal - ensures that detected new disks are healed
|
||||
// 1. Only the concerned erasure set will be listed and healed
|
||||
// 2. Only the node hosting the disk is responsible to perform the heal
|
||||
func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq *healSequence) {
|
||||
func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools) {
|
||||
// Perform automatic disk healing when a disk is replaced locally.
|
||||
diskCheckTimer := time.NewTimer(defaultMonitorNewDiskInterval)
|
||||
defer diskCheckTimer.Stop()
|
||||
@@ -312,139 +390,35 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-diskCheckTimer.C:
|
||||
var erasureSetInPoolDisksToHeal []map[int][]StorageAPI
|
||||
|
||||
healDisks := globalBackgroundHealState.getHealLocalDiskEndpoints()
|
||||
if len(healDisks) > 0 {
|
||||
// Reformat disks
|
||||
bgSeq.queueHealTask(healSource{bucket: SlashSeparator}, madmin.HealItemMetadata)
|
||||
|
||||
// Ensure that reformatting disks is finished
|
||||
bgSeq.queueHealTask(healSource{bucket: nopHeal}, madmin.HealItemMetadata)
|
||||
|
||||
logger.Info(fmt.Sprintf("Found drives to heal %d, proceeding to heal - 'mc admin heal alias/ --verbose' to check the status.",
|
||||
len(healDisks)))
|
||||
|
||||
erasureSetInPoolDisksToHeal = make([]map[int][]StorageAPI, len(z.serverPools))
|
||||
for i := range z.serverPools {
|
||||
erasureSetInPoolDisksToHeal[i] = map[int][]StorageAPI{}
|
||||
}
|
||||
if len(healDisks) == 0 {
|
||||
// Reset for next interval.
|
||||
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
||||
break
|
||||
}
|
||||
|
||||
if serverDebugLog && len(healDisks) > 0 {
|
||||
console.Debugf(color.Green("healDisk:")+" disk check timer fired, attempting to heal %d drives\n", len(healDisks))
|
||||
// Reformat disks immediately
|
||||
_, err := z.HealFormat(context.Background(), false)
|
||||
if err != nil && !errors.Is(err, errNoHealRequired) {
|
||||
logger.LogIf(ctx, err)
|
||||
// Reset for next interval.
|
||||
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
||||
break
|
||||
}
|
||||
|
||||
// heal only if new disks found.
|
||||
for _, endpoint := range healDisks {
|
||||
disk, format, err := connectEndpoint(endpoint)
|
||||
if err != nil {
|
||||
printEndpointError(endpoint, err, true)
|
||||
continue
|
||||
}
|
||||
|
||||
poolIdx := globalEndpoints.GetLocalPoolIdx(disk.Endpoint())
|
||||
if poolIdx < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate the set index where the current endpoint belongs
|
||||
z.serverPools[poolIdx].erasureDisksMu.RLock()
|
||||
// Protect reading reference format.
|
||||
setIndex, _, err := findDiskIndex(z.serverPools[poolIdx].format, format)
|
||||
z.serverPools[poolIdx].erasureDisksMu.RUnlock()
|
||||
if err != nil {
|
||||
printEndpointError(endpoint, err, false)
|
||||
continue
|
||||
}
|
||||
|
||||
erasureSetInPoolDisksToHeal[poolIdx][setIndex] = append(erasureSetInPoolDisksToHeal[poolIdx][setIndex], disk)
|
||||
}
|
||||
|
||||
buckets, _ := z.ListBuckets(ctx)
|
||||
|
||||
// Buckets data are dispersed in multiple zones/sets, make
|
||||
// sure to heal all bucket metadata configuration.
|
||||
buckets = append(buckets, BucketInfo{
|
||||
Name: pathJoin(minioMetaBucket, minioConfigPrefix),
|
||||
}, BucketInfo{
|
||||
Name: pathJoin(minioMetaBucket, bucketMetaPrefix),
|
||||
})
|
||||
|
||||
// Heal latest buckets first.
|
||||
sort.Slice(buckets, func(i, j int) bool {
|
||||
a, b := strings.HasPrefix(buckets[i].Name, minioMetaBucket), strings.HasPrefix(buckets[j].Name, minioMetaBucket)
|
||||
if a != b {
|
||||
return a
|
||||
}
|
||||
return buckets[i].Created.After(buckets[j].Created)
|
||||
})
|
||||
|
||||
// TODO(klauspost): This will block until all heals are done,
|
||||
// in the future this should be able to start healing other sets at once.
|
||||
var wg sync.WaitGroup
|
||||
for i, setMap := range erasureSetInPoolDisksToHeal {
|
||||
i := i
|
||||
for setIndex, disks := range setMap {
|
||||
if len(disks) == 0 {
|
||||
continue
|
||||
for _, disk := range healDisks {
|
||||
go func(disk Endpoint) {
|
||||
globalBackgroundHealState.markDiskForHealing(disk)
|
||||
err := healFreshDisk(ctx, z, disk)
|
||||
if err != nil {
|
||||
printEndpointError(disk, err, false)
|
||||
return
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(setIndex int, disks []StorageAPI) {
|
||||
defer wg.Done()
|
||||
for _, disk := range disks {
|
||||
if serverDebugLog {
|
||||
logger.Info("Healing disk '%v' on %s pool", disk, humanize.Ordinal(i+1))
|
||||
}
|
||||
|
||||
// So someone changed the drives underneath, healing tracker missing.
|
||||
tracker, err := loadHealingTracker(ctx, disk)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Healing tracker missing on '%s', disk was swapped again on %s pool: %w",
|
||||
disk, humanize.Ordinal(i+1), err))
|
||||
tracker = newHealingTracker(disk)
|
||||
}
|
||||
|
||||
// Load bucket totals
|
||||
cache := dataUsageCache{}
|
||||
if err := cache.load(ctx, z.serverPools[i].sets[setIndex], dataUsageCacheName); err == nil {
|
||||
dataUsageInfo := cache.dui(dataUsageRoot, nil)
|
||||
tracker.ObjectsTotalCount = dataUsageInfo.ObjectsTotalCount
|
||||
tracker.ObjectsTotalSize = dataUsageInfo.ObjectsTotalSize
|
||||
}
|
||||
|
||||
tracker.PoolIndex, tracker.SetIndex, tracker.DiskIndex = disk.GetDiskLoc()
|
||||
tracker.setQueuedBuckets(buckets)
|
||||
if err := tracker.save(ctx); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
// Unable to write healing tracker, permission denied or some
|
||||
// other unexpected error occurred. Proceed to look for new
|
||||
// disks to be healed again, we cannot proceed further.
|
||||
return
|
||||
}
|
||||
|
||||
err = z.serverPools[i].sets[setIndex].healErasureSet(ctx, tracker.QueuedBuckets, tracker)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if serverDebugLog {
|
||||
logger.Info("Healing disk '%s' on %s pool, %s set complete", disk,
|
||||
humanize.Ordinal(i+1), humanize.Ordinal(setIndex+1))
|
||||
logger.Info("Summary:\n")
|
||||
tracker.printTo(os.Stdout)
|
||||
logger.Info("\n")
|
||||
}
|
||||
logger.LogIf(ctx, tracker.delete(ctx))
|
||||
|
||||
// Only upon success pop the healed disk.
|
||||
globalBackgroundHealState.popHealLocalDisks(disk.Endpoint())
|
||||
}
|
||||
}(setIndex, disks)
|
||||
}
|
||||
// Only upon success pop the healed disk.
|
||||
globalBackgroundHealState.popHealLocalDisks(disk)
|
||||
}(disk)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Reset for next interval.
|
||||
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
||||
|
||||
@@ -108,7 +108,8 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Store the bucket encryption configuration in the object layer
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, configData); err != nil {
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, configData)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -122,6 +123,7 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
||||
Type: madmin.SRBucketMetaTypeSSEConfig,
|
||||
Bucket: bucket,
|
||||
SSEConfig: &cfgStr,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -202,7 +204,8 @@ func (api objectAPIHandlers) DeleteBucketEncryptionHandler(w http.ResponseWriter
|
||||
}
|
||||
|
||||
// Delete bucket encryption config from object layer
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, nil); err != nil {
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, nil)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -212,6 +215,7 @@ func (api objectAPIHandlers) DeleteBucketEncryptionHandler(w http.ResponseWriter
|
||||
Type: madmin.SRBucketMetaTypeSSEConfig,
|
||||
Bucket: bucket,
|
||||
SSEConfig: nil,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
|
||||
+24
-24
@@ -525,7 +525,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
VersionSuspended: vc.Suspended(),
|
||||
}
|
||||
|
||||
if replicateDeletes || hasLockEnabled || !globalTierConfigMgr.Empty() {
|
||||
if replicateDeletes || object.VersionID != "" && hasLockEnabled || !globalTierConfigMgr.Empty() {
|
||||
if !globalTierConfigMgr.Empty() && object.VersionID == "" && opts.VersionSuspended {
|
||||
opts.VersionID = nullVersionID
|
||||
}
|
||||
@@ -554,7 +554,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
object.ReplicateDecisionStr = dsc.String()
|
||||
}
|
||||
}
|
||||
if hasLockEnabled {
|
||||
if object.VersionID != "" && hasLockEnabled {
|
||||
if apiErrCode := enforceRetentionBypassForDelete(ctx, r, bucket, object, goi, gerr); apiErrCode != ErrNone {
|
||||
apiErr := errorCodes.ToAPIErr(apiErrCode)
|
||||
deleteResults[index].errInfo = DeleteError{
|
||||
@@ -806,19 +806,14 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
// Proceed to creating a bucket.
|
||||
err := objectAPI.MakeBucketWithLocation(ctx, bucket, opts)
|
||||
if _, ok := err.(BucketExists); ok {
|
||||
// Though bucket exists locally, we send the site-replication
|
||||
// hook to ensure all sites have this bucket. If the hook
|
||||
// succeeds, the client will still receive a bucket exists
|
||||
// message.
|
||||
err2 := globalSiteReplicationSys.MakeBucketHook(ctx, bucket, opts)
|
||||
if err2 != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
if err := objectAPI.MakeBucketWithLocation(ctx, bucket, opts); err != nil {
|
||||
if _, ok := err.(BucketExists); ok {
|
||||
// Though bucket exists locally, we send the site-replication
|
||||
// hook to ensure all sites have this bucket. If the hook
|
||||
// succeeds, the client will still receive a bucket exists
|
||||
// message.
|
||||
globalSiteReplicationSys.MakeBucketHook(ctx, bucket, opts)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -827,8 +822,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
||||
|
||||
// Call site replication hook
|
||||
err = globalSiteReplicationSys.MakeBucketHook(ctx, bucket, opts)
|
||||
if err != nil {
|
||||
if err := globalSiteReplicationSys.MakeBucketHook(ctx, bucket, opts); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1391,7 +1385,8 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
return
|
||||
}
|
||||
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, objectLockConfig, configData); err != nil {
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, objectLockConfig, configData)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1405,6 +1400,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
||||
Bucket: bucket,
|
||||
ObjectLockConfig: &cfgStr,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -1496,7 +1492,8 @@ func (api objectAPIHandlers) PutBucketTaggingHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, configData); err != nil {
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, configData)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1507,9 +1504,10 @@ func (api objectAPIHandlers) PutBucketTaggingHandler(w http.ResponseWriter, r *h
|
||||
// errors.
|
||||
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeTags,
|
||||
Bucket: bucket,
|
||||
Tags: &cfgStr,
|
||||
Type: madmin.SRBucketMetaTypeTags,
|
||||
Bucket: bucket,
|
||||
Tags: &cfgStr,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -1578,14 +1576,16 @@ func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, nil); err != nil {
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, nil)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeTags,
|
||||
Bucket: bucket,
|
||||
Type: madmin.SRBucketMetaTypeTags,
|
||||
Bucket: bucket,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
|
||||
@@ -91,7 +91,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketLifecycleConfig, configData); err != nil {
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketLifecycleConfig, configData); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -168,7 +168,7 @@ func (api objectAPIHandlers) DeleteBucketLifecycleHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketLifecycleConfig, nil); err != nil {
|
||||
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketLifecycleConfig, nil); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -101,7 +100,6 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
if err = DecryptETags(ctx, GlobalKMS, listObjectVersionsInfo.Objects); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Failed to decrypt ETag: %v", err)) // TODO(aead): Remove once we are confident that decryption does not fail accidentially
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -167,7 +165,6 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
|
||||
}
|
||||
|
||||
if err = DecryptETags(ctx, GlobalKMS, listObjectsV2Info.Objects); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Failed to decrypt ETag: %v", err)) // TODO(aead): Remove once we are confident that decryption does not fail accidentially
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -246,7 +243,6 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
if err = DecryptETags(ctx, GlobalKMS, listObjectsV2Info.Objects); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Failed to decrypt ETag: %v", err)) // TODO(aead): Remove once we are confident that decryption does not fail accidentially
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -347,7 +343,6 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
if err = DecryptETags(ctx, GlobalKMS, listObjectsInfo.Objects); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Failed to decrypt ETag: %v", err)) // TODO(aead): Remove once we are confident that decryption does not fail accidentially
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
+23
-20
@@ -75,28 +75,28 @@ func (sys *BucketMetadataSys) Set(bucket string, meta BucketMetadata) {
|
||||
|
||||
// Update update bucket metadata for the specified config file.
|
||||
// The configData data should not be modified after being sent here.
|
||||
func (sys *BucketMetadataSys) Update(ctx context.Context, bucket string, configFile string, configData []byte) error {
|
||||
func (sys *BucketMetadataSys) Update(ctx context.Context, bucket string, configFile string, configData []byte) (updatedAt time.Time, err error) {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
return updatedAt, errServerNotInitialized
|
||||
}
|
||||
|
||||
if globalIsGateway && globalGatewayName != NASBackendGateway {
|
||||
if configFile == bucketPolicyConfig {
|
||||
if configData == nil {
|
||||
return objAPI.DeleteBucketPolicy(ctx, bucket)
|
||||
return updatedAt, objAPI.DeleteBucketPolicy(ctx, bucket)
|
||||
}
|
||||
config, err := policy.ParseConfig(bytes.NewReader(configData), bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
return updatedAt, err
|
||||
}
|
||||
return objAPI.SetBucketPolicy(ctx, bucket, config)
|
||||
return updatedAt, objAPI.SetBucketPolicy(ctx, bucket, config)
|
||||
}
|
||||
return NotImplemented{}
|
||||
return updatedAt, NotImplemented{}
|
||||
}
|
||||
|
||||
if bucket == minioMetaBucket {
|
||||
return errInvalidArgument
|
||||
return updatedAt, errInvalidArgument
|
||||
}
|
||||
|
||||
meta, err := loadBucketMetadata(ctx, objAPI, bucket)
|
||||
@@ -105,56 +105,56 @@ func (sys *BucketMetadataSys) Update(ctx context.Context, bucket string, configF
|
||||
// Only single drive mode needs this fallback.
|
||||
meta = newBucketMetadata(bucket)
|
||||
} else {
|
||||
return err
|
||||
return updatedAt, err
|
||||
}
|
||||
}
|
||||
|
||||
updatedAt = UTCNow()
|
||||
switch configFile {
|
||||
case bucketPolicyConfig:
|
||||
meta.PolicyConfigJSON = configData
|
||||
meta.PolicyConfigUpdatedAt = UTCNow()
|
||||
meta.PolicyConfigUpdatedAt = updatedAt
|
||||
case bucketNotificationConfig:
|
||||
meta.NotificationConfigXML = configData
|
||||
case bucketLifecycleConfig:
|
||||
meta.LifecycleConfigXML = configData
|
||||
case bucketSSEConfig:
|
||||
meta.EncryptionConfigXML = configData
|
||||
meta.EncryptionConfigUpdatedAt = UTCNow()
|
||||
meta.EncryptionConfigUpdatedAt = updatedAt
|
||||
case bucketTaggingConfig:
|
||||
meta.TaggingConfigXML = configData
|
||||
meta.TaggingConfigUpdatedAt = UTCNow()
|
||||
meta.TaggingConfigUpdatedAt = updatedAt
|
||||
case bucketQuotaConfigFile:
|
||||
meta.QuotaConfigJSON = configData
|
||||
meta.QuotaConfigUpdatedAt = UTCNow()
|
||||
meta.QuotaConfigUpdatedAt = updatedAt
|
||||
case objectLockConfig:
|
||||
meta.ObjectLockConfigXML = configData
|
||||
meta.ObjectLockConfigUpdatedAt = UTCNow()
|
||||
meta.ObjectLockConfigUpdatedAt = updatedAt
|
||||
case bucketVersioningConfig:
|
||||
meta.VersioningConfigXML = configData
|
||||
meta.VersioningConfigUpdatedAt = UTCNow()
|
||||
meta.VersioningConfigUpdatedAt = updatedAt
|
||||
case bucketReplicationConfig:
|
||||
meta.ReplicationConfigXML = configData
|
||||
meta.ReplicationConfigUpdatedAt = UTCNow()
|
||||
meta.ReplicationConfigUpdatedAt = updatedAt
|
||||
case bucketTargetsFile:
|
||||
meta.BucketTargetsConfigJSON, meta.BucketTargetsConfigMetaJSON, err = encryptBucketMetadata(meta.Name, configData, kms.Context{
|
||||
bucket: meta.Name,
|
||||
bucketTargetsFile: bucketTargetsFile,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error encrypting bucket target metadata %w", err)
|
||||
return updatedAt, fmt.Errorf("Error encrypting bucket target metadata %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Unknown bucket %s metadata update requested %s", bucket, configFile)
|
||||
return updatedAt, fmt.Errorf("Unknown bucket %s metadata update requested %s", bucket, configFile)
|
||||
}
|
||||
|
||||
if err := meta.Save(ctx, objAPI); err != nil {
|
||||
return err
|
||||
return updatedAt, err
|
||||
}
|
||||
|
||||
sys.Set(bucket, meta)
|
||||
globalNotificationSys.LoadBucketMetadata(bgContext(ctx), bucket) // Do not use caller context here
|
||||
|
||||
return nil
|
||||
return updatedAt, nil
|
||||
}
|
||||
|
||||
// Get metadata for a bucket.
|
||||
@@ -338,6 +338,9 @@ func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, ti
|
||||
func (sys *BucketMetadataSys) GetQuotaConfig(ctx context.Context, bucket string) (*madmin.BucketQuota, time.Time, error) {
|
||||
meta, err := sys.GetConfig(ctx, bucket)
|
||||
if err != nil {
|
||||
if errors.Is(err, errConfigNotFound) {
|
||||
return nil, time.Time{}, BucketQuotaConfigNotFound{Bucket: bucket}
|
||||
}
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
return meta.quotaConfig, meta.QuotaConfigUpdatedAt, nil
|
||||
|
||||
@@ -465,7 +465,7 @@ func encryptBucketMetadata(bucket string, input []byte, kmsContext kms.Context)
|
||||
objectKey := crypto.GenerateKey(key.Plaintext, rand.Reader)
|
||||
sealedKey := objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, "")
|
||||
crypto.S3.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey)
|
||||
_, err = sio.Encrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20, CipherSuites: fips.CipherSuitesDARE()})
|
||||
_, err = sio.Encrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20, CipherSuites: fips.DARECiphers()})
|
||||
if err != nil {
|
||||
return output, metabytes, err
|
||||
}
|
||||
@@ -495,6 +495,6 @@ func decryptBucketMetadata(input []byte, bucket string, meta map[string]string,
|
||||
}
|
||||
|
||||
outbuf := bytes.NewBuffer(nil)
|
||||
_, err = sio.Decrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20, CipherSuites: fips.CipherSuitesDARE()})
|
||||
_, err = sio.Decrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20, CipherSuites: fips.DARECiphers()})
|
||||
return outbuf.Bytes(), err
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucketName, bucketNotificationConfig, configData); err != nil {
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, bucketName, bucketNotificationConfig, configData); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,16 +103,18 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, configData); err != nil {
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, configData)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Call site replication hook.
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypePolicy,
|
||||
Bucket: bucket,
|
||||
Policy: bucketPolicyBytes,
|
||||
Type: madmin.SRBucketMetaTypePolicy,
|
||||
Bucket: bucket,
|
||||
Policy: bucketPolicyBytes,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -148,15 +150,17 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, nil); err != nil {
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, nil)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Call site replication hook.
|
||||
if err := globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypePolicy,
|
||||
Bucket: bucket,
|
||||
Type: madmin.SRBucketMetaTypePolicy,
|
||||
Bucket: bucket,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
|
||||
@@ -86,7 +86,7 @@ func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWr
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, configData); err != nil {
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, configData); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -165,7 +165,7 @@ func (api objectAPIHandlers) DeleteBucketReplicationConfigHandler(w http.Respons
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
|
||||
return
|
||||
}
|
||||
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, nil); err != nil {
|
||||
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, nil); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
+13
-20
@@ -19,13 +19,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/madmin-go"
|
||||
minio "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7"
|
||||
miniogo "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
@@ -273,17 +272,20 @@ func (sys *BucketTargetSys) UpdateAllTargets(bucket string, tgts *madmin.BucketT
|
||||
}
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
if tgts == nil || tgts.Empty() {
|
||||
// remove target and arn association
|
||||
if tgts, ok := sys.targetsMap[bucket]; ok {
|
||||
for _, t := range tgts {
|
||||
if tgt, ok := sys.arnRemotesMap[t.Arn]; ok && tgt.healthCancelFn != nil {
|
||||
tgt.healthCancelFn()
|
||||
}
|
||||
delete(sys.arnRemotesMap, t.Arn)
|
||||
|
||||
// Remove existingtarget and arn association
|
||||
if tgts, ok := sys.targetsMap[bucket]; ok {
|
||||
for _, t := range tgts {
|
||||
if tgt, ok := sys.arnRemotesMap[t.Arn]; ok && tgt.healthCancelFn != nil {
|
||||
tgt.healthCancelFn()
|
||||
}
|
||||
delete(sys.arnRemotesMap, t.Arn)
|
||||
}
|
||||
delete(sys.targetsMap, bucket)
|
||||
}
|
||||
|
||||
// No need for more if not adding anything
|
||||
if tgts == nil || tgts.Empty() {
|
||||
sys.updateBandwidthLimit(bucket, 0)
|
||||
return
|
||||
}
|
||||
@@ -325,25 +327,16 @@ func (sys *BucketTargetSys) set(bucket BucketInfo, meta BucketMetadata) {
|
||||
sys.targetsMap[bucket.Name] = cfg.Targets
|
||||
}
|
||||
|
||||
// getRemoteTargetInstanceTransport contains a singleton roundtripper.
|
||||
var (
|
||||
getRemoteTargetInstanceTransport http.RoundTripper
|
||||
getRemoteTargetInstanceTransportOnce sync.Once
|
||||
)
|
||||
|
||||
// Returns a minio-go Client configured to access remote host described in replication target config.
|
||||
func (sys *BucketTargetSys) getRemoteTargetClient(tcfg *madmin.BucketTarget) (*TargetClient, error) {
|
||||
config := tcfg.Credentials
|
||||
creds := credentials.NewStaticV4(config.AccessKey, config.SecretKey, "")
|
||||
getRemoteTargetInstanceTransportOnce.Do(func() {
|
||||
getRemoteTargetInstanceTransport = NewRemoteTargetHTTPTransport()
|
||||
})
|
||||
|
||||
api, err := minio.New(tcfg.Endpoint, &miniogo.Options{
|
||||
Creds: creds,
|
||||
Secure: tcfg.Secure,
|
||||
Region: tcfg.Region,
|
||||
Transport: getRemoteTargetInstanceTransport,
|
||||
Transport: globalRemoteTargetTransport,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -97,7 +97,8 @@ func (api objectAPIHandlers) PutBucketVersioningHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, configData); err != nil {
|
||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, configData)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -111,6 +112,7 @@ func (api objectAPIHandlers) PutBucketVersioningHandler(w http.ResponseWriter, r
|
||||
Type: madmin.SRBucketMetaTypeVersionConfig,
|
||||
Bucket: bucket,
|
||||
Versioning: &cfgStr,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
|
||||
@@ -134,3 +134,17 @@ func performCallhome(ctx context.Context) {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to perform callhome: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
callhomeURL = "https://subnet.min.io/api/callhome"
|
||||
callhomeURLDev = "http://localhost:9000/api/callhome"
|
||||
)
|
||||
|
||||
func sendCallhomeInfo(ch CallhomeInfo) error {
|
||||
url := callhomeURL
|
||||
if globalIsCICD {
|
||||
url = callhomeURLDev
|
||||
}
|
||||
_, err := globalSubnetConfig.Post(url, ch)
|
||||
return err
|
||||
}
|
||||
|
||||
+11
-7
@@ -102,6 +102,11 @@ func init() {
|
||||
PersistOnFailure: false,
|
||||
}
|
||||
|
||||
t, _ := minioVersionToReleaseTime(Version)
|
||||
if !t.IsZero() {
|
||||
globalVersionUnix = uint64(t.Unix())
|
||||
}
|
||||
|
||||
globalIsCICD = env.Get("MINIO_CI_CD", "") != "" || env.Get("CI", "") != ""
|
||||
|
||||
containers := IsKubernetes() || IsDocker() || IsBOSH() || IsDCOS() || IsPCFTile()
|
||||
@@ -490,6 +495,12 @@ func parsEnvEntry(envEntry string) (envKV, error) {
|
||||
Skip: true,
|
||||
}, nil
|
||||
}
|
||||
if strings.HasPrefix(envEntry, "#") {
|
||||
// Skip commented lines
|
||||
return envKV{
|
||||
Skip: true,
|
||||
}, nil
|
||||
}
|
||||
const envSeparator = "="
|
||||
envTokens := strings.SplitN(strings.TrimSpace(strings.TrimPrefix(envEntry, "export")), envSeparator, 2)
|
||||
if len(envTokens) != 2 {
|
||||
@@ -499,13 +510,6 @@ func parsEnvEntry(envEntry string) (envKV, error) {
|
||||
key := envTokens[0]
|
||||
val := envTokens[1]
|
||||
|
||||
if strings.HasPrefix(key, "#") {
|
||||
// Skip commented lines
|
||||
return envKV{
|
||||
Skip: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Remove quotes from the value if found
|
||||
if len(val) >= 2 {
|
||||
quote := val[0]
|
||||
|
||||
@@ -136,6 +136,7 @@ export MINIO_ROOT_PASSWORD=minio123`,
|
||||
},
|
||||
{
|
||||
`
|
||||
# simple comment
|
||||
# MINIO_ROOT_USER=minioadmin
|
||||
# MINIO_ROOT_PASSWORD=minioadmin
|
||||
MINIO_ROOT_USER=minio
|
||||
|
||||
@@ -338,7 +338,7 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
|
||||
etcdClnt.Close()
|
||||
}
|
||||
case config.IdentityOpenIDSubSys:
|
||||
if _, err := openid.LookupConfig(s[config.IdentityOpenIDSubSys],
|
||||
if _, err := openid.LookupConfig(s,
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -364,7 +364,7 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
|
||||
return err
|
||||
}
|
||||
case config.SubnetSubSys:
|
||||
if _, err := subnet.LookupConfig(s[config.SubnetSubSys][config.Default]); err != nil {
|
||||
if _, err := subnet.LookupConfig(s[config.SubnetSubSys][config.Default], nil); err != nil {
|
||||
return err
|
||||
}
|
||||
case config.CallhomeSubSys:
|
||||
@@ -560,7 +560,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
||||
logger.LogIf(ctx, fmt.Errorf("CRITICAL: enabling %s is not recommended in a production environment", xtls.EnvIdentityTLSSkipVerify))
|
||||
}
|
||||
|
||||
globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys],
|
||||
globalOpenIDConfig, err = openid.LookupConfig(s,
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
|
||||
@@ -599,7 +599,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
||||
|
||||
setGlobalAuthZPlugin(polplugin.New(authZPluginCfg))
|
||||
|
||||
globalSubnetConfig, err = subnet.LookupConfig(s[config.SubnetSubSys][config.Default])
|
||||
globalSubnetConfig, err = subnet.LookupConfig(s[config.SubnetSubSys][config.Default], globalProxyTransport)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to parse subnet configuration: %w", err))
|
||||
}
|
||||
|
||||
@@ -803,7 +803,7 @@ func newCacheEncryptReader(content io.Reader, bucket, object string, metadata ma
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey, MinVersion: sio.Version20, CipherSuites: fips.CipherSuitesDARE()})
|
||||
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey, MinVersion: sio.Version20, CipherSuites: fips.DARECiphers()})
|
||||
if err != nil {
|
||||
return nil, crypto.ErrInvalidCustomerKey
|
||||
}
|
||||
@@ -1454,7 +1454,7 @@ func newCachePartEncryptReader(ctx context.Context, bucket, object string, partI
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader, err := sio.EncryptReader(content, sio.Config{Key: partEncryptionKey[:], MinVersion: sio.Version20, CipherSuites: fips.CipherSuitesDARE()})
|
||||
reader, err := sio.EncryptReader(content, sio.Config{Key: partEncryptionKey[:], MinVersion: sio.Version20, CipherSuites: fips.DARECiphers()})
|
||||
if err != nil {
|
||||
return nil, crypto.ErrInvalidCustomerKey
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
@@ -38,6 +37,7 @@ import (
|
||||
"github.com/minio/minio/internal/crypto"
|
||||
"github.com/minio/minio/internal/etag"
|
||||
"github.com/minio/minio/internal/fips"
|
||||
"github.com/minio/minio/internal/hash/sha256"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
@@ -406,7 +406,7 @@ func newEncryptReader(content io.Reader, kind crypto.Type, keyID string, key []b
|
||||
return nil, crypto.ObjectKey{}, err
|
||||
}
|
||||
|
||||
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey[:], MinVersion: sio.Version20, CipherSuites: fips.CipherSuitesDARE()})
|
||||
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey[:], MinVersion: sio.Version20, CipherSuites: fips.DARECiphers()})
|
||||
if err != nil {
|
||||
return nil, crypto.ObjectKey{}, crypto.ErrInvalidCustomerKey
|
||||
}
|
||||
@@ -553,7 +553,7 @@ func newDecryptReaderWithObjectKey(client io.Reader, objectEncryptionKey []byte,
|
||||
reader, err := sio.DecryptReader(client, sio.Config{
|
||||
Key: objectEncryptionKey,
|
||||
SequenceNumber: seqNumber,
|
||||
CipherSuites: fips.CipherSuitesDARE(),
|
||||
CipherSuites: fips.DARECiphers(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, crypto.ErrInvalidCustomerKey
|
||||
|
||||
@@ -42,7 +42,7 @@ type endpointSet struct {
|
||||
|
||||
// Supported set sizes this is used to find the optimal
|
||||
// single set size.
|
||||
var setSizes = []uint64{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
||||
var setSizes = []uint64{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
||||
|
||||
// getDivisibleSize - returns a greatest common divisor of
|
||||
// all the ellipses sizes.
|
||||
|
||||
@@ -201,18 +201,6 @@ func TestGetSetIndexes(t *testing.T) {
|
||||
success bool
|
||||
}{
|
||||
// Invalid inputs.
|
||||
{
|
||||
[]string{"data{1...3}"},
|
||||
[]uint64{3},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"data/controller1/export{1...2}, data/controller2/export{1...4}, data/controller3/export{1...8}"},
|
||||
[]uint64{2, 4, 8},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"data{1...17}/export{1...52}"},
|
||||
[]uint64{14144},
|
||||
@@ -220,6 +208,18 @@ func TestGetSetIndexes(t *testing.T) {
|
||||
false,
|
||||
},
|
||||
// Valid inputs.
|
||||
{
|
||||
[]string{"data{1...3}"},
|
||||
[]uint64{3},
|
||||
[][]uint64{{3}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data/controller1/export{1...2}, data/controller2/export{1...4}, data/controller3/export{1...8}"},
|
||||
[]uint64{2, 4, 8},
|
||||
[][]uint64{{2}, {2, 2}, {2, 2, 2, 2}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data{1...27}"},
|
||||
[]uint64{27},
|
||||
|
||||
+35
-17
@@ -306,9 +306,13 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
// Re-read when we have lock...
|
||||
partsMetadata, errs := readAllFileInfo(ctx, storageDisks, bucket, object, versionID, true)
|
||||
if isAllNotFound(errs) {
|
||||
err := errFileNotFound
|
||||
if versionID != "" {
|
||||
err = errFileVersionNotFound
|
||||
}
|
||||
// Nothing to do, file is already gone.
|
||||
return er.defaultHealResult(FileInfo{}, storageDisks, storageEndpoints,
|
||||
errs, bucket, object, versionID), nil
|
||||
errs, bucket, object, versionID), err
|
||||
}
|
||||
|
||||
readQuorum, _, err := objectQuorumFromMeta(ctx, partsMetadata, errs, er.defaultParityCount)
|
||||
@@ -327,7 +331,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
// present, it is as good as object not found.
|
||||
latestMeta, err := pickValidFileInfo(ctx, partsMetadata, modTime, readQuorum)
|
||||
if err != nil {
|
||||
return result, toObjectErr(err, bucket, object, versionID)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// List of disks having all parts as per latest metadata.
|
||||
@@ -397,8 +401,12 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
|
||||
if isAllNotFound(errs) {
|
||||
// File is fully gone, fileInfo is empty.
|
||||
err := errFileNotFound
|
||||
if versionID != "" {
|
||||
err = errFileVersionNotFound
|
||||
}
|
||||
return er.defaultHealResult(FileInfo{}, storageDisks, storageEndpoints, errs,
|
||||
bucket, object, versionID), nil
|
||||
bucket, object, versionID), err
|
||||
}
|
||||
|
||||
// If less than read quorum number of disks have all the parts
|
||||
@@ -485,7 +493,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
erasure, err := NewErasure(ctx, latestMeta.Erasure.DataBlocks,
|
||||
latestMeta.Erasure.ParityBlocks, latestMeta.Erasure.BlockSize)
|
||||
if err != nil {
|
||||
return result, toObjectErr(err, bucket, object)
|
||||
return result, err
|
||||
}
|
||||
|
||||
erasureInfo := latestMeta.Erasure
|
||||
@@ -523,7 +531,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
closeBitrotReaders(readers)
|
||||
closeBitrotWriters(writers)
|
||||
if err != nil {
|
||||
return result, toObjectErr(err, bucket, object)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// outDatedDisks that had write errors should not be
|
||||
@@ -578,7 +586,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
// Attempt a rename now from healed data to final location.
|
||||
if err = disk.RenameData(ctx, minioMetaTmpBucket, tmpID, partsMetadata[i], bucket, object); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return result, toObjectErr(err, bucket, object)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Remove any remaining parts from outdated disks from before transition.
|
||||
@@ -674,10 +682,16 @@ func (er erasureObjects) healObjectDir(ctx context.Context, bucket, object strin
|
||||
hr.After.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateCorrupt}
|
||||
}
|
||||
}
|
||||
if dryRun || danglingObject || isAllNotFound(errs) {
|
||||
if danglingObject || isAllNotFound(errs) {
|
||||
// Nothing to do, file is already gone.
|
||||
return hr, errFileNotFound
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
// Quit without try to heal the object dir
|
||||
return hr, nil
|
||||
}
|
||||
|
||||
for i, err := range errs {
|
||||
if err == errVolumeNotFound || err == errFileNotFound {
|
||||
// Bucket or prefix/directory not found
|
||||
@@ -837,8 +851,13 @@ func (er erasureObjects) purgeObjectDangling(ctx context.Context, bucket, object
|
||||
// Dangling object successfully purged, size is '0'
|
||||
m.Size = 0
|
||||
}
|
||||
// Generate file/version not found with default heal result
|
||||
err = errFileNotFound
|
||||
if versionID != "" {
|
||||
err = errFileVersionNotFound
|
||||
}
|
||||
return er.defaultHealResult(m, storageDisks, storageEndpoints,
|
||||
errs, bucket, object, versionID), nil
|
||||
errs, bucket, object, versionID), err
|
||||
}
|
||||
|
||||
// Object is considered dangling/corrupted if any only
|
||||
@@ -908,12 +927,6 @@ func isObjectDangling(metaArr []FileInfo, errs []error, dataErrs []error) (valid
|
||||
|
||||
// HealObject - heal the given object, automatically deletes the object if stale/corrupted if `remove` is true.
|
||||
func (er erasureObjects) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (hr madmin.HealResultItem, err error) {
|
||||
defer func() {
|
||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||
err = nil
|
||||
}
|
||||
}()
|
||||
|
||||
// Create context that also contains information about the object and bucket.
|
||||
// The top level handler might not have this information.
|
||||
reqInfo := logger.GetReqInfo(ctx)
|
||||
@@ -927,7 +940,8 @@ func (er erasureObjects) HealObject(ctx context.Context, bucket, object, version
|
||||
|
||||
// Healing directories handle it separately.
|
||||
if HasSuffix(object, SlashSeparator) {
|
||||
return er.healObjectDir(healCtx, bucket, object, opts.DryRun, opts.Remove)
|
||||
hr, err := er.healObjectDir(healCtx, bucket, object, opts.DryRun, opts.Remove)
|
||||
return hr, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
storageDisks := er.getDisks()
|
||||
@@ -942,9 +956,13 @@ func (er erasureObjects) HealObject(ctx context.Context, bucket, object, version
|
||||
// This allows to quickly check if all is ok or all are missing.
|
||||
_, errs := readAllFileInfo(healCtx, storageDisks, bucket, object, versionID, false)
|
||||
if isAllNotFound(errs) {
|
||||
err := errFileNotFound
|
||||
if versionID != "" {
|
||||
err = errFileVersionNotFound
|
||||
}
|
||||
// Nothing to do, file is already gone.
|
||||
return er.defaultHealResult(FileInfo{}, storageDisks, storageEndpoints,
|
||||
errs, bucket, object, versionID), nil
|
||||
errs, bucket, object, versionID), toObjectErr(err, bucket, object, versionID)
|
||||
}
|
||||
|
||||
// Heal the object.
|
||||
@@ -955,5 +973,5 @@ func (er erasureObjects) HealObject(ctx context.Context, bucket, object, version
|
||||
opts.ScanMode = madmin.HealDeepScan
|
||||
hr, err = er.healObject(healCtx, bucket, object, versionID, opts)
|
||||
}
|
||||
return hr, err
|
||||
return hr, toObjectErr(err, bucket, object, versionID)
|
||||
}
|
||||
|
||||
+91
-95
@@ -684,16 +684,18 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
|
||||
// Create an object with multiple parts uploaded in decreasing
|
||||
// part number.
|
||||
uploadID, err := objLayer.NewMultipartUpload(ctx, bucket, object, opts)
|
||||
// Upload a multipart object in the second pool
|
||||
z := objLayer.(*erasureServerPools)
|
||||
set := z.serverPools[1]
|
||||
|
||||
uploadID, err := set.NewMultipartUpload(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create a multipart upload - %v", err)
|
||||
}
|
||||
|
||||
var uploadedParts []CompletePart
|
||||
for _, partID := range []int{2, 1} {
|
||||
pInfo, err1 := objLayer.PutObjectPart(ctx, bucket, object, uploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts)
|
||||
pInfo, err1 := set.PutObjectPart(ctx, bucket, object, uploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts)
|
||||
if err1 != nil {
|
||||
t.Fatalf("Failed to upload a part - %v", err1)
|
||||
}
|
||||
@@ -703,120 +705,114 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
_, err = objLayer.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, ObjectOptions{})
|
||||
_, err = set.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to complete multipart upload - %v", err)
|
||||
}
|
||||
|
||||
// Test 1: Remove the object backend files from the first disk.
|
||||
z := objLayer.(*erasureServerPools)
|
||||
for _, set := range z.serverPools {
|
||||
er := set.sets[0]
|
||||
erasureDisks := er.getDisks()
|
||||
firstDisk := erasureDisks[0]
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete a file - %v", err)
|
||||
}
|
||||
er := set.sets[0]
|
||||
erasureDisks := er.getDisks()
|
||||
firstDisk := erasureDisks[0]
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete a file - %v", err)
|
||||
}
|
||||
|
||||
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to heal object - %v", err)
|
||||
}
|
||||
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to heal object - %v", err)
|
||||
}
|
||||
|
||||
fileInfos, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
fi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||
if errors.Is(err, errFileNotFound) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
fileInfos, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
fi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
|
||||
if _, err = firstDisk.StatInfoFile(context.Background(), bucket, object+"/"+xlStorageFormatFile, false); err != nil {
|
||||
t.Errorf("Expected xl.meta file to be present but stat failed - %v", err)
|
||||
}
|
||||
if _, err = firstDisk.StatInfoFile(context.Background(), bucket, object+"/"+xlStorageFormatFile, false); err != nil {
|
||||
t.Errorf("Expected xl.meta file to be present but stat failed - %v", err)
|
||||
}
|
||||
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
||||
if err != nil {
|
||||
t.Errorf("Failure during deleting part.1 - %v", err)
|
||||
}
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
||||
if err != nil {
|
||||
t.Errorf("Failure during deleting part.1 - %v", err)
|
||||
}
|
||||
|
||||
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte{})
|
||||
if err != nil {
|
||||
t.Errorf("Failure during creating part.1 - %v", err)
|
||||
}
|
||||
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte{})
|
||||
if err != nil {
|
||||
t.Errorf("Failure during creating part.1 - %v", err)
|
||||
}
|
||||
|
||||
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil but received %v", err)
|
||||
}
|
||||
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil but received %v", err)
|
||||
}
|
||||
|
||||
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
nfi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
nfi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
|
||||
fi.DiskMTime = time.Time{}
|
||||
nfi.DiskMTime = time.Time{}
|
||||
if !reflect.DeepEqual(fi, nfi) {
|
||||
t.Fatalf("FileInfo not equal after healing: %v != %v", fi, nfi)
|
||||
}
|
||||
fi.DiskMTime = time.Time{}
|
||||
nfi.DiskMTime = time.Time{}
|
||||
if !reflect.DeepEqual(fi, nfi) {
|
||||
t.Fatalf("FileInfo not equal after healing: %v != %v", fi, nfi)
|
||||
}
|
||||
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
||||
if err != nil {
|
||||
t.Errorf("Failure during deleting part.1 - %v", err)
|
||||
}
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
||||
if err != nil {
|
||||
t.Errorf("Failure during deleting part.1 - %v", err)
|
||||
}
|
||||
|
||||
bdata := bytes.Repeat([]byte("b"), int(nfi.Size))
|
||||
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), bdata)
|
||||
if err != nil {
|
||||
t.Errorf("Failure during creating part.1 - %v", err)
|
||||
}
|
||||
bdata := bytes.Repeat([]byte("b"), int(nfi.Size))
|
||||
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), bdata)
|
||||
if err != nil {
|
||||
t.Errorf("Failure during creating part.1 - %v", err)
|
||||
}
|
||||
|
||||
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil but received %v", err)
|
||||
}
|
||||
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil but received %v", err)
|
||||
}
|
||||
|
||||
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
nfi, err = getLatestFileInfo(ctx, fileInfos, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
nfi, err = getLatestFileInfo(ctx, fileInfos, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
|
||||
fi.DiskMTime = time.Time{}
|
||||
nfi.DiskMTime = time.Time{}
|
||||
if !reflect.DeepEqual(fi, nfi) {
|
||||
t.Fatalf("FileInfo not equal after healing: %v != %v", fi, nfi)
|
||||
}
|
||||
fi.DiskMTime = time.Time{}
|
||||
nfi.DiskMTime = time.Time{}
|
||||
if !reflect.DeepEqual(fi, nfi) {
|
||||
t.Fatalf("FileInfo not equal after healing: %v != %v", fi, nfi)
|
||||
}
|
||||
|
||||
// Test 4: checks if HealObject returns an error when xl.meta is not found
|
||||
// in more than read quorum number of disks, to create a corrupted situation.
|
||||
for i := 0; i <= nfi.Erasure.DataBlocks; i++ {
|
||||
erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||
}
|
||||
// Test 4: checks if HealObject returns an error when xl.meta is not found
|
||||
// in more than read quorum number of disks, to create a corrupted situation.
|
||||
for i := 0; i <= nfi.Erasure.DataBlocks; i++ {
|
||||
erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||
}
|
||||
|
||||
// Try healing now, expect to receive errFileNotFound.
|
||||
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
|
||||
if err != nil {
|
||||
if _, ok := err.(ObjectNotFound); !ok {
|
||||
t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
|
||||
}
|
||||
}
|
||||
|
||||
// since majority of xl.meta's are not available, object should be successfully deleted.
|
||||
_, err = objLayer.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
// Try healing now, expect to receive errFileNotFound.
|
||||
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
|
||||
if err != nil {
|
||||
if _, ok := err.(ObjectNotFound); !ok {
|
||||
t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < (nfi.Erasure.DataBlocks + nfi.Erasure.ParityBlocks); i++ {
|
||||
_, err = erasureDisks[i].StatInfoFile(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||
if err == nil {
|
||||
t.Errorf("Expected xl.meta file to be not present, but succeeeded")
|
||||
}
|
||||
// since majority of xl.meta's are not available, object should be successfully deleted.
|
||||
_, err = objLayer.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
if _, ok := err.(ObjectNotFound); !ok {
|
||||
t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
|
||||
}
|
||||
|
||||
for i := 0; i < (nfi.Erasure.DataBlocks + nfi.Erasure.ParityBlocks); i++ {
|
||||
_, err = erasureDisks[i].StatInfoFile(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||
if err == nil {
|
||||
t.Errorf("Expected xl.meta file to be not present, but succeeeded")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1202,7 +1198,7 @@ func TestHealObjectErasure(t *testing.T) {
|
||||
// since majority of xl.meta's are not available, object quorum
|
||||
// can't be read properly will be deleted automatically and
|
||||
// err is nil
|
||||
if err != nil {
|
||||
if !isErrObjectNotFound(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ func readAllFileInfo(ctx context.Context, disks []StorageAPI, bucket, object, ve
|
||||
errVolumeNotFound,
|
||||
errFileVersionNotFound,
|
||||
errDiskNotFound,
|
||||
errUnformattedDisk,
|
||||
}...) {
|
||||
logger.LogOnceIf(ctx, fmt.Errorf("Drive %s, path (%s/%s) returned an error (%w)",
|
||||
disks[index], bucket, object, err),
|
||||
|
||||
@@ -417,7 +417,7 @@ func objectQuorumFromMeta(ctx context.Context, partsMetaData []FileInfo, errs []
|
||||
}
|
||||
|
||||
parityBlocks := globalStorageClass.GetParityForSC(latestFileInfo.Metadata[xhttp.AmzStorageClass])
|
||||
if parityBlocks <= 0 {
|
||||
if parityBlocks < 0 {
|
||||
parityBlocks = defaultParityCount
|
||||
}
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
||||
|
||||
onlineDisks := er.getDisks()
|
||||
parityDrives := globalStorageClass.GetParityForSC(userDefined[xhttp.AmzStorageClass])
|
||||
if parityDrives <= 0 {
|
||||
if parityDrives < 0 {
|
||||
parityDrives = er.defaultParityCount
|
||||
}
|
||||
|
||||
|
||||
@@ -736,7 +736,7 @@ func (er erasureObjects) putMetacacheObject(ctx context.Context, key string, r *
|
||||
storageDisks := er.getDisks()
|
||||
// Get parity and data drive count based on storage class metadata
|
||||
parityDrives := globalStorageClass.GetParityForSC(opts.UserDefined[xhttp.AmzStorageClass])
|
||||
if parityDrives <= 0 {
|
||||
if parityDrives < 0 {
|
||||
parityDrives = er.defaultParityCount
|
||||
}
|
||||
dataDrives := len(storageDisks) - parityDrives
|
||||
@@ -885,7 +885,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
if !opts.MaxParity {
|
||||
// Get parity and data drive count based on storage class metadata
|
||||
parityDrives = globalStorageClass.GetParityForSC(userDefined[xhttp.AmzStorageClass])
|
||||
if parityDrives <= 0 {
|
||||
if parityDrives < 0 {
|
||||
parityDrives = er.defaultParityCount
|
||||
}
|
||||
|
||||
|
||||
@@ -893,6 +893,14 @@ func testObjectQuorumFromMeta(obj ObjectLayer, instanceType string, dirs []strin
|
||||
|
||||
// Object for test case 1 - No StorageClass defined, no MetaData in PutObject
|
||||
object1 := "object1"
|
||||
globalStorageClass = storageclass.Config{
|
||||
RRS: storageclass.StorageClass{
|
||||
Parity: 2,
|
||||
},
|
||||
Standard: storageclass.StorageClass{
|
||||
Parity: 4,
|
||||
},
|
||||
}
|
||||
_, err = obj.PutObject(ctx, bucket, object1, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to putObject %v", err)
|
||||
@@ -964,17 +972,16 @@ func testObjectQuorumFromMeta(obj ObjectLayer, instanceType string, dirs []strin
|
||||
}
|
||||
|
||||
parts5, errs5 := readAllFileInfo(ctx, erasureDisks, bucket, object5, "", false)
|
||||
parts5SC := storageclass.Config{
|
||||
RRS: storageclass.StorageClass{
|
||||
Parity: 2,
|
||||
},
|
||||
}
|
||||
parts5SC := globalStorageClass
|
||||
|
||||
// Object for test case 6 - RRS StorageClass defined as Parity 2, MetaData in PutObject requesting Standard Storage Class
|
||||
object6 := "object6"
|
||||
metadata6 := make(map[string]string)
|
||||
metadata6["x-amz-storage-class"] = storageclass.STANDARD
|
||||
globalStorageClass = storageclass.Config{
|
||||
Standard: storageclass.StorageClass{
|
||||
Parity: 4,
|
||||
},
|
||||
RRS: storageclass.StorageClass{
|
||||
Parity: 2,
|
||||
},
|
||||
@@ -1035,7 +1042,7 @@ func testObjectQuorumFromMeta(obj ObjectLayer, instanceType string, dirs []strin
|
||||
tt := tt
|
||||
t.(*testing.T).Run("", func(t *testing.T) {
|
||||
globalStorageClass = tt.storageClassCfg
|
||||
actualReadQuorum, actualWriteQuorum, err := objectQuorumFromMeta(ctx, tt.parts, tt.errs, getDefaultParityBlocks(len(erasureDisks)))
|
||||
actualReadQuorum, actualWriteQuorum, err := objectQuorumFromMeta(ctx, tt.parts, tt.errs, storageclass.DefaultParityBlocks(len(erasureDisks)))
|
||||
if tt.expectedError != nil && err == nil {
|
||||
t.Errorf("Expected %s, got %s", tt.expectedError, err)
|
||||
}
|
||||
|
||||
@@ -525,7 +525,7 @@ func (z *erasureServerPools) BackendInfo() (b madmin.BackendInfo) {
|
||||
b.Type = madmin.Erasure
|
||||
|
||||
scParity := globalStorageClass.GetParityForSC(storageclass.STANDARD)
|
||||
if scParity <= 0 {
|
||||
if scParity < 0 {
|
||||
scParity = z.serverPools[0].defaultParityCount
|
||||
}
|
||||
rrSCParity := globalStorageClass.GetParityForSC(storageclass.RRS)
|
||||
@@ -1939,7 +1939,8 @@ func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix str
|
||||
}
|
||||
|
||||
for _, version := range fivs.Versions {
|
||||
if err := healObjectFn(bucket, version.Name, version.VersionID); err != nil {
|
||||
err := healObjectFn(bucket, version.Name, version.VersionID)
|
||||
if err != nil && !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -2000,18 +2001,21 @@ func (z *erasureServerPools) HealObject(ctx context.Context, bucket, object, ver
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return madmin.HealResultItem{}, err
|
||||
// Return the first nil error
|
||||
for idx, err := range errs {
|
||||
if err == nil {
|
||||
return results[idx], nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, result := range results {
|
||||
if result.Object != "" {
|
||||
return result, nil
|
||||
// No pool returned a nil error, return the first non 'not found' error
|
||||
for idx, err := range errs {
|
||||
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
||||
return results[idx], err
|
||||
}
|
||||
}
|
||||
|
||||
// At this stage, all errors are 'not found'
|
||||
if versionID != "" {
|
||||
return madmin.HealResultItem{}, VersionNotFound{
|
||||
Bucket: bucket,
|
||||
|
||||
+3
-43
@@ -92,8 +92,6 @@ type erasureSets struct {
|
||||
distributionAlgo string
|
||||
deploymentID [16]byte
|
||||
|
||||
disksStorageInfoCache timedValue
|
||||
|
||||
lastConnectDisksOpTime time.Time
|
||||
}
|
||||
|
||||
@@ -629,47 +627,6 @@ func (s *erasureSets) ParityCount() int {
|
||||
return s.defaultParityCount
|
||||
}
|
||||
|
||||
// StorageUsageInfo - combines output of StorageInfo across all erasure coded object sets.
|
||||
// This only returns disk usage info for ServerPools to perform placement decision, this call
|
||||
// is not implemented in Object interface and is not meant to be used by other object
|
||||
// layer implementations.
|
||||
func (s *erasureSets) StorageUsageInfo(ctx context.Context) StorageInfo {
|
||||
storageUsageInfo := func() StorageInfo {
|
||||
var storageInfo StorageInfo
|
||||
storageInfos := make([]StorageInfo, len(s.sets))
|
||||
storageInfo.Backend.Type = madmin.Erasure
|
||||
|
||||
g := errgroup.WithNErrs(len(s.sets))
|
||||
for index := range s.sets {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
// ignoring errors on purpose
|
||||
storageInfos[index], _ = s.sets[index].StorageInfo(ctx)
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
|
||||
// Wait for the go routines.
|
||||
g.Wait()
|
||||
|
||||
for _, lstorageInfo := range storageInfos {
|
||||
storageInfo.Disks = append(storageInfo.Disks, lstorageInfo.Disks...)
|
||||
}
|
||||
|
||||
return storageInfo
|
||||
}
|
||||
|
||||
s.disksStorageInfoCache.Once.Do(func() {
|
||||
s.disksStorageInfoCache.TTL = time.Second
|
||||
s.disksStorageInfoCache.Update = func() (interface{}, error) {
|
||||
return storageUsageInfo(), nil
|
||||
}
|
||||
})
|
||||
|
||||
v, _ := s.disksStorageInfoCache.Get()
|
||||
return v.(StorageInfo)
|
||||
}
|
||||
|
||||
// StorageInfo - combines output of StorageInfo across all erasure coded object sets.
|
||||
func (s *erasureSets) StorageInfo(ctx context.Context) (StorageInfo, []error) {
|
||||
var storageInfo madmin.StorageInfo
|
||||
@@ -1047,11 +1004,13 @@ func (s *erasureSets) CopyObject(ctx context.Context, srcBucket, srcObject, dstB
|
||||
// Version ID is set for the destination and source == destination version ID.
|
||||
// perform an in-place update.
|
||||
if dstOpts.VersionID != "" && srcOpts.VersionID == dstOpts.VersionID {
|
||||
srcInfo.Reader.Close() // We are not interested in the reader stream at this point close it.
|
||||
return srcSet.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
|
||||
}
|
||||
// Destination is not versioned and source version ID is empty
|
||||
// perform an in-place update.
|
||||
if !dstOpts.Versioned && srcOpts.VersionID == "" {
|
||||
srcInfo.Reader.Close() // We are not interested in the reader stream at this point close it.
|
||||
return srcSet.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
|
||||
}
|
||||
// CopyObject optimization where we don't create an entire copy
|
||||
@@ -1060,6 +1019,7 @@ func (s *erasureSets) CopyObject(ctx context.Context, srcBucket, srcObject, dstB
|
||||
// that we actually create a new dataDir for legacy objects.
|
||||
if dstOpts.Versioned && srcOpts.VersionID != dstOpts.VersionID && !srcInfo.Legacy {
|
||||
srcInfo.versionOnly = true
|
||||
srcInfo.Reader.Close() // We are not interested in the reader stream at this point close it.
|
||||
return srcSet.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3296,3 +3296,39 @@ func (es *erasureSingle) NSScanner(ctx context.Context, bf *bloomFilter, updates
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// GetRawData will return all files with a given raw path to the callback.
|
||||
// Errors are ignored, only errors from the callback are returned.
|
||||
// For now only direct file paths are supported.
|
||||
func (es *erasureSingle) GetRawData(ctx context.Context, volume, file string, fn func(r io.Reader, host string, disk string, filename string, info StatInfo) error) error {
|
||||
found := 0
|
||||
stats, err := es.disk.StatInfoFile(ctx, volume, file, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, si := range stats {
|
||||
found++
|
||||
var r io.ReadCloser
|
||||
if !si.Dir {
|
||||
r, err = es.disk.ReadFileStream(ctx, volume, si.Name, 0, si.Size)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
r = io.NopCloser(bytes.NewBuffer([]byte{}))
|
||||
}
|
||||
// Keep disk path instead of ID, to ensure that the downloaded zip file can be
|
||||
// easily automated with `minio server hostname{1...n}/disk{1...m}`.
|
||||
err = fn(r, es.disk.Hostname(), es.disk.Endpoint().Path, pathJoin(volume, si.Name), si)
|
||||
r.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if found == 0 {
|
||||
return errFileNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+3
-16
@@ -643,7 +643,7 @@ func saveFormatErasureAll(ctx context.Context, storageDisks []StorageAPI, format
|
||||
}, index)
|
||||
}
|
||||
|
||||
writeQuorum := getWriteQuorum(len(storageDisks))
|
||||
writeQuorum := (len(storageDisks) + 1/2)
|
||||
// Wait for the routines to finish.
|
||||
return reduceWriteQuorumErrs(ctx, g.Wait(), nil, writeQuorum)
|
||||
}
|
||||
@@ -805,26 +805,13 @@ func initFormatErasure(ctx context.Context, storageDisks []StorageAPI, setCount,
|
||||
return getFormatErasureInQuorum(formats)
|
||||
}
|
||||
|
||||
func getDefaultParityBlocks(drive int) int {
|
||||
switch drive {
|
||||
case 3, 2:
|
||||
return 1
|
||||
case 4, 5:
|
||||
return 2
|
||||
case 6, 7:
|
||||
return 3
|
||||
default:
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
// ecDrivesNoConfig returns the erasure coded drives in a set if no config has been set.
|
||||
// It will attempt to read it from env variable and fall back to drives/2.
|
||||
func ecDrivesNoConfig(setDriveCount int) int {
|
||||
sc, _ := storageclass.LookupConfig(config.KVS{}, setDriveCount)
|
||||
ecDrives := sc.GetParityForSC(storageclass.STANDARD)
|
||||
if ecDrives <= 0 {
|
||||
ecDrives = getDefaultParityBlocks(setDriveCount)
|
||||
if ecDrives < 0 {
|
||||
ecDrives = storageclass.DefaultParityBlocks(setDriveCount)
|
||||
}
|
||||
return ecDrives
|
||||
}
|
||||
|
||||
@@ -297,6 +297,10 @@ func ErrorRespToObjectError(err error, params ...string) error {
|
||||
}
|
||||
|
||||
switch minioErr.Code {
|
||||
case "PreconditionFailed":
|
||||
err = PreConditionFailed{}
|
||||
case "InvalidRange":
|
||||
err = InvalidRange{}
|
||||
case "BucketAlreadyOwnedByYou":
|
||||
err = BucketAlreadyOwnedByYou{}
|
||||
case "BucketNotEmpty":
|
||||
|
||||
@@ -32,7 +32,6 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio/internal/color"
|
||||
"github.com/minio/minio/internal/config"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
@@ -221,10 +220,6 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
// Set when gateway is enabled
|
||||
globalIsGateway = true
|
||||
|
||||
// TODO: We need to move this code with globalConfigSys.Init()
|
||||
// for now keep it here such that "s3" gateway layer initializes
|
||||
// itself properly when KMS is set.
|
||||
|
||||
// Initialize server config.
|
||||
srvCfg := newServerConfig()
|
||||
|
||||
@@ -384,15 +379,5 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
logger.Info("======")
|
||||
}
|
||||
|
||||
// TODO: remove the following line by June 1st.
|
||||
logger.Info(
|
||||
color.RedBold(`
|
||||
===================================================================================
|
||||
**** WARNING: MinIO Gateway will be removed by June 1st from MinIO repository *****
|
||||
|
||||
Please read https://github.com/minio/minio/issues/14331
|
||||
===================================================================================
|
||||
`))
|
||||
|
||||
<-globalOSSignalCh
|
||||
}
|
||||
|
||||
@@ -27,6 +27,12 @@ import (
|
||||
|
||||
// Prints the formatted startup message.
|
||||
func printGatewayStartupMessage(apiEndPoints []string, backendType string) {
|
||||
if len(globalSubnetConfig.APIKey) == 0 {
|
||||
var builder strings.Builder
|
||||
startupBanner(&builder)
|
||||
logger.Info("\n" + builder.String())
|
||||
}
|
||||
|
||||
strippedAPIEndpoints := stripStandardPorts(apiEndPoints, globalMinioHost)
|
||||
// If cache layer is enabled, print cache capacity.
|
||||
cacheAPI := newCachedObjectLayerFn()
|
||||
|
||||
@@ -97,12 +97,25 @@ func isHTTPHeaderSizeTooLarge(header http.Header) bool {
|
||||
// Limits body and header to specific allowed maximum limits as per S3/MinIO API requirements.
|
||||
func setRequestLimitHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tc, ok := r.Context().Value(contextTraceReqKey).(*traceCtxt)
|
||||
|
||||
// Reject unsupported reserved metadata first before validation.
|
||||
if containsReservedMetadata(r.Header) {
|
||||
if ok {
|
||||
tc.funcName = "handler.ValidRequest"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrUnsupportedMetadata), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if isHTTPHeaderSizeTooLarge(r.Header) {
|
||||
if ok {
|
||||
tc.funcName = "handler.ValidRequest"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrMetadataTooLarge), r.URL)
|
||||
atomic.AddUint64(&globalHTTPStats.rejectedRequestsHeader, 1)
|
||||
return
|
||||
@@ -373,7 +386,14 @@ func hasMultipleAuth(r *http.Request) bool {
|
||||
// any malicious requests.
|
||||
func setRequestValidityHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tc, ok := r.Context().Value(contextTraceReqKey).(*traceCtxt)
|
||||
|
||||
if err := hasBadHost(r.Host); err != nil {
|
||||
if ok {
|
||||
tc.funcName = "handler.ValidRequest"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
invalidReq := errorCodes.ToAPIErr(ErrInvalidRequest)
|
||||
invalidReq.Description = fmt.Sprintf("%s (%s)", invalidReq.Description, err)
|
||||
writeErrorResponse(r.Context(), w, invalidReq, r.URL)
|
||||
@@ -383,6 +403,11 @@ func setRequestValidityHandler(h http.Handler) http.Handler {
|
||||
|
||||
// Check for bad components in URL path.
|
||||
if hasBadPathComponent(r.URL.Path) {
|
||||
if ok {
|
||||
tc.funcName = "handler.ValidRequest"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL)
|
||||
atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
|
||||
return
|
||||
@@ -391,6 +416,11 @@ func setRequestValidityHandler(h http.Handler) http.Handler {
|
||||
for _, vv := range r.Form {
|
||||
for _, v := range vv {
|
||||
if hasBadPathComponent(v) {
|
||||
if ok {
|
||||
tc.funcName = "handler.ValidRequest"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL)
|
||||
atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
|
||||
return
|
||||
@@ -398,6 +428,11 @@ func setRequestValidityHandler(h http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
if hasMultipleAuth(r) {
|
||||
if ok {
|
||||
tc.funcName = "handler.Auth"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
invalidReq := errorCodes.ToAPIErr(ErrInvalidRequest)
|
||||
invalidReq.Description = fmt.Sprintf("%s (request has multiple authentication types, please use one)", invalidReq.Description)
|
||||
writeErrorResponse(r.Context(), w, invalidReq, r.URL)
|
||||
@@ -408,6 +443,11 @@ func setRequestValidityHandler(h http.Handler) http.Handler {
|
||||
bucketName, _ := request2BucketObjectName(r)
|
||||
if isMinioReservedBucket(bucketName) || isMinioMetaBucket(bucketName) {
|
||||
if !guessIsRPCReq(r) && !guessIsBrowserReq(r) && !guessIsHealthCheckReq(r) && !guessIsMetricsReq(r) && !isAdminReq(r) {
|
||||
if ok {
|
||||
tc.funcName = "handler.ValidRequest"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrAllAccessDisabled), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -415,8 +455,18 @@ func setRequestValidityHandler(h http.Handler) http.Handler {
|
||||
// Deny SSE-C requests if not made over TLS
|
||||
if !globalIsTLS && (crypto.SSEC.IsRequested(r.Header) || crypto.SSECopy.IsRequested(r.Header)) {
|
||||
if r.Method == http.MethodHead {
|
||||
if ok {
|
||||
tc.funcName = "handler.ValidRequest"
|
||||
tc.responseRecorder.LogErrBody = false
|
||||
}
|
||||
|
||||
writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrInsecureSSECustomerRequest))
|
||||
} else {
|
||||
if ok {
|
||||
tc.funcName = "handler.ValidRequest"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInsecureSSECustomerRequest), r.URL)
|
||||
}
|
||||
return
|
||||
|
||||
+16
-2
@@ -171,6 +171,16 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
||||
healBuckets := make([]string, len(buckets))
|
||||
copy(healBuckets, buckets)
|
||||
|
||||
// Heal all buckets first in this erasure set - this is useful
|
||||
// for new objects upload in different buckets to be successful
|
||||
for _, bucket := range healBuckets {
|
||||
_, err := er.HealBucket(ctx, bucket, madmin.HealOpts{ScanMode: scanMode})
|
||||
if err != nil {
|
||||
// Log bucket healing error if any, we shall retry again.
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
var retErr error
|
||||
// Heal all buckets with all objects
|
||||
for _, bucket := range healBuckets {
|
||||
@@ -189,7 +199,8 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
||||
}
|
||||
tracker.Object = ""
|
||||
tracker.Bucket = bucket
|
||||
// Heal current bucket
|
||||
// Heal current bucket again in case if it is failed
|
||||
// in the being of erasure set healing
|
||||
if _, err := er.HealBucket(ctx, bucket, madmin.HealOpts{
|
||||
ScanMode: scanMode,
|
||||
}); err != nil {
|
||||
@@ -258,8 +269,11 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
||||
return
|
||||
}
|
||||
|
||||
// erasureObjects layer needs object names to be encoded
|
||||
encodedEntryName := encodeDirObject(entry.name)
|
||||
|
||||
for _, version := range fivs.Versions {
|
||||
if _, err := er.HealObject(ctx, bucket, version.Name,
|
||||
if _, err := er.HealObject(ctx, bucket, encodedEntryName,
|
||||
version.VersionID, madmin.HealOpts{
|
||||
ScanMode: scanMode,
|
||||
Remove: healDeleteDangling,
|
||||
|
||||
@@ -338,6 +338,8 @@ var (
|
||||
|
||||
globalProxyTransport http.RoundTripper
|
||||
|
||||
globalRemoteTargetTransport http.RoundTripper
|
||||
|
||||
globalDNSCache = &dnscache.Resolver{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
@@ -371,6 +373,9 @@ var (
|
||||
globalObjectPerfBucket = "minio-perf-test-tmp-bucket"
|
||||
globalObjectPerfUserMetadata = "X-Amz-Meta-Minio-Object-Perf" // Clients can set this to bypass S3 API service freeze. Used by object pref tests.
|
||||
|
||||
// MinIO version unix timestamp
|
||||
globalVersionUnix uint64
|
||||
|
||||
// Add new variable global values here.
|
||||
)
|
||||
|
||||
|
||||
+3
-1
@@ -116,7 +116,9 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
|
||||
// + 2 * 10MiB (default erasure block size v1) + 2 * 1MiB (default erasure block size v2)
|
||||
blockSize := xioutil.BlockSizeLarge + xioutil.BlockSizeSmall
|
||||
apiRequestsMaxPerNode = int(maxMem / uint64(maxSetDrives*blockSize+int(blockSizeV1*2+blockSizeV2*2)))
|
||||
logger.Info("Automatically configured API requests per node based on available memory on the system: %d", apiRequestsMaxPerNode)
|
||||
if globalIsDistErasure {
|
||||
logger.Info("Automatically configured API requests per node based on available memory on the system: %d", apiRequestsMaxPerNode)
|
||||
}
|
||||
} else {
|
||||
apiRequestsMaxPerNode = cfg.RequestsMax
|
||||
if len(globalEndpoints.Hostnames()) > 0 {
|
||||
|
||||
@@ -357,30 +357,6 @@ func extractPostPolicyFormValues(ctx context.Context, form *multipart.Form) (fil
|
||||
return filePart, fileName, fileSize, formValues, nil
|
||||
}
|
||||
|
||||
// Log headers and body.
|
||||
func httpTraceAll(f http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if globalTrace.NumSubscribers() == 0 {
|
||||
f.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
trace := Trace(f, true, w, r)
|
||||
globalTrace.Publish(trace)
|
||||
}
|
||||
}
|
||||
|
||||
// Log only the headers.
|
||||
func httpTraceHdrs(f http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if globalTrace.NumSubscribers() == 0 {
|
||||
f.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
trace := Trace(f, false, w, r)
|
||||
globalTrace.Publish(trace)
|
||||
}
|
||||
}
|
||||
|
||||
func collectAPIStats(api string, f http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
globalHTTPStats.currentS3Requests.Inc(api)
|
||||
|
||||
+147
-93
@@ -19,6 +19,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -43,8 +44,6 @@ type recordRequest struct {
|
||||
logBody bool
|
||||
// Internal recording buffer
|
||||
buf bytes.Buffer
|
||||
// request headers
|
||||
headers http.Header
|
||||
// total bytes read including header size
|
||||
bytesRead int
|
||||
}
|
||||
@@ -67,12 +66,8 @@ func (r *recordRequest) Read(p []byte) (n int, err error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *recordRequest) Size() int {
|
||||
sz := r.bytesRead
|
||||
for k, v := range r.headers {
|
||||
sz += len(k) + len(v)
|
||||
}
|
||||
return sz
|
||||
func (r *recordRequest) BodySize() int {
|
||||
return r.bytesRead
|
||||
}
|
||||
|
||||
// Return the bytes that were recorded.
|
||||
@@ -102,96 +97,155 @@ func getOpName(name string) (op string) {
|
||||
op = strings.TrimSuffix(op, "Handler-fm")
|
||||
op = strings.Replace(op, "objectAPIHandlers", "s3", 1)
|
||||
op = strings.Replace(op, "adminAPIHandlers", "admin", 1)
|
||||
op = strings.Replace(op, "(*webAPIHandlers)", "web", 1)
|
||||
op = strings.Replace(op, "(*storageRESTServer)", "internal", 1)
|
||||
op = strings.Replace(op, "(*peerRESTServer)", "internal", 1)
|
||||
op = strings.Replace(op, "(*lockRESTServer)", "internal", 1)
|
||||
op = strings.Replace(op, "(*storageRESTServer)", "storageR", 1)
|
||||
op = strings.Replace(op, "(*peerRESTServer)", "peer", 1)
|
||||
op = strings.Replace(op, "(*lockRESTServer)", "lockR", 1)
|
||||
op = strings.Replace(op, "(*stsAPIHandlers)", "sts", 1)
|
||||
op = strings.Replace(op, "LivenessCheckHandler", "healthcheck", 1)
|
||||
op = strings.Replace(op, "ReadinessCheckHandler", "healthcheck", 1)
|
||||
op = strings.Replace(op, "ClusterCheckHandler", "health.Cluster", 1)
|
||||
op = strings.Replace(op, "ClusterReadCheckHandler", "health.ClusterRead", 1)
|
||||
op = strings.Replace(op, "LivenessCheckHandler", "health.Liveness", 1)
|
||||
op = strings.Replace(op, "ReadinessCheckHandler", "health.Readiness", 1)
|
||||
op = strings.Replace(op, "-fm", "", 1)
|
||||
return op
|
||||
}
|
||||
|
||||
// Trace gets trace of http request
|
||||
func Trace(f http.HandlerFunc, logBody bool, w http.ResponseWriter, r *http.Request) madmin.TraceInfo {
|
||||
name := getOpName(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
|
||||
type contextTraceReqType string
|
||||
|
||||
// Setup a http request body recorder
|
||||
reqHeaders := r.Header.Clone()
|
||||
reqHeaders.Set("Host", r.Host)
|
||||
if len(r.TransferEncoding) == 0 {
|
||||
reqHeaders.Set("Content-Length", strconv.Itoa(int(r.ContentLength)))
|
||||
} else {
|
||||
reqHeaders.Set("Transfer-Encoding", strings.Join(r.TransferEncoding, ","))
|
||||
}
|
||||
const contextTraceReqKey = contextTraceReqType("request-trace-info")
|
||||
|
||||
reqBodyRecorder := &recordRequest{Reader: r.Body, logBody: logBody, headers: reqHeaders}
|
||||
r.Body = reqBodyRecorder
|
||||
|
||||
now := time.Now().UTC()
|
||||
t := madmin.TraceInfo{TraceType: madmin.TraceHTTP, FuncName: name, Time: now}
|
||||
|
||||
t.NodeName = r.Host
|
||||
if globalIsDistErasure {
|
||||
t.NodeName = globalLocalNodeName
|
||||
}
|
||||
|
||||
if t.NodeName == "" {
|
||||
t.NodeName = globalLocalNodeName
|
||||
}
|
||||
|
||||
// strip only standard port from the host address
|
||||
if host, port, err := net.SplitHostPort(t.NodeName); err == nil {
|
||||
if port == "443" || port == "80" {
|
||||
t.NodeName = host
|
||||
}
|
||||
}
|
||||
|
||||
rq := madmin.TraceRequestInfo{
|
||||
Time: now,
|
||||
Proto: r.Proto,
|
||||
Method: r.Method,
|
||||
RawQuery: redactLDAPPwd(r.URL.RawQuery),
|
||||
Client: handlers.GetSourceIP(r),
|
||||
Headers: reqHeaders,
|
||||
}
|
||||
|
||||
path := r.URL.RawPath
|
||||
if path == "" {
|
||||
path = r.URL.Path
|
||||
}
|
||||
rq.Path = path
|
||||
|
||||
rw := logger.NewResponseWriter(w)
|
||||
rw.LogErrBody = true
|
||||
rw.LogAllBody = logBody
|
||||
|
||||
// Execute call.
|
||||
f(rw, r)
|
||||
|
||||
rs := madmin.TraceResponseInfo{
|
||||
Time: time.Now().UTC(),
|
||||
Headers: rw.Header().Clone(),
|
||||
StatusCode: rw.StatusCode,
|
||||
Body: rw.Body(),
|
||||
}
|
||||
|
||||
// Transfer request body
|
||||
rq.Body = reqBodyRecorder.Data()
|
||||
|
||||
if rs.StatusCode == 0 {
|
||||
rs.StatusCode = http.StatusOK
|
||||
}
|
||||
|
||||
t.ReqInfo = rq
|
||||
t.RespInfo = rs
|
||||
|
||||
t.CallStats = madmin.TraceCallStats{
|
||||
Latency: rs.Time.Sub(rw.StartTime),
|
||||
InputBytes: reqBodyRecorder.Size(),
|
||||
OutputBytes: rw.Size(),
|
||||
TimeToFirstByte: rw.TimeToFirstByte,
|
||||
}
|
||||
return t
|
||||
// Hold related tracing data of a http request, any handler
|
||||
// can modify this struct to modify the trace information .
|
||||
type traceCtxt struct {
|
||||
requestRecorder *recordRequest
|
||||
responseRecorder *logger.ResponseWriter
|
||||
funcName string
|
||||
}
|
||||
|
||||
// If trace is enabled, execute the request if it is traced by other handlers
|
||||
// otherwise, generate a trace event with request information but no response.
|
||||
func httpTracer(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if globalTrace.NumSubscribers() == 0 {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Create tracing data structure and associate it to the request context
|
||||
tc := traceCtxt{}
|
||||
ctx := context.WithValue(r.Context(), contextTraceReqKey, &tc)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
// Setup a http request and response body recorder
|
||||
reqRecorder := &recordRequest{Reader: r.Body}
|
||||
respRecorder := logger.NewResponseWriter(w)
|
||||
|
||||
tc.requestRecorder = reqRecorder
|
||||
tc.responseRecorder = respRecorder
|
||||
|
||||
// Execute call.
|
||||
r.Body = reqRecorder
|
||||
|
||||
reqStartTime := time.Now().UTC()
|
||||
h.ServeHTTP(respRecorder, r)
|
||||
reqEndTime := time.Now().UTC()
|
||||
|
||||
// Calculate input body size with headers
|
||||
reqHeaders := r.Header.Clone()
|
||||
reqHeaders.Set("Host", r.Host)
|
||||
if len(r.TransferEncoding) == 0 {
|
||||
reqHeaders.Set("Content-Length", strconv.Itoa(int(r.ContentLength)))
|
||||
} else {
|
||||
reqHeaders.Set("Transfer-Encoding", strings.Join(r.TransferEncoding, ","))
|
||||
}
|
||||
inputBytes := reqRecorder.BodySize()
|
||||
for k, v := range reqHeaders {
|
||||
inputBytes += len(k) + len(v)
|
||||
}
|
||||
|
||||
// Calculate node name
|
||||
nodeName := r.Host
|
||||
if globalIsDistErasure {
|
||||
nodeName = globalLocalNodeName
|
||||
}
|
||||
if host, port, err := net.SplitHostPort(nodeName); err == nil {
|
||||
if port == "443" || port == "80" {
|
||||
nodeName = host
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate reqPath
|
||||
reqPath := r.URL.RawPath
|
||||
if reqPath == "" {
|
||||
reqPath = r.URL.Path
|
||||
}
|
||||
|
||||
// Calculate function name
|
||||
funcName := tc.funcName
|
||||
if funcName == "" {
|
||||
funcName = "<unknown>"
|
||||
}
|
||||
|
||||
rq := madmin.TraceRequestInfo{
|
||||
Time: reqStartTime,
|
||||
Proto: r.Proto,
|
||||
Method: r.Method,
|
||||
RawQuery: redactLDAPPwd(r.URL.RawQuery),
|
||||
Client: handlers.GetSourceIP(r),
|
||||
Headers: reqHeaders,
|
||||
Path: reqPath,
|
||||
Body: reqRecorder.Data(),
|
||||
}
|
||||
|
||||
rs := madmin.TraceResponseInfo{
|
||||
Time: reqEndTime,
|
||||
Headers: respRecorder.Header().Clone(),
|
||||
StatusCode: respRecorder.StatusCode,
|
||||
Body: respRecorder.Body(),
|
||||
}
|
||||
|
||||
cs := madmin.TraceCallStats{
|
||||
Latency: rs.Time.Sub(respRecorder.StartTime),
|
||||
InputBytes: inputBytes,
|
||||
OutputBytes: respRecorder.Size(),
|
||||
TimeToFirstByte: respRecorder.TimeToFirstByte,
|
||||
}
|
||||
|
||||
t := madmin.TraceInfo{
|
||||
TraceType: madmin.TraceHTTP,
|
||||
FuncName: funcName,
|
||||
NodeName: nodeName,
|
||||
Time: reqStartTime,
|
||||
ReqInfo: rq,
|
||||
RespInfo: rs,
|
||||
CallStats: cs,
|
||||
}
|
||||
|
||||
globalTrace.Publish(t)
|
||||
})
|
||||
}
|
||||
|
||||
func httpTrace(f http.HandlerFunc, logBody bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tc, ok := r.Context().Value(contextTraceReqKey).(*traceCtxt)
|
||||
if !ok {
|
||||
// Tracing is not enabled for this request
|
||||
f.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
tc.funcName = getOpName(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
|
||||
tc.requestRecorder.logBody = logBody
|
||||
tc.responseRecorder.LogAllBody = logBody
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
|
||||
f.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func httpTraceAll(f http.HandlerFunc) http.HandlerFunc {
|
||||
return httpTrace(f, true)
|
||||
}
|
||||
|
||||
func httpTraceHdrs(f http.HandlerFunc) http.HandlerFunc {
|
||||
return httpTrace(f, false)
|
||||
}
|
||||
|
||||
-12
@@ -223,8 +223,6 @@ func (sys *IAMSys) Load(ctx context.Context) error {
|
||||
|
||||
// Init - initializes config system by reading entries from config/iam
|
||||
func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etcd.Client, iamRefreshInterval time.Duration) {
|
||||
iamInitStart := time.Now()
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
@@ -297,8 +295,6 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
||||
break
|
||||
}
|
||||
|
||||
iamLoadStart := time.Now()
|
||||
|
||||
// Load IAM data from storage.
|
||||
for {
|
||||
if err := sys.Load(retryCtx); err != nil {
|
||||
@@ -370,9 +366,6 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
||||
}
|
||||
|
||||
sys.printIAMRoles()
|
||||
|
||||
now := time.Now()
|
||||
logger.Info("Finished loading IAM sub-system (took %.1fs of %.1fs to load data).", now.Sub(iamLoadStart).Seconds(), now.Sub(iamInitStart).Seconds())
|
||||
}
|
||||
|
||||
func (sys *IAMSys) validateAndAddRolePolicyMappings(ctx context.Context, m map[arn.ARN]string) {
|
||||
@@ -1597,11 +1590,6 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
|
||||
return false
|
||||
}
|
||||
if len(policies) == 0 {
|
||||
// TODO (deprecated in Dec 2021): Only need to handle
|
||||
// behavior for STS credentials created in older
|
||||
// releases. Otherwise, reject such cases, once older
|
||||
// behavior is deprecated.
|
||||
|
||||
// If there is no parent policy mapping, we fall back to
|
||||
// using policy claim from JWT.
|
||||
policySet, ok := args.GetPolicies(iamPolicyClaimNameOpenID())
|
||||
|
||||
+18
-4
@@ -19,10 +19,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio/internal/color"
|
||||
@@ -163,11 +165,23 @@ func newApp(name string) *cli.App {
|
||||
return app
|
||||
}
|
||||
|
||||
func startupBanner(banner io.Writer) {
|
||||
fmt.Fprintln(banner, color.Blue("Copyright:")+color.Bold(" 2015-%s MinIO, Inc.", CopyrightYear))
|
||||
fmt.Fprintln(banner, color.Blue("License:")+color.Bold(" GNU AGPLv3 <https://www.gnu.org/licenses/agpl-3.0.html>"))
|
||||
fmt.Fprintln(banner, color.Blue("Version:")+color.Bold(" %s (%s %s/%s)", ReleaseTag, runtime.Version(), runtime.GOOS, runtime.GOARCH))
|
||||
}
|
||||
|
||||
func versionBanner(c *cli.Context) io.Reader {
|
||||
banner := &strings.Builder{}
|
||||
fmt.Fprintln(banner, color.Bold("%s version %s (commit-id=%s)", c.App.Name, c.App.Version, CommitID))
|
||||
fmt.Fprintln(banner, color.Blue("Runtime:")+color.Bold(" %s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH))
|
||||
fmt.Fprintln(banner, color.Blue("License:")+color.Bold(" GNU AGPLv3 <https://www.gnu.org/licenses/agpl-3.0.html>"))
|
||||
fmt.Fprintln(banner, color.Blue("Copyright:")+color.Bold(" 2015-%s MinIO, Inc.", CopyrightYear))
|
||||
return strings.NewReader(banner.String())
|
||||
}
|
||||
|
||||
func printMinIOVersion(c *cli.Context) {
|
||||
fmt.Fprintln(c.App.Writer, color.Greenf("%s version %s (commit-id=%s)", c.App.Name, c.App.Version, CommitID))
|
||||
fmt.Fprintln(c.App.Writer, color.Greenf("Runtime: %s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH))
|
||||
fmt.Fprintln(c.App.Writer, color.Greenf("Copyright (c) 2015-%s MinIO, Inc.", CopyrightYear))
|
||||
fmt.Fprintln(c.App.Writer, color.Red("Licence AGPLv3 <https://www.gnu.org/licenses/agpl-3.0.html>"))
|
||||
io.Copy(c.App.Writer, versionBanner(c))
|
||||
}
|
||||
|
||||
// Main main for minio server.
|
||||
|
||||
@@ -630,7 +630,6 @@ func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions,
|
||||
return err
|
||||
}
|
||||
if allAtEOF {
|
||||
// TODO" Maybe, maybe not
|
||||
return io.EOF
|
||||
}
|
||||
return nil
|
||||
|
||||
+12
-6
@@ -182,7 +182,12 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
|
||||
s.walkReadMu.Unlock()
|
||||
diskHealthCheckOK(ctx, err)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
// It is totally possible that xl.meta was overwritten
|
||||
// while being concurrently listed at the same time in
|
||||
// such scenarios the 'xl.meta' might get truncated
|
||||
if !IsErrIgnored(err, io.EOF, io.ErrUnexpectedEOF) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
meta.name = strings.TrimSuffix(entry, xlStorageFormatFile)
|
||||
@@ -200,7 +205,9 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
|
||||
s.walkReadMu.Unlock()
|
||||
diskHealthCheckOK(ctx, err)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
if !IsErrIgnored(err, io.EOF, io.ErrUnexpectedEOF) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
meta.name = strings.TrimSuffix(entry, xlStorageFormatFileV1)
|
||||
@@ -241,11 +248,10 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
|
||||
out <- metaCacheEntry{name: pop}
|
||||
if opts.Recursive {
|
||||
// Scan folder we found. Should be in correct sort order where we are.
|
||||
forward = ""
|
||||
if len(opts.ForwardTo) > 0 && strings.HasPrefix(opts.ForwardTo, pop) {
|
||||
forward = strings.TrimPrefix(opts.ForwardTo, pop)
|
||||
err := scanDir(pop)
|
||||
if err != nil && !IsErrIgnored(err, context.Canceled) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
logger.LogIf(ctx, scanDir(pop))
|
||||
}
|
||||
dirStack = dirStack[:len(dirStack)-1]
|
||||
}
|
||||
|
||||
+1
-12
@@ -264,18 +264,7 @@ func (m *Metric) copyMetric() Metric {
|
||||
// once the TTL expires "read()" registered function is called
|
||||
// to return the new values and updated.
|
||||
func (g *MetricsGroup) Get() (metrics []Metric) {
|
||||
var c interface{}
|
||||
var err error
|
||||
if g.cacheInterval <= 0 {
|
||||
c, err = g.metricsCache.Update()
|
||||
} else {
|
||||
c, err = g.metricsCache.Get()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return []Metric{}
|
||||
}
|
||||
|
||||
c, _ := g.metricsCache.Get()
|
||||
m, ok := c.([]Metric)
|
||||
if !ok {
|
||||
return []Metric{}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"math"
|
||||
"time"
|
||||
@@ -178,6 +179,26 @@ type ObjectInfo struct {
|
||||
SuccessorModTime time.Time
|
||||
}
|
||||
|
||||
// ArchiveInfo returns any saved zip archive meta information
|
||||
func (o ObjectInfo) ArchiveInfo() []byte {
|
||||
if len(o.UserDefined) == 0 {
|
||||
return nil
|
||||
}
|
||||
z, ok := o.UserDefined[archiveInfoMetadataKey]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if len(z) > 0 && z[0] >= 32 {
|
||||
// FS/gateway mode does base64 encoding on roundtrip.
|
||||
// zipindex has version as first byte, which is below any base64 value.
|
||||
zipInfo, _ := base64.StdEncoding.DecodeString(z)
|
||||
if len(zipInfo) != 0 {
|
||||
return zipInfo
|
||||
}
|
||||
}
|
||||
return []byte(z)
|
||||
}
|
||||
|
||||
// Clone - Returns a cloned copy of current objectInfo
|
||||
func (o ObjectInfo) Clone() (cinfo ObjectInfo) {
|
||||
cinfo = ObjectInfo{
|
||||
|
||||
+34
-31
@@ -429,9 +429,12 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj
|
||||
if !proxytgts.Empty() {
|
||||
// proxy to replication target if active-active replication is in place.
|
||||
reader, proxy, perr = proxyGetToReplicationTarget(ctx, bucket, object, rs, r.Header, opts, proxytgts)
|
||||
if perr != nil && !isErrObjectNotFound(ErrorRespToObjectError(perr, bucket, object)) &&
|
||||
!isErrVersionNotFound(ErrorRespToObjectError(perr, bucket, object)) {
|
||||
logger.LogIf(ctx, fmt.Errorf("Replication proxy failed for %s/%s(%s) - %w", bucket, object, opts.VersionID, perr))
|
||||
if perr != nil {
|
||||
proxyGetErr := ErrorRespToObjectError(perr, bucket, object)
|
||||
if !isErrObjectNotFound(proxyGetErr) && !isErrVersionNotFound(proxyGetErr) &&
|
||||
!isErrPreconditionFailed(proxyGetErr) && !isErrInvalidRange(proxyGetErr) {
|
||||
logger.LogIf(ctx, fmt.Errorf("Replication proxy failed for %s/%s(%s) - %w", bucket, object, opts.VersionID, perr))
|
||||
}
|
||||
}
|
||||
if reader != nil && proxy.Proxy && perr == nil {
|
||||
gr = reader
|
||||
@@ -1428,10 +1431,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
srcInfo.metadataOnly = false
|
||||
}
|
||||
|
||||
if srcInfo.metadataOnly {
|
||||
gr.Close() // We are not interested in the reader stream at this point close it.
|
||||
}
|
||||
|
||||
// Check if x-amz-metadata-directive or x-amz-tagging-directive was not set to REPLACE and source,
|
||||
// destination are same objects. Apply this restriction also when
|
||||
// metadataOnly is true indicating that we are not overwriting the object.
|
||||
@@ -1445,9 +1444,11 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
var objInfo ObjectInfo
|
||||
remoteCallRequired := isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI)
|
||||
|
||||
if isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI) {
|
||||
var objInfo ObjectInfo
|
||||
var os *objSweeper
|
||||
if remoteCallRequired {
|
||||
var dstRecords []dns.SrvRecord
|
||||
dstRecords, err = globalDNSConfig.Get(dstBucket)
|
||||
if err != nil {
|
||||
@@ -1483,8 +1484,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
objInfo.ETag = remoteObjInfo.ETag
|
||||
objInfo.ModTime = remoteObjInfo.LastModified
|
||||
} else {
|
||||
|
||||
os := newObjSweeper(dstBucket, dstObject).WithVersioning(dstOpts.Versioned, dstOpts.VersionSuspended)
|
||||
os = newObjSweeper(dstBucket, dstObject).WithVersioning(dstOpts.Versioned, dstOpts.VersionSuspended)
|
||||
// Get appropriate object info to identify the remote object to delete
|
||||
if !srcInfo.metadataOnly {
|
||||
goiOpts := os.GetOpts()
|
||||
@@ -1507,11 +1507,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove the transitioned object whose object version is being overwritten.
|
||||
if !globalTierConfigMgr.Empty() {
|
||||
logger.LogIf(ctx, os.Sweep())
|
||||
}
|
||||
}
|
||||
|
||||
objInfo.ETag = getDecryptedETag(r.Header, objInfo, false)
|
||||
@@ -1543,9 +1538,12 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
UserAgent: r.UserAgent(),
|
||||
Host: handlers.GetSourceIP(r),
|
||||
})
|
||||
if !globalTierConfigMgr.Empty() {
|
||||
|
||||
if !remoteCallRequired && !globalTierConfigMgr.Empty() {
|
||||
// Schedule object for immediate transition if eligible.
|
||||
enqueueTransitionImmediate(objInfo)
|
||||
// Remove the transitioned object whose object version is being overwritten.
|
||||
logger.LogIf(ctx, os.Sweep())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2622,7 +2620,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
copy(objectEncryptionKey[:], key)
|
||||
|
||||
partEncryptionKey := objectEncryptionKey.DerivePartKey(uint32(partID))
|
||||
encReader, err := sio.EncryptReader(reader, sio.Config{Key: partEncryptionKey[:], CipherSuites: fips.CipherSuitesDARE()})
|
||||
encReader, err := sio.EncryptReader(reader, sio.Config{Key: partEncryptionKey[:], CipherSuites: fips.DARECiphers()})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -2885,7 +2883,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
// We add a buffer on bigger files to reduce the number of syscalls upstream.
|
||||
in = bufio.NewReaderSize(hashReader, encryptBufferSize)
|
||||
}
|
||||
reader, err = sio.EncryptReader(in, sio.Config{Key: partEncryptionKey[:], CipherSuites: fips.CipherSuitesDARE()})
|
||||
reader, err = sio.EncryptReader(in, sio.Config{Key: partEncryptionKey[:], CipherSuites: fips.DARECiphers()})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -3101,10 +3099,12 @@ func (w *whiteSpaceWriter) WriteHeader(statusCode int) {
|
||||
// we do background append as and when the parts arrive and completeMultiPartUpload
|
||||
// is quick. Only in a rare case where parts would be out of order will
|
||||
// FS:completeMultiPartUpload() take a longer time.
|
||||
func sendWhiteSpace(w http.ResponseWriter) <-chan bool {
|
||||
func sendWhiteSpace(ctx context.Context, w http.ResponseWriter) <-chan bool {
|
||||
doneCh := make(chan bool)
|
||||
go func() {
|
||||
defer close(doneCh)
|
||||
ticker := time.NewTicker(time.Second * 10)
|
||||
defer ticker.Stop()
|
||||
headerWritten := false
|
||||
for {
|
||||
select {
|
||||
@@ -3124,7 +3124,8 @@ func sendWhiteSpace(w http.ResponseWriter) <-chan bool {
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
case doneCh <- headerWritten:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -3298,7 +3299,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
}
|
||||
|
||||
w = &whiteSpaceWriter{ResponseWriter: w, Flusher: w.(http.Flusher)}
|
||||
completeDoneCh := sendWhiteSpace(w)
|
||||
completeDoneCh := sendWhiteSpace(ctx, w)
|
||||
objInfo, err := completeMultiPartUpload(ctx, bucket, object, uploadID, complMultipartUpload.Parts, opts)
|
||||
// Stop writing white spaces to the client. Note that close(doneCh) style is not used as it
|
||||
// can cause white space to be written after we send XML response in a race condition.
|
||||
@@ -3457,15 +3458,17 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errors.New("force-delete is forbidden in a locked-enabled bucket")), r.URL)
|
||||
return
|
||||
}
|
||||
apiErr = enforceRetentionBypassForDelete(ctx, r, bucket, ObjectToDelete{
|
||||
ObjectV: ObjectV{
|
||||
ObjectName: object,
|
||||
VersionID: vID,
|
||||
},
|
||||
}, goi, gerr)
|
||||
if apiErr != ErrNone && apiErr != ErrNoSuchKey {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErr), r.URL)
|
||||
return
|
||||
if vID != "" {
|
||||
apiErr = enforceRetentionBypassForDelete(ctx, r, bucket, ObjectToDelete{
|
||||
ObjectV: ObjectV{
|
||||
ObjectName: object,
|
||||
VersionID: vID,
|
||||
},
|
||||
}, goi, gerr)
|
||||
if apiErr != ErrNone && apiErr != ErrNoSuchKey {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErr), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -100,8 +100,9 @@ func selfSpeedtest(ctx context.Context, size, concurrent int, duration time.Dura
|
||||
reader := newRandomReader(size)
|
||||
tmpObjName := fmt.Sprintf("%s%d.%d", objNamePrefix, i, objCountPerThread[i])
|
||||
info, err := client.PutObject(uploadsCtx, globalObjectPerfBucket, tmpObjName, reader, int64(size), minio.PutObjectOptions{
|
||||
UserMetadata: userMetadata,
|
||||
DisableMultipart: true,
|
||||
UserMetadata: userMetadata,
|
||||
DisableContentSha256: true,
|
||||
DisableMultipart: true,
|
||||
}) // Bypass S3 API freeze
|
||||
if err != nil {
|
||||
if !contextCanceled(uploadsCtx) && !errors.Is(err, context.Canceled) {
|
||||
|
||||
@@ -40,6 +40,9 @@ func registerDistErasureRouters(router *mux.Router, endpointServerPools Endpoint
|
||||
|
||||
// List of some generic handlers which are applied for all incoming requests.
|
||||
var globalHandlers = []mux.MiddlewareFunc{
|
||||
// The generic tracer needs to be the first handler
|
||||
// to catch all requests returned early by any other handler
|
||||
httpTracer,
|
||||
// Auth handler verifies incoming authorization headers and
|
||||
// routes them accordingly. Client receives a HTTP error for
|
||||
// invalid/unsupported signatures.
|
||||
|
||||
+30
-35
@@ -140,21 +140,10 @@ func (api objectAPIHandlers) getObjectInArchiveFileHandler(ctx context.Context,
|
||||
return
|
||||
}
|
||||
|
||||
var zipInfo []byte
|
||||
|
||||
if z, ok := zipObjInfo.UserDefined[archiveInfoMetadataKey]; ok {
|
||||
if globalIsErasure {
|
||||
zipInfo = []byte(z)
|
||||
} else {
|
||||
zipInfo, err = base64.StdEncoding.DecodeString(z)
|
||||
logger.LogIf(ctx, err)
|
||||
// Will attempt to re-read...
|
||||
}
|
||||
}
|
||||
zipInfo := zipObjInfo.ArchiveInfo()
|
||||
if len(zipInfo) == 0 {
|
||||
zipInfo, err = updateObjectMetadataWithZipInfo(ctx, objectAPI, bucket, zipPath, opts)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -247,15 +236,11 @@ func listObjectsV2InArchive(ctx context.Context, objectAPI ObjectLayer, bucket,
|
||||
return ListObjectsV2Info{}, nil
|
||||
}
|
||||
|
||||
var zipInfo []byte
|
||||
|
||||
if z, ok := zipObjInfo.UserDefined[archiveInfoMetadataKey]; ok {
|
||||
zipInfo = []byte(z)
|
||||
} else {
|
||||
zipInfo := zipObjInfo.ArchiveInfo()
|
||||
if len(zipInfo) == 0 {
|
||||
// Always update the latest version
|
||||
zipInfo, err = updateObjectMetadataWithZipInfo(ctx, objectAPI, bucket, zipPath, ObjectOptions{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return ListObjectsV2Info{}, err
|
||||
}
|
||||
@@ -332,11 +317,10 @@ func getFilesListFromZIPObject(ctx context.Context, objectAPI ObjectLayer, bucke
|
||||
return nil, ObjectInfo{}, err
|
||||
}
|
||||
b, err := ioutil.ReadAll(gr)
|
||||
gr.Close()
|
||||
if err != nil {
|
||||
gr.Close()
|
||||
return nil, ObjectInfo{}, err
|
||||
}
|
||||
gr.Close()
|
||||
if size > len(b) {
|
||||
size = len(b)
|
||||
}
|
||||
@@ -439,11 +423,8 @@ func (api objectAPIHandlers) headObjectInArchiveFileHandler(ctx context.Context,
|
||||
return
|
||||
}
|
||||
|
||||
var zipInfo []byte
|
||||
|
||||
if z, ok := zipObjInfo.UserDefined[archiveInfoMetadataKey]; ok {
|
||||
zipInfo = []byte(z)
|
||||
} else {
|
||||
zipInfo := zipObjInfo.ArchiveInfo()
|
||||
if len(zipInfo) == 0 {
|
||||
zipInfo, err = updateObjectMetadataWithZipInfo(ctx, objectAPI, bucket, zipPath, opts)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -498,20 +479,34 @@ func updateObjectMetadataWithZipInfo(ctx context.Context, objectAPI ObjectLayer,
|
||||
}
|
||||
|
||||
srcInfo.UserDefined[archiveTypeMetadataKey] = archiveType
|
||||
if globalIsErasure {
|
||||
srcInfo.UserDefined[archiveInfoMetadataKey] = string(zipInfo)
|
||||
var zipInfoStr string
|
||||
if globalIsGateway {
|
||||
zipInfoStr = base64.StdEncoding.EncodeToString(zipInfo)
|
||||
} else {
|
||||
srcInfo.UserDefined[archiveInfoMetadataKey] = base64.StdEncoding.EncodeToString(zipInfo)
|
||||
zipInfoStr = string(zipInfo)
|
||||
}
|
||||
srcInfo.metadataOnly = true
|
||||
|
||||
// Always update the same version id & modtime
|
||||
if globalIsGateway {
|
||||
srcInfo.UserDefined[archiveInfoMetadataKey] = zipInfoStr
|
||||
|
||||
// Passing opts twice as source & destination options will update the metadata
|
||||
// of the same object version to avoid creating a new version.
|
||||
_, err = objectAPI.CopyObject(ctx, bucket, object, bucket, object, srcInfo, opts, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Use CopyObject API only for Gateway mode.
|
||||
if _, err = objectAPI.CopyObject(ctx, bucket, object, bucket, object, srcInfo, opts, opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
popts := ObjectOptions{
|
||||
MTime: srcInfo.ModTime,
|
||||
VersionID: srcInfo.VersionID,
|
||||
EvalMetadataFn: func(oi ObjectInfo) error {
|
||||
oi.UserDefined[archiveTypeMetadataKey] = archiveType
|
||||
oi.UserDefined[archiveInfoMetadataKey] = zipInfoStr
|
||||
return nil
|
||||
},
|
||||
}
|
||||
// For all other modes use in-place update to update metadata on a specific version.
|
||||
if _, err = objectAPI.PutObjectMetadata(ctx, bucket, object, popts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return zipInfo, nil
|
||||
|
||||
+5
-4
@@ -206,17 +206,18 @@ func serverHandleCmdArgs(ctx *cli.Context) {
|
||||
// allow transport to be HTTP/1.1 for proxying.
|
||||
globalProxyTransport = newCustomHTTPProxyTransport(&tls.Config{
|
||||
RootCAs: globalRootCAs,
|
||||
CipherSuites: fips.CipherSuitesTLS(),
|
||||
CurvePreferences: fips.EllipticCurvesTLS(),
|
||||
CipherSuites: fips.TLSCiphers(),
|
||||
CurvePreferences: fips.TLSCurveIDs(),
|
||||
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
|
||||
}, rest.DefaultTimeout)()
|
||||
globalProxyEndpoints = GetProxyEndpoints(globalEndpoints)
|
||||
globalInternodeTransport = newInternodeHTTPTransport(&tls.Config{
|
||||
RootCAs: globalRootCAs,
|
||||
CipherSuites: fips.CipherSuitesTLS(),
|
||||
CurvePreferences: fips.EllipticCurvesTLS(),
|
||||
CipherSuites: fips.TLSCiphers(),
|
||||
CurvePreferences: fips.TLSCurveIDs(),
|
||||
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
|
||||
}, rest.DefaultTimeout)()
|
||||
globalRemoteTargetTransport = NewRemoteTargetHTTPTransport()()
|
||||
|
||||
// On macOS, if a process already listens on LOCALIPADDR:PORT, net.Listen() falls back
|
||||
// to IPv6 address ie minio will start listening on IPv6 address whereas another
|
||||
|
||||
@@ -44,12 +44,19 @@ func mustGetStorageInfo(objAPI ObjectLayer) StorageInfo {
|
||||
|
||||
// Prints the formatted startup message.
|
||||
func printStartupMessage(apiEndpoints []string, err error) {
|
||||
logger.Info(color.Bold("MinIO Object Storage Server"))
|
||||
if err != nil {
|
||||
if globalConsoleSys != nil {
|
||||
globalConsoleSys.Send(fmt.Sprintf("Server startup failed with '%v', some features may be missing", err), string(logger.All))
|
||||
}
|
||||
}
|
||||
|
||||
if len(globalSubnetConfig.APIKey) == 0 && err == nil {
|
||||
var builder strings.Builder
|
||||
startupBanner(&builder)
|
||||
logger.Info(builder.String())
|
||||
}
|
||||
|
||||
strippedAPIEndpoints := stripStandardPorts(apiEndpoints, globalMinioHost)
|
||||
// If cache layer is enabled, print cache capacity.
|
||||
cachedObjAPI := newCachedObjectLayerFn()
|
||||
@@ -72,12 +79,6 @@ func printStartupMessage(apiEndpoints []string, err error) {
|
||||
|
||||
// Prints documentation message.
|
||||
printObjectAPIMsg()
|
||||
|
||||
if globalMinioConsolePortAuto && globalBrowserEnabled {
|
||||
msg := fmt.Sprintf("\nWARNING: Console endpoint is listening on a dynamic port (%s), please use --console-address \":PORT\" to choose a static port.",
|
||||
globalMinioConsolePort)
|
||||
logger.Info(color.RedBold(msg))
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if input is IPv6
|
||||
@@ -141,9 +142,13 @@ func printServerCommonMsg(apiEndpoints []string) {
|
||||
}
|
||||
printEventNotifiers()
|
||||
|
||||
if globalMinioConsolePortAuto && globalBrowserEnabled {
|
||||
logger.Info(color.RedBold("\nWARNING: Console endpoint is listening on a dynamic port (%s), please use --console-address \":PORT\" to choose a static port.",
|
||||
globalMinioConsolePort))
|
||||
}
|
||||
if globalBrowserEnabled {
|
||||
consoleEndpointStr := strings.Join(stripStandardPorts(getConsoleEndpoints(), globalMinioConsoleHost), " ")
|
||||
logger.Info(color.Blue("\nConsole: ") + color.Bold(fmt.Sprintf("%s ", consoleEndpointStr)))
|
||||
logger.Info(color.Blue("Console: ") + color.Bold(fmt.Sprintf("%s ", consoleEndpointStr)))
|
||||
if color.IsTerminal() && (!globalCLIContext.Anonymous && !globalCLIContext.JSON) {
|
||||
logger.Info(color.Blue("RootUser: ") + color.Bold(fmt.Sprintf("%s ", cred.AccessKey)))
|
||||
logger.Info(color.Blue("RootPass: ") + color.Bold(fmt.Sprintf("%s ", cred.SecretKey)))
|
||||
|
||||
+242
-183
@@ -149,7 +149,14 @@ type SRError struct {
|
||||
}
|
||||
|
||||
func (c SRError) Error() string {
|
||||
return c.Cause.Error()
|
||||
if c.Cause != nil {
|
||||
return c.Cause.Error()
|
||||
}
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
func (c SRError) Unwrap() error {
|
||||
return c.Cause
|
||||
}
|
||||
|
||||
func wrapSRErr(err error) SRError {
|
||||
@@ -249,13 +256,13 @@ func (c *SiteReplicationSys) saveToDisk(ctx context.Context, state srState) erro
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
err = saveConfig(ctx, objAPI, getSRStateFilePath(), buf)
|
||||
if err != nil {
|
||||
|
||||
if err = saveConfig(ctx, objAPI, getSRStateFilePath(), buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, e := range globalNotificationSys.ReloadSiteReplicationConfig(ctx) {
|
||||
logger.LogIf(ctx, e)
|
||||
for _, err := range globalNotificationSys.ReloadSiteReplicationConfig(ctx) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
@@ -606,6 +613,14 @@ func (c *SiteReplicationSys) GetClusterInfo(ctx context.Context) (info madmin.Si
|
||||
return info, nil
|
||||
}
|
||||
|
||||
const (
|
||||
makeBucketWithVersion = "MakeBucketWithVersioning"
|
||||
configureReplication = "ConfigureReplication"
|
||||
deleteBucket = "DeleteBucket"
|
||||
replicateIAMItem = "SRPeerReplicateIAMItem"
|
||||
replicateBucketMetadata = "SRPeerReplicateBucketMeta"
|
||||
)
|
||||
|
||||
// MakeBucketHook - called during a regular make bucket call when cluster
|
||||
// replication is enabled. It is responsible for the creation of the same bucket
|
||||
// on remote clusters, and creating replication rules on local and peer
|
||||
@@ -637,9 +652,7 @@ func (c *SiteReplicationSys) MakeBucketHook(ctx context.Context, bucket string,
|
||||
// Create bucket and enable versioning on all peers.
|
||||
makeBucketConcErr := c.concDo(
|
||||
func() error {
|
||||
err := c.PeerBucketMakeWithVersioningHandler(ctx, bucket, opts)
|
||||
logger.LogIf(ctx, c.annotateErr("MakeWithVersioning", err))
|
||||
return err
|
||||
return c.annotateErr(makeBucketWithVersion, c.PeerBucketMakeWithVersioningHandler(ctx, bucket, opts))
|
||||
},
|
||||
func(deploymentID string, p madmin.PeerInfo) error {
|
||||
admClient, err := c.getAdminClient(ctx, deploymentID)
|
||||
@@ -647,27 +660,15 @@ func (c *SiteReplicationSys) MakeBucketHook(ctx context.Context, bucket string,
|
||||
return err
|
||||
}
|
||||
|
||||
err = admClient.SRPeerBucketOps(ctx, bucket, madmin.MakeWithVersioningBktOp, optsMap)
|
||||
logger.LogIf(ctx, c.annotatePeerErr(p.Name, "MakeWithVersioning", err))
|
||||
return err
|
||||
return c.annotatePeerErr(p.Name, makeBucketWithVersion, admClient.SRPeerBucketOps(ctx, bucket, madmin.MakeWithVersioningBktOp, optsMap))
|
||||
},
|
||||
makeBucketWithVersion,
|
||||
)
|
||||
// If all make-bucket-and-enable-versioning operations failed, nothing
|
||||
// more to do.
|
||||
if makeBucketConcErr.allFailed() {
|
||||
return makeBucketConcErr
|
||||
}
|
||||
|
||||
// Log any errors in make-bucket operations.
|
||||
logger.LogIf(ctx, makeBucketConcErr.summaryErr)
|
||||
|
||||
// Create bucket remotes and add replication rules for the bucket on
|
||||
// self and peers.
|
||||
// Create bucket remotes and add replication rules for the bucket on self and peers.
|
||||
makeRemotesConcErr := c.concDo(
|
||||
func() error {
|
||||
err := c.PeerBucketConfigureReplHandler(ctx, bucket)
|
||||
logger.LogIf(ctx, c.annotateErr("ConfigureRepl", err))
|
||||
return err
|
||||
return c.annotateErr(configureReplication, c.PeerBucketConfigureReplHandler(ctx, bucket))
|
||||
},
|
||||
func(deploymentID string, p madmin.PeerInfo) error {
|
||||
admClient, err := c.getAdminClient(ctx, deploymentID)
|
||||
@@ -675,13 +676,16 @@ func (c *SiteReplicationSys) MakeBucketHook(ctx context.Context, bucket string,
|
||||
return err
|
||||
}
|
||||
|
||||
err = admClient.SRPeerBucketOps(ctx, bucket, madmin.ConfigureReplBktOp, nil)
|
||||
logger.LogIf(ctx, c.annotatePeerErr(p.Name, "ConfigureRepl", err))
|
||||
return err
|
||||
return c.annotatePeerErr(p.Name, configureReplication, admClient.SRPeerBucketOps(ctx, bucket, madmin.ConfigureReplBktOp, nil))
|
||||
},
|
||||
configureReplication,
|
||||
)
|
||||
err := makeRemotesConcErr.summaryErr
|
||||
if err != nil {
|
||||
|
||||
if err := errors.Unwrap(makeBucketConcErr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := errors.Unwrap(makeRemotesConcErr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -706,17 +710,17 @@ func (c *SiteReplicationSys) DeleteBucketHook(ctx context.Context, bucket string
|
||||
}
|
||||
|
||||
// Send bucket delete to other clusters.
|
||||
cErr := c.concDo(nil, func(deploymentID string, p madmin.PeerInfo) error {
|
||||
cerr := c.concDo(nil, func(deploymentID string, p madmin.PeerInfo) error {
|
||||
admClient, err := c.getAdminClient(ctx, deploymentID)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
|
||||
err = admClient.SRPeerBucketOps(ctx, bucket, op, nil)
|
||||
logger.LogIf(ctx, c.annotatePeerErr(p.Name, "DeleteBucket", err))
|
||||
return err
|
||||
})
|
||||
return cErr.summaryErr
|
||||
return c.annotatePeerErr(p.Name, deleteBucket, admClient.SRPeerBucketOps(ctx, bucket, op, nil))
|
||||
},
|
||||
deleteBucket,
|
||||
)
|
||||
return errors.Unwrap(cerr)
|
||||
}
|
||||
|
||||
// PeerBucketMakeWithVersioningHandler - creates bucket and enables versioning.
|
||||
@@ -732,8 +736,7 @@ func (c *SiteReplicationSys) PeerBucketMakeWithVersioningHandler(ctx context.Con
|
||||
_, ok1 := err.(BucketExists)
|
||||
_, ok2 := err.(BucketAlreadyExists)
|
||||
if !ok1 && !ok2 {
|
||||
logger.LogIf(ctx, c.annotateErr("MakeBucketErr on peer call", err))
|
||||
return wrapSRErr(err)
|
||||
return wrapSRErr(c.annotateErr(makeBucketWithVersion, err))
|
||||
}
|
||||
} else {
|
||||
// Load updated bucket metadata into memory as new
|
||||
@@ -743,8 +746,7 @@ func (c *SiteReplicationSys) PeerBucketMakeWithVersioningHandler(ctx context.Con
|
||||
|
||||
meta, err := globalBucketMetadataSys.Get(bucket)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, c.annotateErr("MakeBucketErr on peer call", err))
|
||||
return wrapSRErr(err)
|
||||
return wrapSRErr(c.annotateErr(makeBucketWithVersion, err))
|
||||
}
|
||||
|
||||
meta.VersioningConfigXML = enabledBucketVersioningConfig
|
||||
@@ -805,8 +807,7 @@ func (c *SiteReplicationSys) PeerBucketConfigureReplHandler(ctx context.Context,
|
||||
bucketTarget.Arn = globalBucketTargetSys.getRemoteARN(bucket, &bucketTarget)
|
||||
err := globalBucketTargetSys.SetTarget(ctx, bucket, &bucketTarget, false)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peer.Name, "Bucket target creation error", err))
|
||||
return err
|
||||
return c.annotatePeerErr(peer.Name, "Bucket target creation error", err)
|
||||
}
|
||||
targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
||||
if err != nil {
|
||||
@@ -816,7 +817,7 @@ func (c *SiteReplicationSys) PeerBucketConfigureReplHandler(ctx context.Context,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
targetARN = bucketTarget.Arn
|
||||
@@ -901,11 +902,10 @@ func (c *SiteReplicationSys) PeerBucketConfigureReplHandler(ctx context.Context,
|
||||
default:
|
||||
err = replicationConfig.AddRule(opts)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peer.Name, "Error adding bucket replication rule", err))
|
||||
return err
|
||||
return c.annotatePeerErr(peer.Name, "Error adding bucket replication rule", err)
|
||||
}
|
||||
|
||||
// Now convert the configuration back to server's type so we can
|
||||
// do some validation.
|
||||
newReplCfgBytes, err := xml.Marshal(replicationConfig)
|
||||
@@ -929,9 +929,9 @@ func (c *SiteReplicationSys) PeerBucketConfigureReplHandler(ctx context.Context,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, replCfgData)
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peer.Name, "Error updating replication configuration", err))
|
||||
return err
|
||||
|
||||
_, err = globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, replCfgData)
|
||||
return c.annotatePeerErr(peer.Name, "Error updating replication configuration", err)
|
||||
}
|
||||
|
||||
c.RLock()
|
||||
@@ -941,11 +941,9 @@ func (c *SiteReplicationSys) PeerBucketConfigureReplHandler(ctx context.Context,
|
||||
if d == globalDeploymentID {
|
||||
continue
|
||||
}
|
||||
if err := configurePeerFn(d, peer); err != nil {
|
||||
errMap[d] = err
|
||||
}
|
||||
errMap[d] = configurePeerFn(d, peer)
|
||||
}
|
||||
return c.toErrorFromErrMap(errMap)
|
||||
return c.toErrorFromErrMap(errMap, configureReplication)
|
||||
}
|
||||
|
||||
// PeerBucketDeleteHandler - deletes bucket on local in response to a delete
|
||||
@@ -1009,17 +1007,17 @@ func (c *SiteReplicationSys) IAMChangeHook(ctx context.Context, item madmin.SRIA
|
||||
return nil
|
||||
}
|
||||
|
||||
cErr := c.concDo(nil, func(d string, p madmin.PeerInfo) error {
|
||||
cerr := c.concDo(nil, func(d string, p madmin.PeerInfo) error {
|
||||
admClient, err := c.getAdminClient(ctx, d)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
|
||||
err = admClient.SRPeerReplicateIAMItem(ctx, item)
|
||||
logger.LogIf(ctx, c.annotatePeerErr(p.Name, "SRPeerReplicateIAMItem", err))
|
||||
return err
|
||||
})
|
||||
return cErr.summaryErr
|
||||
return c.annotatePeerErr(p.Name, replicateIAMItem, admClient.SRPeerReplicateIAMItem(ctx, item))
|
||||
},
|
||||
replicateIAMItem,
|
||||
)
|
||||
return errors.Unwrap(cerr)
|
||||
}
|
||||
|
||||
// PeerAddPolicyHandler - copies IAM policy to local. A nil policy argument,
|
||||
@@ -1165,14 +1163,13 @@ func (c *SiteReplicationSys) PeerSTSAccHandler(ctx context.Context, stsCred *mad
|
||||
// Verify the session token of the stsCred
|
||||
claims, err := auth.ExtractClaims(stsCred.SessionToken, globalActiveCred.SecretKey)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return fmt.Errorf("STS credential could not be verified")
|
||||
return fmt.Errorf("STS credential could not be verified: %w", err)
|
||||
}
|
||||
|
||||
mapClaims := claims.Map()
|
||||
expiry, err := auth.ExpToInt64(mapClaims["exp"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Expiry claim was not found: %v", mapClaims)
|
||||
return fmt.Errorf("Expiry claim was not found: %v: %w", mapClaims, err)
|
||||
}
|
||||
|
||||
cred := auth.Credentials{
|
||||
@@ -1191,7 +1188,7 @@ func (c *SiteReplicationSys) PeerSTSAccHandler(ctx context.Context, stsCred *mad
|
||||
// Need to lookup the groups from LDAP.
|
||||
_, ldapGroups, err := globalLDAPConfig.LookupUserDN(ldapUser)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to query LDAP server for %s: %v", ldapUser, err)
|
||||
return fmt.Errorf("unable to query LDAP server for %s: %w", ldapUser, err)
|
||||
}
|
||||
|
||||
cred.Groups = ldapGroups
|
||||
@@ -1199,7 +1196,7 @@ func (c *SiteReplicationSys) PeerSTSAccHandler(ctx context.Context, stsCred *mad
|
||||
|
||||
// Set these credentials to IAM.
|
||||
if err := globalIAMSys.SetTempUser(ctx, cred.AccessKey, cred, stsCred.ParentPolicyMapping); err != nil {
|
||||
return fmt.Errorf("unable to save STS credential and/or parent policy mapping: %v", err)
|
||||
return fmt.Errorf("unable to save STS credential and/or parent policy mapping: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1217,27 +1214,33 @@ func (c *SiteReplicationSys) BucketMetaHook(ctx context.Context, item madmin.SRB
|
||||
return nil
|
||||
}
|
||||
|
||||
cErr := c.concDo(nil, func(d string, p madmin.PeerInfo) error {
|
||||
cerr := c.concDo(nil, func(d string, p madmin.PeerInfo) error {
|
||||
admClient, err := c.getAdminClient(ctx, d)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
|
||||
err = admClient.SRPeerReplicateBucketMeta(ctx, item)
|
||||
logger.LogIf(ctx, c.annotatePeerErr(p.Name, "SRPeerReplicateBucketMeta", err))
|
||||
return err
|
||||
})
|
||||
return cErr.summaryErr
|
||||
return c.annotatePeerErr(p.Name, replicateBucketMetadata, admClient.SRPeerReplicateBucketMeta(ctx, item))
|
||||
},
|
||||
replicateBucketMetadata,
|
||||
)
|
||||
return errors.Unwrap(cerr)
|
||||
}
|
||||
|
||||
// PeerBucketVersioningHandler - updates versioning config to local cluster.
|
||||
func (c *SiteReplicationSys) PeerBucketVersioningHandler(ctx context.Context, bucket string, versioning *string) error {
|
||||
func (c *SiteReplicationSys) PeerBucketVersioningHandler(ctx context.Context, bucket string, versioning *string, updatedAt time.Time) error {
|
||||
if versioning != nil {
|
||||
// skip overwrite if local update is newer than peer update.
|
||||
if !updatedAt.IsZero() {
|
||||
if _, updateTm, err := globalBucketMetadataSys.GetVersioningConfig(bucket); err == nil && updateTm.After(updatedAt) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
configData, err := base64.StdEncoding.DecodeString(*versioning)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
err = globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, configData)
|
||||
_, err = globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, configData)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
@@ -1248,14 +1251,21 @@ func (c *SiteReplicationSys) PeerBucketVersioningHandler(ctx context.Context, bu
|
||||
}
|
||||
|
||||
// PeerBucketPolicyHandler - copies/deletes policy to local cluster.
|
||||
func (c *SiteReplicationSys) PeerBucketPolicyHandler(ctx context.Context, bucket string, policy *policy.Policy) error {
|
||||
func (c *SiteReplicationSys) PeerBucketPolicyHandler(ctx context.Context, bucket string, policy *policy.Policy, updatedAt time.Time) error {
|
||||
// skip overwrite if local update is newer than peer update.
|
||||
if !updatedAt.IsZero() {
|
||||
if _, updateTm, err := globalBucketMetadataSys.GetPolicyConfig(bucket); err == nil && updateTm.After(updatedAt) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if policy != nil {
|
||||
configData, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
|
||||
err = globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, configData)
|
||||
_, err = globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, configData)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
@@ -1263,7 +1273,7 @@ func (c *SiteReplicationSys) PeerBucketPolicyHandler(ctx context.Context, bucket
|
||||
}
|
||||
|
||||
// Delete the bucket policy
|
||||
err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, nil)
|
||||
_, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, nil)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
@@ -1272,13 +1282,20 @@ func (c *SiteReplicationSys) PeerBucketPolicyHandler(ctx context.Context, bucket
|
||||
}
|
||||
|
||||
// PeerBucketTaggingHandler - copies/deletes tags to local cluster.
|
||||
func (c *SiteReplicationSys) PeerBucketTaggingHandler(ctx context.Context, bucket string, tags *string) error {
|
||||
func (c *SiteReplicationSys) PeerBucketTaggingHandler(ctx context.Context, bucket string, tags *string, updatedAt time.Time) error {
|
||||
// skip overwrite if local update is newer than peer update.
|
||||
if !updatedAt.IsZero() {
|
||||
if _, updateTm, err := globalBucketMetadataSys.GetTaggingConfig(bucket); err == nil && updateTm.After(updatedAt) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if tags != nil {
|
||||
configData, err := base64.StdEncoding.DecodeString(*tags)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
err = globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, configData)
|
||||
_, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, configData)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
@@ -1286,7 +1303,7 @@ func (c *SiteReplicationSys) PeerBucketTaggingHandler(ctx context.Context, bucke
|
||||
}
|
||||
|
||||
// Delete the tags
|
||||
err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, nil)
|
||||
_, err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, nil)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
@@ -1295,13 +1312,20 @@ func (c *SiteReplicationSys) PeerBucketTaggingHandler(ctx context.Context, bucke
|
||||
}
|
||||
|
||||
// PeerBucketObjectLockConfigHandler - sets object lock on local bucket.
|
||||
func (c *SiteReplicationSys) PeerBucketObjectLockConfigHandler(ctx context.Context, bucket string, objectLockData *string) error {
|
||||
func (c *SiteReplicationSys) PeerBucketObjectLockConfigHandler(ctx context.Context, bucket string, objectLockData *string, updatedAt time.Time) error {
|
||||
if objectLockData != nil {
|
||||
// skip overwrite if local update is newer than peer update.
|
||||
if !updatedAt.IsZero() {
|
||||
if _, updateTm, err := globalBucketMetadataSys.GetObjectLockConfig(bucket); err == nil && updateTm.After(updatedAt) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
configData, err := base64.StdEncoding.DecodeString(*objectLockData)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
err = globalBucketMetadataSys.Update(ctx, bucket, objectLockConfig, configData)
|
||||
_, err = globalBucketMetadataSys.Update(ctx, bucket, objectLockConfig, configData)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
@@ -1312,13 +1336,20 @@ func (c *SiteReplicationSys) PeerBucketObjectLockConfigHandler(ctx context.Conte
|
||||
}
|
||||
|
||||
// PeerBucketSSEConfigHandler - copies/deletes SSE config to local cluster.
|
||||
func (c *SiteReplicationSys) PeerBucketSSEConfigHandler(ctx context.Context, bucket string, sseConfig *string) error {
|
||||
func (c *SiteReplicationSys) PeerBucketSSEConfigHandler(ctx context.Context, bucket string, sseConfig *string, updatedAt time.Time) error {
|
||||
// skip overwrite if local update is newer than peer update.
|
||||
if !updatedAt.IsZero() {
|
||||
if _, updateTm, err := globalBucketMetadataSys.GetSSEConfig(bucket); err == nil && updateTm.After(updatedAt) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if sseConfig != nil {
|
||||
configData, err := base64.StdEncoding.DecodeString(*sseConfig)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
err = globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, configData)
|
||||
_, err = globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, configData)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
@@ -1326,7 +1357,7 @@ func (c *SiteReplicationSys) PeerBucketSSEConfigHandler(ctx context.Context, buc
|
||||
}
|
||||
|
||||
// Delete sse config
|
||||
err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, nil)
|
||||
_, err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, nil)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
@@ -1334,14 +1365,21 @@ func (c *SiteReplicationSys) PeerBucketSSEConfigHandler(ctx context.Context, buc
|
||||
}
|
||||
|
||||
// PeerBucketQuotaConfigHandler - copies/deletes policy to local cluster.
|
||||
func (c *SiteReplicationSys) PeerBucketQuotaConfigHandler(ctx context.Context, bucket string, quota *madmin.BucketQuota) error {
|
||||
func (c *SiteReplicationSys) PeerBucketQuotaConfigHandler(ctx context.Context, bucket string, quota *madmin.BucketQuota, updatedAt time.Time) error {
|
||||
// skip overwrite if local update is newer than peer update.
|
||||
if !updatedAt.IsZero() {
|
||||
if _, updateTm, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket); err == nil && updateTm.After(updatedAt) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if quota != nil {
|
||||
quotaData, err := json.Marshal(quota)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, quotaData); err != nil {
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, quotaData); err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
|
||||
@@ -1349,7 +1387,7 @@ func (c *SiteReplicationSys) PeerBucketQuotaConfigHandler(ctx context.Context, b
|
||||
}
|
||||
|
||||
// Delete the bucket policy
|
||||
err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, nil)
|
||||
_, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, nil)
|
||||
if err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
@@ -1440,7 +1478,7 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Replicate bucket policy if present.
|
||||
policy, err := globalPolicySys.Get(bucket)
|
||||
policy, tm, err := globalBucketMetadataSys.GetPolicyConfig(bucket)
|
||||
found := true
|
||||
if _, ok := err.(BucketPolicyNotFound); ok {
|
||||
found = false
|
||||
@@ -1453,9 +1491,10 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context) error {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypePolicy,
|
||||
Bucket: bucket,
|
||||
Policy: policyJSON,
|
||||
Type: madmin.SRBucketMetaTypePolicy,
|
||||
Bucket: bucket,
|
||||
Policy: policyJSON,
|
||||
UpdatedAt: tm,
|
||||
})
|
||||
if err != nil {
|
||||
return errSRBucketMetaError(err)
|
||||
@@ -1463,7 +1502,7 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Replicate bucket tags if present.
|
||||
tags, _, err := globalBucketMetadataSys.GetTaggingConfig(bucket)
|
||||
tags, tm, err := globalBucketMetadataSys.GetTaggingConfig(bucket)
|
||||
found = true
|
||||
if _, ok := err.(BucketTaggingNotFound); ok {
|
||||
found = false
|
||||
@@ -1477,9 +1516,10 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context) error {
|
||||
}
|
||||
tagCfgStr := base64.StdEncoding.EncodeToString(tagCfg)
|
||||
err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeTags,
|
||||
Bucket: bucket,
|
||||
Tags: &tagCfgStr,
|
||||
Type: madmin.SRBucketMetaTypeTags,
|
||||
Bucket: bucket,
|
||||
Tags: &tagCfgStr,
|
||||
UpdatedAt: tm,
|
||||
})
|
||||
if err != nil {
|
||||
return errSRBucketMetaError(err)
|
||||
@@ -1487,7 +1527,7 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Replicate object-lock config if present.
|
||||
objLockCfg, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
|
||||
objLockCfg, tm, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
|
||||
found = true
|
||||
if _, ok := err.(BucketObjectLockConfigNotFound); ok {
|
||||
found = false
|
||||
@@ -1501,9 +1541,10 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context) error {
|
||||
}
|
||||
objLockStr := base64.StdEncoding.EncodeToString(objLockCfgData)
|
||||
err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
||||
Bucket: bucket,
|
||||
Tags: &objLockStr,
|
||||
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
||||
Bucket: bucket,
|
||||
Tags: &objLockStr,
|
||||
UpdatedAt: tm,
|
||||
})
|
||||
if err != nil {
|
||||
return errSRBucketMetaError(err)
|
||||
@@ -1511,7 +1552,7 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Replicate existing bucket bucket encryption settings
|
||||
sseConfig, _, err := globalBucketMetadataSys.GetSSEConfig(bucket)
|
||||
sseConfig, tm, err := globalBucketMetadataSys.GetSSEConfig(bucket)
|
||||
found = true
|
||||
if _, ok := err.(BucketSSEConfigNotFound); ok {
|
||||
found = false
|
||||
@@ -1528,13 +1569,14 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context) error {
|
||||
Type: madmin.SRBucketMetaTypeSSEConfig,
|
||||
Bucket: bucket,
|
||||
SSEConfig: &sseConfigStr,
|
||||
UpdatedAt: tm,
|
||||
})
|
||||
if err != nil {
|
||||
return errSRBucketMetaError(err)
|
||||
}
|
||||
}
|
||||
|
||||
quotaConfig, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket)
|
||||
quotaConfig, tm, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket)
|
||||
found = true
|
||||
if _, ok := err.(BucketQuotaConfigNotFound); ok {
|
||||
found = false
|
||||
@@ -1547,9 +1589,10 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context) error {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
err = c.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeQuotaConfig,
|
||||
Bucket: bucket,
|
||||
Quota: quotaConfigJSON,
|
||||
Type: madmin.SRBucketMetaTypeQuotaConfig,
|
||||
Bucket: bucket,
|
||||
Quota: quotaConfigJSON,
|
||||
UpdatedAt: tm,
|
||||
})
|
||||
if err != nil {
|
||||
return errSRBucketMetaError(err)
|
||||
@@ -1795,52 +1838,63 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context) error {
|
||||
// Concurrency helpers
|
||||
|
||||
type concErr struct {
|
||||
numActions int
|
||||
errMap map[string]error
|
||||
summaryErr error
|
||||
}
|
||||
|
||||
func (c concErr) Error() string {
|
||||
return c.summaryErr.Error()
|
||||
if c.summaryErr != nil {
|
||||
return c.summaryErr.Error()
|
||||
}
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
func (c concErr) allFailed() bool {
|
||||
return len(c.errMap) == c.numActions
|
||||
func (c concErr) Unwrap() error {
|
||||
return c.summaryErr
|
||||
}
|
||||
|
||||
func (c *SiteReplicationSys) toErrorFromErrMap(errMap map[string]error) error {
|
||||
func (c *SiteReplicationSys) toErrorFromErrMap(errMap map[string]error, actionName string) error {
|
||||
if len(errMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var success int
|
||||
msgs := []string{}
|
||||
for d, err := range errMap {
|
||||
name := c.state.Peers[d].Name
|
||||
msgs = append(msgs, fmt.Sprintf("Site %s (%s): %v", name, d, err))
|
||||
if err == nil {
|
||||
msgs = append(msgs, fmt.Sprintf("'%s' on site %s (%s): succeeded", actionName, name, d))
|
||||
success++
|
||||
} else {
|
||||
msgs = append(msgs, fmt.Sprintf("'%s' on site %s (%s): failed(%v)", actionName, name, d, err))
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("Site replication error(s): %s", strings.Join(msgs, "; "))
|
||||
if success == len(errMap) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Site replication error(s): \n%s", strings.Join(msgs, "\n"))
|
||||
}
|
||||
|
||||
func (c *SiteReplicationSys) newConcErr(numActions int, errMap map[string]error) concErr {
|
||||
func (c *SiteReplicationSys) newConcErr(errMap map[string]error, actionName string) error {
|
||||
return concErr{
|
||||
numActions: numActions,
|
||||
errMap: errMap,
|
||||
summaryErr: c.toErrorFromErrMap(errMap),
|
||||
summaryErr: c.toErrorFromErrMap(errMap, actionName),
|
||||
}
|
||||
}
|
||||
|
||||
// concDo calls actions concurrently. selfActionFn is run for the current
|
||||
// cluster and peerActionFn is run for each peer replication cluster.
|
||||
func (c *SiteReplicationSys) concDo(selfActionFn func() error, peerActionFn func(deploymentID string, p madmin.PeerInfo) error) concErr {
|
||||
func (c *SiteReplicationSys) concDo(selfActionFn func() error, peerActionFn func(deploymentID string, p madmin.PeerInfo) error, actionName string) error {
|
||||
depIDs := make([]string, 0, len(c.state.Peers))
|
||||
for d := range c.state.Peers {
|
||||
depIDs = append(depIDs, d)
|
||||
}
|
||||
errs := make([]error, len(c.state.Peers))
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(depIDs))
|
||||
for i := range depIDs {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
if depIDs[i] == globalDeploymentID {
|
||||
if selfActionFn != nil {
|
||||
errs[i] = selfActionFn()
|
||||
@@ -1848,35 +1902,28 @@ func (c *SiteReplicationSys) concDo(selfActionFn func() error, peerActionFn func
|
||||
} else {
|
||||
errs[i] = peerActionFn(depIDs[i], c.state.Peers[depIDs[i]])
|
||||
}
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
errMap := make(map[string]error, len(c.state.Peers))
|
||||
for i, depID := range depIDs {
|
||||
if errs[i] != nil {
|
||||
errMap[depID] = errs[i]
|
||||
}
|
||||
errMap[depID] = errs[i]
|
||||
}
|
||||
numActions := len(c.state.Peers) - 1
|
||||
if selfActionFn != nil {
|
||||
numActions++
|
||||
}
|
||||
return c.newConcErr(numActions, errMap)
|
||||
return c.newConcErr(errMap, actionName)
|
||||
}
|
||||
|
||||
func (c *SiteReplicationSys) annotateErr(annotation string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s: %s: %v", c.state.Name, annotation, err)
|
||||
return fmt.Errorf("%s: %s: %w", c.state.Name, annotation, err)
|
||||
}
|
||||
|
||||
func (c *SiteReplicationSys) annotatePeerErr(dstPeer string, annotation string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s->%s: %s: %v", c.state.Name, dstPeer, annotation, err)
|
||||
return fmt.Errorf("%s->%s: %s: %w", c.state.Name, dstPeer, annotation, err)
|
||||
}
|
||||
|
||||
// isEnabled returns true if site replication is enabled
|
||||
@@ -2068,11 +2115,11 @@ func (c *SiteReplicationSys) RemoveRemoteTargetsForEndpoint(ctx context.Context,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = globalBucketMetadataSys.Update(ctx, b.Name, bucketReplicationConfig, configData); err != nil {
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, b.Name, bucketReplicationConfig, configData); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := globalBucketMetadataSys.Update(ctx, b.Name, bucketReplicationConfig, nil); err != nil {
|
||||
if _, err := globalBucketMetadataSys.Update(ctx, b.Name, bucketReplicationConfig, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -2093,7 +2140,7 @@ func getAdminClient(endpoint, accessKey, secretKey string) (*madmin.AdminClient,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.SetCustomTransport(NewRemoteTargetHTTPTransport())
|
||||
client.SetCustomTransport(globalRemoteTargetTransport)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@@ -2105,7 +2152,7 @@ func getS3Client(pc madmin.PeerSite) (*minioClient.Client, error) {
|
||||
return minioClient.New(ep.Host, &minioClient.Options{
|
||||
Creds: credentials.NewStaticV4(pc.AccessKey, pc.SecretKey, ""),
|
||||
Secure: ep.Scheme == "https",
|
||||
Transport: NewRemoteTargetHTTPTransport(),
|
||||
Transport: globalRemoteTargetTransport,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2230,6 +2277,10 @@ func (c *SiteReplicationSys) SiteReplicationStatus(ctx context.Context, objAPI O
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
replicationStatus = "ReplicationStatus"
|
||||
)
|
||||
|
||||
// siteReplicationStatus returns the site replication status across clusters participating in site replication.
|
||||
func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI ObjectLayer, opts madmin.SRStatusOptions) (info srStatusInfo, err error) {
|
||||
c.RLock()
|
||||
@@ -2269,10 +2320,11 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O
|
||||
sris[depIdx[deploymentID]] = srInfo
|
||||
return nil
|
||||
},
|
||||
replicationStatus,
|
||||
)
|
||||
|
||||
if metaInfoConcErr.summaryErr != nil {
|
||||
return info, errSRBackendIssue(metaInfoConcErr.summaryErr)
|
||||
if err := errors.Unwrap(metaInfoConcErr); err != nil {
|
||||
return info, errSRBackendIssue(err)
|
||||
}
|
||||
|
||||
info.Enabled = true
|
||||
@@ -3434,14 +3486,20 @@ type srStatusInfo struct {
|
||||
}
|
||||
|
||||
func (c *SiteReplicationSys) healBuckets(ctx context.Context, objAPI ObjectLayer) error {
|
||||
info, err := c.siteReplicationStatus(ctx, objAPI, madmin.SRStatusOptions{
|
||||
Buckets: true,
|
||||
})
|
||||
buckets, err := c.listBuckets(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for bucket := range info.BucketStats {
|
||||
for _, bi := range buckets {
|
||||
bucket := bi.Name
|
||||
info, err := c.siteReplicationStatus(ctx, objAPI, madmin.SRStatusOptions{
|
||||
Entity: madmin.SRBucketEntity,
|
||||
EntityValue: bucket,
|
||||
})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
c.healCreateMissingBucket(ctx, objAPI, bucket, info)
|
||||
c.healVersioningMetadata(ctx, objAPI, bucket, info)
|
||||
c.healOLockConfigMetadata(ctx, objAPI, bucket, info)
|
||||
@@ -3503,7 +3561,7 @@ func (c *SiteReplicationSys) healTagMetadata(ctx context.Context, objAPI ObjectL
|
||||
continue
|
||||
}
|
||||
if dID == globalDeploymentID {
|
||||
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, latestTaggingConfigBytes); err != nil {
|
||||
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, latestTaggingConfigBytes); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Error healing tagging metadata from peer site %s : %w", latestPeerName, err))
|
||||
}
|
||||
continue
|
||||
@@ -3520,7 +3578,8 @@ func (c *SiteReplicationSys) healTagMetadata(ctx context.Context, objAPI ObjectL
|
||||
Tags: latestTaggingConfig,
|
||||
})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, "SRPeerReplicateBucketMeta", fmt.Errorf("Error healing tagging metadata for peer %s from peer %s : %w", peerName, latestPeerName, err)))
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
||||
fmt.Errorf("Error healing tagging metadata for peer %s from peer %s : %w", peerName, latestPeerName, err)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -3566,7 +3625,7 @@ func (c *SiteReplicationSys) healBucketPolicies(ctx context.Context, objAPI Obje
|
||||
continue
|
||||
}
|
||||
if dID == globalDeploymentID {
|
||||
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, latestIAMPolicy); err != nil {
|
||||
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, latestIAMPolicy); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Error healing bucket policy metadata from peer site %s : %w", latestPeerName, err))
|
||||
}
|
||||
continue
|
||||
@@ -3578,12 +3637,14 @@ func (c *SiteReplicationSys) healBucketPolicies(ctx context.Context, objAPI Obje
|
||||
}
|
||||
peerName := info.Sites[dID].Name
|
||||
if err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypePolicy,
|
||||
Bucket: bucket,
|
||||
Policy: latestIAMPolicy,
|
||||
Type: madmin.SRBucketMetaTypePolicy,
|
||||
Bucket: bucket,
|
||||
Policy: latestIAMPolicy,
|
||||
UpdatedAt: lastUpdate,
|
||||
}); err != nil {
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, "SRPeerReplicateBucketMeta", fmt.Errorf("Error healing bucket policy metadata for peer %s from peer %s : %w",
|
||||
peerName, latestPeerName, err)))
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
||||
fmt.Errorf("Error healing bucket policy metadata for peer %s from peer %s : %w",
|
||||
peerName, latestPeerName, err)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -3639,8 +3700,8 @@ func (c *SiteReplicationSys) healBucketQuotaConfig(ctx context.Context, objAPI O
|
||||
continue
|
||||
}
|
||||
if dID == globalDeploymentID {
|
||||
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, latestQuotaConfigBytes); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Error healing quota metadata from peer site %s : %s", latestPeerName, err.Error()))
|
||||
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, latestQuotaConfigBytes); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Error healing quota metadata from peer site %s : %w", latestPeerName, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -3652,12 +3713,14 @@ func (c *SiteReplicationSys) healBucketQuotaConfig(ctx context.Context, objAPI O
|
||||
peerName := info.Sites[dID].Name
|
||||
|
||||
if err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeQuotaConfig,
|
||||
Bucket: bucket,
|
||||
Quota: latestQuotaConfigBytes,
|
||||
Type: madmin.SRBucketMetaTypeQuotaConfig,
|
||||
Bucket: bucket,
|
||||
Quota: latestQuotaConfigBytes,
|
||||
UpdatedAt: lastUpdate,
|
||||
}); err != nil {
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, "SRPeerReplicateBucketMeta", fmt.Errorf("Error healing quota config metadata for peer %s from peer %s : %s",
|
||||
peerName, latestPeerName, err.Error())))
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
||||
fmt.Errorf("Error healing quota config metadata for peer %s from peer %s : %w",
|
||||
peerName, latestPeerName, err)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -3712,8 +3775,8 @@ func (c *SiteReplicationSys) healVersioningMetadata(ctx context.Context, objAPI
|
||||
continue
|
||||
}
|
||||
if dID == globalDeploymentID {
|
||||
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, latestVersioningConfigBytes); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Error healing sse metadata from peer site %s : %w", latestPeerName, err))
|
||||
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, latestVersioningConfigBytes); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Error healing versioning metadata from peer site %s : %w", latestPeerName, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -3727,10 +3790,12 @@ func (c *SiteReplicationSys) healVersioningMetadata(ctx context.Context, objAPI
|
||||
Type: madmin.SRBucketMetaTypeVersionConfig,
|
||||
Bucket: bucket,
|
||||
Versioning: latestVersioningConfig,
|
||||
UpdatedAt: lastUpdate,
|
||||
})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, "SRPeerReplicateBucketMeta", fmt.Errorf("Error healing versioning config metadata for peer %s from peer %s : %s",
|
||||
peerName, latestPeerName, err.Error())))
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
||||
fmt.Errorf("Error healing versioning config metadata for peer %s from peer %s : %w",
|
||||
peerName, latestPeerName, err)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -3785,7 +3850,7 @@ func (c *SiteReplicationSys) healSSEMetadata(ctx context.Context, objAPI ObjectL
|
||||
continue
|
||||
}
|
||||
if dID == globalDeploymentID {
|
||||
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, latestSSEConfigBytes); err != nil {
|
||||
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, latestSSEConfigBytes); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Error healing sse metadata from peer site %s : %w", latestPeerName, err))
|
||||
}
|
||||
continue
|
||||
@@ -3800,10 +3865,12 @@ func (c *SiteReplicationSys) healSSEMetadata(ctx context.Context, objAPI ObjectL
|
||||
Type: madmin.SRBucketMetaTypeSSEConfig,
|
||||
Bucket: bucket,
|
||||
SSEConfig: latestSSEConfig,
|
||||
UpdatedAt: lastUpdate,
|
||||
})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, "SRPeerReplicateBucketMeta", fmt.Errorf("Error healing SSE config metadata for peer %s from peer %s : %s",
|
||||
peerName, latestPeerName, err.Error())))
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
||||
fmt.Errorf("Error healing SSE config metadata for peer %s from peer %s : %w",
|
||||
peerName, latestPeerName, err)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -3858,7 +3925,7 @@ func (c *SiteReplicationSys) healOLockConfigMetadata(ctx context.Context, objAPI
|
||||
continue
|
||||
}
|
||||
if dID == globalDeploymentID {
|
||||
if err := globalBucketMetadataSys.Update(ctx, bucket, objectLockConfig, latestObjLockConfigBytes); err != nil {
|
||||
if _, err := globalBucketMetadataSys.Update(ctx, bucket, objectLockConfig, latestObjLockConfigBytes); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Error healing objectlock config metadata from peer site %s : %w", latestPeerName, err))
|
||||
}
|
||||
continue
|
||||
@@ -3870,13 +3937,15 @@ func (c *SiteReplicationSys) healOLockConfigMetadata(ctx context.Context, objAPI
|
||||
}
|
||||
peerName := info.Sites[dID].Name
|
||||
err = admClient.SRPeerReplicateBucketMeta(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
||||
Bucket: bucket,
|
||||
Tags: latestObjLockConfig,
|
||||
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
||||
Bucket: bucket,
|
||||
Tags: latestObjLockConfig,
|
||||
UpdatedAt: lastUpdate,
|
||||
})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, "SRPeerReplicateBucketMeta", fmt.Errorf("Error healing object lock config metadata for peer %s from peer %s : %w",
|
||||
peerName, latestPeerName, err)))
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, replicateBucketMetadata,
|
||||
fmt.Errorf("Error healing object lock config metadata for peer %s from peer %s : %w",
|
||||
peerName, latestPeerName, err)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -3939,25 +4008,19 @@ func (c *SiteReplicationSys) healCreateMissingBucket(ctx context.Context, objAPI
|
||||
if dID == globalDeploymentID {
|
||||
err := c.PeerBucketMakeWithVersioningHandler(ctx, bucket, opts)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, c.annotateErr("MakeWithVersioning", fmt.Errorf("error healing bucket for site replication %w from %s -> %s",
|
||||
err, latestPeerName, peerName)))
|
||||
return err
|
||||
return c.annotateErr(makeBucketWithVersion, fmt.Errorf("error healing bucket for site replication %w from %s -> %s",
|
||||
err, latestPeerName, peerName))
|
||||
}
|
||||
} else {
|
||||
admClient, err := c.getAdminClient(ctx, dID)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, c.annotateErr("ConfigureRepl", fmt.Errorf("admin client not found: %w", err)))
|
||||
return err
|
||||
return c.annotateErr(configureReplication, fmt.Errorf("unable to use admin client for %s: %w", dID, err))
|
||||
}
|
||||
err = admClient.SRPeerBucketOps(ctx, bucket, madmin.MakeWithVersioningBktOp, optsMap)
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, "MakeWithVersioning", err))
|
||||
if err != nil {
|
||||
return err
|
||||
if err = admClient.SRPeerBucketOps(ctx, bucket, madmin.MakeWithVersioningBktOp, optsMap); err != nil {
|
||||
return c.annotatePeerErr(peerName, makeBucketWithVersion, err)
|
||||
}
|
||||
err = admClient.SRPeerBucketOps(ctx, bucket, madmin.ConfigureReplBktOp, nil)
|
||||
logger.LogIf(ctx, c.annotatePeerErr(peerName, "ConfigureRepl", err))
|
||||
if err != nil {
|
||||
return err
|
||||
if err = admClient.SRPeerBucketOps(ctx, bucket, madmin.ConfigureReplBktOp, nil); err != nil {
|
||||
return c.annotatePeerErr(peerName, configureReplication, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3965,8 +4028,7 @@ func (c *SiteReplicationSys) healCreateMissingBucket(ctx context.Context, objAPI
|
||||
// configure replication from current cluster to other clusters
|
||||
err := c.PeerBucketConfigureReplHandler(ctx, bucket)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, c.annotateErr("ConfigureRepl", err))
|
||||
return err
|
||||
return c.annotateErr(configureReplication, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4007,10 +4069,7 @@ func (c *SiteReplicationSys) healBucketReplicationConfig(ctx context.Context, ob
|
||||
}
|
||||
|
||||
if replMismatch {
|
||||
err := c.PeerBucketConfigureReplHandler(ctx, bucket)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, c.annotateErr("ConfigureRepl", err))
|
||||
}
|
||||
logger.LogIf(ctx, c.annotateErr(configureReplication, c.PeerBucketConfigureReplHandler(ctx, bucket)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -176,6 +176,10 @@ type FileInfo struct {
|
||||
// File mode bits.
|
||||
Mode uint32 `msg:"m"`
|
||||
|
||||
// WrittenByVersion is the unix time stamp of the MinIO
|
||||
// version that created this version of the object.
|
||||
WrittenByVersion uint64 `msg:"wv"`
|
||||
|
||||
// File metadata
|
||||
Metadata map[string]string `msg:"meta"`
|
||||
|
||||
|
||||
@@ -550,8 +550,8 @@ func (z *FileInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if zb0001 != 26 {
|
||||
err = msgp.ArrayError{Wanted: 26, Got: zb0001}
|
||||
if zb0001 != 27 {
|
||||
err = msgp.ArrayError{Wanted: 27, Got: zb0001}
|
||||
return
|
||||
}
|
||||
z.Volume, err = dc.ReadString()
|
||||
@@ -629,6 +629,11 @@ func (z *FileInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err, "Mode")
|
||||
return
|
||||
}
|
||||
z.WrittenByVersion, err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "WrittenByVersion")
|
||||
return
|
||||
}
|
||||
var zb0002 uint32
|
||||
zb0002, err = dc.ReadMapHeader()
|
||||
if err != nil {
|
||||
@@ -726,8 +731,8 @@ func (z *FileInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z *FileInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// array header, size 26
|
||||
err = en.Append(0xdc, 0x0, 0x1a)
|
||||
// array header, size 27
|
||||
err = en.Append(0xdc, 0x0, 0x1b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -806,6 +811,11 @@ func (z *FileInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = msgp.WrapError(err, "Mode")
|
||||
return
|
||||
}
|
||||
err = en.WriteUint64(z.WrittenByVersion)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "WrittenByVersion")
|
||||
return
|
||||
}
|
||||
err = en.WriteMapHeader(uint32(len(z.Metadata)))
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Metadata")
|
||||
@@ -886,8 +896,8 @@ func (z *FileInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z *FileInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// array header, size 26
|
||||
o = append(o, 0xdc, 0x0, 0x1a)
|
||||
// array header, size 27
|
||||
o = append(o, 0xdc, 0x0, 0x1b)
|
||||
o = msgp.AppendString(o, z.Volume)
|
||||
o = msgp.AppendString(o, z.Name)
|
||||
o = msgp.AppendString(o, z.VersionID)
|
||||
@@ -903,6 +913,7 @@ func (z *FileInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.AppendTime(o, z.ModTime)
|
||||
o = msgp.AppendInt64(o, z.Size)
|
||||
o = msgp.AppendUint32(o, z.Mode)
|
||||
o = msgp.AppendUint64(o, z.WrittenByVersion)
|
||||
o = msgp.AppendMapHeader(o, uint32(len(z.Metadata)))
|
||||
for za0001, za0002 := range z.Metadata {
|
||||
o = msgp.AppendString(o, za0001)
|
||||
@@ -944,8 +955,8 @@ func (z *FileInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if zb0001 != 26 {
|
||||
err = msgp.ArrayError{Wanted: 26, Got: zb0001}
|
||||
if zb0001 != 27 {
|
||||
err = msgp.ArrayError{Wanted: 27, Got: zb0001}
|
||||
return
|
||||
}
|
||||
z.Volume, bts, err = msgp.ReadStringBytes(bts)
|
||||
@@ -1023,6 +1034,11 @@ func (z *FileInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "Mode")
|
||||
return
|
||||
}
|
||||
z.WrittenByVersion, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "WrittenByVersion")
|
||||
return
|
||||
}
|
||||
var zb0002 uint32
|
||||
zb0002, bts, err = msgp.ReadMapHeaderBytes(bts)
|
||||
if err != nil {
|
||||
@@ -1121,7 +1137,7 @@ func (z *FileInfo) 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 *FileInfo) Msgsize() (s int) {
|
||||
s = 3 + msgp.StringPrefixSize + len(z.Volume) + msgp.StringPrefixSize + len(z.Name) + msgp.StringPrefixSize + len(z.VersionID) + msgp.BoolSize + msgp.BoolSize + msgp.StringPrefixSize + len(z.TransitionStatus) + msgp.StringPrefixSize + len(z.TransitionedObjName) + msgp.StringPrefixSize + len(z.TransitionTier) + msgp.StringPrefixSize + len(z.TransitionVersionID) + msgp.BoolSize + msgp.StringPrefixSize + len(z.DataDir) + msgp.BoolSize + msgp.TimeSize + msgp.Int64Size + msgp.Uint32Size + msgp.MapHeaderSize
|
||||
s = 3 + msgp.StringPrefixSize + len(z.Volume) + msgp.StringPrefixSize + len(z.Name) + msgp.StringPrefixSize + len(z.VersionID) + msgp.BoolSize + msgp.BoolSize + msgp.StringPrefixSize + len(z.TransitionStatus) + msgp.StringPrefixSize + len(z.TransitionedObjName) + msgp.StringPrefixSize + len(z.TransitionTier) + msgp.StringPrefixSize + len(z.TransitionVersionID) + msgp.BoolSize + msgp.StringPrefixSize + len(z.DataDir) + msgp.BoolSize + msgp.TimeSize + msgp.Int64Size + msgp.Uint32Size + msgp.Uint64Size + msgp.MapHeaderSize
|
||||
if z.Metadata != nil {
|
||||
for za0001, za0002 := range z.Metadata {
|
||||
_ = za0002
|
||||
|
||||
+20
-39
@@ -127,7 +127,6 @@ type storageRESTClient struct {
|
||||
poolIndex, setIndex, diskIndex int
|
||||
|
||||
diskInfoCache timedValue
|
||||
diskHealCache timedValue
|
||||
}
|
||||
|
||||
// Retrieve location indexes.
|
||||
@@ -187,25 +186,9 @@ func (client *storageRESTClient) Endpoint() Endpoint {
|
||||
}
|
||||
|
||||
func (client *storageRESTClient) Healing() *healingTracker {
|
||||
client.diskHealCache.Once.Do(func() {
|
||||
// Update at least every second.
|
||||
client.diskHealCache.TTL = time.Second
|
||||
client.diskHealCache.Update = func() (interface{}, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
b, err := client.ReadAll(ctx, minioMetaBucket,
|
||||
pathJoin(bucketMetaPrefix, healingTrackerFilename))
|
||||
if err != nil {
|
||||
// If error, likely not healing.
|
||||
return (*healingTracker)(nil), nil
|
||||
}
|
||||
var h healingTracker
|
||||
_, err = h.UnmarshalMsg(b)
|
||||
return &h, err
|
||||
}
|
||||
})
|
||||
val, _ := client.diskHealCache.Get()
|
||||
return val.(*healingTracker)
|
||||
// This call is not implemented for remote client on purpose.
|
||||
// healing tracker is always for local disks.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *storageRESTClient) NSScanner(ctx context.Context, cache dataUsageCache, updates chan<- dataUsageEntry, scanMode madmin.HealScanMode) (dataUsageCache, error) {
|
||||
@@ -269,24 +252,6 @@ func (client *storageRESTClient) SetDiskID(id string) {
|
||||
client.diskID = id
|
||||
}
|
||||
|
||||
func (client *storageRESTClient) diskInfo() (interface{}, error) {
|
||||
var info DiskInfo
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
respBody, err := client.call(ctx, storageRESTMethodDiskInfo, nil, nil, -1)
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
defer xhttp.DrainBody(respBody)
|
||||
if err = msgp.Decode(respBody, &info); err != nil {
|
||||
return info, err
|
||||
}
|
||||
if info.Error != "" {
|
||||
return info, toStorageErr(errors.New(info.Error))
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// DiskInfo - fetch disk information for a remote disk.
|
||||
func (client *storageRESTClient) DiskInfo(ctx context.Context) (info DiskInfo, err error) {
|
||||
if !client.IsOnline() {
|
||||
@@ -299,7 +264,23 @@ func (client *storageRESTClient) DiskInfo(ctx context.Context) (info DiskInfo, e
|
||||
}
|
||||
client.diskInfoCache.Once.Do(func() {
|
||||
client.diskInfoCache.TTL = time.Second
|
||||
client.diskInfoCache.Update = client.diskInfo
|
||||
client.diskInfoCache.Update = func() (interface{}, error) {
|
||||
var info DiskInfo
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
respBody, err := client.call(ctx, storageRESTMethodDiskInfo, nil, nil, -1)
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
defer xhttp.DrainBody(respBody)
|
||||
if err = msgp.Decode(respBody, &info); err != nil {
|
||||
return info, err
|
||||
}
|
||||
if info.Error != "" {
|
||||
return info, toStorageErr(errors.New(info.Error))
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
})
|
||||
val, err := client.diskInfoCache.Get()
|
||||
if val != nil {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
@@ -18,7 +18,7 @@
|
||||
package cmd
|
||||
|
||||
const (
|
||||
storageRESTVersion = "v45" // Added ReadXL API
|
||||
storageRESTVersion = "v46" // Added MinIO version to FileInfo
|
||||
storageRESTVersionPrefix = SlashSeparator + storageRESTVersion
|
||||
storageRESTPrefix = minioReservedBucketPath + "/storage"
|
||||
)
|
||||
|
||||
+33
-30
@@ -121,15 +121,6 @@ func path2BucketObject(s string) (bucket, prefix string) {
|
||||
return path2BucketObjectWithBasePath("", s)
|
||||
}
|
||||
|
||||
func getWriteQuorum(drive int) int {
|
||||
parity := getDefaultParityBlocks(drive)
|
||||
quorum := drive - parity
|
||||
if quorum == parity {
|
||||
quorum++
|
||||
}
|
||||
return quorum
|
||||
}
|
||||
|
||||
// CloneMSS is an exposed function of cloneMSS for gateway usage.
|
||||
var CloneMSS = cloneMSS
|
||||
|
||||
@@ -561,9 +552,8 @@ func newCustomHTTPProxyTransport(tlsConfig *tls.Config, dialTimeout time.Duratio
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: xhttp.DialContextWithDNSCache(globalDNSCache, xhttp.NewInternodeDialContext(dialTimeout)),
|
||||
MaxIdleConnsPerHost: 1024,
|
||||
MaxConnsPerHost: 1024,
|
||||
WriteBufferSize: 16 << 10, // 16KiB moving up from 4KiB default
|
||||
ReadBufferSize: 16 << 10, // 16KiB moving up from 4KiB default
|
||||
WriteBufferSize: 32 << 10, // 32KiB moving up from 4KiB default
|
||||
ReadBufferSize: 32 << 10, // 32KiB moving up from 4KiB default
|
||||
IdleConnTimeout: 15 * time.Second,
|
||||
ResponseHeaderTimeout: 30 * time.Minute, // Set larger timeouts for proxied requests.
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
@@ -587,10 +577,10 @@ func newCustomHTTPTransport(tlsConfig *tls.Config, dialTimeout time.Duration) fu
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: xhttp.DialContextWithDNSCache(globalDNSCache, xhttp.NewInternodeDialContext(dialTimeout)),
|
||||
MaxIdleConnsPerHost: 1024,
|
||||
WriteBufferSize: 16 << 10, // 16KiB moving up from 4KiB default
|
||||
ReadBufferSize: 16 << 10, // 16KiB moving up from 4KiB default
|
||||
WriteBufferSize: 32 << 10, // 32KiB moving up from 4KiB default
|
||||
ReadBufferSize: 32 << 10, // 32KiB moving up from 4KiB default
|
||||
IdleConnTimeout: 15 * time.Second,
|
||||
ResponseHeaderTimeout: 3 * time.Minute, // Set conservative timeouts for MinIO internode.
|
||||
ResponseHeaderTimeout: 3 * time.Minute,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 10 * time.Second,
|
||||
TLSClientConfig: tlsConfig,
|
||||
@@ -666,7 +656,7 @@ func newGatewayHTTPTransport(timeout time.Duration) *http.Transport {
|
||||
|
||||
// NewRemoteTargetHTTPTransport returns a new http configuration
|
||||
// used while communicating with the remote replication targets.
|
||||
func NewRemoteTargetHTTPTransport() *http.Transport {
|
||||
func NewRemoteTargetHTTPTransport() func() *http.Transport {
|
||||
// For more details about various values used here refer
|
||||
// https://golang.org/pkg/net/http/#Transport documentation
|
||||
tr := &http.Transport{
|
||||
@@ -676,8 +666,8 @@ func NewRemoteTargetHTTPTransport() *http.Transport {
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConnsPerHost: 1024,
|
||||
WriteBufferSize: 16 << 10, // 16KiB moving up from 4KiB default
|
||||
ReadBufferSize: 16 << 10, // 16KiB moving up from 4KiB default
|
||||
WriteBufferSize: 32 << 10, // 32KiB moving up from 4KiB default
|
||||
ReadBufferSize: 32 << 10, // 32KiB moving up from 4KiB default
|
||||
IdleConnTimeout: 15 * time.Second,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
ExpectContinueTimeout: 5 * time.Second,
|
||||
@@ -690,7 +680,9 @@ func NewRemoteTargetHTTPTransport() *http.Transport {
|
||||
// in raw stream.
|
||||
DisableCompression: true,
|
||||
}
|
||||
return tr
|
||||
return func() *http.Transport {
|
||||
return tr
|
||||
}
|
||||
}
|
||||
|
||||
// Load the json (typically from disk file).
|
||||
@@ -800,6 +792,8 @@ func likelyUnescapeGeneric(p string, escapeFn func(string) (string, error)) stri
|
||||
func updateReqContext(ctx context.Context, objects ...ObjectV) context.Context {
|
||||
req := logger.GetReqInfo(ctx)
|
||||
if req != nil {
|
||||
req.Lock()
|
||||
defer req.Unlock()
|
||||
req.Objects = make([]logger.ObjectVersion, 0, len(objects))
|
||||
for _, ov := range objects {
|
||||
req.Objects = append(req.Objects, logger.ObjectVersion{
|
||||
@@ -949,13 +943,20 @@ type timedValue struct {
|
||||
// Get will return a cached value or fetch a new one.
|
||||
// If the Update function returns an error the value is forwarded as is and not cached.
|
||||
func (t *timedValue) Get() (interface{}, error) {
|
||||
v := t.get()
|
||||
v := t.get(t.ttl())
|
||||
if v != nil {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
v, err := t.Update()
|
||||
if err != nil {
|
||||
// if update fails, return current
|
||||
// cached value along with error.
|
||||
//
|
||||
// Let the caller decide if they want
|
||||
// to use the returned value based
|
||||
// on error.
|
||||
v = t.get(0)
|
||||
return v, err
|
||||
}
|
||||
|
||||
@@ -963,14 +964,21 @@ func (t *timedValue) Get() (interface{}, error) {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (t *timedValue) get() (v interface{}) {
|
||||
func (t *timedValue) ttl() time.Duration {
|
||||
ttl := t.TTL
|
||||
if ttl <= 0 {
|
||||
ttl = time.Second
|
||||
}
|
||||
return ttl
|
||||
}
|
||||
|
||||
func (t *timedValue) get(ttl time.Duration) (v interface{}) {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
v = t.value
|
||||
if ttl <= 0 {
|
||||
return v
|
||||
}
|
||||
if time.Since(t.lastUpdate) < ttl {
|
||||
return v
|
||||
}
|
||||
@@ -1067,17 +1075,12 @@ func newTLSConfig(getCert certs.GetCertificateFunc) *tls.Config {
|
||||
tlsConfig.ClientAuth = tls.RequestClientCert
|
||||
}
|
||||
|
||||
secureCiphers := env.Get(api.EnvAPISecureCiphers, config.EnableOn) == config.EnableOn
|
||||
if secureCiphers || fips.Enabled {
|
||||
// Hardened ciphers
|
||||
tlsConfig.CipherSuites = fips.CipherSuitesTLS()
|
||||
tlsConfig.CurvePreferences = fips.EllipticCurvesTLS()
|
||||
if secureCiphers := env.Get(api.EnvAPISecureCiphers, config.EnableOn) == config.EnableOn; secureCiphers {
|
||||
tlsConfig.CipherSuites = fips.TLSCiphers()
|
||||
} else {
|
||||
// Default ciphers while excluding those with security issues
|
||||
for _, cipher := range tls.CipherSuites() {
|
||||
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, cipher.ID)
|
||||
}
|
||||
tlsConfig.CipherSuites = fips.TLSCiphersBackwardCompatible()
|
||||
}
|
||||
tlsConfig.CurvePreferences = fips.TLSCurveIDs()
|
||||
return tlsConfig
|
||||
}
|
||||
|
||||
|
||||
+21
-11
@@ -178,10 +178,11 @@ type xlMetaV2Object struct {
|
||||
// on what Type field carries, it is imperative for the caller
|
||||
// to verify which journal type first before accessing rest of the fields.
|
||||
type xlMetaV2Version struct {
|
||||
Type VersionType `json:"Type" msg:"Type"`
|
||||
ObjectV1 *xlMetaV1Object `json:"V1Obj,omitempty" msg:"V1Obj,omitempty"`
|
||||
ObjectV2 *xlMetaV2Object `json:"V2Obj,omitempty" msg:"V2Obj,omitempty"`
|
||||
DeleteMarker *xlMetaV2DeleteMarker `json:"DelObj,omitempty" msg:"DelObj,omitempty"`
|
||||
Type VersionType `json:"Type" msg:"Type"`
|
||||
ObjectV1 *xlMetaV1Object `json:"V1Obj,omitempty" msg:"V1Obj,omitempty"`
|
||||
ObjectV2 *xlMetaV2Object `json:"V2Obj,omitempty" msg:"V2Obj,omitempty"`
|
||||
DeleteMarker *xlMetaV2DeleteMarker `json:"DelObj,omitempty" msg:"DelObj,omitempty"`
|
||||
WrittenByVersion uint64 `msg:"v"` // Tracks written by MinIO version
|
||||
}
|
||||
|
||||
// xlFlags contains flags on the object.
|
||||
@@ -409,16 +410,22 @@ func (j xlMetaV2Version) getVersionID() [16]byte {
|
||||
}
|
||||
|
||||
// ToFileInfo returns FileInfo of the underlying type.
|
||||
func (j *xlMetaV2Version) ToFileInfo(volume, path string) (FileInfo, error) {
|
||||
func (j *xlMetaV2Version) ToFileInfo(volume, path string) (fi FileInfo, err error) {
|
||||
if j == nil {
|
||||
return fi, errFileNotFound
|
||||
}
|
||||
switch j.Type {
|
||||
case ObjectType:
|
||||
return j.ObjectV2.ToFileInfo(volume, path)
|
||||
fi, err = j.ObjectV2.ToFileInfo(volume, path)
|
||||
case DeleteType:
|
||||
return j.DeleteMarker.ToFileInfo(volume, path)
|
||||
fi, err = j.DeleteMarker.ToFileInfo(volume, path)
|
||||
case LegacyType:
|
||||
return j.ObjectV1.ToFileInfo(volume, path)
|
||||
fi, err = j.ObjectV1.ToFileInfo(volume, path)
|
||||
default:
|
||||
return fi, errFileNotFound
|
||||
}
|
||||
return FileInfo{}, errFileNotFound
|
||||
fi.WrittenByVersion = j.WrittenByVersion
|
||||
return fi, err
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -1182,6 +1189,7 @@ func (x *xlMetaV2) DeleteVersion(fi FileInfo) (string, error) {
|
||||
ModTime: fi.ModTime.UnixNano(),
|
||||
MetaSys: make(map[string][]byte),
|
||||
},
|
||||
WrittenByVersion: globalVersionUnix,
|
||||
}
|
||||
if !ventry.Valid() {
|
||||
return "", errors.New("internal error: invalid version entry generated")
|
||||
@@ -1424,7 +1432,9 @@ func (x *xlMetaV2) AddVersion(fi FileInfo) error {
|
||||
}
|
||||
}
|
||||
|
||||
ventry := xlMetaV2Version{}
|
||||
ventry := xlMetaV2Version{
|
||||
WrittenByVersion: globalVersionUnix,
|
||||
}
|
||||
|
||||
if fi.Deleted {
|
||||
ventry.Type = DeleteType
|
||||
@@ -1589,7 +1599,7 @@ func (x *xlMetaV2) AddLegacy(m *xlMetaV1Object) error {
|
||||
}
|
||||
m.VersionID = nullVersionID
|
||||
|
||||
return x.addVersion(xlMetaV2Version{ObjectV1: m, Type: LegacyType})
|
||||
return x.addVersion(xlMetaV2Version{ObjectV1: m, Type: LegacyType, WrittenByVersion: globalVersionUnix})
|
||||
}
|
||||
|
||||
// ToFileInfo converts xlMetaV2 into a common FileInfo datastructure
|
||||
|
||||
@@ -1774,6 +1774,12 @@ func (z *xlMetaV2Version) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
case "v":
|
||||
z.WrittenByVersion, err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "WrittenByVersion")
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = dc.Skip()
|
||||
if err != nil {
|
||||
@@ -1788,8 +1794,8 @@ func (z *xlMetaV2Version) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z *xlMetaV2Version) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// omitempty: check for empty values
|
||||
zb0001Len := uint32(4)
|
||||
var zb0001Mask uint8 /* 4 bits */
|
||||
zb0001Len := uint32(5)
|
||||
var zb0001Mask uint8 /* 5 bits */
|
||||
if z.ObjectV1 == nil {
|
||||
zb0001Len--
|
||||
zb0001Mask |= 0x2
|
||||
@@ -1877,6 +1883,16 @@ func (z *xlMetaV2Version) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// write "v"
|
||||
err = en.Append(0xa1, 0x76)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteUint64(z.WrittenByVersion)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "WrittenByVersion")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1884,8 +1900,8 @@ func (z *xlMetaV2Version) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
func (z *xlMetaV2Version) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// omitempty: check for empty values
|
||||
zb0001Len := uint32(4)
|
||||
var zb0001Mask uint8 /* 4 bits */
|
||||
zb0001Len := uint32(5)
|
||||
var zb0001Mask uint8 /* 5 bits */
|
||||
if z.ObjectV1 == nil {
|
||||
zb0001Len--
|
||||
zb0001Mask |= 0x2
|
||||
@@ -1945,6 +1961,9 @@ func (z *xlMetaV2Version) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// string "v"
|
||||
o = append(o, 0xa1, 0x76)
|
||||
o = msgp.AppendUint64(o, z.WrittenByVersion)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2027,6 +2046,12 @@ func (z *xlMetaV2Version) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
case "v":
|
||||
z.WrittenByVersion, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "WrittenByVersion")
|
||||
return
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
@@ -2059,6 +2084,7 @@ func (z *xlMetaV2Version) Msgsize() (s int) {
|
||||
} else {
|
||||
s += z.DeleteMarker.Msgsize()
|
||||
}
|
||||
s += 2 + msgp.Uint64Size
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func (j xlMetaV2Object) InitFreeVersion(fi FileInfo) (xlMetaV2Version, bool) {
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Invalid Tier Object delete marker versionId %s %v", fi.TierFreeVersionID(), err))
|
||||
}
|
||||
freeEntry := xlMetaV2Version{Type: DeleteType}
|
||||
freeEntry := xlMetaV2Version{Type: DeleteType, WrittenByVersion: globalVersionUnix}
|
||||
freeEntry.DeleteMarker = &xlMetaV2DeleteMarker{
|
||||
VersionID: vID,
|
||||
ModTime: j.ModTime, // fi.ModTime may be empty
|
||||
|
||||
+7
-10
@@ -575,22 +575,19 @@ func (s *xlStorage) DiskInfo(context.Context) (info DiskInfo, err error) {
|
||||
dcinfo.FreeInodes = di.Ffree
|
||||
dcinfo.FSType = di.FSType
|
||||
diskID, err := s.GetDiskID()
|
||||
if errors.Is(err, errUnformattedDisk) {
|
||||
// if we found an unformatted disk then
|
||||
// healing is automatically true.
|
||||
dcinfo.Healing = true
|
||||
} else {
|
||||
// Check if the disk is being healed if GetDiskID
|
||||
// returned any error other than fresh disk
|
||||
dcinfo.Healing = s.Healing() != nil
|
||||
}
|
||||
// Healing is 'true' when
|
||||
// - if we found an unformatted disk (no 'format.json')
|
||||
// - if we found healing tracker 'healing.bin'
|
||||
dcinfo.Healing = errors.Is(err, errUnformattedDisk) || (s.Healing() != nil)
|
||||
dcinfo.ID = diskID
|
||||
return dcinfo, err
|
||||
}
|
||||
})
|
||||
|
||||
v, err := s.diskInfoCache.Get()
|
||||
info = v.(DiskInfo)
|
||||
if v != nil {
|
||||
info = v.(DiskInfo)
|
||||
}
|
||||
return info, err
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Configuration Directory
|
||||
|
||||
MinIO stores all its config as part of the server deployment, config is erasure coded on MinIO. On a fresh deployment MinIO automatically generates a new `config` and this config is available to be configured via `mc admin config` command. MinIO also encrypts all the config, IAM and policies content if KMS is configured. Please refer to how to encrypt your config and IAM credentials [here](https://github.com/minio/minio/blob/master/docs/kms/IAM.md)
|
||||
MinIO stores all its config as part of the server deployment, config is erasure coded on MinIO. On a fresh deployment MinIO automatically generates a new `config` and this config is available to be configured via `mc admin config` command. MinIO also encrypts all the config, IAM and policies content if KMS is configured. Please refer to how to encrypt your config and IAM credentials [here](https://github.com/minio/minio/blob/master/docs/kms/IAM.md).
|
||||
|
||||
### Certificate Directory
|
||||
|
||||
@@ -123,7 +123,7 @@ MINIO_CACHE_COMMENT (sentence) optionally add a comment to this setting
|
||||
|
||||
#### Etcd
|
||||
|
||||
MinIO supports storing encrypted IAM assets in etcd, if KMS is configured. Please refer to how to encrypt your config and IAM credentials [here](https://github.com/minio/minio/blob/master/docs/kms/IAM.md)
|
||||
MinIO supports storing encrypted IAM assets in etcd, if KMS is configured. Please refer to how to encrypt your config and IAM credentials [here](https://github.com/minio/minio/blob/master/docs/kms/IAM.md).
|
||||
|
||||
> NOTE: if *path_prefix* is set then MinIO will not federate your buckets, namespaced IAM assets are assumed as isolated tenants, only buckets are considered globally unique but performing a lookup with a *bucket* which belongs to a different tenant will fail unlike federated setups where MinIO would port-forward and route the request to relevant cluster accordingly. This is a special feature, federated deployments should not need to set *path_prefix*.
|
||||
|
||||
@@ -181,7 +181,7 @@ MINIO_API_REMOTE_TRANSPORT_DEADLINE (duration) set the deadline for API reques
|
||||
|
||||
#### Notifications
|
||||
|
||||
Notification targets supported by MinIO are in the following list. To configure individual targets please refer to more detailed documentation [here](https://docs.min.io/docs/minio-bucket-notification-guide.html)
|
||||
Notification targets supported by MinIO are in the following list. To configure individual targets please refer to more detailed documentation [here](https://docs.min.io/docs/minio-bucket-notification-guide.html).
|
||||
|
||||
```
|
||||
notify_webhook publish bucket notifications to webhook endpoints
|
||||
@@ -244,7 +244,7 @@ MINIO_ETCD_CLIENT_CERT_KEY (path) client cert key for mTLS authentication
|
||||
MINIO_ETCD_COMMENT (sentence) optionally add a comment to this setting
|
||||
```
|
||||
|
||||
This behavior is consistent across all keys, each key self documents itself with valid examples.
|
||||
This behavior is consistent across all keys; each key self-documents itself with valid examples.
|
||||
|
||||
## Dynamic systems without restarting server
|
||||
|
||||
@@ -274,7 +274,7 @@ delay (float) scanner delay multiplier, defaults to '10.0'
|
||||
max_wait (duration) maximum wait time between operations, defaults to '15s'
|
||||
```
|
||||
|
||||
Example: Following setting will decrease the scanner speed by a factor of 3, reducing the system resource use, but increasing the latency of updates being reflected.
|
||||
Example: the following setting will decrease the scanner speed by a factor of 3, reducing the system resource use, but increasing the latency of updates being reflected.
|
||||
|
||||
```sh
|
||||
~ mc admin config set alias/ scanner delay=30.0
|
||||
@@ -326,7 +326,8 @@ minio server /data
|
||||
|
||||
### Domain
|
||||
|
||||
By default, MinIO supports path-style requests that are of the format <http://mydomain.com/bucket/object>. `MINIO_DOMAIN` environment variable is used to enable virtual-host-style requests. If the request `Host` header matches with `(.+).mydomain.com` then the matched pattern `$1` is used as bucket and the path is used as object. More information on path-style and virtual-host-style [here](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAPI.html)
|
||||
By default, MinIO supports path-style requests that are of the format <http://mydomain.com/bucket/object>. `MINIO_DOMAIN` environment variable is used to enable virtual-host-style requests. If the request `Host` header matches with `(.+).mydomain.com` then the matched pattern `$1` is used as bucket and the path is used as object. Read more about path-style and virtual-host-style [here](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAPI.html).
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -23,12 +23,14 @@ import (
|
||||
"crypto/sha256"
|
||||
"flag"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
@@ -39,10 +41,39 @@ var (
|
||||
sourceBucket, sourcePrefix string
|
||||
targetEndpoint, targetAccessKey, targetSecretKey string
|
||||
targetBucket, targetPrefix string
|
||||
minimumObjectAge string
|
||||
debug bool
|
||||
insecure bool
|
||||
)
|
||||
|
||||
func buildS3Client(endpoint, accessKey, secretKey string, insecure bool) (*minio.Client, error) {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secure := strings.EqualFold(u.Scheme, "https")
|
||||
transport, err := minio.DefaultTransport(secure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if insecure {
|
||||
// skip TLS verification
|
||||
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
|
||||
clnt, err := minio.New(u.Host, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
|
||||
Secure: secure,
|
||||
Transport: transport,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return clnt, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&sourceEndpoint, "source-endpoint", "https://play.min.io", "S3 endpoint URL")
|
||||
flag.StringVar(&sourceAccessKey, "source-access-key", "Q3AM3UQ867SPQQA43P2F", "S3 Access Key")
|
||||
@@ -56,6 +87,7 @@ func main() {
|
||||
flag.StringVar(&targetBucket, "target-bucket", "", "Select a specific bucket")
|
||||
flag.StringVar(&targetPrefix, "target-prefix", "", "Select a prefix")
|
||||
|
||||
flag.StringVar(&minimumObjectAge, "minimum-object-age", "0s", "Ignore objects younger than the specified age")
|
||||
flag.BoolVar(&debug, "debug", false, "Prints HTTP network calls to S3 endpoint")
|
||||
flag.BoolVar(&insecure, "insecure", false, "Disable TLS verification")
|
||||
flag.Parse()
|
||||
@@ -92,35 +124,47 @@ func main() {
|
||||
log.Fatalln("--target-prefix is specified without --target-bucket.")
|
||||
}
|
||||
|
||||
u, err := url.Parse(sourceEndpoint)
|
||||
srcU, err := url.Parse(sourceEndpoint)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
secure := strings.EqualFold(u.Scheme, "https")
|
||||
transport, err := minio.DefaultTransport(secure)
|
||||
ssecure := strings.EqualFold(srcU.Scheme, "https")
|
||||
stransport, err := minio.DefaultTransport(ssecure)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
if insecure {
|
||||
// skip TLS verification
|
||||
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||
stransport.TLSClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
|
||||
sclnt, err := minio.New(u.Host, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(sourceAccessKey, sourceSecretKey, ""),
|
||||
Secure: secure,
|
||||
Transport: transport,
|
||||
})
|
||||
ageDelta, err := time.ParseDuration(minimumObjectAge)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
tclnt, err := minio.New(u.Host, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(targetAccessKey, targetSecretKey, ""),
|
||||
Secure: secure,
|
||||
Transport: transport,
|
||||
})
|
||||
maxObjectModTime := time.Now().Add(-ageDelta)
|
||||
|
||||
// Next object is used to ignore new objects in the source & target
|
||||
nextObject := func(ch <-chan minio.ObjectInfo) (ctnt minio.ObjectInfo, ok bool) {
|
||||
for {
|
||||
ctnt, ok := <-ch
|
||||
if !ok {
|
||||
return minio.ObjectInfo{}, false
|
||||
}
|
||||
if ctnt.LastModified.Before(maxObjectModTime) {
|
||||
return ctnt, ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sclnt, err := buildS3Client(sourceEndpoint, sourceAccessKey, sourceSecretKey, insecure)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
tclnt, err := buildS3Client(targetEndpoint, targetAccessKey, targetSecretKey, insecure)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
@@ -131,26 +175,30 @@ func main() {
|
||||
}
|
||||
|
||||
sopts := minio.ListObjectsOptions{
|
||||
Recursive: true,
|
||||
Prefix: sourcePrefix,
|
||||
WithMetadata: true,
|
||||
Recursive: true,
|
||||
Prefix: sourcePrefix,
|
||||
}
|
||||
|
||||
topts := minio.ListObjectsOptions{
|
||||
Recursive: true,
|
||||
Prefix: targetPrefix,
|
||||
WithMetadata: true,
|
||||
Recursive: true,
|
||||
Prefix: targetPrefix,
|
||||
}
|
||||
|
||||
srcCh := sclnt.ListObjects(context.Background(), sourceBucket, sopts)
|
||||
tgtCh := tclnt.ListObjects(context.Background(), targetBucket, topts)
|
||||
|
||||
srcCtnt, srcOk := <-srcCh
|
||||
tgtCtnt, tgtOk := <-tgtCh
|
||||
srcCtnt, srcOk := nextObject(srcCh)
|
||||
tgtCtnt, tgtOk := nextObject(tgtCh)
|
||||
|
||||
var srcEOF, tgtEOF bool
|
||||
|
||||
srcSha256 := sha256.New()
|
||||
tgtSha256 := sha256.New()
|
||||
|
||||
for {
|
||||
srcSha256.Reset()
|
||||
tgtSha256.Reset()
|
||||
|
||||
srcEOF = !srcOk
|
||||
tgtEOF = !tgtOk
|
||||
|
||||
@@ -177,88 +225,93 @@ func main() {
|
||||
// The same for target
|
||||
if tgtEOF {
|
||||
fmt.Printf("only in source: %s\n", srcCtnt.Key)
|
||||
srcCtnt, srcOk = <-srcCh
|
||||
srcCtnt, srcOk = nextObject(srcCh)
|
||||
continue
|
||||
}
|
||||
|
||||
if srcCtnt.Key < tgtCtnt.Key {
|
||||
fmt.Printf("only in source: %s\n", srcCtnt.Key)
|
||||
srcCtnt, srcOk = nextObject(srcCh)
|
||||
continue
|
||||
}
|
||||
|
||||
if srcCtnt.Key == tgtCtnt.Key {
|
||||
var allgood bool
|
||||
if srcCtnt.Size != tgtCtnt.Size {
|
||||
fmt.Printf("differ in size sourceSize: %d, targetSize: %d\n", srcCtnt.Size, tgtCtnt.Size)
|
||||
} else if srcCtnt.ContentType != tgtCtnt.ContentType {
|
||||
fmt.Printf("differ in contentType source: %s, target: %s\n", srcCtnt.ContentType, tgtCtnt.ContentType)
|
||||
} else {
|
||||
opts := minio.GetObjectOptions{}
|
||||
sobj, err := sclnt.GetObject(context.Background(), sourceBucket, srcCtnt.Key, opts)
|
||||
if err == nil {
|
||||
tobj, err := tclnt.GetObject(context.Background(), targetBucket, tgtCtnt.Key, opts)
|
||||
if err != nil {
|
||||
fmt.Printf("unreadable on target: %s (%s)\n", tgtCtnt.Key, err)
|
||||
} else {
|
||||
srcSha256 := sha256.New()
|
||||
tgtSha256 := sha256.New()
|
||||
|
||||
var sourceFailed, targetFailed bool
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
srcSize, err := io.Copy(srcSha256, sobj)
|
||||
if err != nil {
|
||||
fmt.Printf("unreadable on source: %s (%s)\n", srcCtnt.Key, err)
|
||||
sourceFailed = true
|
||||
return
|
||||
}
|
||||
if srcSize != srcCtnt.Size {
|
||||
fmt.Printf("unreadable on source - size differs upon read: %s\n", srcCtnt.Key)
|
||||
sourceFailed = true
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
tgtSize, err := io.Copy(tgtSha256, tobj)
|
||||
if err != nil {
|
||||
fmt.Printf("unreadable on target: %s (%s)\n", tgtCtnt.Key, err)
|
||||
targetFailed = true
|
||||
return
|
||||
}
|
||||
if tgtSize != tgtCtnt.Size {
|
||||
fmt.Printf("unreadable on target - size differs upon read: %s\n", tgtCtnt.Key)
|
||||
targetFailed = true
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
tobj.Close()
|
||||
|
||||
if !sourceFailed && !targetFailed {
|
||||
ssum := srcSha256.Sum(nil)
|
||||
tsum := tgtSha256.Sum(nil)
|
||||
if !bytes.Equal(ssum, tsum) {
|
||||
fmt.Printf("sha256 sum mismatch: %s -> Expected(%x), Found(%x)\n", srcCtnt.Key, ssum, tsum)
|
||||
} else {
|
||||
allgood = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sobj.Close()
|
||||
} else {
|
||||
fmt.Printf("unreadable on source: %s (%s)\n", srcCtnt.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
if allgood {
|
||||
if verifyChecksum(sclnt, srcSha256, tgtSha256, srcCtnt, tgtCtnt) {
|
||||
fmt.Printf("all readable source and target: %s -> %s\n", srcCtnt.Key, tgtCtnt.Key)
|
||||
}
|
||||
|
||||
srcCtnt, srcOk = <-srcCh
|
||||
tgtCtnt, tgtOk = <-tgtCh
|
||||
srcCtnt, srcOk = nextObject(srcCh)
|
||||
tgtCtnt, tgtOk = nextObject(tgtCh)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("only in target: %s (%s)\n", tgtCtnt.Key, tgtCtnt.VersionID)
|
||||
tgtCtnt, tgtOk = <-tgtCh
|
||||
fmt.Printf("only in target: %s\n", tgtCtnt.Key)
|
||||
tgtCtnt, tgtOk = nextObject(tgtCh)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyChecksum(sclnt *minio.Client, srcSha256, tgtSha256 hash.Hash, srcCtnt, tgtCtnt minio.ObjectInfo) (allgood bool) {
|
||||
opts := minio.GetObjectOptions{}
|
||||
if srcCtnt.Size != tgtCtnt.Size {
|
||||
fmt.Printf("differ in size sourceSize: %d, targetSize: %d\n", srcCtnt.Size, tgtCtnt.Size)
|
||||
return false
|
||||
} else if srcCtnt.ContentType != tgtCtnt.ContentType {
|
||||
fmt.Printf("differ in contentType source: %s, target: %s\n", srcCtnt.ContentType, tgtCtnt.ContentType)
|
||||
return false
|
||||
}
|
||||
|
||||
core := minio.Core{Client: sclnt}
|
||||
sobj, _, _, err := core.GetObject(context.Background(), sourceBucket, srcCtnt.Key, opts)
|
||||
if err != nil {
|
||||
fmt.Printf("unreadable on source: %s (%s)\n", srcCtnt.Key, err)
|
||||
return false
|
||||
}
|
||||
|
||||
tobj, _, _, err := core.GetObject(context.Background(), targetBucket, tgtCtnt.Key, opts)
|
||||
if err != nil {
|
||||
fmt.Printf("unreadable on target: %s (%s)\n", tgtCtnt.Key, err)
|
||||
return false
|
||||
}
|
||||
|
||||
var sourceFailed, targetFailed bool
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
srcSize, err := io.Copy(srcSha256, sobj)
|
||||
if err != nil {
|
||||
fmt.Printf("unreadable on source: %s (%s)\n", srcCtnt.Key, err)
|
||||
sourceFailed = true
|
||||
} else if srcSize != srcCtnt.Size {
|
||||
fmt.Printf("unreadable on source - size differs upon read: %s\n", srcCtnt.Key)
|
||||
sourceFailed = true
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
tgtSize, err := io.Copy(tgtSha256, tobj)
|
||||
if err != nil {
|
||||
fmt.Printf("unreadable on target: %s (%s)\n", tgtCtnt.Key, err)
|
||||
targetFailed = true
|
||||
} else if tgtSize != tgtCtnt.Size {
|
||||
fmt.Printf("unreadable on target - size differs upon read: %s\n", tgtCtnt.Key)
|
||||
targetFailed = true
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
sobj.Close()
|
||||
tobj.Close()
|
||||
|
||||
if !sourceFailed && !targetFailed {
|
||||
ssum := srcSha256.Sum(nil)
|
||||
tsum := tgtSha256.Sum(nil)
|
||||
allgood = bytes.Equal(ssum, tsum)
|
||||
if !allgood {
|
||||
fmt.Printf("sha256 sum mismatch: %s -> Expected(%x), Found(%x)\n", srcCtnt.Key, ssum, tsum)
|
||||
}
|
||||
}
|
||||
|
||||
return allgood
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ Expansion of ellipses and choice of erasure sets based on this expansion is an a
|
||||
|
||||
- Erasure coding used by MinIO is [Reed-Solomon](https://github.com/klauspost/reedsolomon) erasure coding scheme, which has a total shard maximum of 256 i.e 128 data and 128 parity. MinIO design goes beyond this limitation by doing some practical architecture choices.
|
||||
|
||||
- Erasure set is a single erasure coding unit within a MinIO deployment. An object is sharded within an erasure set. Erasure set size is automatically calculated based on the number of disks. MinIO supports unlimited number of disks but each erasure set can be upto 16 disks and a minimum of 4 disks.
|
||||
- Erasure set is a single erasure coding unit within a MinIO deployment. An object is sharded within an erasure set. Erasure set size is automatically calculated based on the number of disks. MinIO supports unlimited number of disks but each erasure set can be upto 16 disks and a minimum of 2 disks.
|
||||
|
||||
- We limited the number of drives to 16 for erasure set because, erasure code shards more than 16 can become chatty and do not have any performance advantages. Additionally since 16 drive erasure set gives you tolerance of 8 disks per object by default which is plenty in any practical scenario.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ MinIO in distributed mode can help you setup a highly-available storage system w
|
||||
|
||||
### Data protection
|
||||
|
||||
Distributed MinIO provides protection against multiple node/drive failures and [bit rot](https://github.com/minio/minio/blob/master/docs/erasure/README.md#what-is-bit-rot-protection) using [erasure code](https://docs.min.io/docs/minio-erasure-code-quickstart-guide). As the minimum disks required for distributed MinIO is 4 (same as minimum disks required for erasure coding), erasure code automatically kicks in as you launch distributed MinIO.
|
||||
Distributed MinIO provides protection against multiple node/drive failures and [bit rot](https://github.com/minio/minio/blob/master/docs/erasure/README.md#what-is-bit-rot-protection) using [erasure code](https://docs.min.io/docs/minio-erasure-code-quickstart-guide). As the minimum disks required for distributed MinIO is 2 (same as minimum disks required for erasure coding), erasure code automatically kicks in as you launch distributed MinIO.
|
||||
|
||||
If one or more disks are offline at the start of a PutObject or NewMultipartUpload operation the object will have additional data protection bits added automatically to provide additional safety for these objects.
|
||||
|
||||
@@ -22,9 +22,12 @@ Refer to sizing guide for more understanding on default values chosen depending
|
||||
|
||||
### Consistency Guarantees
|
||||
|
||||
MinIO follows strict **read-after-write** and **list-after-write** consistency model for all i/o operations both in distributed and standalone modes. This consistency model is only guaranteed if you use disk filesystems such as xfs, ext4 or zfs etc.. for distributed setup.
|
||||
MinIO follows strict **read-after-write** and **list-after-write** consistency model for all i/o operations both in distributed and standalone modes. This consistency model is only guaranteed if you use disk filesystems such as xfs, zfs or btrfs etc.. for distributed setup.
|
||||
|
||||
**In our tests we also found ext4 does not honor POSIX O_DIRECT/Fdatasync semantics, ext4 trades performance for consistency guarantees. Please avoid ext4 in your setup.**
|
||||
|
||||
**If MinIO distributed setup is using NFS volumes underneath it is not guaranteed MinIO will provide these consistency guarantees since NFS is not strictly consistent (If you must use NFS we recommend that you atleast use NFSv4 instead of NFSv3 for relatively better outcomes).**
|
||||
|
||||
**If MinIO distributed setup is using NFS volumes underneath it is not guaranteed MinIO will provide these consistency guarantees since NFS is not consistent filesystem by design (If you must use NFS we recommend that you atleast use NFSv4 instead of NFSv3).**
|
||||
|
||||
## Get started
|
||||
|
||||
@@ -41,7 +44,7 @@ To start a distributed MinIO instance, you just need to pass drive locations as
|
||||
**NOTE:**
|
||||
|
||||
- All the nodes running distributed MinIO should share a common root credentials, for the nodes to connect and trust each other. To achieve this, it is **recommended** to export root user and root password as environment variables, `MINIO_ROOT_USER` and `MINIO_ROOT_PASSWORD`, on all the nodes before executing MinIO server command. If not exported, default `minioadmin/minioadmin` credentials shall be used.
|
||||
- **MinIO creates erasure-coding sets of _4_ to _16_ drives per set. The number of drives you provide in total must be a multiple of one of those numbers.**
|
||||
- **MinIO creates erasure-coding sets of _2_ to _16_ drives per set. The number of drives you provide in total must be a multiple of one of those numbers.**
|
||||
- **MinIO chooses the largest EC set size which divides into the total number of drives or total number of nodes given - making sure to keep the uniform distribution i.e each node participates equal number of drives per set**.
|
||||
- **Each object is written to a single EC set, and therefore is spread over no more than 16 drives.**
|
||||
- **All the nodes running distributed MinIO setup are recommended to be homogeneous, i.e. same operating system, same number of disks and same network interconnects.**
|
||||
|
||||
@@ -24,7 +24,7 @@ MinIO's erasure coded backend uses high speed [HighwayHash](https://github.com/m
|
||||
|
||||
## How are drives used for Erasure Code?
|
||||
|
||||
MinIO divides the drives you provide into erasure-coding sets of *4 to 16* drives. Therefore, the number of drives you present must be a multiple of one of these numbers. Each object is written to a single erasure-coding set.
|
||||
MinIO divides the drives you provide into erasure-coding sets of *2 to 16* drives. Therefore, the number of drives you present must be a multiple of one of these numbers. Each object is written to a single erasure-coding set.
|
||||
|
||||
Minio uses the largest possible EC set size which divides into the number of drives given. For example, *18 drives* are configured as *2 sets of 9 drives*, and *24 drives* are configured as *2 sets of 12 drives*. This is true for scenarios when running MinIO as a standalone erasure coded deployment. In [distributed setup however node (affinity) based](https://docs.minio.io/docs/distributed-minio-quickstart-guide.html) erasure stripe sizes are chosen.
|
||||
|
||||
|
||||
@@ -62,9 +62,7 @@ For more complete documentation on Erasure Set sizing, see the [MinIO Documentat
|
||||
- Less than N/2, if `STANDARD` parity is not set.
|
||||
- Less than `STANDARD` Parity, if it is set.
|
||||
|
||||
As parity below 2 is not recommended, `REDUCED_REDUNDANCY` storage class is not supported for 4 disks erasure coding setup.
|
||||
|
||||
Default value for `REDUCED_REDUNDANCY` storage class is `2`.
|
||||
Default value for `REDUCED_REDUNDANCY` storage class is `1`.
|
||||
|
||||
## Get started with Storage Class
|
||||
|
||||
|
||||
+7
-8
@@ -43,7 +43,7 @@ MinIO Gateway comes with an embedded web based object browser. Point your web br
|
||||
|
||||
## Test using MinIO Client `mc`
|
||||
|
||||
`mc` provides a modern alternative to UNIX commands such as ls, cat, cp, mirror, diff etc. It supports filesystems and Amazon S3 compatible cloud storage services.
|
||||
`mc` provides a modern alternative to UNIX commands such as ls, cat, cp, mirror, diff, etc. It supports filesystems and Amazon S3 compatible cloud storage services.
|
||||
|
||||
### Configure `mc`
|
||||
|
||||
@@ -88,25 +88,24 @@ export MINIO_NOTIFY_WEBHOOK_ENDPOINT_1=http://localhost:8080/
|
||||
export MINIO_NOTIFY_WEBHOOK_QUEUE_DIR_1=/tmp/webhk
|
||||
```
|
||||
|
||||
> NOTE: Please check the docs for the corresponding ENV setting. Alternatively, We can obtain other ENVs in the form `mc admin config set alias/ <sub-sys> --env`
|
||||
> NOTE: Please check the docs for the corresponding ENV setting. Alternatively, we can obtain other ENVs in the form `mc admin config set alias/ <sub-sys> --env`
|
||||
|
||||
## Symlink support
|
||||
|
||||
NAS gateway implementation allows symlinks on regular files,
|
||||
NAS gateway implementation allows symlinks on regular files.
|
||||
|
||||
### Behavior
|
||||
|
||||
- For reads symlink resolves to file symlink points to.
|
||||
- For reads symlinks resolve to the file the symlink points to.
|
||||
- For deletes
|
||||
- Delete of symlink deletes the symlink but not the real file to which the symlink points.
|
||||
- Delete of actual file automatically makes symlink'ed file invisible, dangling symlinks won't be visible.
|
||||
- Deleting a symlink deletes the symlink but not the real file to which the symlink points.
|
||||
- Deleting the real file a symlink points to automatically makes the dangling symlink invisible.
|
||||
|
||||
#### Caveats
|
||||
|
||||
- Disallows follow of directory symlinks to avoid security issues, and leaving them as is on namespace makes them very inconsistent.
|
||||
- Dangling symlinks are ignored automatically.
|
||||
|
||||
*Directory symlinks is not and will not be supported as there are no safe ways to handle them.*
|
||||
*Directory symlinks are not and will not be supported as there are no safe ways to handle them.*
|
||||
|
||||
## Explore Further
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ For best deployment experience MinIO recommends operating systems RHEL/CentOS 8.
|
||||
| Maximum number of servers per cluster | no-limit |
|
||||
| Maximum number of federated clusters | no-limit |
|
||||
| Minimum number of servers | 02 |
|
||||
| Minimum number of drives per server when server count is 1 | 04 |
|
||||
| Minimum number of drives per server when server count is 2 or 3 | 02 |
|
||||
| Minimum number of drives per server when server count is 1 | 02 |
|
||||
| Minimum number of drives per server when server count is 2 or 3 | 01 |
|
||||
| Minimum number of drives per server when server count is 4 | 01 |
|
||||
| Maximum number of drives per server | no-limit |
|
||||
| Read quorum | N/2 |
|
||||
@@ -53,7 +53,7 @@ We found the following APIs to be redundant or less useful outside of AWS S3. If
|
||||
## Object name restrictions on MinIO
|
||||
|
||||
- Object names that contain characters `^*|\/&";` are unsupported on Windows platform or any other file systems that do not support filenames with special charaters. **This list is non exhaustive, it depends on the operating system and filesystem under use - please consult your operating system vendor**. MinIO recommends using Linux based deployments for production workloads.
|
||||
- Objects should not have conflicting objects as parents, applications using this behavior should change their behavior and use proper unique keys, for example situations such as following conflicting key patterns are not supported.
|
||||
- Objects should not have conflicting objects as parent objects, applications using this behavior should change their behavior and use proper unique keys, for example situations such as following conflicting key patterns are not supported.
|
||||
|
||||
```
|
||||
PUT <bucketname>/a/b/1.txt
|
||||
|
||||
@@ -2,7 +2,7 @@ version: '3.7'
|
||||
|
||||
# Settings and configurations that are common for all containers
|
||||
x-minio-common: &minio-common
|
||||
image: quay.io/minio/minio:RELEASE.2022-06-11T19-55-32Z
|
||||
image: quay.io/minio/minio:RELEASE.2022-06-25T15-50-16Z
|
||||
command: server --console-address ":9001" http://minio{1...4}/data{1...2}
|
||||
expose:
|
||||
- "9000"
|
||||
|
||||
@@ -42,8 +42,11 @@ if [ ! -f ./mc ]; then
|
||||
fi
|
||||
|
||||
minio server --config-dir /tmp/minio-ldap --address ":9001" /tmp/minio-ldap-idp1/{1...4} >/tmp/minio1_1.log 2>&1 &
|
||||
site1_pid=$!
|
||||
minio server --config-dir /tmp/minio-ldap --address ":9002" /tmp/minio-ldap-idp2/{1...4} >/tmp/minio2_1.log 2>&1 &
|
||||
site2_pid=$!
|
||||
minio server --config-dir /tmp/minio-ldap --address ":9003" /tmp/minio-ldap-idp3/{1...4} >/tmp/minio3_1.log 2>&1 &
|
||||
site3_pid=$!
|
||||
|
||||
sleep 10
|
||||
|
||||
@@ -237,4 +240,31 @@ if [ "${enabled_minio1}" != "Enabled" ]; then
|
||||
exit_1;
|
||||
fi
|
||||
|
||||
# "Test if most recent tag update is replicated"
|
||||
./mc tag set minio2/newbucket "key=val1"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "expecting tag set to be successful. exiting.."
|
||||
exit_1;
|
||||
fi
|
||||
sleep 5
|
||||
|
||||
val=$(./mc tag list minio1/newbucket --json | jq -r .tagset | jq -r .key)
|
||||
if [ "${val}" != "val1" ]; then
|
||||
echo "expected bucket tag to have replicated, exiting..."
|
||||
exit_1;
|
||||
fi
|
||||
# stop minio1
|
||||
kill -9 ${site1_pid}
|
||||
# Update tag on minio2/newbucket when minio1 is down
|
||||
./mc tag set minio2/newbucket "key=val2"
|
||||
# Restart minio1 instance
|
||||
minio server --config-dir /tmp/minio-ldap --address ":9001" /tmp/minio-ldap-idp1/{1...4} >/tmp/minio1_1.log 2>&1 &
|
||||
sleep 10
|
||||
# Test whether most recent tag update on minio2 is replicated to minio1
|
||||
val=$(./mc tag list minio1/newbucket --json | jq -r .tagset | jq -r .key )
|
||||
if [ "${val}" != "val2" ]; then
|
||||
echo "expected bucket tag to have replicated, exiting..."
|
||||
exit_1;
|
||||
fi
|
||||
|
||||
cleanup
|
||||
|
||||
@@ -34,8 +34,11 @@ if [ ! -f ./mc ]; then
|
||||
fi
|
||||
|
||||
minio server --config-dir /tmp/minio-internal --address ":9001" /tmp/minio-internal-idp1/{1...4} >/tmp/minio1_1.log 2>&1 &
|
||||
site1_pid=$!
|
||||
minio server --config-dir /tmp/minio-internal --address ":9002" /tmp/minio-internal-idp2/{1...4} >/tmp/minio2_1.log 2>&1 &
|
||||
site2_pid=$!
|
||||
minio server --config-dir /tmp/minio-internal --address ":9003" /tmp/minio-internal-idp3/{1...4} >/tmp/minio3_1.log 2>&1 &
|
||||
site3_pid=$!
|
||||
|
||||
sleep 10
|
||||
|
||||
@@ -268,3 +271,30 @@ if [ "${enabled_minio1}" != "Enabled" ]; then
|
||||
echo "expected bucket to be mirrored with object-lock enabled, exiting..."
|
||||
exit_1;
|
||||
fi
|
||||
|
||||
# "Test if most recent tag update is replicated"
|
||||
./mc tag set minio2/newbucket "key=val1"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "expecting tag set to be successful. exiting.."
|
||||
exit_1;
|
||||
fi
|
||||
sleep 5
|
||||
|
||||
val=$(./mc tag list minio1/newbucket --json | jq -r .tagset | jq -r .key)
|
||||
if [ "${val}" != "val1" ]; then
|
||||
echo "expected bucket tag to have replicated, exiting..."
|
||||
exit_1;
|
||||
fi
|
||||
|
||||
kill -9 ${site1_pid}
|
||||
# Update tag on minio2/newbucket when minio1 is down
|
||||
./mc tag set minio2/newbucket "key=val2"
|
||||
# Restart minio1 instance
|
||||
minio server --config-dir /tmp/minio-internal --address ":9001" /tmp/minio-internal-idp1/{1...4} >/tmp/minio1_1.log 2>&1 &
|
||||
sleep 10
|
||||
# Test whether most recent tag update on minio2 is replicated to minio1
|
||||
val=$(./mc tag list minio1/newbucket --json | jq -r .tagset | jq -r .key )
|
||||
if [ "${val}" != "val2" ]; then
|
||||
echo "expected bucket tag to have replicated, exiting..."
|
||||
exit_1;
|
||||
fi
|
||||
|
||||
@@ -35,12 +35,14 @@ export MINIO_IDENTITY_OPENID_SCOPES="openid,groups"
|
||||
|
||||
export MINIO_IDENTITY_OPENID_REDIRECT_URI="http://127.0.0.1:10000/oauth_callback"
|
||||
minio server --address ":9001" --console-address ":10000" /tmp/minio1/{1...4} >/tmp/minio1_1.log 2>&1 &
|
||||
|
||||
site1_pid=$!
|
||||
export MINIO_IDENTITY_OPENID_REDIRECT_URI="http://127.0.0.1:11000/oauth_callback"
|
||||
minio server --address ":9002" --console-address ":11000" /tmp/minio2/{1...4} >/tmp/minio2_1.log 2>&1 &
|
||||
site2_pid=$!
|
||||
|
||||
export MINIO_IDENTITY_OPENID_REDIRECT_URI="http://127.0.0.1:12000/oauth_callback"
|
||||
minio server --address ":9003" --console-address ":12000" /tmp/minio3/{1...4} >/tmp/minio3_1.log 2>&1 &
|
||||
site3_pid=$!
|
||||
|
||||
if [ ! -f ./mc ]; then
|
||||
wget -O mc https://dl.minio.io/client/mc/release/linux-amd64/mc \
|
||||
@@ -222,3 +224,30 @@ if [ "${enabled_minio1}" != "Enabled" ]; then
|
||||
echo "expected bucket to be mirrored with object-lock enabled, exiting..."
|
||||
exit_1;
|
||||
fi
|
||||
|
||||
# "Test if most recent tag update is replicated"
|
||||
./mc tag set minio2/newbucket "key=val1"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "expecting tag set to be successful. exiting.."
|
||||
exit_1;
|
||||
fi
|
||||
sleep 5
|
||||
|
||||
val=$(./mc tag list minio1/newbucket --json | jq -r .tagset | jq -r .key)
|
||||
if [ "${val}" != "val1" ]; then
|
||||
echo "expected bucket tag to have replicated, exiting..."
|
||||
exit_1;
|
||||
fi
|
||||
# stop minio1 instance
|
||||
kill -9 ${site1_pid}
|
||||
# Update tag on minio2/newbucket when minio1 is down
|
||||
./mc tag set minio2/newbucket "key=val2"
|
||||
# Restart minio1 instance
|
||||
minio server --address ":9001" --console-address ":10000" /tmp/minio1/{1...4} >/tmp/minio1_1.log 2>&1 &
|
||||
sleep 10
|
||||
# Test whether most recent tag update on minio2 is replicated to minio1
|
||||
val=$(./mc tag list minio1/newbucket --json | jq -r .tagset | jq -r .key )
|
||||
if [ "${val}" != "val2" ]; then
|
||||
echo "expected bucket tag to have replicated, exiting..."
|
||||
exit_1;
|
||||
fi
|
||||
|
||||
@@ -35,29 +35,28 @@ require (
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/inconshreveable/mousetrap v1.0.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/klauspost/compress v1.15.1
|
||||
github.com/klauspost/cpuid/v2 v2.0.11
|
||||
github.com/klauspost/compress v1.15.6
|
||||
github.com/klauspost/cpuid/v2 v2.0.14
|
||||
github.com/klauspost/pgzip v1.2.5
|
||||
github.com/klauspost/readahead v1.4.0
|
||||
github.com/klauspost/reedsolomon v1.9.15
|
||||
github.com/klauspost/reedsolomon v1.10.0
|
||||
github.com/lib/pq v1.10.4
|
||||
github.com/miekg/dns v1.1.48
|
||||
github.com/minio/cli v1.22.0
|
||||
github.com/minio/console v0.18.1
|
||||
github.com/minio/console v0.19.0
|
||||
github.com/minio/csvparser v1.0.0
|
||||
github.com/minio/dperf v0.4.2
|
||||
github.com/minio/highwayhash v1.0.2
|
||||
github.com/minio/kes v0.19.2
|
||||
github.com/minio/madmin-go v1.3.14
|
||||
github.com/minio/minio-go/v7 v7.0.28
|
||||
github.com/minio/pkg v1.1.24
|
||||
github.com/minio/madmin-go v1.3.20
|
||||
github.com/minio/minio-go/v7 v7.0.30
|
||||
github.com/minio/pkg v1.1.26
|
||||
github.com/minio/selfupdate v0.4.0
|
||||
github.com/minio/sha256-simd v1.0.0
|
||||
github.com/minio/simdjson-go v0.4.2
|
||||
github.com/minio/sio v0.3.0
|
||||
github.com/minio/zipindex v0.2.1
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/montanaflynn/stats v0.6.6
|
||||
github.com/nats-io/nats-server/v2 v2.7.4
|
||||
github.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d
|
||||
github.com/nats-io/stan.go v0.10.2
|
||||
@@ -72,7 +71,7 @@ require (
|
||||
github.com/rs/cors v1.7.0
|
||||
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417
|
||||
github.com/secure-io/sio-go v0.3.1
|
||||
github.com/shirou/gopsutil/v3 v3.22.2
|
||||
github.com/shirou/gopsutil/v3 v3.22.5
|
||||
github.com/streadway/amqp v1.0.0
|
||||
github.com/tinylib/msgp v1.1.7-0.20211026165309-e818a1881b0e
|
||||
github.com/valyala/bytebufferpool v1.0.0
|
||||
@@ -170,6 +169,7 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/montanaflynn/stats v0.6.6 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
|
||||
|
||||
@@ -31,11 +31,11 @@ var (
|
||||
return !color.NoColor
|
||||
}
|
||||
|
||||
Bold = func() func(a ...interface{}) string {
|
||||
Bold = func() func(format string, a ...interface{}) string {
|
||||
if IsTerminal() {
|
||||
return color.New(color.Bold).SprintFunc()
|
||||
return color.New(color.Bold).SprintfFunc()
|
||||
}
|
||||
return fmt.Sprint
|
||||
return fmt.Sprintf
|
||||
}()
|
||||
|
||||
RedBold = func() func(format string, a ...interface{}) string {
|
||||
|
||||
@@ -940,3 +940,197 @@ func (c Config) SetKVS(s string, defaultKVS map[string]KVS) (dynamic bool, err e
|
||||
c[subSys][tgt] = currKVS
|
||||
return dynamic, nil
|
||||
}
|
||||
|
||||
// CheckValidKeys - checks if the config parameters for the given subsystem and
|
||||
// target are valid. It checks both the configuration store as well as
|
||||
// environment variables.
|
||||
func (c Config) CheckValidKeys(subSys string, deprecatedKeys []string) error {
|
||||
defKVS, ok := DefaultKVS[subSys]
|
||||
if !ok {
|
||||
return fmt.Errorf("Subsystem %s does not exist", subSys)
|
||||
}
|
||||
|
||||
// Make a list of valid keys for the subsystem including the `comment`
|
||||
// key.
|
||||
validKeys := make([]string, 0, len(defKVS)+1)
|
||||
for _, param := range defKVS {
|
||||
validKeys = append(validKeys, param.Key)
|
||||
}
|
||||
validKeys = append(validKeys, Comment)
|
||||
|
||||
subSysEnvVars := env.List(fmt.Sprintf("%s%s", EnvPrefix, strings.ToUpper(subSys)))
|
||||
|
||||
// Set of env vars for the sub-system to validate.
|
||||
candidates := set.CreateStringSet(subSysEnvVars...)
|
||||
|
||||
// Remove all default target env vars from the candidates set (as they
|
||||
// are valid).
|
||||
for _, param := range validKeys {
|
||||
paramEnvName := getEnvVarName(subSys, Default, param)
|
||||
candidates.Remove(paramEnvName)
|
||||
}
|
||||
|
||||
isSingleTarget := SubSystemsSingleTargets.Contains(subSys)
|
||||
if isSingleTarget && len(candidates) > 0 {
|
||||
return fmt.Errorf("The following environment variables are unknown: %s",
|
||||
strings.Join(candidates.ToSlice(), ", "))
|
||||
}
|
||||
|
||||
if !isSingleTarget {
|
||||
// Validate other env vars for all targets.
|
||||
envVars := candidates.ToSlice()
|
||||
for _, envVar := range envVars {
|
||||
for _, param := range validKeys {
|
||||
pEnvName := getEnvVarName(subSys, Default, param) + Default
|
||||
if len(envVar) > len(pEnvName) && strings.HasPrefix(envVar, pEnvName) {
|
||||
// This envVar is valid - it has a
|
||||
// non-empty target.
|
||||
candidates.Remove(envVar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Whatever remains are invalid env vars - return an error.
|
||||
if len(candidates) > 0 {
|
||||
return fmt.Errorf("The following environment variables are unknown: %s",
|
||||
strings.Join(candidates.ToSlice(), ", "))
|
||||
}
|
||||
}
|
||||
|
||||
validKeysSet := set.CreateStringSet(validKeys...)
|
||||
validKeysSet = validKeysSet.Difference(set.CreateStringSet(deprecatedKeys...))
|
||||
kvsMap := c[subSys]
|
||||
for tgt, kvs := range kvsMap {
|
||||
invalidKV := KVS{}
|
||||
for _, kv := range kvs {
|
||||
if !validKeysSet.Contains(kv.Key) {
|
||||
invalidKV = append(invalidKV, kv)
|
||||
}
|
||||
}
|
||||
if len(invalidKV) > 0 {
|
||||
return Errorf(
|
||||
"found invalid keys (%s) for '%s:%s' sub-system, use 'mc admin config reset myminio %s:%s' to fix invalid keys",
|
||||
invalidKV.String(), subSys, tgt, subSys, tgt)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAvailableTargets - returns a list of targets configured for the given
|
||||
// subsystem (whether they are enabled or not). A target could be configured via
|
||||
// environment variables or via the configuration store. The default target is
|
||||
// `_` and is always returned.
|
||||
func (c Config) GetAvailableTargets(subSys string) ([]string, error) {
|
||||
if SubSystemsSingleTargets.Contains(subSys) {
|
||||
return []string{Default}, nil
|
||||
}
|
||||
|
||||
defKVS, ok := DefaultKVS[subSys]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Subsystem %s does not exist", subSys)
|
||||
}
|
||||
|
||||
kvsMap := c[subSys]
|
||||
s := set.NewStringSet()
|
||||
|
||||
// Add all targets that are configured in the config store.
|
||||
for k := range kvsMap {
|
||||
s.Add(k)
|
||||
}
|
||||
|
||||
// Add targets that are configured via environment variables.
|
||||
for _, param := range defKVS {
|
||||
envVarPrefix := getEnvVarName(subSys, Default, param.Key) + Default
|
||||
envsWithPrefix := env.List(envVarPrefix)
|
||||
for _, k := range envsWithPrefix {
|
||||
tgtName := strings.TrimPrefix(k, envVarPrefix)
|
||||
if tgtName != "" {
|
||||
s.Add(tgtName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s.ToSlice(), nil
|
||||
}
|
||||
|
||||
func getEnvVarName(subSys, target, param string) string {
|
||||
if target == Default {
|
||||
return fmt.Sprintf("%s%s%s%s", EnvPrefix, strings.ToUpper(subSys), Default, strings.ToUpper(param))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s%s%s%s%s", EnvPrefix, strings.ToUpper(subSys), Default, strings.ToUpper(param), Default, target)
|
||||
}
|
||||
|
||||
var resolvableSubsystems = set.CreateStringSet(IdentityOpenIDSubSys)
|
||||
|
||||
// ValueSource represents the source of a config parameter value.
|
||||
type ValueSource uint8
|
||||
|
||||
// Constants for ValueSource
|
||||
const (
|
||||
ValueSourceAbsent ValueSource = iota // this is an error case
|
||||
ValueSourceDef
|
||||
ValueSourceCfg
|
||||
ValueSourceEnv
|
||||
)
|
||||
|
||||
// ResolveConfigParam returns the effective value of a configuration parameter,
|
||||
// within a subsystem and subsystem target. The effective value is, in order of
|
||||
// decreasing precedence:
|
||||
//
|
||||
// 1. the value of the corresponding environment variable if set,
|
||||
// 2. the value of the parameter in the config store if set,
|
||||
// 3. the default value,
|
||||
//
|
||||
// This function only works for a subset of sub-systems, others return
|
||||
// `ValueSourceAbsent`. FIXME: some parameters have custom environment
|
||||
// variables for which support needs to be added.
|
||||
func (c Config) ResolveConfigParam(subSys, target, cfgParam string) (value string, cs ValueSource) {
|
||||
// cs = ValueSourceAbsent initially as it is iota by default.
|
||||
|
||||
// Initially only support OpenID
|
||||
if !resolvableSubsystems.Contains(subSys) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if config param requested is valid.
|
||||
defKVS, ok := DefaultKVS[subSys]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
defValue, isFound := defKVS.Lookup(cfgParam)
|
||||
if !isFound {
|
||||
return
|
||||
}
|
||||
|
||||
if target == "" {
|
||||
target = Default
|
||||
}
|
||||
|
||||
envVar := getEnvVarName(subSys, target, cfgParam)
|
||||
|
||||
// Lookup Env var.
|
||||
value = env.Get(envVar, "")
|
||||
if value != "" {
|
||||
cs = ValueSourceEnv
|
||||
return
|
||||
}
|
||||
|
||||
// Lookup config store.
|
||||
if subSysStore, ok := c[subSys]; ok {
|
||||
if kvs, ok2 := subSysStore[target]; ok2 {
|
||||
var ok3 bool
|
||||
value, ok3 = kvs.Lookup(cfgParam)
|
||||
if ok3 {
|
||||
cs = ValueSourceCfg
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the default value.
|
||||
value = defValue
|
||||
cs = ValueSourceDef
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ var (
|
||||
ErrInvalidErasureSetSize = newErrFn(
|
||||
"Invalid erasure set size",
|
||||
"Please check the passed value",
|
||||
"Erasure set can only accept any of [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] values",
|
||||
"Erasure set can only accept any of [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] values",
|
||||
)
|
||||
|
||||
ErrInvalidWormValue = newErrFn(
|
||||
@@ -177,7 +177,7 @@ var (
|
||||
|
||||
ErrInvalidNumberOfErasureEndpoints = newErrFn(
|
||||
"Invalid total number of endpoints for erasure mode",
|
||||
"Please provide an even number of endpoints greater or equal to 4",
|
||||
"Please provide number of endpoints greater or equal to 2",
|
||||
"For more information, please refer to https://docs.min.io/docs/minio-erasure-code-quickstart-guide",
|
||||
)
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ func TestKeycloakProviderInitialization(t *testing.T) {
|
||||
testKvs.Set(Vendor, "keycloak")
|
||||
testKvs.Set(KeyCloakRealm, "TestRealm")
|
||||
testKvs.Set(KeyCloakAdminURL, "http://keycloak.test/auth/admin")
|
||||
cfgGet := func(env, param string) string {
|
||||
cfgGet := func(param string) string {
|
||||
return testKvs.Get(param)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,49 +36,32 @@ import (
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/config/identity/openid/provider"
|
||||
"github.com/minio/minio/internal/hash/sha256"
|
||||
"github.com/minio/pkg/env"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
xnet "github.com/minio/pkg/net"
|
||||
)
|
||||
|
||||
// OpenID keys and envs.
|
||||
const (
|
||||
JwksURL = "jwks_url"
|
||||
ClientID = "client_id"
|
||||
ClientSecret = "client_secret"
|
||||
ConfigURL = "config_url"
|
||||
ClaimName = "claim_name"
|
||||
ClaimUserinfo = "claim_userinfo"
|
||||
ClaimPrefix = "claim_prefix"
|
||||
ClientID = "client_id"
|
||||
ClientSecret = "client_secret"
|
||||
RolePolicy = "role_policy"
|
||||
DisplayName = "display_name"
|
||||
|
||||
Vendor = "vendor"
|
||||
Scopes = "scopes"
|
||||
RedirectURI = "redirect_uri"
|
||||
RedirectURIDynamic = "redirect_uri_dynamic"
|
||||
Vendor = "vendor"
|
||||
|
||||
// Vendor specific ENV only enabled if the Vendor matches == "vendor"
|
||||
KeyCloakRealm = "keycloak_realm"
|
||||
KeyCloakAdminURL = "keycloak_admin_url"
|
||||
|
||||
EnvIdentityOpenIDEnable = "MINIO_IDENTITY_OPENID_ENABLE"
|
||||
EnvIdentityOpenIDVendor = "MINIO_IDENTITY_OPENID_VENDOR"
|
||||
EnvIdentityOpenIDClientID = "MINIO_IDENTITY_OPENID_CLIENT_ID"
|
||||
EnvIdentityOpenIDClientSecret = "MINIO_IDENTITY_OPENID_CLIENT_SECRET"
|
||||
EnvIdentityOpenIDURL = "MINIO_IDENTITY_OPENID_CONFIG_URL"
|
||||
EnvIdentityOpenIDClaimName = "MINIO_IDENTITY_OPENID_CLAIM_NAME"
|
||||
EnvIdentityOpenIDClaimUserInfo = "MINIO_IDENTITY_OPENID_CLAIM_USERINFO"
|
||||
EnvIdentityOpenIDClaimPrefix = "MINIO_IDENTITY_OPENID_CLAIM_PREFIX"
|
||||
EnvIdentityOpenIDRolePolicy = "MINIO_IDENTITY_OPENID_ROLE_POLICY"
|
||||
EnvIdentityOpenIDRedirectURI = "MINIO_IDENTITY_OPENID_REDIRECT_URI"
|
||||
EnvIdentityOpenIDRedirectURIDynamic = "MINIO_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC"
|
||||
EnvIdentityOpenIDScopes = "MINIO_IDENTITY_OPENID_SCOPES"
|
||||
EnvIdentityOpenIDDisplayName = "MINIO_IDENTITY_OPENID_DISPLAY_NAME"
|
||||
|
||||
// Vendor specific ENVs only enabled if the Vendor matches == "vendor"
|
||||
EnvIdentityOpenIDKeyCloakRealm = "MINIO_IDENTITY_OPENID_KEYCLOAK_REALM"
|
||||
EnvIdentityOpenIDKeyCloakAdminURL = "MINIO_IDENTITY_OPENID_KEYCLOAK_ADMIN_URL"
|
||||
// Removed params
|
||||
JwksURL = "jwks_url"
|
||||
ClaimPrefix = "claim_prefix"
|
||||
)
|
||||
|
||||
// DefaultKVS - default config for OpenID config
|
||||
@@ -191,7 +174,7 @@ func (r *Config) Clone() Config {
|
||||
}
|
||||
|
||||
// LookupConfig lookup jwks from config, override with any ENVs.
|
||||
func LookupConfig(kvsMap map[string]config.KVS, transport http.RoundTripper, closeRespFn func(io.ReadCloser), serverRegion string) (c Config, err error) {
|
||||
func LookupConfig(s config.Config, transport http.RoundTripper, closeRespFn func(io.ReadCloser), serverRegion string) (c Config, err error) {
|
||||
openIDClientTransport := http.DefaultTransport
|
||||
if transport != nil {
|
||||
openIDClientTransport = transport
|
||||
@@ -209,48 +192,37 @@ func LookupConfig(kvsMap map[string]config.KVS, transport http.RoundTripper, clo
|
||||
closeRespFn: closeRespFn,
|
||||
}
|
||||
|
||||
// Make a copy of the config we received so we can mutate it safely.
|
||||
kvsMap2 := make(map[string]config.KVS, len(kvsMap))
|
||||
for k, v := range kvsMap {
|
||||
kvsMap2[k] = v
|
||||
}
|
||||
|
||||
// Add in each configuration name found from environment variables, i.e.
|
||||
// if we see MINIO_IDENTITY_OPENID_CONFIG_URL_2, we add the key "2" to
|
||||
// `kvsMap2` if it does not already exist.
|
||||
envs := env.List(EnvIdentityOpenIDURL + config.Default)
|
||||
for _, k := range envs {
|
||||
cfgName := strings.TrimPrefix(k, EnvIdentityOpenIDURL+config.Default)
|
||||
if cfgName == "" {
|
||||
return c, config.Errorf("Environment variable must have a non-empty config name: %s", k)
|
||||
}
|
||||
|
||||
// It is possible that some variables were specified via config
|
||||
// commands and some variables are intended to be overridden
|
||||
// from the environment, so we ensure that the key is not
|
||||
// overwritten in `kvsMap2` as it may have existing config.
|
||||
if _, ok := kvsMap2[cfgName]; !ok {
|
||||
kvsMap2[cfgName] = DefaultKVS
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
hasLegacyPolicyMapping = false
|
||||
seenClientIDs = set.NewStringSet()
|
||||
)
|
||||
for cfgName, kvs := range kvsMap2 {
|
||||
// remove this since we have removed support for this already.
|
||||
kvs.Delete(JwksURL)
|
||||
|
||||
if err = config.CheckValidKeys(config.IdentityOpenIDSubSys, kvs, DefaultKVS); err != nil {
|
||||
return c, err
|
||||
deprecatedKeys := []string{JwksURL}
|
||||
|
||||
// remove this since we have removed support for this already.
|
||||
for k := range s[config.IdentityOpenIDSubSys] {
|
||||
for _, dk := range deprecatedKeys {
|
||||
kvs := s[config.IdentityOpenIDSubSys][k]
|
||||
kvs.Delete(dk)
|
||||
s[config.IdentityOpenIDSubSys][k] = kvs
|
||||
}
|
||||
}
|
||||
|
||||
getCfgVal := func(envVar, cfgParam string) string {
|
||||
if cfgName != config.Default {
|
||||
envVar += config.Default + cfgName
|
||||
}
|
||||
return env.Get(envVar, kvs.Get(cfgParam))
|
||||
if err := s.CheckValidKeys(config.IdentityOpenIDSubSys, deprecatedKeys); err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
openIDTargets, err := s.GetAvailableTargets(config.IdentityOpenIDSubSys)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
for _, cfgName := range openIDTargets {
|
||||
getCfgVal := func(cfgParam string) string {
|
||||
// As parameters are already validated, we skip checking
|
||||
// if the config param was found.
|
||||
val, _ := s.ResolveConfigParam(config.IdentityOpenIDSubSys, cfgName, cfgParam)
|
||||
return val
|
||||
}
|
||||
|
||||
// In the past, when only one openID provider was allowed, there
|
||||
@@ -261,7 +233,7 @@ func LookupConfig(kvsMap map[string]config.KVS, transport http.RoundTripper, clo
|
||||
// setting, otherwise we treat it as enabled if some important
|
||||
// parameters are non-empty.
|
||||
var (
|
||||
cfgEnableVal = getCfgVal(EnvIdentityOpenIDEnable, config.Enable)
|
||||
cfgEnableVal = getCfgVal(config.Enable)
|
||||
isExplicitlyEnabled = false
|
||||
)
|
||||
if cfgEnableVal != "" {
|
||||
@@ -281,7 +253,7 @@ func LookupConfig(kvsMap map[string]config.KVS, transport http.RoundTripper, clo
|
||||
}
|
||||
|
||||
p := newProviderCfgFromConfig(getCfgVal)
|
||||
configURL := getCfgVal(EnvIdentityOpenIDURL, ConfigURL)
|
||||
configURL := getCfgVal(ConfigURL)
|
||||
|
||||
if !isExplicitlyEnabled {
|
||||
enabled = true
|
||||
@@ -315,7 +287,7 @@ func LookupConfig(kvsMap map[string]config.KVS, transport http.RoundTripper, clo
|
||||
return c, errors.New("please specify config_url to enable fetching claims from UserInfo endpoint")
|
||||
}
|
||||
|
||||
if scopeList := getCfgVal(EnvIdentityOpenIDScopes, Scopes); scopeList != "" {
|
||||
if scopeList := getCfgVal(Scopes); scopeList != "" {
|
||||
var scopes []string
|
||||
for _, scope := range strings.Split(scopeList, ",") {
|
||||
scope = strings.TrimSpace(scope)
|
||||
|
||||
@@ -52,17 +52,17 @@ type providerCfg struct {
|
||||
provider provider.Provider
|
||||
}
|
||||
|
||||
func newProviderCfgFromConfig(getCfgVal func(env, cfgName string) string) providerCfg {
|
||||
func newProviderCfgFromConfig(getCfgVal func(cfgName string) string) providerCfg {
|
||||
return providerCfg{
|
||||
DisplayName: getCfgVal(EnvIdentityOpenIDDisplayName, DisplayName),
|
||||
ClaimName: getCfgVal(EnvIdentityOpenIDClaimName, ClaimName),
|
||||
ClaimUserinfo: getCfgVal(EnvIdentityOpenIDClaimUserInfo, ClaimUserinfo) == config.EnableOn,
|
||||
ClaimPrefix: getCfgVal(EnvIdentityOpenIDClaimPrefix, ClaimPrefix),
|
||||
RedirectURI: getCfgVal(EnvIdentityOpenIDRedirectURI, RedirectURI),
|
||||
RedirectURIDynamic: getCfgVal(EnvIdentityOpenIDRedirectURIDynamic, RedirectURIDynamic) == config.EnableOn,
|
||||
ClientID: getCfgVal(EnvIdentityOpenIDClientID, ClientID),
|
||||
ClientSecret: getCfgVal(EnvIdentityOpenIDClientSecret, ClientSecret),
|
||||
RolePolicy: getCfgVal(EnvIdentityOpenIDRolePolicy, RolePolicy),
|
||||
DisplayName: getCfgVal(DisplayName),
|
||||
ClaimName: getCfgVal(ClaimName),
|
||||
ClaimUserinfo: getCfgVal(ClaimUserinfo) == config.EnableOn,
|
||||
ClaimPrefix: getCfgVal(ClaimPrefix),
|
||||
RedirectURI: getCfgVal(RedirectURI),
|
||||
RedirectURIDynamic: getCfgVal(RedirectURIDynamic) == config.EnableOn,
|
||||
ClientID: getCfgVal(ClientID),
|
||||
ClientSecret: getCfgVal(ClientSecret),
|
||||
RolePolicy: getCfgVal(RolePolicy),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,16 +72,16 @@ const (
|
||||
|
||||
// initializeProvider initializes if any additional vendor specific information
|
||||
// was provided, initialization will return an error initial login fails.
|
||||
func (p *providerCfg) initializeProvider(cfgGet func(string, string) string, transport http.RoundTripper) error {
|
||||
vendor := cfgGet(EnvIdentityOpenIDVendor, Vendor)
|
||||
func (p *providerCfg) initializeProvider(cfgGet func(string) string, transport http.RoundTripper) error {
|
||||
vendor := cfgGet(Vendor)
|
||||
if vendor == "" {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
switch vendor {
|
||||
case keyCloakVendor:
|
||||
adminURL := cfgGet(EnvIdentityOpenIDKeyCloakAdminURL, KeyCloakAdminURL)
|
||||
realm := cfgGet(EnvIdentityOpenIDKeyCloakRealm, KeyCloakRealm)
|
||||
adminURL := cfgGet(KeyCloakAdminURL)
|
||||
realm := cfgGet(KeyCloakRealm)
|
||||
p.provider, err = provider.KeyCloak(
|
||||
provider.WithAdminURL(adminURL),
|
||||
provider.WithOpenIDConfig(provider.DiscoveryDoc(p.DiscoveryDoc)),
|
||||
|
||||
@@ -50,10 +50,10 @@ const (
|
||||
schemePrefix = "EC"
|
||||
|
||||
// Min parity disks
|
||||
minParityDisks = 2
|
||||
minParityDisks = 0
|
||||
|
||||
// Default RRS parity is always minimum parity.
|
||||
defaultRRSParity = minParityDisks
|
||||
defaultRRSParity = 1
|
||||
)
|
||||
|
||||
// DefaultKVS - default storage class config
|
||||
@@ -65,7 +65,7 @@ var (
|
||||
},
|
||||
config.KV{
|
||||
Key: ClassRRS,
|
||||
Value: "EC:2",
|
||||
Value: "EC:1",
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -76,7 +76,7 @@ type StorageClass struct {
|
||||
}
|
||||
|
||||
// ConfigLock is a global lock for storage-class config
|
||||
var ConfigLock = sync.RWMutex{}
|
||||
var ConfigLock sync.RWMutex
|
||||
|
||||
// Config storage class configuration
|
||||
type Config struct {
|
||||
@@ -154,7 +154,9 @@ func parseStorageClass(storageClassEnv string) (sc StorageClass, err error) {
|
||||
if err != nil {
|
||||
return StorageClass{}, config.ErrStorageClassValue(err)
|
||||
}
|
||||
|
||||
if parityDisks < 0 {
|
||||
return StorageClass{}, config.ErrStorageClassValue(nil).Msg("Unsupported parity value " + s[1] + " provided")
|
||||
}
|
||||
return StorageClass{
|
||||
Parity: parityDisks,
|
||||
}, nil
|
||||
@@ -196,7 +198,7 @@ func validateParity(ssParity, rrsParity, setDriveCount int) (err error) {
|
||||
}
|
||||
|
||||
if rrsParity > setDriveCount/2 {
|
||||
return fmt.Errorf("Reduced redundancy storage class parity %d should be less than or equal to %d", rrsParity, setDriveCount/2)
|
||||
return fmt.Errorf("Reduced redundancy storage class parity %d should be less than or equal to %d", rrsParity, setDriveCount/2)
|
||||
}
|
||||
|
||||
if ssParity > 0 && rrsParity > 0 {
|
||||
@@ -223,10 +225,6 @@ func (sCfg Config) GetParityForSC(sc string) (parity int) {
|
||||
defer ConfigLock.RUnlock()
|
||||
switch strings.TrimSpace(sc) {
|
||||
case RRS:
|
||||
// set the rrs parity if available
|
||||
if sCfg.RRS.Parity == 0 {
|
||||
return defaultRRSParity
|
||||
}
|
||||
return sCfg.RRS.Parity
|
||||
default:
|
||||
return sCfg.Standard.Parity
|
||||
@@ -248,6 +246,22 @@ func Enabled(kvs config.KVS) bool {
|
||||
return ssc != "" || rrsc != ""
|
||||
}
|
||||
|
||||
// DefaultParityBlocks returns default parity blocks for 'drive' count
|
||||
func DefaultParityBlocks(drive int) int {
|
||||
switch drive {
|
||||
case 1:
|
||||
return 0
|
||||
case 3, 2:
|
||||
return 1
|
||||
case 4, 5:
|
||||
return 2
|
||||
case 6, 7:
|
||||
return 3
|
||||
default:
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
// LookupConfig - lookup storage class config and override with valid environment settings if any.
|
||||
func LookupConfig(kvs config.KVS, setDriveCount int) (cfg Config, err error) {
|
||||
cfg = Config{}
|
||||
@@ -274,10 +288,15 @@ func LookupConfig(kvs config.KVS, setDriveCount int) (cfg Config, err error) {
|
||||
return Config{}, err
|
||||
}
|
||||
}
|
||||
if cfg.RRS.Parity == 0 {
|
||||
|
||||
if cfg.RRS.Parity == 0 && rrsc == "" {
|
||||
cfg.RRS.Parity = defaultRRSParity
|
||||
}
|
||||
|
||||
if cfg.Standard.Parity == 0 && ssc == "" {
|
||||
cfg.Standard.Parity = DefaultParityBlocks(setDriveCount)
|
||||
}
|
||||
|
||||
// Validation is done after parsing both the storage classes. This is needed because we need one
|
||||
// storage class value to deduce the correct value of the other storage class.
|
||||
if err = validateParity(cfg.Standard.Parity, cfg.RRS.Parity, setDriveCount); err != nil {
|
||||
|
||||
@@ -102,7 +102,8 @@ func TestValidateParity(t *testing.T) {
|
||||
{2, 4, true, 16},
|
||||
{3, 3, true, 16},
|
||||
{0, 0, true, 16},
|
||||
{1, 4, false, 16},
|
||||
{1, 4, true, 16},
|
||||
{0, 4, true, 16},
|
||||
{7, 6, false, 16},
|
||||
{9, 0, false, 16},
|
||||
{9, 9, false, 16},
|
||||
@@ -140,7 +141,7 @@ func TestParityCount(t *testing.T) {
|
||||
Parity: 8,
|
||||
},
|
||||
RRS: StorageClass{
|
||||
Parity: 0,
|
||||
Parity: 2,
|
||||
},
|
||||
}
|
||||
// Set env var for test case 4
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
package subnet
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/pkg/env"
|
||||
xnet "github.com/minio/pkg/net"
|
||||
@@ -49,10 +53,13 @@ type Config struct {
|
||||
|
||||
// The HTTP(S) proxy URL to use for connecting to SUBNET
|
||||
ProxyURL *xnet.URL `json:"proxy_url"`
|
||||
|
||||
// Transport configured with proxy_url if set optionally.
|
||||
transport *http.Transport
|
||||
}
|
||||
|
||||
// LookupConfig - lookup config and override with valid environment settings if any.
|
||||
func LookupConfig(kvs config.KVS) (cfg Config, err error) {
|
||||
func LookupConfig(kvs config.KVS, transport http.RoundTripper) (cfg Config, err error) {
|
||||
if err = config.CheckValidKeys(config.SubnetSubSys, kvs, DefaultKVS); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
@@ -63,10 +70,24 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) {
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
cfg.License = env.Get(config.EnvMinIOSubnetLicense, kvs.Get(config.License))
|
||||
cfg.APIKey = env.Get(config.EnvMinIOSubnetAPIKey, kvs.Get(config.APIKey))
|
||||
cfg.License = strings.TrimSpace(env.Get(config.EnvMinIOSubnetLicense, kvs.Get(config.License)))
|
||||
cfg.APIKey = strings.TrimSpace(env.Get(config.EnvMinIOSubnetAPIKey, kvs.Get(config.APIKey)))
|
||||
|
||||
if transport == nil {
|
||||
// when transport is nil, it means we are just validating the
|
||||
// inputs not performing any network calls.
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Make sure to clone the transport before editing the ProxyURL
|
||||
ctransport := transport.(*http.Transport).Clone()
|
||||
if cfg.ProxyURL != nil {
|
||||
ctransport.Proxy = http.ProxyURL((*url.URL)(cfg.ProxyURL))
|
||||
}
|
||||
cfg.transport = ctransport
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
package subnet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -25,58 +25,20 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
)
|
||||
|
||||
const (
|
||||
subnetRespBodyLimit = 1 << 20 // 1 MiB
|
||||
callhomeURL = "https://subnet.min.io/api/callhome"
|
||||
callhomeURLDev = "http://localhost:9000/api/callhome"
|
||||
respBodyLimit = 1 << 20 // 1 MiB
|
||||
)
|
||||
|
||||
func httpClient(timeout time.Duration) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: globalProxyTransport,
|
||||
// Post submit 'payload' to specified URL
|
||||
func (c Config) Post(reqURL string, payload interface{}) (string, error) {
|
||||
if len(c.APIKey) == 0 {
|
||||
return "", errors.New("Deployment is not registered with SUBNET. Please register the deployment via 'mc support register ALIAS'")
|
||||
}
|
||||
}
|
||||
|
||||
func subnetHTTPDo(req *http.Request) (*http.Response, error) {
|
||||
client := httpClient(10 * time.Second)
|
||||
if globalSubnetConfig.ProxyURL != nil {
|
||||
client.Transport.(*http.Transport).Proxy = http.ProxyURL((*url.URL)(globalSubnetConfig.ProxyURL))
|
||||
}
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func subnetReqDo(r *http.Request, authToken string) (string, error) {
|
||||
r.Header.Set("Authorization", authToken)
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := subnetHTTPDo(r)
|
||||
if resp != nil {
|
||||
defer xhttp.DrainBody(resp.Body)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
respBytes, err := ioutil.ReadAll(io.LimitReader(resp.Body, subnetRespBodyLimit))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
respStr := string(respBytes)
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return respStr, nil
|
||||
}
|
||||
return respStr, fmt.Errorf("SUBNET request failed with code %d and error: %s", resp.StatusCode, respStr)
|
||||
}
|
||||
|
||||
func subnetPostReq(reqURL string, payload interface{}, authToken string) (string, error) {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -85,18 +47,30 @@ func subnetPostReq(reqURL string, payload interface{}, authToken string) (string
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return subnetReqDo(r, authToken)
|
||||
}
|
||||
|
||||
func sendCallhomeInfo(ch CallhomeInfo) error {
|
||||
if len(globalSubnetConfig.APIKey) == 0 {
|
||||
return errors.New("Cluster is not registered with SUBNET. Please register by running 'mc support register ALIAS'")
|
||||
r.Header.Set("Authorization", c.APIKey)
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: c.transport,
|
||||
}
|
||||
|
||||
url := callhomeURL
|
||||
if globalIsCICD {
|
||||
url = callhomeURLDev
|
||||
resp, err := client.Do(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err := subnetPostReq(url, ch, globalSubnetConfig.APIKey)
|
||||
return err
|
||||
defer xhttp.DrainBody(resp.Body)
|
||||
|
||||
respBytes, err := ioutil.ReadAll(io.LimitReader(resp.Body, respBodyLimit))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
respStr := string(respBytes)
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return respStr, nil
|
||||
}
|
||||
|
||||
return respStr, fmt.Errorf("SUBNET request failed with code %d and error: %s", resp.StatusCode, respStr)
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func (key ObjectKey) Seal(extKey []byte, iv [32]byte, domain, bucket, object str
|
||||
mac.Write([]byte(SealAlgorithm))
|
||||
mac.Write([]byte(path.Join(bucket, object))) // use path.Join for canonical 'bucket/object'
|
||||
mac.Sum(sealingKey[:0])
|
||||
if n, err := sio.Encrypt(&encryptedKey, bytes.NewReader(key[:]), sio.Config{Key: sealingKey[:], CipherSuites: fips.CipherSuitesDARE()}); n != 64 || err != nil {
|
||||
if n, err := sio.Encrypt(&encryptedKey, bytes.NewReader(key[:]), sio.Config{Key: sealingKey[:], CipherSuites: fips.DARECiphers()}); n != 64 || err != nil {
|
||||
logger.CriticalIf(context.Background(), errors.New("Unable to generate sealed key"))
|
||||
}
|
||||
sealedKey := SealedKey{
|
||||
@@ -121,12 +121,12 @@ func (key *ObjectKey) Unseal(extKey []byte, sealedKey SealedKey, domain, bucket,
|
||||
mac.Write([]byte(domain))
|
||||
mac.Write([]byte(SealAlgorithm))
|
||||
mac.Write([]byte(path.Join(bucket, object))) // use path.Join for canonical 'bucket/object'
|
||||
unsealConfig = sio.Config{MinVersion: sio.Version20, Key: mac.Sum(nil), CipherSuites: fips.CipherSuitesDARE()}
|
||||
unsealConfig = sio.Config{MinVersion: sio.Version20, Key: mac.Sum(nil), CipherSuites: fips.DARECiphers()}
|
||||
case InsecureSealAlgorithm:
|
||||
sha := sha256.New()
|
||||
sha.Write(extKey)
|
||||
sha.Write(sealedKey.IV[:])
|
||||
unsealConfig = sio.Config{MinVersion: sio.Version10, Key: sha.Sum(nil), CipherSuites: fips.CipherSuitesDARE()}
|
||||
unsealConfig = sio.Config{MinVersion: sio.Version10, Key: sha.Sum(nil), CipherSuites: fips.DARECiphers()}
|
||||
}
|
||||
|
||||
if out, err := sio.DecryptBuffer(key[:0], sealedKey.Key[:], unsealConfig); len(out) != 32 || err != nil {
|
||||
@@ -157,7 +157,7 @@ func (key ObjectKey) SealETag(etag []byte) []byte {
|
||||
var buffer bytes.Buffer
|
||||
mac := hmac.New(sha256.New, key[:])
|
||||
mac.Write([]byte("SSE-etag"))
|
||||
if _, err := sio.Encrypt(&buffer, bytes.NewReader(etag), sio.Config{Key: mac.Sum(nil), CipherSuites: fips.CipherSuitesDARE()}); err != nil {
|
||||
if _, err := sio.Encrypt(&buffer, bytes.NewReader(etag), sio.Config{Key: mac.Sum(nil), CipherSuites: fips.DARECiphers()}); err != nil {
|
||||
logger.CriticalIf(context.Background(), errors.New("Unable to encrypt ETag using object key"))
|
||||
}
|
||||
return buffer.Bytes()
|
||||
@@ -173,5 +173,5 @@ func (key ObjectKey) UnsealETag(etag []byte) ([]byte, error) {
|
||||
}
|
||||
mac := hmac.New(sha256.New, key[:])
|
||||
mac.Write([]byte("SSE-etag"))
|
||||
return sio.DecryptBuffer(make([]byte, 0, len(etag)), etag, sio.Config{Key: mac.Sum(nil), CipherSuites: fips.CipherSuitesDARE()})
|
||||
return sio.DecryptBuffer(make([]byte, 0, len(etag)), etag, sio.Config{Key: mac.Sum(nil), CipherSuites: fips.DARECiphers()})
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func unsealObjectKey(clientKey []byte, metadata map[string]string, bucket, objec
|
||||
// EncryptSinglePart encrypts an io.Reader which must be the
|
||||
// the body of a single-part PUT request.
|
||||
func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader {
|
||||
r, err := sio.EncryptReader(r, sio.Config{MinVersion: sio.Version20, Key: key[:], CipherSuites: fips.CipherSuitesDARE()})
|
||||
r, err := sio.EncryptReader(r, sio.Config{MinVersion: sio.Version20, Key: key[:], CipherSuites: fips.DARECiphers()})
|
||||
if err != nil {
|
||||
logger.CriticalIf(context.Background(), errors.New("Unable to encrypt io.Reader using object key"))
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func DecryptSinglePart(w io.Writer, offset, length int64, key ObjectKey) io.Writ
|
||||
const PayloadSize = 1 << 16 // DARE 2.0
|
||||
w = ioutil.LimitedWriter(w, offset%PayloadSize, length)
|
||||
|
||||
decWriter, err := sio.DecryptWriter(w, sio.Config{Key: key[:], CipherSuites: fips.CipherSuitesDARE()})
|
||||
decWriter, err := sio.DecryptWriter(w, sio.Config{Key: key[:], CipherSuites: fips.DARECiphers()})
|
||||
if err != nil {
|
||||
logger.CriticalIf(context.Background(), errors.New("Unable to decrypt io.Writer using object key"))
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ func Decrypt(key []byte, etag ETag) (ETag, error) {
|
||||
plaintext := make([]byte, 0, 16)
|
||||
etag, err := sio.DecryptBuffer(plaintext, etag, sio.Config{
|
||||
Key: decryptionKey,
|
||||
CipherSuites: fips.CipherSuitesDARE(),
|
||||
CipherSuites: fips.DARECiphers(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user