mirror of
https://github.com/pgsty/minio.git
synced 2026-10-16 14:45:59 +03:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddcd419b4c | |||
| a9d9b520ec | |||
| 0c666379fe | |||
| a3f7d575e0 | |||
| f175ff8f66 | |||
| 39874b77ed | |||
| 7106e9394c | |||
| e124d88788 | |||
| de00b641da | |||
| e936871b83 | |||
| 922f5da8dc | |||
| 294bafd58b | |||
| 23ef25b57a | |||
| 9a31030e74 | |||
| df2871de53 | |||
| a3079a7de2 | |||
| 17a37241f0 | |||
| 026a005b64 | |||
| b865b9f43d | |||
| 209e6d00c6 | |||
| dbd7f74bb9 | |||
| 4a2cb23690 | |||
| 320e1533c4 | |||
| 7f45e80755 | |||
| 0db1c94e7d | |||
| e7a4967726 | |||
| d0963974a5 | |||
| 6ea083d197 | |||
| ab7584eeac | |||
| 69e0faa278 | |||
| fa98014bbe | |||
| c25ad94b7e | |||
| 27e07e80bc | |||
| b13bca85d0 | |||
| 29eea52e14 | |||
| b0b4696a64 | |||
| ead8778305 |
@@ -32,10 +32,11 @@ jobs:
|
||||
CGO_ENABLED: 0
|
||||
GO111MODULE: on
|
||||
run: |
|
||||
sudo apt install jq -y
|
||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||
nancy_version=$(curl --retry 10 -Ls -o /dev/null -w "%{url_effective}" https://github.com/sonatype-nexus-community/nancy/releases/latest | sed "s/https:\/\/github.com\/sonatype-nexus-community\/nancy\/releases\/tag\///")
|
||||
curl -L -o nancy https://github.com/sonatype-nexus-community/nancy/releases/download/${nancy_version}/nancy-${nancy_version}-linux-amd64 && chmod +x nancy
|
||||
go list -m all | ./nancy sleuth
|
||||
go list -deps -json ./... | jq -s 'unique_by(.Module.Path)|.[]|select(has("Module"))|.Module' | ./nancy sleuth
|
||||
make
|
||||
make test-race
|
||||
|
||||
@@ -216,6 +216,11 @@ For deployments behind a load balancer, proxy, or ingress rule where the MinIO h
|
||||
|
||||
For example, consider a MinIO deployment behind a proxy `https://minio.example.net`, `https://console.minio.example.net` with rules for forwarding traffic on port :9000 and :9001 to MinIO and the MinIO Console respectively on the internal network. Set `MINIO_BROWSER_REDIRECT_URL` to `https://console.minio.example.net` to ensure the browser receives a valid reachable URL.
|
||||
|
||||
Similarly, if your TLS certificates do not have the IP SAN for the MinIO server host, the MinIO Console may fail to validate the connection to the server. Use the `MINIO_SERVER_URL` environment variable and specify the proxy-accessible hostname of the MinIO server to allow the Console to use the MinIO server API using the TLS certificate.
|
||||
|
||||
For example: `export MINIO_SERVER_URL="https://minio.example.net"`
|
||||
|
||||
|
||||
| Dashboard | Creating a bucket |
|
||||
| ------------- | ------------- |
|
||||
|  |  |
|
||||
|
||||
@@ -562,10 +562,12 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
var ldapUsername string
|
||||
if globalLDAPConfig.Enabled && targetUser != "" {
|
||||
// If LDAP enabled, service accounts need
|
||||
// to be created only for LDAP users.
|
||||
var err error
|
||||
ldapUsername = targetUser
|
||||
targetUser, targetGroups, err = globalLDAPConfig.LookupUserDN(targetUser)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
@@ -604,6 +606,9 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
secretKey: createReq.SecretKey,
|
||||
sessionPolicy: sp,
|
||||
}
|
||||
if ldapUsername != "" {
|
||||
opts.ldapUsername = ldapUsername
|
||||
}
|
||||
newCred, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
@@ -1090,6 +1095,7 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
acctInfo := madmin.AccountInfo{
|
||||
AccountName: accountName,
|
||||
Server: objectAPI.BackendInfo(),
|
||||
Policy: buf,
|
||||
}
|
||||
|
||||
|
||||
@@ -1655,7 +1655,8 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
anonymizeCmdLine := func(cmdLine string) string {
|
||||
if !globalIsDistErasure {
|
||||
// FS mode - single server - hard code to `server1`
|
||||
return strings.Replace(cmdLine, globalLocalNodeName, "server1", -1)
|
||||
anonCmdLine := strings.Replace(cmdLine, globalLocalNodeName, "server1", -1)
|
||||
return strings.Replace(anonCmdLine, globalMinioConsoleHost, "server1", -1)
|
||||
}
|
||||
|
||||
// Server start command regex groups:
|
||||
|
||||
+1
-32
@@ -28,7 +28,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/crypto"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
)
|
||||
@@ -186,42 +185,12 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
||||
}
|
||||
|
||||
if lc, err := globalLifecycleSys.Get(objInfo.Bucket); err == nil {
|
||||
if opts.VersionID == "" {
|
||||
if ruleID, expiryTime := lc.PredictExpiryTime(lifecycle.ObjectOpts{
|
||||
Name: objInfo.Name,
|
||||
UserTags: objInfo.UserTags,
|
||||
VersionID: objInfo.VersionID,
|
||||
ModTime: objInfo.ModTime,
|
||||
IsLatest: objInfo.IsLatest,
|
||||
DeleteMarker: objInfo.DeleteMarker,
|
||||
SuccessorModTime: objInfo.SuccessorModTime,
|
||||
}); !expiryTime.IsZero() {
|
||||
w.Header()[xhttp.AmzExpiration] = []string{
|
||||
fmt.Sprintf(`expiry-date="%s", rule-id="%s"`, expiryTime.Format(http.TimeFormat), ruleID),
|
||||
}
|
||||
}
|
||||
}
|
||||
if objInfo.IsRemote() {
|
||||
// Check if object is being restored. For more information on x-amz-restore header see
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html#API_HeadObject_ResponseSyntax
|
||||
w.Header()[xhttp.AmzStorageClass] = []string{objInfo.TransitionTier}
|
||||
}
|
||||
ruleID, transitionTime := lc.PredictTransitionTime(lifecycle.ObjectOpts{
|
||||
Name: objInfo.Name,
|
||||
UserTags: objInfo.UserTags,
|
||||
VersionID: objInfo.VersionID,
|
||||
ModTime: objInfo.ModTime,
|
||||
IsLatest: objInfo.IsLatest,
|
||||
DeleteMarker: objInfo.DeleteMarker,
|
||||
TransitionStatus: objInfo.TransitionStatus,
|
||||
})
|
||||
if !transitionTime.IsZero() {
|
||||
// This header is a MinIO centric extension to show expected transition date in a similar spirit as x-amz-expiration
|
||||
w.Header()[xhttp.MinIOTransition] = []string{
|
||||
fmt.Sprintf(`transition-date="%s", rule-id="%s"`, transitionTime.Format(http.TimeFormat), ruleID),
|
||||
}
|
||||
}
|
||||
|
||||
lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+22
-19
@@ -213,27 +213,17 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
|
||||
if globalPolicyOPA == nil {
|
||||
// If OPA is not set and if ldap claim key is set, allow the claim.
|
||||
if _, ok := claims.MapClaims[ldapUser]; ok {
|
||||
return claims.Map(), nil
|
||||
}
|
||||
// If OPA is set, return without any further checks.
|
||||
if globalPolicyOPA != nil {
|
||||
return claims.Map(), nil
|
||||
}
|
||||
|
||||
// If OPA is not set, session token should
|
||||
// have a policy and its mandatory, reject
|
||||
// requests without policy claim.
|
||||
_, pokOpenID := claims.MapClaims[iamPolicyClaimNameOpenID()]
|
||||
_, pokSA := claims.MapClaims[iamPolicyClaimNameSA()]
|
||||
if !pokOpenID && !pokSA {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
|
||||
sp, spok := claims.Lookup(iampolicy.SessionPolicyName)
|
||||
if !spok {
|
||||
return claims.Map(), nil
|
||||
}
|
||||
// Check if a session policy is set. If so, decode it here.
|
||||
sp, spok := claims.Lookup(iampolicy.SessionPolicyName)
|
||||
if spok {
|
||||
// Looks like subpolicy is set and is a string, if set then its
|
||||
// base64 encoded, decode it. Decoding fails reject such requests.
|
||||
// base64 encoded, decode it. Decoding fails reject such
|
||||
// requests.
|
||||
spBytes, err := base64.StdEncoding.DecodeString(sp)
|
||||
if err != nil {
|
||||
// Base64 decoding fails, we should log to indicate
|
||||
@@ -244,6 +234,19 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
||||
claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes)
|
||||
}
|
||||
|
||||
// If LDAP claim key is set, return here.
|
||||
if _, ok := claims.MapClaims[ldapUser]; ok {
|
||||
return claims.Map(), nil
|
||||
}
|
||||
|
||||
// Session token must have a policy, reject requests without policy
|
||||
// claim.
|
||||
_, pokOpenID := claims.MapClaims[iamPolicyClaimNameOpenID()]
|
||||
_, pokSA := claims.MapClaims[iamPolicyClaimNameSA()]
|
||||
if !pokOpenID && !pokSA {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
|
||||
return claims.Map(), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -48,18 +48,23 @@ const (
|
||||
type healingTracker struct {
|
||||
disk StorageAPI `msg:"-"`
|
||||
|
||||
ID string
|
||||
PoolIndex int
|
||||
SetIndex int
|
||||
DiskIndex int
|
||||
Path string
|
||||
Endpoint string
|
||||
Started time.Time
|
||||
LastUpdate time.Time
|
||||
ObjectsHealed uint64
|
||||
ObjectsFailed uint64
|
||||
BytesDone uint64
|
||||
BytesFailed uint64
|
||||
ID string
|
||||
PoolIndex int
|
||||
SetIndex int
|
||||
DiskIndex int
|
||||
Path string
|
||||
Endpoint string
|
||||
Started time.Time
|
||||
LastUpdate time.Time
|
||||
|
||||
ObjectsTotalCount uint64
|
||||
ObjectsTotalSize uint64
|
||||
|
||||
ItemsHealed uint64
|
||||
ItemsFailed uint64
|
||||
|
||||
BytesDone uint64
|
||||
BytesFailed uint64
|
||||
|
||||
// Last object scanned.
|
||||
Bucket string `json:"-"`
|
||||
@@ -67,10 +72,10 @@ type healingTracker struct {
|
||||
|
||||
// Numbers when current bucket started healing,
|
||||
// for resuming with correct numbers.
|
||||
ResumeObjectsHealed uint64 `json:"-"`
|
||||
ResumeObjectsFailed uint64 `json:"-"`
|
||||
ResumeBytesDone uint64 `json:"-"`
|
||||
ResumeBytesFailed uint64 `json:"-"`
|
||||
ResumeItemsHealed uint64 `json:"-"`
|
||||
ResumeItemsFailed uint64 `json:"-"`
|
||||
ResumeBytesDone uint64 `json:"-"`
|
||||
ResumeBytesFailed uint64 `json:"-"`
|
||||
|
||||
// Filled on startup/restarts.
|
||||
QueuedBuckets []string
|
||||
@@ -175,8 +180,8 @@ func (h *healingTracker) isHealed(bucket string) bool {
|
||||
|
||||
// resume will reset progress to the numbers at the start of the bucket.
|
||||
func (h *healingTracker) resume() {
|
||||
h.ObjectsHealed = h.ResumeObjectsHealed
|
||||
h.ObjectsFailed = h.ResumeObjectsFailed
|
||||
h.ItemsHealed = h.ResumeItemsHealed
|
||||
h.ItemsFailed = h.ResumeItemsFailed
|
||||
h.BytesDone = h.ResumeBytesDone
|
||||
h.BytesFailed = h.ResumeBytesFailed
|
||||
}
|
||||
@@ -184,8 +189,8 @@ func (h *healingTracker) resume() {
|
||||
// bucketDone should be called when a bucket is done healing.
|
||||
// Adds the bucket to the list of healed buckets and updates resume numbers.
|
||||
func (h *healingTracker) bucketDone(bucket string) {
|
||||
h.ResumeObjectsHealed = h.ObjectsHealed
|
||||
h.ResumeObjectsFailed = h.ObjectsFailed
|
||||
h.ResumeItemsHealed = h.ItemsHealed
|
||||
h.ResumeItemsFailed = h.ItemsFailed
|
||||
h.ResumeBytesDone = h.BytesDone
|
||||
h.ResumeBytesFailed = h.BytesFailed
|
||||
h.HealedBuckets = append(h.HealedBuckets, bucket)
|
||||
@@ -220,22 +225,28 @@ func (h *healingTracker) printTo(writer io.Writer) {
|
||||
// toHealingDisk converts the information to madmin.HealingDisk
|
||||
func (h *healingTracker) toHealingDisk() madmin.HealingDisk {
|
||||
return madmin.HealingDisk{
|
||||
ID: h.ID,
|
||||
Endpoint: h.Endpoint,
|
||||
PoolIndex: h.PoolIndex,
|
||||
SetIndex: h.SetIndex,
|
||||
DiskIndex: h.DiskIndex,
|
||||
Path: h.Path,
|
||||
Started: h.Started.UTC(),
|
||||
LastUpdate: h.LastUpdate.UTC(),
|
||||
ObjectsHealed: h.ObjectsHealed,
|
||||
ObjectsFailed: h.ObjectsFailed,
|
||||
BytesDone: h.BytesDone,
|
||||
BytesFailed: h.BytesFailed,
|
||||
Bucket: h.Bucket,
|
||||
Object: h.Object,
|
||||
QueuedBuckets: h.QueuedBuckets,
|
||||
HealedBuckets: h.HealedBuckets,
|
||||
ID: h.ID,
|
||||
Endpoint: h.Endpoint,
|
||||
PoolIndex: h.PoolIndex,
|
||||
SetIndex: h.SetIndex,
|
||||
DiskIndex: h.DiskIndex,
|
||||
Path: h.Path,
|
||||
Started: h.Started.UTC(),
|
||||
LastUpdate: h.LastUpdate.UTC(),
|
||||
ObjectsTotalCount: h.ObjectsTotalCount,
|
||||
ObjectsTotalSize: h.ObjectsTotalSize,
|
||||
ItemsHealed: h.ItemsHealed,
|
||||
ItemsFailed: h.ItemsFailed,
|
||||
BytesDone: h.BytesDone,
|
||||
BytesFailed: h.BytesFailed,
|
||||
Bucket: h.Bucket,
|
||||
Object: h.Object,
|
||||
QueuedBuckets: h.QueuedBuckets,
|
||||
HealedBuckets: h.HealedBuckets,
|
||||
|
||||
ObjectsHealed: h.ItemsHealed, // Deprecated July 2021
|
||||
ObjectsFailed: h.ItemsFailed, // Deprecated July 2021
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,6 +424,14 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
|
||||
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 {
|
||||
|
||||
@@ -72,16 +72,28 @@ func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err, "LastUpdate")
|
||||
return
|
||||
}
|
||||
case "ObjectsHealed":
|
||||
z.ObjectsHealed, err = dc.ReadUint64()
|
||||
case "ObjectsTotalCount":
|
||||
z.ObjectsTotalCount, err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjectsHealed")
|
||||
err = msgp.WrapError(err, "ObjectsTotalCount")
|
||||
return
|
||||
}
|
||||
case "ObjectsFailed":
|
||||
z.ObjectsFailed, err = dc.ReadUint64()
|
||||
case "ObjectsTotalSize":
|
||||
z.ObjectsTotalSize, err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjectsFailed")
|
||||
err = msgp.WrapError(err, "ObjectsTotalSize")
|
||||
return
|
||||
}
|
||||
case "ItemsHealed":
|
||||
z.ItemsHealed, err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ItemsHealed")
|
||||
return
|
||||
}
|
||||
case "ItemsFailed":
|
||||
z.ItemsFailed, err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ItemsFailed")
|
||||
return
|
||||
}
|
||||
case "BytesDone":
|
||||
@@ -108,16 +120,16 @@ func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err, "Object")
|
||||
return
|
||||
}
|
||||
case "ResumeObjectsHealed":
|
||||
z.ResumeObjectsHealed, err = dc.ReadUint64()
|
||||
case "ResumeItemsHealed":
|
||||
z.ResumeItemsHealed, err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ResumeObjectsHealed")
|
||||
err = msgp.WrapError(err, "ResumeItemsHealed")
|
||||
return
|
||||
}
|
||||
case "ResumeObjectsFailed":
|
||||
z.ResumeObjectsFailed, err = dc.ReadUint64()
|
||||
case "ResumeItemsFailed":
|
||||
z.ResumeItemsFailed, err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ResumeObjectsFailed")
|
||||
err = msgp.WrapError(err, "ResumeItemsFailed")
|
||||
return
|
||||
}
|
||||
case "ResumeBytesDone":
|
||||
@@ -183,9 +195,9 @@ func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// map header, size 20
|
||||
// map header, size 22
|
||||
// write "ID"
|
||||
err = en.Append(0xde, 0x0, 0x14, 0xa2, 0x49, 0x44)
|
||||
err = en.Append(0xde, 0x0, 0x16, 0xa2, 0x49, 0x44)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -264,24 +276,44 @@ func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = msgp.WrapError(err, "LastUpdate")
|
||||
return
|
||||
}
|
||||
// write "ObjectsHealed"
|
||||
err = en.Append(0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
|
||||
// write "ObjectsTotalCount"
|
||||
err = en.Append(0xb1, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteUint64(z.ObjectsHealed)
|
||||
err = en.WriteUint64(z.ObjectsTotalCount)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjectsHealed")
|
||||
err = msgp.WrapError(err, "ObjectsTotalCount")
|
||||
return
|
||||
}
|
||||
// write "ObjectsFailed"
|
||||
err = en.Append(0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
|
||||
// write "ObjectsTotalSize"
|
||||
err = en.Append(0xb0, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteUint64(z.ObjectsFailed)
|
||||
err = en.WriteUint64(z.ObjectsTotalSize)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjectsFailed")
|
||||
err = msgp.WrapError(err, "ObjectsTotalSize")
|
||||
return
|
||||
}
|
||||
// write "ItemsHealed"
|
||||
err = en.Append(0xab, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteUint64(z.ItemsHealed)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ItemsHealed")
|
||||
return
|
||||
}
|
||||
// write "ItemsFailed"
|
||||
err = en.Append(0xab, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteUint64(z.ItemsFailed)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ItemsFailed")
|
||||
return
|
||||
}
|
||||
// write "BytesDone"
|
||||
@@ -324,24 +356,24 @@ func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = msgp.WrapError(err, "Object")
|
||||
return
|
||||
}
|
||||
// write "ResumeObjectsHealed"
|
||||
err = en.Append(0xb3, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
|
||||
// write "ResumeItemsHealed"
|
||||
err = en.Append(0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteUint64(z.ResumeObjectsHealed)
|
||||
err = en.WriteUint64(z.ResumeItemsHealed)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ResumeObjectsHealed")
|
||||
err = msgp.WrapError(err, "ResumeItemsHealed")
|
||||
return
|
||||
}
|
||||
// write "ResumeObjectsFailed"
|
||||
err = en.Append(0xb3, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
|
||||
// write "ResumeItemsFailed"
|
||||
err = en.Append(0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteUint64(z.ResumeObjectsFailed)
|
||||
err = en.WriteUint64(z.ResumeItemsFailed)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ResumeObjectsFailed")
|
||||
err = msgp.WrapError(err, "ResumeItemsFailed")
|
||||
return
|
||||
}
|
||||
// write "ResumeBytesDone"
|
||||
@@ -404,9 +436,9 @@ func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z *healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// map header, size 20
|
||||
// map header, size 22
|
||||
// string "ID"
|
||||
o = append(o, 0xde, 0x0, 0x14, 0xa2, 0x49, 0x44)
|
||||
o = append(o, 0xde, 0x0, 0x16, 0xa2, 0x49, 0x44)
|
||||
o = msgp.AppendString(o, z.ID)
|
||||
// string "PoolIndex"
|
||||
o = append(o, 0xa9, 0x50, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x78)
|
||||
@@ -429,12 +461,18 @@ func (z *healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
// string "LastUpdate"
|
||||
o = append(o, 0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65)
|
||||
o = msgp.AppendTime(o, z.LastUpdate)
|
||||
// string "ObjectsHealed"
|
||||
o = append(o, 0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
|
||||
o = msgp.AppendUint64(o, z.ObjectsHealed)
|
||||
// string "ObjectsFailed"
|
||||
o = append(o, 0xad, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
|
||||
o = msgp.AppendUint64(o, z.ObjectsFailed)
|
||||
// string "ObjectsTotalCount"
|
||||
o = append(o, 0xb1, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74)
|
||||
o = msgp.AppendUint64(o, z.ObjectsTotalCount)
|
||||
// string "ObjectsTotalSize"
|
||||
o = append(o, 0xb0, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65)
|
||||
o = msgp.AppendUint64(o, z.ObjectsTotalSize)
|
||||
// string "ItemsHealed"
|
||||
o = append(o, 0xab, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
|
||||
o = msgp.AppendUint64(o, z.ItemsHealed)
|
||||
// string "ItemsFailed"
|
||||
o = append(o, 0xab, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
|
||||
o = msgp.AppendUint64(o, z.ItemsFailed)
|
||||
// string "BytesDone"
|
||||
o = append(o, 0xa9, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65)
|
||||
o = msgp.AppendUint64(o, z.BytesDone)
|
||||
@@ -447,12 +485,12 @@ func (z *healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
// string "Object"
|
||||
o = append(o, 0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
|
||||
o = msgp.AppendString(o, z.Object)
|
||||
// string "ResumeObjectsHealed"
|
||||
o = append(o, 0xb3, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
|
||||
o = msgp.AppendUint64(o, z.ResumeObjectsHealed)
|
||||
// string "ResumeObjectsFailed"
|
||||
o = append(o, 0xb3, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
|
||||
o = msgp.AppendUint64(o, z.ResumeObjectsFailed)
|
||||
// string "ResumeItemsHealed"
|
||||
o = append(o, 0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x65, 0x64)
|
||||
o = msgp.AppendUint64(o, z.ResumeItemsHealed)
|
||||
// string "ResumeItemsFailed"
|
||||
o = append(o, 0xb1, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64)
|
||||
o = msgp.AppendUint64(o, z.ResumeItemsFailed)
|
||||
// string "ResumeBytesDone"
|
||||
o = append(o, 0xaf, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x42, 0x79, 0x74, 0x65, 0x73, 0x44, 0x6f, 0x6e, 0x65)
|
||||
o = msgp.AppendUint64(o, z.ResumeBytesDone)
|
||||
@@ -540,16 +578,28 @@ func (z *healingTracker) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "LastUpdate")
|
||||
return
|
||||
}
|
||||
case "ObjectsHealed":
|
||||
z.ObjectsHealed, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
case "ObjectsTotalCount":
|
||||
z.ObjectsTotalCount, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjectsHealed")
|
||||
err = msgp.WrapError(err, "ObjectsTotalCount")
|
||||
return
|
||||
}
|
||||
case "ObjectsFailed":
|
||||
z.ObjectsFailed, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
case "ObjectsTotalSize":
|
||||
z.ObjectsTotalSize, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjectsFailed")
|
||||
err = msgp.WrapError(err, "ObjectsTotalSize")
|
||||
return
|
||||
}
|
||||
case "ItemsHealed":
|
||||
z.ItemsHealed, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ItemsHealed")
|
||||
return
|
||||
}
|
||||
case "ItemsFailed":
|
||||
z.ItemsFailed, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ItemsFailed")
|
||||
return
|
||||
}
|
||||
case "BytesDone":
|
||||
@@ -576,16 +626,16 @@ func (z *healingTracker) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "Object")
|
||||
return
|
||||
}
|
||||
case "ResumeObjectsHealed":
|
||||
z.ResumeObjectsHealed, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
case "ResumeItemsHealed":
|
||||
z.ResumeItemsHealed, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ResumeObjectsHealed")
|
||||
err = msgp.WrapError(err, "ResumeItemsHealed")
|
||||
return
|
||||
}
|
||||
case "ResumeObjectsFailed":
|
||||
z.ResumeObjectsFailed, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
case "ResumeItemsFailed":
|
||||
z.ResumeItemsFailed, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ResumeObjectsFailed")
|
||||
err = msgp.WrapError(err, "ResumeItemsFailed")
|
||||
return
|
||||
}
|
||||
case "ResumeBytesDone":
|
||||
@@ -652,7 +702,7 @@ func (z *healingTracker) 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 *healingTracker) Msgsize() (s int) {
|
||||
s = 3 + 3 + msgp.StringPrefixSize + len(z.ID) + 10 + msgp.IntSize + 9 + msgp.IntSize + 10 + msgp.IntSize + 5 + msgp.StringPrefixSize + len(z.Path) + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 8 + msgp.TimeSize + 11 + msgp.TimeSize + 14 + msgp.Uint64Size + 14 + msgp.Uint64Size + 10 + msgp.Uint64Size + 12 + msgp.Uint64Size + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Object) + 20 + msgp.Uint64Size + 20 + msgp.Uint64Size + 16 + msgp.Uint64Size + 18 + msgp.Uint64Size + 14 + msgp.ArrayHeaderSize
|
||||
s = 3 + 3 + msgp.StringPrefixSize + len(z.ID) + 10 + msgp.IntSize + 9 + msgp.IntSize + 10 + msgp.IntSize + 5 + msgp.StringPrefixSize + len(z.Path) + 9 + msgp.StringPrefixSize + len(z.Endpoint) + 8 + msgp.TimeSize + 11 + msgp.TimeSize + 18 + msgp.Uint64Size + 17 + msgp.Uint64Size + 12 + msgp.Uint64Size + 12 + msgp.Uint64Size + 10 + msgp.Uint64Size + 12 + msgp.Uint64Size + 7 + msgp.StringPrefixSize + len(z.Bucket) + 7 + msgp.StringPrefixSize + len(z.Object) + 18 + msgp.Uint64Size + 18 + msgp.Uint64Size + 16 + msgp.Uint64Size + 18 + msgp.Uint64Size + 14 + msgp.ArrayHeaderSize
|
||||
for za0001 := range z.QueuedBuckets {
|
||||
s += msgp.StringPrefixSize + len(z.QueuedBuckets[za0001])
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Validate the transition storage ARNs
|
||||
if err = validateTransitionTier(ctx, bucketLifecycle); err != nil {
|
||||
if err = validateTransitionTier(bucketLifecycle); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
+23
-20
@@ -180,8 +180,8 @@ func initBackgroundTransition(ctx context.Context, objectAPI ObjectLayer) {
|
||||
|
||||
var errInvalidStorageClass = errors.New("invalid storage class")
|
||||
|
||||
func validateTransitionTier(ctx context.Context, lfc *lifecycle.Lifecycle) error {
|
||||
for _, rule := range lfc.Rules {
|
||||
func validateTransitionTier(lc *lifecycle.Lifecycle) error {
|
||||
for _, rule := range lc.Rules {
|
||||
if rule.Transition.StorageClass == "" {
|
||||
continue
|
||||
}
|
||||
@@ -269,13 +269,13 @@ func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *Ob
|
||||
}
|
||||
|
||||
// generate an object name for transitioned object
|
||||
func genTransitionObjName() (string, error) {
|
||||
func genTransitionObjName(bucket string) (string, error) {
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
us := u.String()
|
||||
obj := fmt.Sprintf("%s/%s/%s", us[0:2], us[2:4], us)
|
||||
obj := fmt.Sprintf("%s/%s/%s/%s/%s", globalDeploymentID, bucket, us[0:2], us[2:4], us)
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
@@ -288,15 +288,10 @@ func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lcOpts := lifecycle.ObjectOpts{
|
||||
Name: oi.Name,
|
||||
UserTags: oi.UserTags,
|
||||
}
|
||||
tierName := getLifeCycleTransitionTier(ctx, lc, oi.Bucket, lcOpts)
|
||||
opts := ObjectOptions{
|
||||
Transition: TransitionOptions{
|
||||
Status: lifecycle.TransitionPending,
|
||||
Tier: tierName,
|
||||
Tier: lc.TransitionTier(oi.ToLifecycleOpts()),
|
||||
ETag: oi.ETag,
|
||||
},
|
||||
VersionID: oi.VersionID,
|
||||
@@ -306,16 +301,6 @@ func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo)
|
||||
return objectAPI.TransitionObject(ctx, oi.Bucket, oi.Name, opts)
|
||||
}
|
||||
|
||||
// getLifeCycleTransitionTier returns storage class for transition target
|
||||
func getLifeCycleTransitionTier(ctx context.Context, lc *lifecycle.Lifecycle, bucket string, obj lifecycle.ObjectOpts) string {
|
||||
for _, rule := range lc.FilterActionableRules(obj) {
|
||||
if rule.Transition.StorageClass != "" {
|
||||
return rule.Transition.StorageClass
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getTransitionedObjectReader returns a reader from the transitioned tier.
|
||||
func getTransitionedObjectReader(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, oi ObjectInfo, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||
tgtClient, err := globalTierConfigMgr.getDriver(oi.TransitionTier)
|
||||
@@ -673,3 +658,21 @@ func isRestoredObjectOnDisk(meta map[string]string) (onDisk bool) {
|
||||
}
|
||||
return onDisk
|
||||
}
|
||||
|
||||
// ToLifecycleOpts returns lifecycle.ObjectOpts value for oi.
|
||||
func (oi ObjectInfo) ToLifecycleOpts() lifecycle.ObjectOpts {
|
||||
return lifecycle.ObjectOpts{
|
||||
Name: oi.Name,
|
||||
UserTags: oi.UserTags,
|
||||
VersionID: oi.VersionID,
|
||||
ModTime: oi.ModTime,
|
||||
IsLatest: oi.IsLatest,
|
||||
NumVersions: oi.NumVersions,
|
||||
DeleteMarker: oi.DeleteMarker,
|
||||
SuccessorModTime: oi.SuccessorModTime,
|
||||
RestoreOngoing: oi.RestoreOngoing,
|
||||
RestoreExpires: oi.RestoreExpires,
|
||||
TransitionStatus: oi.TransitionStatus,
|
||||
RemoteTiersImmediately: globalDebugRemoteTiersImmediately,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -240,7 +239,7 @@ func TestValidateTransitionTier(t *testing.T) {
|
||||
t.Fatalf("Test %d: Failed to parse lifecycle config %v", i+1, err)
|
||||
}
|
||||
|
||||
err = validateTransitionTier(context.Background(), lc)
|
||||
err = validateTransitionTier(lc)
|
||||
if err != tc.expectedErr {
|
||||
t.Fatalf("Test %d: Expected %v but got %v", i+1, tc.expectedErr, err)
|
||||
}
|
||||
|
||||
+20
-25
@@ -111,7 +111,10 @@ const consolePrefix = "CONSOLE_"
|
||||
func minioConfigToConsoleFeatures() {
|
||||
os.Setenv("CONSOLE_PBKDF_SALT", globalDeploymentID)
|
||||
os.Setenv("CONSOLE_PBKDF_PASSPHRASE", globalDeploymentID)
|
||||
os.Setenv("CONSOLE_MINIO_SERVER", getAPIEndpoints()[0])
|
||||
if globalMinioEndpoint == "" {
|
||||
logger.Fatal(errInvalidArgument, "Unable to start console service MinIO Endpoint is empty")
|
||||
}
|
||||
os.Setenv("CONSOLE_MINIO_SERVER", globalMinioEndpoint)
|
||||
if value := env.Get("MINIO_LOG_QUERY_URL", ""); value != "" {
|
||||
os.Setenv("CONSOLE_LOG_QUERY_URL", value)
|
||||
if value := env.Get("MINIO_LOG_QUERY_AUTH_TOKEN", ""); value != "" {
|
||||
@@ -200,30 +203,6 @@ func initConsoleServer() (*restapi.Server, error) {
|
||||
restapi.Hostname = globalMinioConsoleHost
|
||||
}
|
||||
|
||||
// subnet license refresh process
|
||||
go func() {
|
||||
// start refreshing subnet license after 5 seconds..
|
||||
time.Sleep(time.Second * 5)
|
||||
|
||||
failedAttempts := 0
|
||||
for {
|
||||
if err := restapi.RefreshLicense(); err != nil {
|
||||
failedAttempts++
|
||||
// end license refresh after 3 consecutive failed attempts
|
||||
if failedAttempts >= 3 {
|
||||
return
|
||||
}
|
||||
// wait 5 minutes and retry again
|
||||
time.Sleep(time.Minute * 5)
|
||||
continue
|
||||
}
|
||||
// if license refreshed successfully reset the counter
|
||||
failedAttempts = 0
|
||||
// try to refresh license every 24 hrs
|
||||
time.Sleep(time.Hour * 24)
|
||||
}
|
||||
}()
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
@@ -433,6 +412,22 @@ func handleCommonEnvVars() {
|
||||
}
|
||||
}
|
||||
|
||||
if serverURL := env.Get(config.EnvMinIOServerURL, globalEndpoints.Localhost()); serverURL != "" {
|
||||
u, err := xnet.ParseHTTPURL(serverURL)
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Invalid MINIO_SERVER_URL value in environment variable")
|
||||
}
|
||||
// Look for if URL has invalid values and return error.
|
||||
if !((u.Scheme == "http" || u.Scheme == "https") &&
|
||||
(u.Path == "/" || u.Path == "") && u.Opaque == "" &&
|
||||
!u.ForceQuery && u.RawQuery == "" && u.Fragment == "") {
|
||||
err := fmt.Errorf("URL contains unexpected resources, expected URL to be of http(s)://minio.example.com format: %v", u)
|
||||
logger.Fatal(err, "Invalid MINIO_SERVER_URL value is environment variable")
|
||||
}
|
||||
u.Path = "" // remove any path component such as `/`
|
||||
globalMinioEndpoint = u.String()
|
||||
}
|
||||
|
||||
globalFSOSync, err = config.ParseBool(env.Get(config.EnvFSOSync, config.EnableOff))
|
||||
if err != nil {
|
||||
logger.Fatal(config.ErrInvalidFSOSyncValue(err), "Invalid MINIO_FS_OSYNC value in environment variable")
|
||||
|
||||
+2
-31
@@ -1020,22 +1020,7 @@ func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, meta acti
|
||||
}
|
||||
|
||||
func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, obj ObjectInfo, debug bool) (action lifecycle.Action) {
|
||||
lcOpts := lifecycle.ObjectOpts{
|
||||
Name: obj.Name,
|
||||
UserTags: obj.UserTags,
|
||||
ModTime: obj.ModTime,
|
||||
VersionID: obj.VersionID,
|
||||
DeleteMarker: obj.DeleteMarker,
|
||||
IsLatest: obj.IsLatest,
|
||||
NumVersions: obj.NumVersions,
|
||||
SuccessorModTime: obj.SuccessorModTime,
|
||||
RestoreOngoing: obj.RestoreOngoing,
|
||||
RestoreExpires: obj.RestoreExpires,
|
||||
TransitionStatus: obj.TransitionStatus,
|
||||
RemoteTiersImmediately: globalDebugRemoteTiersImmediately,
|
||||
}
|
||||
|
||||
action = lc.ComputeAction(lcOpts)
|
||||
action = lc.ComputeAction(obj.ToLifecycleOpts())
|
||||
if debug {
|
||||
console.Debugf(applyActionsLogPrefix+" lifecycle: Secondary scan: %v\n", action)
|
||||
}
|
||||
@@ -1086,25 +1071,11 @@ func applyTransitionAction(ctx context.Context, action lifecycle.Action, objLaye
|
||||
}
|
||||
|
||||
func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, restoredObject bool) bool {
|
||||
lcOpts := lifecycle.ObjectOpts{
|
||||
Name: obj.Name,
|
||||
UserTags: obj.UserTags,
|
||||
ModTime: obj.ModTime,
|
||||
VersionID: obj.VersionID,
|
||||
DeleteMarker: obj.DeleteMarker,
|
||||
IsLatest: obj.IsLatest,
|
||||
NumVersions: obj.NumVersions,
|
||||
SuccessorModTime: obj.SuccessorModTime,
|
||||
RestoreOngoing: obj.RestoreOngoing,
|
||||
RestoreExpires: obj.RestoreExpires,
|
||||
TransitionStatus: obj.TransitionStatus,
|
||||
}
|
||||
|
||||
action := expireObj
|
||||
if restoredObject {
|
||||
action = expireRestoredObj
|
||||
}
|
||||
if err := expireTransitionedObject(ctx, objLayer, &obj, lcOpts, action); err != nil {
|
||||
if err := expireTransitionedObject(ctx, objLayer, &obj, obj.ToLifecycleOpts(), action); err != nil {
|
||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
+2
-2
@@ -20,7 +20,6 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -66,7 +65,8 @@ func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, dui <-chan
|
||||
func loadPrefixUsageFromBackend(ctx context.Context, objAPI ObjectLayer, bucket string) (map[string]uint64, error) {
|
||||
z, ok := objAPI.(*erasureServerPools)
|
||||
if !ok {
|
||||
return nil, errors.New("prefix usage is not supported")
|
||||
// Prefix usage is empty
|
||||
return map[string]uint64{}, nil
|
||||
}
|
||||
|
||||
cache := dataUsageCache{}
|
||||
|
||||
+6
-2
@@ -245,7 +245,7 @@ func (l *EndpointServerPools) Add(zeps PoolEndpoints) error {
|
||||
func (l EndpointServerPools) Localhost() string {
|
||||
for _, ep := range l {
|
||||
for _, endpoint := range ep.Endpoints {
|
||||
if endpoint.IsLocal {
|
||||
if endpoint.IsLocal && endpoint.Host != "" {
|
||||
u := &url.URL{
|
||||
Scheme: endpoint.Scheme,
|
||||
Host: endpoint.Host,
|
||||
@@ -254,7 +254,11 @@ func (l EndpointServerPools) Localhost() string {
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
host := globalMinioHost
|
||||
if host == "" {
|
||||
host = sortIPs(localIP4.ToSlice())[0]
|
||||
}
|
||||
return fmt.Sprintf("%s://%s", getURLScheme(globalIsTLS), net.JoinHostPort(host, globalMinioPort))
|
||||
}
|
||||
|
||||
// LocalDisksPaths returns the disk paths of the local disks
|
||||
|
||||
@@ -47,38 +47,6 @@ func (er erasureObjects) getLoadBalancedLocalDisks() (newDisks []StorageAPI) {
|
||||
return newDisks
|
||||
}
|
||||
|
||||
func (er erasureObjects) getOnlineDisks() (newDisks []StorageAPI) {
|
||||
disks := er.getDisks()
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
for _, i := range hashOrder(UTCNow().String(), len(disks)) {
|
||||
i := i
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if disks[i-1] == nil {
|
||||
return
|
||||
}
|
||||
di, err := disks[i-1].DiskInfo(context.Background())
|
||||
if err != nil || di.Healing {
|
||||
// - Do not consume disks which are not reachable
|
||||
// unformatted or simply not accessible for some reason.
|
||||
//
|
||||
// - Do not consume disks which are being healed
|
||||
//
|
||||
// - Future: skip busy disks
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
newDisks = append(newDisks, disks[i-1])
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return newDisks
|
||||
}
|
||||
|
||||
// getLoadBalancedDisks - fetches load balanced (sufficiently randomized) disk slice.
|
||||
// ensures to skip disks if they are not healing and online.
|
||||
func (er erasureObjects) getLoadBalancedDisks(optimized bool) []StorageAPI {
|
||||
|
||||
@@ -189,35 +189,6 @@ func getLatestFileInfo(ctx context.Context, partsMetadata []FileInfo, errs []err
|
||||
return latestFileInfo, nil
|
||||
}
|
||||
|
||||
// fileInfoConsistent whether all fileinfos are consistent with each other.
|
||||
// Will return false if any fileinfo mismatches.
|
||||
func fileInfoConsistent(ctx context.Context, partsMetadata []FileInfo, errs []error) bool {
|
||||
// There should be atleast half correct entries, if not return failure
|
||||
if reducedErr := reduceReadQuorumErrs(ctx, errs, nil, len(partsMetadata)/2); reducedErr != nil {
|
||||
return false
|
||||
}
|
||||
if len(partsMetadata) == 1 {
|
||||
return true
|
||||
}
|
||||
// Reference
|
||||
ref := partsMetadata[0]
|
||||
if !ref.IsValid() {
|
||||
return false
|
||||
}
|
||||
for _, meta := range partsMetadata[1:] {
|
||||
if !meta.IsValid() {
|
||||
return false
|
||||
}
|
||||
if !meta.ModTime.Equal(ref.ModTime) {
|
||||
return false
|
||||
}
|
||||
if meta.DataDir != ref.DataDir {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// disksWithAllParts - This function needs to be called with
|
||||
// []StorageAPI returned by listOnlineDisks. Returns,
|
||||
//
|
||||
@@ -225,8 +196,9 @@ func fileInfoConsistent(ctx context.Context, partsMetadata []FileInfo, errs []er
|
||||
//
|
||||
// - slice of errors about the state of data files on disk - can have
|
||||
// a not-found error or a hash-mismatch error.
|
||||
func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo, errs []error, bucket,
|
||||
object string, scanMode madmin.HealScanMode) ([]StorageAPI, []error) {
|
||||
func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo,
|
||||
errs []error, bucket, object string, scanMode madmin.HealScanMode) ([]StorageAPI, []error) {
|
||||
|
||||
// List of disks having latest version of the object er.meta (by modtime)
|
||||
_, modTime, dataDir := listOnlineDisks(onlineDisks, partsMetadata, errs)
|
||||
|
||||
@@ -239,15 +211,17 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
||||
// consider the offline disks as consistent.
|
||||
continue
|
||||
}
|
||||
if len(meta.Erasure.Distribution) != len(onlineDisks) {
|
||||
// Erasure distribution seems to have lesser
|
||||
// number of items than number of online disks.
|
||||
inconsistent++
|
||||
continue
|
||||
}
|
||||
if meta.Erasure.Distribution[i] != meta.Erasure.Index {
|
||||
// Mismatch indexes with distribution order
|
||||
inconsistent++
|
||||
if !meta.Deleted {
|
||||
if len(meta.Erasure.Distribution) != len(onlineDisks) {
|
||||
// Erasure distribution seems to have lesser
|
||||
// number of items than number of online disks.
|
||||
inconsistent++
|
||||
continue
|
||||
}
|
||||
if meta.Erasure.Distribution[i] != meta.Erasure.Index {
|
||||
// Mismatch indexes with distribution order
|
||||
inconsistent++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,8 +241,8 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
||||
dataErrs[i] = errDiskNotFound
|
||||
continue
|
||||
}
|
||||
meta := partsMetadata[i]
|
||||
|
||||
meta := partsMetadata[i]
|
||||
if !meta.ModTime.Equal(modTime) || meta.DataDir != dataDir {
|
||||
dataErrs[i] = errFileCorrupt
|
||||
partsMetadata[i] = FileInfo{}
|
||||
@@ -280,20 +254,22 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
||||
continue
|
||||
}
|
||||
|
||||
if len(meta.Erasure.Distribution) != len(onlineDisks) {
|
||||
// Erasure distribution is not the same as onlineDisks
|
||||
// attempt a fix if possible, assuming other entries
|
||||
// might have the right erasure distribution.
|
||||
partsMetadata[i] = FileInfo{}
|
||||
dataErrs[i] = errFileCorrupt
|
||||
continue
|
||||
}
|
||||
if !meta.Deleted {
|
||||
if len(meta.Erasure.Distribution) != len(onlineDisks) {
|
||||
// Erasure distribution is not the same as onlineDisks
|
||||
// attempt a fix if possible, assuming other entries
|
||||
// might have the right erasure distribution.
|
||||
partsMetadata[i] = FileInfo{}
|
||||
dataErrs[i] = errFileCorrupt
|
||||
continue
|
||||
}
|
||||
|
||||
// Since erasure.Distribution is trustable we can fix the mismatching erasure.Index
|
||||
if meta.Erasure.Distribution[i] != meta.Erasure.Index {
|
||||
partsMetadata[i] = FileInfo{}
|
||||
dataErrs[i] = errFileCorrupt
|
||||
continue
|
||||
// Since erasure.Distribution is trustable we can fix the mismatching erasure.Index
|
||||
if meta.Erasure.Distribution[i] != meta.Erasure.Index {
|
||||
partsMetadata[i] = FileInfo{}
|
||||
dataErrs[i] = errFileCorrupt
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,11 +296,11 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
||||
// disk has a valid xl.meta but may not have all the
|
||||
// parts. This is considered an outdated disk, since
|
||||
// it needs healing too.
|
||||
if !partsMetadata[i].IsRemote() {
|
||||
if !partsMetadata[i].Deleted && !partsMetadata[i].IsRemote() {
|
||||
dataErrs[i] = onlineDisk.VerifyFile(ctx, bucket, object, partsMetadata[i])
|
||||
}
|
||||
case madmin.HealNormalScan:
|
||||
if !partsMetadata[i].IsRemote() {
|
||||
if !partsMetadata[i].Deleted && !partsMetadata[i].IsRemote() {
|
||||
dataErrs[i] = onlineDisk.CheckParts(ctx, bucket, object, partsMetadata[i])
|
||||
}
|
||||
}
|
||||
|
||||
+27
-41
@@ -212,7 +212,7 @@ func shouldHealObjectOnDisk(erErr, dataErr error, meta FileInfo, quorumModTime t
|
||||
return true
|
||||
}
|
||||
if erErr == nil {
|
||||
if !meta.IsRemote() {
|
||||
if !meta.Deleted && !meta.IsRemote() {
|
||||
// If xl.meta was read fine but there may be problem with the part.N files.
|
||||
if IsErr(dataErr, []error{
|
||||
errFileNotFound,
|
||||
@@ -267,10 +267,10 @@ 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)
|
||||
|
||||
_, err = getLatestFileInfo(ctx, partsMetadata, errs)
|
||||
if err != nil {
|
||||
if _, err = getLatestFileInfo(ctx, partsMetadata, errs); err != nil {
|
||||
return er.purgeObjectDangling(ctx, bucket, object, versionID, partsMetadata, errs, []error{}, opts)
|
||||
}
|
||||
|
||||
// List of disks having latest version of the object er.meta
|
||||
// (by modtime).
|
||||
_, modTime, dataDir := listOnlineDisks(storageDisks, partsMetadata, errs)
|
||||
@@ -292,7 +292,8 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
// used here for reconstruction. This is done to ensure that
|
||||
// we do not skip drives that have inconsistent metadata to be
|
||||
// skipped from purging when they are stale.
|
||||
availableDisks, dataErrs := disksWithAllParts(ctx, storageDisks, partsMetadata, errs, bucket, object, scanMode)
|
||||
availableDisks, dataErrs := disksWithAllParts(ctx, storageDisks, partsMetadata,
|
||||
errs, bucket, object, scanMode)
|
||||
|
||||
// Loop to find number of disks with valid data, per-drive
|
||||
// data state and a list of outdated disks on which data needs
|
||||
@@ -357,7 +358,8 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
err = toObjectErr(errFileVersionNotFound, bucket, object, versionID)
|
||||
}
|
||||
// File is fully gone, fileInfo is empty.
|
||||
return defaultHealResult(FileInfo{}, storageDisks, storageEndpoints, errs, bucket, object, versionID, er.defaultParityCount), err
|
||||
return er.defaultHealResult(FileInfo{}, storageDisks, storageEndpoints, errs,
|
||||
bucket, object, versionID), err
|
||||
}
|
||||
|
||||
// If less than read quorum number of disks have all the parts
|
||||
@@ -642,30 +644,25 @@ func (er erasureObjects) healObjectDir(ctx context.Context, bucket, object strin
|
||||
|
||||
// Populates default heal result item entries with possible values when we are returning prematurely.
|
||||
// This is to ensure that in any circumstance we are not returning empty arrays with wrong values.
|
||||
func defaultHealResult(lfi FileInfo, storageDisks []StorageAPI, storageEndpoints []string, errs []error, bucket, object, versionID string, defaultParityCount int) madmin.HealResultItem {
|
||||
func (er erasureObjects) defaultHealResult(lfi FileInfo, storageDisks []StorageAPI, storageEndpoints []string, errs []error, bucket, object, versionID string) madmin.HealResultItem {
|
||||
// Initialize heal result object
|
||||
result := madmin.HealResultItem{
|
||||
Type: madmin.HealItemObject,
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
VersionID: versionID,
|
||||
DiskCount: len(storageDisks),
|
||||
Type: madmin.HealItemObject,
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
ObjectSize: lfi.Size,
|
||||
VersionID: versionID,
|
||||
DiskCount: len(storageDisks),
|
||||
}
|
||||
|
||||
if lfi.IsValid() {
|
||||
result.ObjectSize = lfi.Size
|
||||
result.ParityBlocks = lfi.Erasure.ParityBlocks
|
||||
} else {
|
||||
// Default to most common configuration for erasure blocks.
|
||||
result.ParityBlocks = defaultParityCount
|
||||
result.ParityBlocks = er.defaultParityCount
|
||||
}
|
||||
result.DataBlocks = len(storageDisks) - result.ParityBlocks
|
||||
|
||||
if errs == nil {
|
||||
// No disks related errors are provided, quit
|
||||
return result
|
||||
}
|
||||
|
||||
for index, disk := range storageDisks {
|
||||
if disk == nil {
|
||||
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
|
||||
@@ -684,6 +681,8 @@ func defaultHealResult(lfi FileInfo, storageDisks []StorageAPI, storageEndpoints
|
||||
switch errs[index] {
|
||||
case errFileNotFound, errVolumeNotFound:
|
||||
driveState = madmin.DriveStateMissing
|
||||
case nil:
|
||||
driveState = madmin.DriveStateOk
|
||||
}
|
||||
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
@@ -697,15 +696,6 @@ func defaultHealResult(lfi FileInfo, storageDisks []StorageAPI, storageEndpoints
|
||||
})
|
||||
}
|
||||
|
||||
if !lfi.IsValid() {
|
||||
// Default to most common configuration for erasure blocks.
|
||||
result.ParityBlocks = defaultParityCount
|
||||
result.DataBlocks = len(storageDisks) - defaultParityCount
|
||||
} else {
|
||||
result.ParityBlocks = lfi.Erasure.ParityBlocks
|
||||
result.DataBlocks = lfi.Erasure.DataBlocks
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -820,15 +810,18 @@ func (er erasureObjects) purgeObjectDangling(ctx context.Context, bucket, object
|
||||
if versionID != "" {
|
||||
err = toObjectErr(errFileVersionNotFound, bucket, object, versionID)
|
||||
}
|
||||
return defaultHealResult(m, storageDisks, storageEndpoints, errs, bucket, object, versionID, er.defaultParityCount), err
|
||||
return er.defaultHealResult(m, storageDisks, storageEndpoints,
|
||||
errs, bucket, object, versionID), err
|
||||
}
|
||||
return defaultHealResult(m, storageDisks, storageEndpoints, errs, bucket, object, versionID, er.defaultParityCount), toObjectErr(err, bucket, object, versionID)
|
||||
return er.defaultHealResult(m, storageDisks, storageEndpoints,
|
||||
errs, bucket, object, versionID), toObjectErr(err, bucket, object, versionID)
|
||||
}
|
||||
|
||||
readQuorum := len(storageDisks) - er.defaultParityCount
|
||||
|
||||
err := toObjectErr(reduceReadQuorumErrs(ctx, errs, objectOpIgnoredErrs, readQuorum), bucket, object, versionID)
|
||||
return defaultHealResult(m, storageDisks, storageEndpoints, errs, bucket, object, versionID, er.defaultParityCount), err
|
||||
err := toObjectErr(reduceReadQuorumErrs(ctx, errs, objectOpIgnoredErrs, readQuorum),
|
||||
bucket, object, versionID)
|
||||
return er.defaultHealResult(m, storageDisks, storageEndpoints, errs, bucket, object, versionID), err
|
||||
}
|
||||
|
||||
// Object is considered dangling/corrupted if any only
|
||||
@@ -914,22 +907,15 @@ func (er erasureObjects) HealObject(ctx context.Context, bucket, object, version
|
||||
|
||||
// Perform quick read without lock.
|
||||
// This allows to quickly check if all is ok or all are missing.
|
||||
partsMetadata, errs := readAllFileInfo(healCtx, storageDisks, bucket, object, versionID, false)
|
||||
_, errs := readAllFileInfo(healCtx, storageDisks, bucket, object, versionID, false)
|
||||
if isAllNotFound(errs) {
|
||||
err = toObjectErr(errFileNotFound, bucket, object)
|
||||
if versionID != "" {
|
||||
err = toObjectErr(errFileVersionNotFound, bucket, object, versionID)
|
||||
}
|
||||
// Nothing to do, file is already gone.
|
||||
return defaultHealResult(FileInfo{}, storageDisks, storageEndpoints, errs, bucket, object, versionID, er.defaultParityCount), err
|
||||
}
|
||||
|
||||
// Return early if all ok and not deep scanning.
|
||||
if opts.ScanMode == madmin.HealNormalScan && fileInfoConsistent(ctx, partsMetadata, errs) {
|
||||
fi, err := getLatestFileInfo(ctx, partsMetadata, errs)
|
||||
if err == nil && fi.VersionID == versionID {
|
||||
return defaultHealResult(fi, storageDisks, storageEndpoints, errs, bucket, object, versionID, fi.Erasure.ParityBlocks), nil
|
||||
}
|
||||
return er.defaultHealResult(FileInfo{}, storageDisks, storageEndpoints,
|
||||
errs, bucket, object, versionID), err
|
||||
}
|
||||
|
||||
// Heal the object.
|
||||
|
||||
@@ -945,7 +945,7 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
if disk != nil && disk.IsOnline() {
|
||||
continue
|
||||
}
|
||||
er.addPartial(bucket, object, fi.VersionID)
|
||||
er.addPartial(bucket, object, fi.VersionID, fi.Size)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
+35
-22
@@ -267,7 +267,7 @@ func (er erasureObjects) getObjectWithFileInfo(ctx context.Context, bucket, obje
|
||||
// due to a bug in RenameData() the fi.Data is not niled leading to
|
||||
// GetObject thinking that fi.Data is valid while fi.Size has
|
||||
// changed already.
|
||||
if _, ok := fi.Metadata[ReservedMetadataPrefixLower+"inline-data"]; ok {
|
||||
if fi.InlineData() {
|
||||
shardFileSize := erasure.ShardFileSize(fi.Size)
|
||||
if shardFileSize >= 0 && shardFileSize >= smallFileThreshold {
|
||||
for i := range metaArr {
|
||||
@@ -442,7 +442,9 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
||||
}
|
||||
|
||||
// if missing metadata can be reconstructed, attempt to reconstruct.
|
||||
if missingBlocks > 0 && missingBlocks < readQuorum {
|
||||
// additionally do not heal delete markers inline, let them be
|
||||
// healed upon regular heal process.
|
||||
if !fi.Deleted && missingBlocks > 0 && missingBlocks < readQuorum {
|
||||
if _, healing := er.getOnlineDisksWithHealing(); !healing {
|
||||
go healObject(bucket, object, fi.VersionID, madmin.HealNormalScan)
|
||||
}
|
||||
@@ -816,7 +818,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
if len(inlineBuffers) > 0 {
|
||||
// Set an additional header when data is inlined.
|
||||
for index := range partsMetadata {
|
||||
partsMetadata[index].Metadata[ReservedMetadataPrefixLower+"inline-data"] = "true"
|
||||
partsMetadata[index].SetInlineData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,16 +828,6 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
// Whether a disk was initially or becomes offline
|
||||
// during this upload, send it to the MRF list.
|
||||
for i := 0; i < len(onlineDisks); i++ {
|
||||
if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
|
||||
continue
|
||||
}
|
||||
er.addPartial(bucket, object, fi.VersionID)
|
||||
break
|
||||
}
|
||||
|
||||
for i := 0; i < len(onlineDisks); i++ {
|
||||
if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
|
||||
// Object info is the same in all disks, so we can pick
|
||||
@@ -844,6 +836,17 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Whether a disk was initially or becomes offline
|
||||
// during this upload, send it to the MRF list.
|
||||
for i := 0; i < len(onlineDisks); i++ {
|
||||
if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
|
||||
continue
|
||||
}
|
||||
er.addPartial(bucket, object, fi.VersionID, fi.Size)
|
||||
break
|
||||
}
|
||||
|
||||
online = countOnlineDisks(onlineDisks)
|
||||
|
||||
return fi.ToObjectInfo(bucket, object), nil
|
||||
@@ -1029,7 +1032,7 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
||||
|
||||
// all other direct versionId references we should
|
||||
// ensure no dangling file is left over.
|
||||
er.addPartial(bucket, version.Name, version.VersionID)
|
||||
er.addPartial(bucket, version.Name, version.VersionID, -1)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1177,7 +1180,7 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
||||
if disk != nil && disk.IsOnline() {
|
||||
continue
|
||||
}
|
||||
er.addPartial(bucket, object, opts.VersionID)
|
||||
er.addPartial(bucket, object, opts.VersionID, -1)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1192,11 +1195,15 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
||||
|
||||
// Send the successful but partial upload/delete, however ignore
|
||||
// if the channel is blocked by other items.
|
||||
func (er erasureObjects) addPartial(bucket, object, versionID string) {
|
||||
select {
|
||||
case er.mrfOpCh <- partialOperation{bucket: bucket, object: object, versionID: versionID}:
|
||||
default:
|
||||
}
|
||||
func (er erasureObjects) addPartial(bucket, object, versionID string, size int64) {
|
||||
globalMRFState.addPartialOp(partialOperation{
|
||||
bucket: bucket,
|
||||
object: object,
|
||||
versionID: versionID,
|
||||
size: size,
|
||||
setIndex: er.setIndex,
|
||||
poolIndex: er.poolIndex,
|
||||
})
|
||||
}
|
||||
|
||||
func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
|
||||
@@ -1388,7 +1395,13 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
|
||||
return toObjectErr(err, bucket, object)
|
||||
}
|
||||
}
|
||||
destObj, err := genTransitionObjName()
|
||||
|
||||
// object content is small enough that it's inlined, skipping transition
|
||||
if fi.InlineData() {
|
||||
return nil
|
||||
}
|
||||
|
||||
destObj, err := genTransitionObjName(bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1421,7 +1434,7 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
|
||||
if disk != nil && disk.IsOnline() {
|
||||
continue
|
||||
}
|
||||
er.addPartial(bucket, object, opts.VersionID)
|
||||
er.addPartial(bucket, object, opts.VersionID, -1)
|
||||
break
|
||||
}
|
||||
// Notify object deleted event.
|
||||
|
||||
+1
-81
@@ -96,8 +96,6 @@ type erasureSets struct {
|
||||
|
||||
disksStorageInfoCache timedValue
|
||||
|
||||
mrfMU sync.Mutex
|
||||
mrfOperations map[healSource]int
|
||||
lastConnectDisksOpTime time.Time
|
||||
}
|
||||
|
||||
@@ -268,20 +266,11 @@ func (s *erasureSets) connectDisks() {
|
||||
wg.Wait()
|
||||
|
||||
go func() {
|
||||
idler := time.NewTimer(100 * time.Millisecond)
|
||||
defer idler.Stop()
|
||||
|
||||
for setIndex, justConnected := range setsJustConnected {
|
||||
if !justConnected {
|
||||
continue
|
||||
}
|
||||
|
||||
// Send a new set connect event with a timeout
|
||||
idler.Reset(100 * time.Millisecond)
|
||||
select {
|
||||
case s.setReconnectEvent <- setIndex:
|
||||
case <-idler.C:
|
||||
}
|
||||
globalMRFState.newSetReconnected(setIndex, s.poolIndex)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -375,7 +364,6 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
|
||||
setReconnectEvent: make(chan int),
|
||||
distributionAlgo: format.Erasure.DistributionAlgo,
|
||||
deploymentID: uuid.MustParse(format.ID),
|
||||
mrfOperations: make(map[healSource]int),
|
||||
poolIndex: poolIdx,
|
||||
}
|
||||
|
||||
@@ -446,7 +434,6 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
|
||||
nsMutex: mutex,
|
||||
bp: bp,
|
||||
bpOld: bpOld,
|
||||
mrfOpCh: make(chan partialOperation, 10000),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,8 +453,6 @@ func newErasureSets(ctx context.Context, endpoints Endpoints, storageDisks []Sto
|
||||
|
||||
// Start the disk monitoring and connect routine.
|
||||
go s.monitorAndConnectEndpoints(ctx, defaultMonitorConnectEndpointInterval)
|
||||
go s.maintainMRFList()
|
||||
go s.healMRFRoutine()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
@@ -1388,71 +1373,6 @@ func (s *erasureSets) GetObjectTags(ctx context.Context, bucket, object string,
|
||||
return er.GetObjectTags(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
// maintainMRFList gathers the list of successful partial uploads
|
||||
// from all underlying er.sets and puts them in a global map which
|
||||
// should not have more than 10000 entries.
|
||||
func (s *erasureSets) maintainMRFList() {
|
||||
var agg = make(chan partialOperation, 10000)
|
||||
for i, er := range s.sets {
|
||||
go func(c <-chan partialOperation, setIndex int) {
|
||||
for msg := range c {
|
||||
msg.failedSet = setIndex
|
||||
select {
|
||||
case agg <- msg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}(er.mrfOpCh, i)
|
||||
}
|
||||
|
||||
for fOp := range agg {
|
||||
s.mrfMU.Lock()
|
||||
if len(s.mrfOperations) > 10000 {
|
||||
s.mrfMU.Unlock()
|
||||
continue
|
||||
}
|
||||
s.mrfOperations[healSource{
|
||||
bucket: fOp.bucket,
|
||||
object: fOp.object,
|
||||
versionID: fOp.versionID,
|
||||
opts: &madmin.HealOpts{Remove: true},
|
||||
}] = fOp.failedSet
|
||||
s.mrfMU.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// healMRFRoutine monitors new disks connection, sweep the MRF list
|
||||
// to find objects related to the new disk that needs to be healed.
|
||||
func (s *erasureSets) healMRFRoutine() {
|
||||
// Wait until background heal state is initialized
|
||||
bgSeq := mustGetHealSequence(GlobalContext)
|
||||
|
||||
for setIndex := range s.setReconnectEvent {
|
||||
// Get the list of objects related the er.set
|
||||
// to which the connected disk belongs.
|
||||
var mrfOperations []healSource
|
||||
s.mrfMU.Lock()
|
||||
for k, v := range s.mrfOperations {
|
||||
if v == setIndex {
|
||||
mrfOperations = append(mrfOperations, k)
|
||||
}
|
||||
}
|
||||
s.mrfMU.Unlock()
|
||||
|
||||
// Heal objects
|
||||
for _, u := range mrfOperations {
|
||||
waitForLowHTTPReq(globalHealConfig.IOCount, globalHealConfig.Sleep)
|
||||
|
||||
// Send an object to background heal
|
||||
bgSeq.sourceCh <- u
|
||||
|
||||
s.mrfMU.Lock()
|
||||
delete(s.mrfOperations, u)
|
||||
s.mrfMU.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TransitionObject - transition object content to target tier.
|
||||
func (s *erasureSets) TransitionObject(ctx context.Context, bucket, object string, opts ObjectOptions) error {
|
||||
return s.getHashedSet(object).TransitionObject(ctx, bucket, object, opts)
|
||||
|
||||
@@ -39,15 +39,6 @@ import (
|
||||
// OfflineDisk represents an unavailable disk.
|
||||
var OfflineDisk StorageAPI // zero value is nil
|
||||
|
||||
// partialOperation is a successful upload/delete of an object
|
||||
// but not written in all disks (having quorum)
|
||||
type partialOperation struct {
|
||||
bucket string
|
||||
object string
|
||||
versionID string
|
||||
failedSet int
|
||||
}
|
||||
|
||||
// erasureObjects - Implements ER object layer.
|
||||
type erasureObjects struct {
|
||||
GatewayUnsupported
|
||||
@@ -78,8 +69,6 @@ type erasureObjects struct {
|
||||
// legacy objects.
|
||||
bpOld *bpool.BytePoolCap
|
||||
|
||||
mrfOpCh chan partialOperation
|
||||
|
||||
deletedCleanupSleeper *dynamicSleeper
|
||||
}
|
||||
|
||||
|
||||
@@ -209,14 +209,6 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
// To avoid this error situation we check for port availability.
|
||||
logger.FatalIf(checkPortAvailability(globalMinioHost, globalMinioPort), "Unable to start the gateway")
|
||||
|
||||
globalMinioEndpoint = func() string {
|
||||
host := globalMinioHost
|
||||
if host == "" {
|
||||
host = sortIPs(localIP4.ToSlice())[0]
|
||||
}
|
||||
return fmt.Sprintf("%s://%s", getURLScheme(globalIsTLS), net.JoinHostPort(host, globalMinioPort))
|
||||
}()
|
||||
|
||||
// Handle gateway specific env
|
||||
gatewayHandleEnvVars()
|
||||
|
||||
|
||||
@@ -553,11 +553,16 @@ func addCustomHeaders(h http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// addSecurityHeaders adds various HTTP(S) response headers.
|
||||
// Security Headers enable various security protections behaviors in the client's browser.
|
||||
func addSecurityHeaders(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
header := w.Header()
|
||||
header.Set("X-XSS-Protection", "1; mode=block") // Prevents against XSS attacks
|
||||
header.Set("Content-Security-Policy", "block-all-mixed-content") // prevent mixed (HTTP / HTTPS content)
|
||||
header.Set("X-XSS-Protection", "1; mode=block") // Prevents against XSS attacks
|
||||
header.Set("Content-Security-Policy", "block-all-mixed-content") // prevent mixed (HTTP / HTTPS content)
|
||||
header.Set("X-Frame-Options", "deny") // Prevent against Clickjack attack
|
||||
header.Set("X-Content-Type-Options", "nosniff") // Prevent mime-sniff
|
||||
header.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") // HSTS mitigates variants of MITM attacks
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
+17
-5
@@ -79,13 +79,20 @@ func getBackgroundHealStatus(ctx context.Context, o ObjectLayer) (madmin.BgHealS
|
||||
return madmin.BgHealState{}, false
|
||||
}
|
||||
|
||||
status := madmin.BgHealState{
|
||||
ScannedItemsCount: bgSeq.getScannedItemsCount(),
|
||||
}
|
||||
|
||||
if globalMRFState.initialized() {
|
||||
status.MRF = map[string]madmin.MRFStatus{
|
||||
globalLocalNodeName: globalMRFState.getCurrentMRFRoundInfo(),
|
||||
}
|
||||
}
|
||||
|
||||
var healDisksMap = map[string]struct{}{}
|
||||
for _, ep := range getLocalDisksToHeal() {
|
||||
healDisksMap[ep.String()] = struct{}{}
|
||||
}
|
||||
status := madmin.BgHealState{
|
||||
ScannedItemsCount: bgSeq.getScannedItemsCount(),
|
||||
}
|
||||
|
||||
if o == nil {
|
||||
healing := globalBackgroundHealState.getLocalHealingDisks()
|
||||
@@ -225,12 +232,12 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []BucketIn
|
||||
ScanMode: madmin.HealNormalScan, Remove: healDeleteDangling}); err != nil {
|
||||
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
||||
// If not deleted, assume they failed.
|
||||
tracker.ObjectsFailed++
|
||||
tracker.ItemsFailed++
|
||||
tracker.BytesFailed += uint64(version.Size)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
} else {
|
||||
tracker.ObjectsHealed++
|
||||
tracker.ItemsHealed++
|
||||
tracker.BytesDone += uint64(version.Size)
|
||||
}
|
||||
bgSeq.logHeal(madmin.HealItemObject)
|
||||
@@ -265,6 +272,11 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []BucketIn
|
||||
finished: nil,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
// If context is canceled don't mark as done...
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -293,6 +293,8 @@ var (
|
||||
globalBackgroundHealRoutine *healRoutine
|
||||
globalBackgroundHealState *allHealState
|
||||
|
||||
globalMRFState mrfState
|
||||
|
||||
// If writes to FS backend should be O_SYNC.
|
||||
globalFSOSync bool
|
||||
|
||||
|
||||
@@ -30,11 +30,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio/internal/handlers"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
jsonrpc "github.com/minio/rpc"
|
||||
)
|
||||
|
||||
// recordRequest - records the first recLen bytes
|
||||
@@ -110,75 +108,6 @@ func getOpName(name string) (op string) {
|
||||
return op
|
||||
}
|
||||
|
||||
// WebTrace gets trace of web request
|
||||
func WebTrace(ri *jsonrpc.RequestInfo) madmin.TraceInfo {
|
||||
r := ri.Request
|
||||
w := ri.ResponseWriter
|
||||
|
||||
name := ri.Method
|
||||
// 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, ","))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
rq := madmin.TraceRequestInfo{
|
||||
Time: now,
|
||||
Proto: r.Proto,
|
||||
Method: r.Method,
|
||||
Path: SlashSeparator + pathJoin(vars["bucket"], vars["object"]),
|
||||
RawQuery: redactLDAPPwd(r.URL.RawQuery),
|
||||
Client: handlers.GetSourceIP(r),
|
||||
Headers: reqHeaders,
|
||||
}
|
||||
|
||||
rw, ok := w.(*logger.ResponseWriter)
|
||||
if ok {
|
||||
rs := madmin.TraceResponseInfo{
|
||||
Time: time.Now().UTC(),
|
||||
Headers: rw.Header().Clone(),
|
||||
StatusCode: rw.StatusCode,
|
||||
Body: logger.BodyPlaceHolder,
|
||||
}
|
||||
|
||||
if rs.StatusCode == 0 {
|
||||
rs.StatusCode = http.StatusOK
|
||||
}
|
||||
|
||||
t.RespInfo = rs
|
||||
t.CallStats = madmin.TraceCallStats{
|
||||
Latency: rs.Time.Sub(rw.StartTime),
|
||||
InputBytes: int(r.ContentLength),
|
||||
OutputBytes: rw.Size(),
|
||||
TimeToFirstByte: rw.TimeToFirstByte,
|
||||
}
|
||||
}
|
||||
|
||||
t.ReqInfo = rq
|
||||
return t
|
||||
}
|
||||
|
||||
// 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())
|
||||
|
||||
+166
-27
@@ -650,14 +650,23 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer) {
|
||||
break
|
||||
}
|
||||
|
||||
if globalOpenIDConfig.ProviderEnabled() {
|
||||
// Set up polling for expired accounts and credentials purging.
|
||||
switch {
|
||||
case globalOpenIDConfig.ProviderEnabled():
|
||||
go func() {
|
||||
// Purge expired credentials
|
||||
for {
|
||||
time.Sleep(globalRefreshIAMInterval)
|
||||
sys.purgeExpiredCredentialsForExternalSSO(ctx)
|
||||
}
|
||||
}()
|
||||
case globalLDAPConfig.EnabledWithLookupBind():
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(globalRefreshIAMInterval)
|
||||
sys.purgeExpiredCredentialsForLDAP(ctx)
|
||||
sys.updateGroupMembershipsForLDAP(ctx)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go sys.store.watch(ctx, sys)
|
||||
@@ -1157,6 +1166,9 @@ type newServiceAccountOpts struct {
|
||||
sessionPolicy *iampolicy.Policy
|
||||
accessKey string
|
||||
secretKey string
|
||||
|
||||
// LDAP username
|
||||
ldapUsername string
|
||||
}
|
||||
|
||||
// NewServiceAccount - create a new service account
|
||||
@@ -1214,6 +1226,11 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
|
||||
m[iamPolicyClaimNameSA()] = "inherited-policy"
|
||||
}
|
||||
|
||||
// For LDAP service account, save the ldap username in the metadata.
|
||||
if opts.ldapUsername != "" {
|
||||
m[ldapUserN] = opts.ldapUsername
|
||||
}
|
||||
|
||||
var (
|
||||
cred auth.Credentials
|
||||
)
|
||||
@@ -1527,20 +1544,20 @@ func (sys *IAMSys) loadUserFromStore(accessKey string) {
|
||||
// by checking remote IDP if the relevant users are still active and present.
|
||||
func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) {
|
||||
sys.store.lock()
|
||||
parentUsersMap := make(map[string]auth.Credentials, len(sys.iamUsersMap))
|
||||
parentUsersMap := make(map[string][]auth.Credentials, len(sys.iamUsersMap))
|
||||
for _, cred := range sys.iamUsersMap {
|
||||
if cred.IsServiceAccount() || cred.IsTemp() {
|
||||
userid, err := parseOpenIDParentUser(cred.ParentUser)
|
||||
if err == errSkipFile {
|
||||
continue
|
||||
}
|
||||
parentUsersMap[userid] = cred
|
||||
parentUsersMap[userid] = append(parentUsersMap[userid], cred)
|
||||
}
|
||||
}
|
||||
sys.store.unlock()
|
||||
|
||||
expiredUsers := make([]auth.Credentials, 0, len(parentUsersMap))
|
||||
for userid, cred := range parentUsersMap {
|
||||
for userid, creds := range parentUsersMap {
|
||||
u, err := globalOpenIDConfig.LookupUser(userid)
|
||||
if err != nil {
|
||||
logger.LogIf(GlobalContext, err)
|
||||
@@ -1548,7 +1565,7 @@ func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) {
|
||||
}
|
||||
// Disabled parentUser purge the entries locally
|
||||
if !u.Enabled {
|
||||
expiredUsers = append(expiredUsers, cred)
|
||||
expiredUsers = append(expiredUsers, creds...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1571,6 +1588,140 @@ func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) {
|
||||
sys.store.unlock()
|
||||
}
|
||||
|
||||
// purgeExpiredCredentialsForLDAP - validates if local credentials are still
|
||||
// valid by checking LDAP server if the relevant users are still present.
|
||||
func (sys *IAMSys) purgeExpiredCredentialsForLDAP(ctx context.Context) {
|
||||
sys.store.lock()
|
||||
parentUsersMap := make(map[string][]auth.Credentials, len(sys.iamUsersMap))
|
||||
parentUsers := make([]string, 0, len(sys.iamUsersMap))
|
||||
for _, cred := range sys.iamUsersMap {
|
||||
if cred.IsServiceAccount() || cred.IsTemp() {
|
||||
if globalLDAPConfig.IsLDAPUserDN(cred.ParentUser) {
|
||||
if _, ok := parentUsersMap[cred.ParentUser]; !ok {
|
||||
parentUsers = append(parentUsers, cred.ParentUser)
|
||||
}
|
||||
parentUsersMap[cred.ParentUser] = append(parentUsersMap[cred.ParentUser], cred)
|
||||
}
|
||||
}
|
||||
}
|
||||
sys.store.unlock()
|
||||
|
||||
expiredUsers, err := globalLDAPConfig.GetNonExistentUserDistNames(parentUsers)
|
||||
if err != nil {
|
||||
// Log and return on error - perhaps it'll work the next time.
|
||||
logger.LogIf(GlobalContext, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, expiredUser := range expiredUsers {
|
||||
for _, cred := range parentUsersMap[expiredUser] {
|
||||
userType := regUser
|
||||
if cred.IsServiceAccount() {
|
||||
userType = svcUser
|
||||
} else if cred.IsTemp() {
|
||||
userType = stsUser
|
||||
}
|
||||
sys.store.deleteIAMConfig(ctx, getUserIdentityPath(cred.AccessKey, userType))
|
||||
sys.store.deleteIAMConfig(ctx, getMappedPolicyPath(cred.AccessKey, userType, false))
|
||||
}
|
||||
}
|
||||
|
||||
sys.store.lock()
|
||||
for _, user := range expiredUsers {
|
||||
for _, cred := range parentUsersMap[user] {
|
||||
delete(sys.iamUsersMap, cred.AccessKey)
|
||||
delete(sys.iamUserPolicyMap, cred.AccessKey)
|
||||
}
|
||||
}
|
||||
sys.store.unlock()
|
||||
}
|
||||
|
||||
// updateGroupMembershipsForLDAP - updates the list of groups associated with the credential.
|
||||
func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
|
||||
// 1. Collect all LDAP users with active creds.
|
||||
sys.store.lock()
|
||||
// List of unique LDAP (parent) user DNs that have active creds
|
||||
parentUsers := make([]string, 0, len(sys.iamUsersMap))
|
||||
// Map of LDAP user to list of active credential objects
|
||||
parentUserToCredsMap := make(map[string][]auth.Credentials, len(sys.iamUsersMap))
|
||||
// DN to ldap username mapping for each LDAP user
|
||||
parentUserToLDAPUsernameMap := make(map[string]string, len(sys.iamUsersMap))
|
||||
for _, cred := range sys.iamUsersMap {
|
||||
if cred.IsServiceAccount() || cred.IsTemp() {
|
||||
if globalLDAPConfig.IsLDAPUserDN(cred.ParentUser) {
|
||||
// Check if this is the first time we are
|
||||
// encountering this LDAP user.
|
||||
if _, ok := parentUserToCredsMap[cred.ParentUser]; !ok {
|
||||
// Try to find the ldapUsername for this
|
||||
// parentUser by extracting JWT claims
|
||||
jwtClaims, err := auth.ExtractClaims(cred.SessionToken, globalActiveCred.SecretKey)
|
||||
if err != nil {
|
||||
// skip this cred - session token seems
|
||||
// invalid
|
||||
continue
|
||||
}
|
||||
ldapUsername, ok := jwtClaims.Lookup(ldapUserN)
|
||||
if !ok {
|
||||
// skip this cred - we dont have the
|
||||
// username info needed
|
||||
continue
|
||||
}
|
||||
|
||||
// Collect each new cred.ParentUser into parentUsers
|
||||
parentUsers = append(parentUsers, cred.ParentUser)
|
||||
|
||||
// Update the ldapUsernameMap
|
||||
parentUserToLDAPUsernameMap[cred.ParentUser] = ldapUsername
|
||||
}
|
||||
parentUserToCredsMap[cred.ParentUser] = append(parentUserToCredsMap[cred.ParentUser], cred)
|
||||
}
|
||||
}
|
||||
}
|
||||
sys.store.unlock()
|
||||
|
||||
// 2. Query LDAP server for groups of the LDAP users collected.
|
||||
updatedGroups, err := globalLDAPConfig.LookupGroupMemberships(parentUsers, parentUserToLDAPUsernameMap)
|
||||
if err != nil {
|
||||
// Log and return on error - perhaps it'll work the next time.
|
||||
logger.LogIf(GlobalContext, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Update creds for those users whose groups are changed
|
||||
sys.store.lock()
|
||||
defer sys.store.unlock()
|
||||
for _, parentUser := range parentUsers {
|
||||
currGroupsSet := updatedGroups[parentUser]
|
||||
currGroups := currGroupsSet.ToSlice()
|
||||
for _, cred := range parentUserToCredsMap[parentUser] {
|
||||
gSet := set.CreateStringSet(cred.Groups...)
|
||||
if gSet.Equals(currGroupsSet) {
|
||||
// No change to groups memberships for this
|
||||
// credential.
|
||||
continue
|
||||
}
|
||||
|
||||
cred.Groups = currGroups
|
||||
userType := regUser
|
||||
if cred.IsServiceAccount() {
|
||||
userType = svcUser
|
||||
} else if cred.IsTemp() {
|
||||
userType = stsUser
|
||||
}
|
||||
// Overwrite the user identity here. As store should be
|
||||
// atomic, it shouldn't cause any corruption.
|
||||
if err := sys.store.saveUserIdentity(ctx, cred.AccessKey, userType, newUserIdentity(cred)); err != nil {
|
||||
// Log and continue error - perhaps it'll work the next time.
|
||||
logger.LogIf(GlobalContext, err)
|
||||
continue
|
||||
}
|
||||
// If we wrote the updated creds to IAM storage, we can
|
||||
// update the in memory map.
|
||||
sys.iamUsersMap[cred.AccessKey] = cred
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetUser - get user credentials
|
||||
func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) {
|
||||
if !sys.Initialized() {
|
||||
@@ -2149,20 +2300,15 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
|
||||
|
||||
// IsAllowedLDAPSTS - checks for LDAP specific claims and values
|
||||
func (sys *IAMSys) IsAllowedLDAPSTS(args iampolicy.Args, parentUser string) bool {
|
||||
parentInClaimIface, ok := args.Claims[ldapUser]
|
||||
if ok {
|
||||
parentInClaim, ok := parentInClaimIface.(string)
|
||||
if !ok {
|
||||
// ldap parentInClaim name is not a string reject it.
|
||||
return false
|
||||
}
|
||||
|
||||
if parentInClaim != parentUser {
|
||||
// ldap claim has been modified maliciously reject it.
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// no ldap parentInClaim claim present reject it.
|
||||
// parentUser value must match the ldap user in the claim.
|
||||
if parentInClaimIface, ok := args.Claims[ldapUser]; !ok {
|
||||
// no ldapUser claim present reject it.
|
||||
return false
|
||||
} else if parentInClaim, ok := parentInClaimIface.(string); !ok {
|
||||
// not the right type, reject it.
|
||||
return false
|
||||
} else if parentInClaim != parentUser {
|
||||
// ldap claim has been modified maliciously reject it.
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2297,13 +2443,6 @@ func isAllowedBySessionPolicy(args iampolicy.Args) (hasSessionPolicy bool, isAll
|
||||
return
|
||||
}
|
||||
|
||||
policyBytes, err := base64.StdEncoding.DecodeString(spolicyStr)
|
||||
if err != nil {
|
||||
// Got a malformed base64 string
|
||||
return
|
||||
}
|
||||
spolicyStr = string(policyBytes)
|
||||
|
||||
// Check if policy is parseable.
|
||||
subPolicy, err := iampolicy.ParseConfig(bytes.NewReader([]byte(spolicyStr)))
|
||||
if err != nil {
|
||||
|
||||
+92
-48
@@ -351,18 +351,18 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
// All operations are performed without locks, so we must be careful and allow for failures.
|
||||
// Read metadata associated with the object from a disk.
|
||||
if retries > 0 {
|
||||
disks := er.getOnlineDisks()
|
||||
if len(disks) == 0 {
|
||||
time.Sleep(retryDelay)
|
||||
retries++
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := disks[0].ReadVersion(ctx, minioMetaBucket, o.objectPath(0), "", false)
|
||||
if err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
retries++
|
||||
continue
|
||||
for _, disk := range er.getDisks() {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
_, err := disk.ReadVersion(ctx, minioMetaBucket,
|
||||
o.objectPath(0), "", false)
|
||||
if err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
retries++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,20 +421,21 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
|
||||
if retries > 0 {
|
||||
// Load from one disk only
|
||||
disks := er.getOnlineDisks()
|
||||
if len(disks) == 0 {
|
||||
time.Sleep(retryDelay)
|
||||
retries++
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := disks[0].ReadVersion(ctx, minioMetaBucket, o.objectPath(partN), "", false)
|
||||
if err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
retries++
|
||||
continue
|
||||
for _, disk := range er.getDisks() {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
_, err := disk.ReadVersion(ctx, minioMetaBucket,
|
||||
o.objectPath(partN), "", false)
|
||||
if err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
retries++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Load first part metadata...
|
||||
fi, metaArr, onlineDisks, err = er.getObjectFileInfo(ctx, minioMetaBucket, o.objectPath(partN), ObjectOptions{}, true)
|
||||
if err != nil {
|
||||
@@ -512,7 +513,8 @@ func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, resul
|
||||
|
||||
askDisks := o.AskDisks
|
||||
listingQuorum := o.AskDisks - 1
|
||||
disks := er.getOnlineDisks()
|
||||
disks := er.getDisks()
|
||||
var fallbackDisks []StorageAPI
|
||||
|
||||
// Special case: ask all disks if the drive count is 4
|
||||
if askDisks == -1 || er.setDriveCount == 4 {
|
||||
@@ -527,6 +529,7 @@ func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, resul
|
||||
rand.Shuffle(len(disks), func(i, j int) {
|
||||
disks[i], disks[j] = disks[j], disks[i]
|
||||
})
|
||||
fallbackDisks = disks[askDisks:]
|
||||
disks = disks[:askDisks]
|
||||
}
|
||||
|
||||
@@ -539,13 +542,14 @@ func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, resul
|
||||
|
||||
ctxDone := ctx.Done()
|
||||
return listPathRaw(ctx, listPathRawOptions{
|
||||
disks: disks,
|
||||
bucket: o.Bucket,
|
||||
path: o.BaseDir,
|
||||
recursive: o.Recursive,
|
||||
filterPrefix: o.FilterPrefix,
|
||||
minDisks: listingQuorum,
|
||||
forwardTo: o.Marker,
|
||||
disks: disks,
|
||||
fallbackDisks: fallbackDisks,
|
||||
bucket: o.Bucket,
|
||||
path: o.BaseDir,
|
||||
recursive: o.Recursive,
|
||||
filterPrefix: o.FilterPrefix,
|
||||
minDisks: listingQuorum,
|
||||
forwardTo: o.Marker,
|
||||
agreed: func(entry metaCacheEntry) {
|
||||
select {
|
||||
case <-ctxDone:
|
||||
@@ -710,9 +714,10 @@ func (er *erasureObjects) saveMetaCacheStream(ctx context.Context, mc *metaCache
|
||||
}
|
||||
|
||||
type listPathRawOptions struct {
|
||||
disks []StorageAPI
|
||||
bucket, path string
|
||||
recursive bool
|
||||
disks []StorageAPI
|
||||
fallbackDisks []StorageAPI
|
||||
bucket, path string
|
||||
recursive bool
|
||||
|
||||
// Only return results with this prefix.
|
||||
filterPrefix string
|
||||
@@ -752,10 +757,18 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
|
||||
if len(disks) == 0 {
|
||||
return fmt.Errorf("listPathRaw: 0 drives provided")
|
||||
}
|
||||
|
||||
// Cancel upstream if we finish before we expect.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
fallback := func(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return err.Error() == errUnformattedDisk.Error() ||
|
||||
err.Error() == errVolumeNotFound.Error()
|
||||
}
|
||||
askDisks := len(disks)
|
||||
readers := make([]*metacacheReader, askDisks)
|
||||
for i := range disks {
|
||||
@@ -768,15 +781,44 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
|
||||
|
||||
// Send request to each disk.
|
||||
go func() {
|
||||
werr := d.WalkDir(ctx, WalkDirOptions{
|
||||
Bucket: opts.bucket,
|
||||
BaseDir: opts.path,
|
||||
Recursive: opts.recursive,
|
||||
ReportNotFound: opts.reportNotFound,
|
||||
FilterPrefix: opts.filterPrefix,
|
||||
ForwardTo: opts.forwardTo,
|
||||
}, w)
|
||||
var werr error
|
||||
if d == nil {
|
||||
werr = errDiskNotFound
|
||||
} else {
|
||||
werr = d.WalkDir(ctx, WalkDirOptions{
|
||||
Bucket: opts.bucket,
|
||||
BaseDir: opts.path,
|
||||
Recursive: opts.recursive,
|
||||
ReportNotFound: opts.reportNotFound,
|
||||
FilterPrefix: opts.filterPrefix,
|
||||
ForwardTo: opts.forwardTo,
|
||||
}, w)
|
||||
}
|
||||
|
||||
// fallback only when set.
|
||||
if len(opts.fallbackDisks) > 0 && fallback(werr) {
|
||||
// This fallback is only set when
|
||||
// askDisks is less than total
|
||||
// number of disks per set.
|
||||
for _, fd := range opts.fallbackDisks {
|
||||
if fd == nil {
|
||||
continue
|
||||
}
|
||||
werr = fd.WalkDir(ctx, WalkDirOptions{
|
||||
Bucket: opts.bucket,
|
||||
BaseDir: opts.path,
|
||||
Recursive: opts.recursive,
|
||||
ReportNotFound: opts.reportNotFound,
|
||||
FilterPrefix: opts.filterPrefix,
|
||||
ForwardTo: opts.forwardTo,
|
||||
}, w)
|
||||
if werr == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
w.CloseWithError(werr)
|
||||
|
||||
if werr != io.EOF && werr != nil &&
|
||||
werr.Error() != errFileNotFound.Error() &&
|
||||
werr.Error() != errVolumeNotFound.Error() &&
|
||||
@@ -795,10 +837,8 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
|
||||
for i := range topEntries {
|
||||
topEntries[i] = metaCacheEntry{}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if contextCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
for i, r := range readers {
|
||||
if errs[i] != nil {
|
||||
@@ -866,8 +906,12 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
|
||||
var combinedErr []string
|
||||
for i, err := range errs {
|
||||
if err != nil {
|
||||
combinedErr = append(combinedErr,
|
||||
fmt.Sprintf("disk %s returned: %s", disks[i], err))
|
||||
if disks[i] != nil {
|
||||
combinedErr = append(combinedErr,
|
||||
fmt.Sprintf("disk %s returned: %s", disks[i], err))
|
||||
} else {
|
||||
combinedErr = append(combinedErr, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.New(strings.Join(combinedErr, ", "))
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -66,8 +65,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
|
||||
}
|
||||
|
||||
// Stat a volume entry.
|
||||
_, err = os.Lstat(volumeDir)
|
||||
if err != nil {
|
||||
if err = Access(volumeDir); err != nil {
|
||||
if osIsNotExist(err) {
|
||||
return errVolumeNotFound
|
||||
} else if isSysErrIO(err) {
|
||||
@@ -100,7 +98,8 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
|
||||
metadata: metadata,
|
||||
}
|
||||
} else {
|
||||
if st, err := Lstat(pathJoin(volumeDir, opts.BaseDir, xlStorageFormatFile)); err == nil && st.Mode().IsRegular() {
|
||||
st, sterr := Lstat(pathJoin(volumeDir, opts.BaseDir, xlStorageFormatFile))
|
||||
if sterr == nil && st.Mode().IsRegular() {
|
||||
return errFileNotFound
|
||||
}
|
||||
}
|
||||
@@ -118,7 +117,6 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
|
||||
forward = forward[:idx]
|
||||
}
|
||||
}
|
||||
|
||||
if contextCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -134,6 +132,9 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
|
||||
// Forward some errors?
|
||||
return nil
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
dirObjects := make(map[string]struct{})
|
||||
for i, entry := range entries {
|
||||
if len(prefix) > 0 && !strings.HasPrefix(entry, prefix) {
|
||||
@@ -245,8 +246,6 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
|
||||
case osIsNotExist(err):
|
||||
meta.metadata, err = xioutil.ReadFile(pathJoin(volumeDir, meta.name, xlStorageFormatFileV1))
|
||||
if err == nil {
|
||||
// Maybe rename? Would make it inconsistent across disks though.
|
||||
// os.Rename(pathJoin(volumeDir, meta.name, xlStorageFormatFileV1), pathJoin(volumeDir, meta.name, xlStorageFormatFile))
|
||||
// It was an object
|
||||
out <- meta
|
||||
continue
|
||||
@@ -265,6 +264,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// If directory entry left on stack, pop it now.
|
||||
for len(dirStack) > 0 {
|
||||
pop := dirStack[len(dirStack)-1]
|
||||
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// 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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
var mrfHealingOpts = madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: healDeleteDangling}
|
||||
|
||||
const (
|
||||
mrfInfoResetInterval = 10 * time.Second
|
||||
mrfOpsQueueSize = 10000
|
||||
)
|
||||
|
||||
// partialOperation is a successful upload/delete of an object
|
||||
// but not written in all disks (having quorum)
|
||||
type partialOperation struct {
|
||||
bucket string
|
||||
object string
|
||||
versionID string
|
||||
size int64
|
||||
setIndex int
|
||||
poolIndex int
|
||||
}
|
||||
|
||||
type setInfo struct {
|
||||
index, pool int
|
||||
}
|
||||
|
||||
// mrfState sncapsulates all the information
|
||||
// related to the global background MRF.
|
||||
type mrfState struct {
|
||||
ready int32
|
||||
|
||||
ctx context.Context
|
||||
objectAPI ObjectLayer
|
||||
|
||||
mu sync.Mutex
|
||||
opCh chan partialOperation
|
||||
pendingOps map[partialOperation]setInfo
|
||||
setReconnectEvent chan setInfo
|
||||
|
||||
itemsHealed uint64
|
||||
bytesHealed uint64
|
||||
pendingItems uint64
|
||||
pendingBytes uint64
|
||||
|
||||
triggeredAt time.Time
|
||||
}
|
||||
|
||||
// Initialize healing MRF subsystem
|
||||
func (m *mrfState) init(ctx context.Context, objAPI ObjectLayer) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.ctx = ctx
|
||||
m.objectAPI = objAPI
|
||||
m.opCh = make(chan partialOperation, mrfOpsQueueSize)
|
||||
m.pendingOps = make(map[partialOperation]setInfo)
|
||||
m.setReconnectEvent = make(chan setInfo)
|
||||
|
||||
go globalMRFState.maintainMRFList()
|
||||
go globalMRFState.healRoutine()
|
||||
|
||||
atomic.StoreInt32(&m.ready, 1)
|
||||
}
|
||||
|
||||
func (m *mrfState) initialized() bool {
|
||||
return atomic.LoadInt32(&m.ready) != 0
|
||||
}
|
||||
|
||||
// Add a partial S3 operation (put/delete) when one or more disks are offline.
|
||||
func (m *mrfState) addPartialOp(op partialOperation) {
|
||||
if !m.initialized() {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case m.opCh <- op:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Receive the new set (disk) reconnection event
|
||||
func (m *mrfState) newSetReconnected(pool, set int) {
|
||||
if !m.initialized() {
|
||||
return
|
||||
}
|
||||
|
||||
idler := time.NewTimer(100 * time.Millisecond)
|
||||
defer idler.Stop()
|
||||
|
||||
select {
|
||||
case m.setReconnectEvent <- setInfo{index: set, pool: pool}:
|
||||
case <-idler.C:
|
||||
}
|
||||
}
|
||||
|
||||
// Get current MRF stats of the last MRF activity
|
||||
func (m *mrfState) getCurrentMRFRoundInfo() madmin.MRFStatus {
|
||||
m.mu.Lock()
|
||||
triggeredAt := m.triggeredAt
|
||||
itemsHealed := m.itemsHealed
|
||||
bytesHealed := m.bytesHealed
|
||||
pendingItems := m.pendingItems
|
||||
pendingBytes := m.pendingBytes
|
||||
m.mu.Unlock()
|
||||
|
||||
if pendingItems == 0 {
|
||||
return madmin.MRFStatus{}
|
||||
}
|
||||
|
||||
return madmin.MRFStatus{
|
||||
Started: triggeredAt,
|
||||
ItemsHealed: itemsHealed,
|
||||
BytesHealed: bytesHealed,
|
||||
TotalItems: itemsHealed + pendingItems,
|
||||
TotalBytes: bytesHealed + pendingBytes,
|
||||
}
|
||||
}
|
||||
|
||||
// maintainMRFList gathers the list of successful partial uploads
|
||||
// from all underlying er.sets and puts them in a global map which
|
||||
// should not have more than 10000 entries.
|
||||
func (m *mrfState) maintainMRFList() {
|
||||
for fOp := range m.opCh {
|
||||
m.mu.Lock()
|
||||
if len(m.pendingOps) > mrfOpsQueueSize {
|
||||
m.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
m.pendingOps[fOp] = setInfo{index: fOp.setIndex, pool: fOp.poolIndex}
|
||||
m.pendingItems++
|
||||
if fOp.size > 0 {
|
||||
m.pendingBytes += uint64(fOp.size)
|
||||
}
|
||||
|
||||
m.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Reset current MRF stats
|
||||
func (m *mrfState) resetMRFInfoIfNoPendingOps() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.pendingItems > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
m.itemsHealed = 0
|
||||
m.bytesHealed = 0
|
||||
m.pendingItems = 0
|
||||
m.pendingBytes = 0
|
||||
m.triggeredAt = time.Time{}
|
||||
}
|
||||
|
||||
// healRoutine listens to new disks reconnection events and
|
||||
// issues healing requests for queued objects belonging to the
|
||||
// corresponding erasure set
|
||||
func (m *mrfState) healRoutine() {
|
||||
idler := time.NewTimer(mrfInfoResetInterval)
|
||||
defer idler.Stop()
|
||||
|
||||
for {
|
||||
idler.Reset(mrfInfoResetInterval)
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return
|
||||
case <-idler.C:
|
||||
m.resetMRFInfoIfNoPendingOps()
|
||||
case setInfo := <-m.setReconnectEvent:
|
||||
// Get the list of objects related the er.set
|
||||
// to which the connected disk belongs.
|
||||
var mrfOperations []partialOperation
|
||||
m.mu.Lock()
|
||||
for k, v := range m.pendingOps {
|
||||
if v == setInfo {
|
||||
mrfOperations = append(mrfOperations, k)
|
||||
}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
if len(mrfOperations) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.triggeredAt = time.Now().UTC()
|
||||
m.mu.Unlock()
|
||||
|
||||
// Heal objects
|
||||
for _, u := range mrfOperations {
|
||||
waitForLowHTTPReq(globalHealConfig.IOCount, globalHealConfig.Sleep)
|
||||
if _, err := m.objectAPI.HealObject(m.ctx, u.bucket, u.object, u.versionID, mrfHealingOpts); err != nil {
|
||||
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
||||
// If not deleted, assume they failed.
|
||||
logger.LogIf(m.ctx, err)
|
||||
} else {
|
||||
m.mu.Lock()
|
||||
m.itemsHealed++
|
||||
m.pendingItems--
|
||||
m.mu.Unlock()
|
||||
}
|
||||
} else {
|
||||
m.mu.Lock()
|
||||
m.itemsHealed++
|
||||
m.pendingItems--
|
||||
m.bytesHealed += uint64(u.size)
|
||||
m.pendingBytes -= uint64(u.size)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
delete(m.pendingOps, u)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize healing MRF
|
||||
func initHealMRF(ctx context.Context, obj ObjectLayer) {
|
||||
globalMRFState.init(ctx, obj)
|
||||
}
|
||||
@@ -19,13 +19,11 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
)
|
||||
|
||||
@@ -262,19 +260,7 @@ func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, delete bool) {
|
||||
|
||||
if objInfo.Bucket != "" && objInfo.Name != "" {
|
||||
if lc, err := globalLifecycleSys.Get(objInfo.Bucket); err == nil && !delete {
|
||||
ruleID, expiryTime := lc.PredictExpiryTime(lifecycle.ObjectOpts{
|
||||
Name: objInfo.Name,
|
||||
UserTags: objInfo.UserTags,
|
||||
VersionID: objInfo.VersionID,
|
||||
ModTime: objInfo.ModTime,
|
||||
IsLatest: objInfo.IsLatest,
|
||||
DeleteMarker: objInfo.DeleteMarker,
|
||||
})
|
||||
if !expiryTime.IsZero() {
|
||||
w.Header()[xhttp.AmzExpiration] = []string{
|
||||
fmt.Sprintf(`expiry-date="%s", rule-id="%s"`, expiryTime.Format(http.TimeFormat), ruleID),
|
||||
}
|
||||
}
|
||||
lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-8
@@ -459,14 +459,6 @@ func serverMain(ctx *cli.Context) {
|
||||
// Initialize all sub-systems
|
||||
newAllSubsystems()
|
||||
|
||||
globalMinioEndpoint = func() string {
|
||||
host := globalMinioHost
|
||||
if host == "" {
|
||||
host = sortIPs(localIP4.ToSlice())[0]
|
||||
}
|
||||
return fmt.Sprintf("%s://%s", getURLScheme(globalIsTLS), net.JoinHostPort(host, globalMinioPort))
|
||||
}()
|
||||
|
||||
// Is distributed setup, error out if no certificates are found for HTTPS endpoints.
|
||||
if globalIsDistErasure {
|
||||
if globalEndpoints.HTTPS() && !globalIsTLS {
|
||||
@@ -537,6 +529,7 @@ func serverMain(ctx *cli.Context) {
|
||||
// Enable background operations for erasure coding
|
||||
if globalIsErasure {
|
||||
initAutoHeal(GlobalContext, newObject)
|
||||
initHealMRF(GlobalContext, newObject)
|
||||
initBackgroundTransition(GlobalContext, newObject)
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,11 @@ func isNotIPv4(host string) bool {
|
||||
// port "80" and "443" before displaying on the startup
|
||||
// banner. Returns a new list of API endpoints.
|
||||
func stripStandardPorts(apiEndpoints []string, host string) (newAPIEndpoints []string) {
|
||||
if len(apiEndpoints) == 1 && globalBrowserRedirectURL != nil {
|
||||
if apiEndpoints[0] == globalBrowserRedirectURL.String() {
|
||||
return []string{globalBrowserRedirectURL.String()}
|
||||
}
|
||||
}
|
||||
newAPIEndpoints = make([]string, len(apiEndpoints))
|
||||
// Check all API endpoints for standard ports and strip them.
|
||||
for i, apiEndpoint := range apiEndpoints {
|
||||
|
||||
@@ -181,6 +181,17 @@ type FileInfo struct {
|
||||
SuccessorModTime time.Time
|
||||
}
|
||||
|
||||
// InlineData returns true if object contents are inlined alongside its metadata.
|
||||
func (fi FileInfo) InlineData() bool {
|
||||
_, ok := fi.Metadata[ReservedMetadataPrefixLower+"inline-data"]
|
||||
return ok
|
||||
}
|
||||
|
||||
// SetInlineData marks object (version) as inline.
|
||||
func (fi FileInfo) SetInlineData() {
|
||||
fi.Metadata[ReservedMetadataPrefixLower+"inline-data"] = "true"
|
||||
}
|
||||
|
||||
// VersionPurgeStatusKey denotes purge status in metadata
|
||||
const VersionPurgeStatusKey = "purgestatus"
|
||||
|
||||
|
||||
+20
-2
@@ -58,6 +58,7 @@ const (
|
||||
// JWT claim keys
|
||||
expClaim = "exp"
|
||||
subClaim = "sub"
|
||||
audClaim = "aud"
|
||||
issClaim = "iss"
|
||||
|
||||
// JWT claim to check the parent user
|
||||
@@ -332,13 +333,25 @@ func (sts *stsAPIHandlers) AssumeRoleWithSSO(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
var audFromToken string
|
||||
if v, ok := m[audClaim]; ok {
|
||||
audFromToken, _ = v.(string)
|
||||
}
|
||||
|
||||
var subFromToken string
|
||||
if v, ok := m[subClaim]; ok {
|
||||
subFromToken, _ = v.(string)
|
||||
}
|
||||
|
||||
if subFromToken == "" {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, errors.New("STS JWT Token has `sub` claim missing, `sub` claim is mandatory"))
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
|
||||
errors.New("STS JWT Token has `sub` claim missing, `sub` claim is mandatory"))
|
||||
return
|
||||
}
|
||||
|
||||
if audFromToken != globalOpenIDConfig.ClientID {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue,
|
||||
errors.New("STS JWT Token has `aud` claim invalid, `aud` must match configured OpenID Client ID"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -541,7 +554,12 @@ func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *
|
||||
return
|
||||
}
|
||||
|
||||
expiryDur := globalLDAPConfig.GetExpiryDuration()
|
||||
expiryDur, err := globalLDAPConfig.GetExpiryDuration(r.Form.Get(stsDurationSeconds))
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(ctx, w, true, ErrSTSInvalidParameterValue, err)
|
||||
return
|
||||
}
|
||||
|
||||
m := map[string]interface{}{
|
||||
expClaim: UTCNow().Add(expiryDur).Unix(),
|
||||
ldapUser: ldapUserDN,
|
||||
|
||||
+25
-20
@@ -218,25 +218,27 @@ func contains(slice interface{}, elem interface{}) bool {
|
||||
// disk since the name of this latter is randomly generated.
|
||||
type profilerWrapper struct {
|
||||
// Profile recorded at start of benchmark.
|
||||
base []byte
|
||||
stopFn func() ([]byte, error)
|
||||
ext string
|
||||
records map[string][]byte
|
||||
stopFn func() ([]byte, error)
|
||||
ext string
|
||||
}
|
||||
|
||||
// recordBase will record the profile and store it as the base.
|
||||
func (p *profilerWrapper) recordBase(name string, debug int) {
|
||||
// record will record the profile and store it as the base.
|
||||
func (p *profilerWrapper) record(profileType string, debug int, recordName string) {
|
||||
var buf bytes.Buffer
|
||||
p.base = nil
|
||||
err := pprof.Lookup(name).WriteTo(&buf, debug)
|
||||
if p.records == nil {
|
||||
p.records = make(map[string][]byte)
|
||||
}
|
||||
err := pprof.Lookup(profileType).WriteTo(&buf, debug)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.base = buf.Bytes()
|
||||
p.records[recordName] = buf.Bytes()
|
||||
}
|
||||
|
||||
// Base returns the recorded base if any.
|
||||
func (p profilerWrapper) Base() []byte {
|
||||
return p.base
|
||||
// Records returns the recorded profiling if any.
|
||||
func (p profilerWrapper) Records() map[string][]byte {
|
||||
return p.records
|
||||
}
|
||||
|
||||
// Stop the currently running benchmark.
|
||||
@@ -268,9 +270,10 @@ func getProfileData() (map[string][]byte, error) {
|
||||
if err == nil {
|
||||
dst[typ+"."+prof.Extension()] = buf
|
||||
}
|
||||
buf = prof.Base()
|
||||
if len(buf) > 0 {
|
||||
dst[typ+"-before"+"."+prof.Extension()] = buf
|
||||
for name, buf := range prof.Records() {
|
||||
if len(buf) > 0 {
|
||||
dst[typ+"-"+name+"."+prof.Extension()] = buf
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst, nil
|
||||
@@ -314,7 +317,7 @@ func startProfiler(profilerType string) (minioProfiler, error) {
|
||||
}
|
||||
case madmin.ProfilerMEM:
|
||||
runtime.GC()
|
||||
prof.recordBase("heap", 0)
|
||||
prof.record("heap", 0, "before")
|
||||
prof.stopFn = func() ([]byte, error) {
|
||||
runtime.GC()
|
||||
var buf bytes.Buffer
|
||||
@@ -330,7 +333,7 @@ func startProfiler(profilerType string) (minioProfiler, error) {
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
case madmin.ProfilerMutex:
|
||||
prof.recordBase("mutex", 0)
|
||||
prof.record("mutex", 0, "before")
|
||||
runtime.SetMutexProfileFraction(1)
|
||||
prof.stopFn = func() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
@@ -339,7 +342,7 @@ func startProfiler(profilerType string) (minioProfiler, error) {
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
case madmin.ProfilerThreads:
|
||||
prof.recordBase("threadcreate", 0)
|
||||
prof.record("threadcreate", 0, "before")
|
||||
prof.stopFn = func() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
err := pprof.Lookup("threadcreate").WriteTo(&buf, 0)
|
||||
@@ -347,7 +350,8 @@ func startProfiler(profilerType string) (minioProfiler, error) {
|
||||
}
|
||||
case madmin.ProfilerGoroutines:
|
||||
prof.ext = "txt"
|
||||
prof.recordBase("goroutine", 1)
|
||||
prof.record("goroutine", 1, "before")
|
||||
prof.record("goroutine", 2, "before,debug=2")
|
||||
prof.stopFn = func() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
err := pprof.Lookup("goroutine").WriteTo(&buf, 1)
|
||||
@@ -386,8 +390,8 @@ func startProfiler(profilerType string) (minioProfiler, error) {
|
||||
|
||||
// minioProfiler - minio profiler interface.
|
||||
type minioProfiler interface {
|
||||
// Return base profile. 'nil' if none.
|
||||
Base() []byte
|
||||
// Return recorded profiles, each profile associated with a distinct generic name.
|
||||
Records() map[string][]byte
|
||||
// Stop the profiler
|
||||
Stop() ([]byte, error)
|
||||
// Return extension of profile
|
||||
@@ -509,6 +513,7 @@ 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
|
||||
IdleConnTimeout: 15 * time.Second,
|
||||
|
||||
@@ -970,7 +970,7 @@ func (j xlMetaV2DeleteMarker) ToFileInfo(volume, path string) (FileInfo, error)
|
||||
|
||||
// UsesDataDir returns true if this object version uses its data directory for
|
||||
// its contents and false otherwise.
|
||||
func (j *xlMetaV2Object) UsesDataDir() bool {
|
||||
func (j xlMetaV2Object) UsesDataDir() bool {
|
||||
// Skip if this version is not transitioned, i.e it uses its data directory.
|
||||
if !bytes.Equal(j.MetaSys[ReservedMetadataPrefixLower+TransitionStatus], []byte(lifecycle.TransitionComplete)) {
|
||||
return true
|
||||
@@ -980,6 +980,19 @@ func (j *xlMetaV2Object) UsesDataDir() bool {
|
||||
return isRestoredObjectOnDisk(j.MetaUser)
|
||||
}
|
||||
|
||||
func (j *xlMetaV2Object) SetTransition(fi FileInfo) {
|
||||
j.MetaSys[ReservedMetadataPrefixLower+TransitionStatus] = []byte(fi.TransitionStatus)
|
||||
j.MetaSys[ReservedMetadataPrefixLower+TransitionedObjectName] = []byte(fi.TransitionedObjName)
|
||||
j.MetaSys[ReservedMetadataPrefixLower+TransitionedVersionID] = []byte(fi.TransitionVersionID)
|
||||
j.MetaSys[ReservedMetadataPrefixLower+TransitionTier] = []byte(fi.TransitionTier)
|
||||
}
|
||||
|
||||
func (j *xlMetaV2Object) RemoveRestoreHdrs() {
|
||||
delete(j.MetaUser, xhttp.AmzRestore)
|
||||
delete(j.MetaUser, xhttp.AmzRestoreExpiryDays)
|
||||
delete(j.MetaUser, xhttp.AmzRestoreRequestDate)
|
||||
}
|
||||
|
||||
func (j xlMetaV2Object) ToFileInfo(volume, path string) (FileInfo, error) {
|
||||
versionID := ""
|
||||
var uv uuid.UUID
|
||||
@@ -1211,14 +1224,11 @@ func (z *xlMetaV2) DeleteVersion(fi FileInfo) (string, bool, error) {
|
||||
if version.ObjectV2.VersionID == uv {
|
||||
switch {
|
||||
case fi.ExpireRestored:
|
||||
delete(z.Versions[i].ObjectV2.MetaUser, xhttp.AmzRestore)
|
||||
delete(z.Versions[i].ObjectV2.MetaUser, xhttp.AmzRestoreExpiryDays)
|
||||
delete(z.Versions[i].ObjectV2.MetaUser, xhttp.AmzRestoreRequestDate)
|
||||
z.Versions[i].ObjectV2.RemoveRestoreHdrs()
|
||||
|
||||
case fi.TransitionStatus == lifecycle.TransitionComplete:
|
||||
z.Versions[i].ObjectV2.MetaSys[ReservedMetadataPrefixLower+TransitionStatus] = []byte(fi.TransitionStatus)
|
||||
z.Versions[i].ObjectV2.MetaSys[ReservedMetadataPrefixLower+TransitionedObjectName] = []byte(fi.TransitionedObjName)
|
||||
z.Versions[i].ObjectV2.MetaSys[ReservedMetadataPrefixLower+TransitionedVersionID] = []byte(fi.TransitionVersionID)
|
||||
z.Versions[i].ObjectV2.MetaSys[ReservedMetadataPrefixLower+TransitionTier] = []byte(fi.TransitionTier)
|
||||
z.Versions[i].ObjectV2.SetTransition(fi)
|
||||
|
||||
default:
|
||||
z.Versions = append(z.Versions[:i], z.Versions[i+1:]...)
|
||||
// if uv has tiered content we add a
|
||||
|
||||
+37
-32
@@ -519,7 +519,6 @@ func (s *xlStorage) DiskInfo(context.Context) (info DiskInfo, err error) {
|
||||
dcinfo.UsedInodes = di.Files - di.Ffree
|
||||
dcinfo.FreeInodes = di.Ffree
|
||||
dcinfo.FSType = di.FSType
|
||||
|
||||
diskID, err := s.GetDiskID()
|
||||
if errors.Is(err, errUnformattedDisk) {
|
||||
// if we found an unformatted disk then
|
||||
@@ -530,7 +529,6 @@ func (s *xlStorage) DiskInfo(context.Context) (info DiskInfo, err error) {
|
||||
// returned any error other than fresh disk
|
||||
dcinfo.Healing = s.Healing() != nil
|
||||
}
|
||||
|
||||
dcinfo.ID = diskID
|
||||
return dcinfo, err
|
||||
}
|
||||
@@ -553,28 +551,7 @@ func (s *xlStorage) getVolDir(volume string) (string, error) {
|
||||
return volumeDir, nil
|
||||
}
|
||||
|
||||
// GetDiskID - returns the cached disk uuid
|
||||
func (s *xlStorage) GetDiskID() (string, error) {
|
||||
s.RLock()
|
||||
diskID := s.diskID
|
||||
fileInfo := s.formatFileInfo
|
||||
lastCheck := s.formatLastCheck
|
||||
s.RUnlock()
|
||||
|
||||
// check if we have a valid disk ID that is less than 1 second old.
|
||||
if fileInfo != nil && diskID != "" && time.Since(lastCheck) <= time.Second {
|
||||
return diskID, nil
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
// If somebody else updated the disk ID and changed the time, return what they got.
|
||||
if !lastCheck.IsZero() && !s.formatLastCheck.Equal(lastCheck) && diskID != "" {
|
||||
s.Unlock()
|
||||
// Somebody else got the lock first.
|
||||
return diskID, nil
|
||||
}
|
||||
s.Unlock()
|
||||
|
||||
func (s *xlStorage) checkFormatJSON() (os.FileInfo, error) {
|
||||
formatFile := pathJoin(s.diskPath, minioMetaBucket, formatConfigFile)
|
||||
fi, err := Lstat(formatFile)
|
||||
if err != nil {
|
||||
@@ -582,20 +559,41 @@ func (s *xlStorage) GetDiskID() (string, error) {
|
||||
if osIsNotExist(err) {
|
||||
if err = Access(s.diskPath); err == nil {
|
||||
// Disk is present but missing `format.json`
|
||||
return "", errUnformattedDisk
|
||||
return nil, errUnformattedDisk
|
||||
}
|
||||
if osIsNotExist(err) {
|
||||
return "", errDiskNotFound
|
||||
return nil, errDiskNotFound
|
||||
} else if osIsPermission(err) {
|
||||
return "", errDiskAccessDenied
|
||||
return nil, errDiskAccessDenied
|
||||
}
|
||||
logger.LogIf(GlobalContext, err) // log unexpected errors
|
||||
return "", errCorruptedFormat
|
||||
return nil, errCorruptedFormat
|
||||
} else if osIsPermission(err) {
|
||||
return "", errDiskAccessDenied
|
||||
return nil, errDiskAccessDenied
|
||||
}
|
||||
logger.LogIf(GlobalContext, err) // log unexpected errors
|
||||
return "", errCorruptedFormat
|
||||
return nil, errCorruptedFormat
|
||||
}
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
// GetDiskID - returns the cached disk uuid
|
||||
func (s *xlStorage) GetDiskID() (string, error) {
|
||||
s.RLock()
|
||||
diskID := s.diskID
|
||||
fileInfo := s.formatFileInfo
|
||||
lastCheck := s.formatLastCheck
|
||||
|
||||
// check if we have a valid disk ID that is less than 1 second old.
|
||||
if fileInfo != nil && diskID != "" && time.Since(lastCheck) <= time.Second {
|
||||
s.RUnlock()
|
||||
return diskID, nil
|
||||
}
|
||||
s.RUnlock()
|
||||
|
||||
fi, err := s.checkFormatJSON()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if xioutil.SameFile(fi, fileInfo) && diskID != "" {
|
||||
@@ -606,6 +604,7 @@ func (s *xlStorage) GetDiskID() (string, error) {
|
||||
return diskID, nil
|
||||
}
|
||||
|
||||
formatFile := pathJoin(s.diskPath, minioMetaBucket, formatConfigFile)
|
||||
b, err := xioutil.ReadFile(formatFile)
|
||||
if err != nil {
|
||||
// If the disk is still not initialized.
|
||||
@@ -1099,8 +1098,8 @@ func (s *xlStorage) ReadVersion(ctx context.Context, volume, path, versionID str
|
||||
if readData {
|
||||
if len(fi.Data) > 0 || fi.Size == 0 {
|
||||
if len(fi.Data) > 0 {
|
||||
if _, ok := fi.Metadata[ReservedMetadataPrefixLower+"inline-data"]; !ok {
|
||||
fi.Metadata[ReservedMetadataPrefixLower+"inline-data"] = "true"
|
||||
if !fi.InlineData() {
|
||||
fi.SetInlineData()
|
||||
}
|
||||
}
|
||||
return fi, nil
|
||||
@@ -2055,6 +2054,12 @@ func (s *xlStorage) RenameData(ctx context.Context, srcVolume, srcPath string, f
|
||||
// Purge the destination path as we are not preserving anything
|
||||
// versioned object was not requested.
|
||||
oldDstDataPath = pathJoin(dstVolumeDir, dstPath, ofi.DataDir)
|
||||
// if old destination path is same as new destination path
|
||||
// there is nothing to purge, this is true in case of healing
|
||||
// avoid setting oldDstDataPath at that point.
|
||||
if oldDstDataPath == dstDataPath {
|
||||
oldDstDataPath = ""
|
||||
}
|
||||
xlMeta.data.remove(nullVersionID, ofi.DataDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,52 @@
|
||||
version: '3.7'
|
||||
|
||||
# Settings and configurations that are common for all containers
|
||||
x-minio-common: &minio-common
|
||||
image: minio/minio:RELEASE.2021-07-22T05-23-32Z
|
||||
command: server --console-address ":9001" http://minio{1...4}/data{1...2}
|
||||
expose:
|
||||
- "9000"
|
||||
- "9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minio
|
||||
MINIO_ROOT_PASSWORD: minio123
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
|
||||
# starts 4 docker containers running minio server instances.
|
||||
# using nginx reverse proxy, load balancing, you can access
|
||||
# it through port 9000.
|
||||
services:
|
||||
minio1:
|
||||
image: minio/minio:RELEASE.2021-07-12T02-44-53Z
|
||||
<<: *minio-common
|
||||
hostname: minio1
|
||||
volumes:
|
||||
- data1-1:/data1
|
||||
- data1-2:/data2
|
||||
expose:
|
||||
- "9000"
|
||||
- "9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minio
|
||||
MINIO_ROOT_PASSWORD: minio123
|
||||
command: server --console-address ":9001" http://minio{1...4}/data{1...2}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
|
||||
minio2:
|
||||
image: minio/minio:RELEASE.2021-07-12T02-44-53Z
|
||||
<<: *minio-common
|
||||
hostname: minio2
|
||||
volumes:
|
||||
- data2-1:/data1
|
||||
- data2-2:/data2
|
||||
expose:
|
||||
- "9000"
|
||||
- "9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minio
|
||||
MINIO_ROOT_PASSWORD: minio123
|
||||
command: server --console-address ":9001" http://minio{1...4}/data{1...2}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
|
||||
minio3:
|
||||
image: minio/minio:RELEASE.2021-07-12T02-44-53Z
|
||||
<<: *minio-common
|
||||
hostname: minio3
|
||||
volumes:
|
||||
- data3-1:/data1
|
||||
- data3-2:/data2
|
||||
expose:
|
||||
- "9000"
|
||||
- "9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minio
|
||||
MINIO_ROOT_PASSWORD: minio123
|
||||
command: server --console-address ":9001" http://minio{1...4}/data{1...2}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
|
||||
minio4:
|
||||
image: minio/minio:RELEASE.2021-07-12T02-44-53Z
|
||||
<<: *minio-common
|
||||
hostname: minio4
|
||||
volumes:
|
||||
- data4-1:/data1
|
||||
- data4-2:/data2
|
||||
expose:
|
||||
- "9000"
|
||||
- "9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minio
|
||||
MINIO_ROOT_PASSWORD: minio123
|
||||
command: server --console-address ":9001" http://minio{1...4}/data{1...2}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
|
||||
nginx:
|
||||
image: nginx:1.19.2-alpine
|
||||
|
||||
+12
-6
@@ -27,6 +27,7 @@ import (
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
cr "github.com/minio/minio-go/v7/pkg/credentials"
|
||||
@@ -43,6 +44,9 @@ var (
|
||||
// Display credentials flag
|
||||
displayCreds bool
|
||||
|
||||
// Credential expiry duration
|
||||
expiryDuration time.Duration
|
||||
|
||||
// Bucket to list
|
||||
bucketToList string
|
||||
|
||||
@@ -55,6 +59,7 @@ func init() {
|
||||
flag.StringVar(&ldapUsername, "u", "", "AD/LDAP Username")
|
||||
flag.StringVar(&ldapPassword, "p", "", "AD/LDAP Password")
|
||||
flag.BoolVar(&displayCreds, "d", false, "Only show generated credentials")
|
||||
flag.DurationVar(&expiryDuration, "e", 0, "Request a duration of validity for the generated credential")
|
||||
flag.StringVar(&bucketToList, "b", "", "Bucket to list (defaults to ldap username)")
|
||||
flag.StringVar(&sessionPolicyFile, "s", "", "File containing session policy to apply to the STS request")
|
||||
}
|
||||
@@ -70,11 +75,8 @@ func main() {
|
||||
// LDAP STS API.
|
||||
|
||||
// Initialize LDAP credentials
|
||||
var li *cr.Credentials
|
||||
var err error
|
||||
if sessionPolicyFile == "" {
|
||||
li, err = cr.NewLDAPIdentity(stsEndpoint, ldapUsername, ldapPassword)
|
||||
} else {
|
||||
var ldapOpts []cr.LDAPIdentityOpt
|
||||
if sessionPolicyFile != "" {
|
||||
var policy string
|
||||
if f, err := os.Open(sessionPolicyFile); err != nil {
|
||||
log.Fatalf("Unable to open session policy file: %v", sessionPolicyFile, err)
|
||||
@@ -85,8 +87,12 @@ func main() {
|
||||
}
|
||||
policy = string(bs)
|
||||
}
|
||||
li, err = cr.NewLDAPIdentityWithSessionPolicy(stsEndpoint, ldapUsername, ldapPassword, policy)
|
||||
ldapOpts = append(ldapOpts, cr.LDAPIdentityPolicyOpt(policy))
|
||||
}
|
||||
if expiryDuration != 0 {
|
||||
ldapOpts = append(ldapOpts, cr.LDAPIdentityExpiryOpt(expiryDuration))
|
||||
}
|
||||
li, err := cr.NewLDAPIdentity(stsEndpoint, ldapUsername, ldapPassword, ldapOpts...)
|
||||
if err != nil {
|
||||
log.Fatalf("Error initializing LDAP Identity: %v", err)
|
||||
}
|
||||
|
||||
+48
-6
@@ -1,5 +1,33 @@
|
||||
# AssumeRoleWithLDAPIdentity [](https://slack.min.io)
|
||||
|
||||
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
|
||||
**Table of Contents**
|
||||
|
||||
- [AssumeRoleWithLDAPIdentity [](https://slack.min.io)](#assumerolewithldapidentity-slackhttpsslackminioslacktypesvghttpsslackminio)
|
||||
- [Introduction](#introduction)
|
||||
- [Configuring AD/LDAP on MinIO](#configuring-adldap-on-minio)
|
||||
- [Supported modes of operation](#supported-modes-of-operation)
|
||||
- [Lookup-Bind Mode](#lookup-bind-mode)
|
||||
- [Username-Format Mode](#username-format-mode)
|
||||
- [Group membership search](#group-membership-search)
|
||||
- [Variable substitution in AD/LDAP configuration strings](#variable-substitution-in-adldap-configuration-strings)
|
||||
- [Managing User/Group Access Policy](#managing-usergroup-access-policy)
|
||||
- [API Request Parameters](#api-request-parameters)
|
||||
- [LDAPUsername](#ldapusername)
|
||||
- [LDAPPassword](#ldappassword)
|
||||
- [Version](#version)
|
||||
- [DurationSeconds](#durationseconds)
|
||||
- [Policy](#policy)
|
||||
- [Response Elements](#response-elements)
|
||||
- [Errors](#errors)
|
||||
- [Sample `POST` Request](#sample-post-request)
|
||||
- [Sample Response](#sample-response)
|
||||
- [Using LDAP STS API](#using-ldap-sts-api)
|
||||
- [Explore Further](#explore-further)
|
||||
|
||||
<!-- markdown-toc end -->
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
MinIO provides a custom STS API that allows integration with LDAP based corporate environments including Microsoft Active Directory. The MinIO server can be configured in two possible modes: either using a LDAP separate service account, called lookup-bind mode or in username-format mode. In either case the login flow for a user is the same as the STS flow:
|
||||
@@ -26,7 +54,6 @@ identity_ldap enable LDAP SSO support
|
||||
|
||||
ARGS:
|
||||
MINIO_IDENTITY_LDAP_SERVER_ADDR* (address) AD/LDAP server address e.g. "myldapserver.com:636"
|
||||
MINIO_IDENTITY_LDAP_STS_EXPIRY (duration) temporary credentials validity duration in s,m,h,d. Default is "1h"
|
||||
MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN (string) DN for LDAP read-only service account used to perform DN and group lookups
|
||||
MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD (string) Password for LDAP read-only service account used to perform DN and group lookups
|
||||
MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN (string) Base LDAP DN to search for user DN
|
||||
@@ -63,9 +90,18 @@ MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER (string) Search filter to lookup
|
||||
|
||||
If you set an empty lookup bind password, the lookup bind will use the unauthenticated authentication mechanism, as described in [RFC 4513 Section 5.1.2](https://tools.ietf.org/html/rfc4513#section-5.1.2).
|
||||
|
||||
**Automatic LDAP sync:** In Lookup-Bind mode, the server periodically queries the LDAP service to:
|
||||
|
||||
1. find accounts (user DNs) that have been removed;
|
||||
2. find accounts whose group memberships have changed.
|
||||
|
||||
In case 1 above, any active STS credentials or MinIO service accounts belonging to these users are purged.
|
||||
|
||||
In case 2 above, access policies available to the credential are updated to reflect the change - i.e. they will lose any privileges associated with a group they are removed from, and gain any privileges associated with a group they are added to.
|
||||
|
||||
#### Username-Format Mode ####
|
||||
|
||||
In this mode, the server does not use a separate LDAP service account. Instead, the username and password provided in the STS API call are used to login to the LDAP server and also to lookup the user's groups. This mode preserves older behavior for compatibility, but users are encouraged to use the Lookup-Bind mode.
|
||||
In this mode, the server does not use a separate LDAP service account. Instead, the username and password provided in the STS API call are used to login to the LDAP server and also to lookup the user's groups. This mode preserves older behavior for compatibility, but users are encouraged to use the Lookup-Bind mode. Some newer features may not be available in this mode of operation.
|
||||
|
||||
The DN to use to login to LDAP is computed from a username format configuration parameter. This is a list of possible DN templates to be used. For each such template, the username is substituted and the DN is generated. Each generated DN is tried by the MinIO server to login to LDAP. If exactly one successful DN is found, it is used to perform the groups lookup as well.
|
||||
|
||||
@@ -95,12 +131,9 @@ export MINIO_IDENTITY_LDAP_SERVER_ADDR=myldapserver.com:636
|
||||
export MINIO_IDENTITY_LDAP_USERNAME_FORMAT="uid=%s,cn=accounts,dc=myldapserver,dc=com"
|
||||
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN="dc=myldapserver,dc=com"
|
||||
export MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER="(&(objectclass=groupOfNames)(memberUid=%s)$)"
|
||||
export MINIO_IDENTITY_LDAP_STS_EXPIRY=720h
|
||||
export MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY=on
|
||||
```
|
||||
|
||||
> NOTE: In this example STS_EXPIRY is set to 1month, maximum expiry that can be set is 365 days.
|
||||
|
||||
### Variable substitution in AD/LDAP configuration strings ###
|
||||
|
||||
In the configuration variables, `%s` is substituted with the *username* from the STS request and `%d` is substituted with the *distinguished username (user DN)* of the LDAP user. Please see the following table for which configuration variables support these substitution variables:
|
||||
@@ -165,6 +198,15 @@ Indicates STS API version information, the only supported value is '2011-06-15'.
|
||||
| *Type* | *String* |
|
||||
| *Required* | *Yes* |
|
||||
|
||||
### DurationSeconds
|
||||
The duration, in seconds. The value can range from 900 seconds (15 minutes) up to 365 days. If value is higher than this setting, then operation fails. By default, the value is set to 3600 seconds.
|
||||
|
||||
| Params | Value |
|
||||
| :-- | :-- |
|
||||
| *Type* | *Integer* |
|
||||
| *Valid Range* | *Minimum value of 900. Maximum value of 31536000.* |
|
||||
| *Required* | *No* |
|
||||
|
||||
### Policy
|
||||
An IAM policy in JSON format that you want to use as an inline session policy. This parameter is optional. Passing policies to this operation returns new temporary credentials. The resulting session's permissions are the intersection of the canned policy name and the policy set here. You cannot use this policy to grant more permissions than those allowed by the canned policy name being assumed.
|
||||
|
||||
@@ -182,7 +224,7 @@ XML error response for this API is similar to [AWS STS AssumeRoleWithWebIdentity
|
||||
|
||||
## Sample `POST` Request
|
||||
```
|
||||
http://minio.cluster:9000?Action=AssumeRoleWithLDAPIdentity&LDAPUsername=foouser&LDAPPassword=foouserpassword&Version=2011-06-15
|
||||
http://minio.cluster:9000?Action=AssumeRoleWithLDAPIdentity&LDAPUsername=foouser&LDAPPassword=foouserpassword&Version=2011-06-15&DurationSeconds=7200
|
||||
```
|
||||
|
||||
## Sample Response
|
||||
|
||||
@@ -41,15 +41,14 @@ require (
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/miekg/dns v1.1.35
|
||||
github.com/minio/cli v1.22.0
|
||||
github.com/minio/console v0.7.6-0.20210715212137-d8840cc81026
|
||||
github.com/minio/console v0.8.0
|
||||
github.com/minio/csvparser v1.0.0
|
||||
github.com/minio/highwayhash v1.0.2
|
||||
github.com/minio/kes v0.14.0
|
||||
github.com/minio/madmin-go v1.0.16-0.20210713203601-e2a5b7c4d8ae
|
||||
github.com/minio/madmin-go v1.0.17
|
||||
github.com/minio/minio-go/v7 v7.0.13-0.20210715203016-9e713532886e
|
||||
github.com/minio/parquet-go v1.0.0
|
||||
github.com/minio/pkg v1.0.10
|
||||
github.com/minio/rpc v1.0.0
|
||||
github.com/minio/selfupdate v0.3.1
|
||||
github.com/minio/sha256-simd v1.0.0
|
||||
github.com/minio/simdjson-go v0.2.1
|
||||
@@ -57,10 +56,9 @@ require (
|
||||
github.com/minio/zipindex v0.2.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/montanaflynn/stats v0.6.6
|
||||
github.com/nats-io/nats-server/v2 v2.1.9
|
||||
github.com/nats-io/nats-server/v2 v2.3.2
|
||||
github.com/nats-io/nats-streaming-server v0.21.2 // indirect
|
||||
github.com/nats-io/nats.go v1.10.0
|
||||
github.com/nats-io/nkeys v0.2.0 // indirect
|
||||
github.com/nats-io/nats.go v1.11.1-0.20210623165838-4b75fc59ae30
|
||||
github.com/nats-io/stan.go v0.8.3
|
||||
github.com/ncw/directio v1.0.5
|
||||
github.com/nsqio/go-nsq v1.0.8
|
||||
@@ -82,8 +80,8 @@ require (
|
||||
go.etcd.io/etcd/client/v3 v3.5.0-beta.4
|
||||
go.uber.org/atomic v1.7.0
|
||||
go.uber.org/zap v1.16.1-0.20210329175301-c23abee72d19
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||
golang.org/x/tools v0.1.1 // indirect
|
||||
google.golang.org/api v0.31.0
|
||||
|
||||
@@ -329,7 +329,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses=
|
||||
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
@@ -880,6 +879,7 @@ github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
|
||||
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.12/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
|
||||
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
@@ -1007,14 +1007,15 @@ github.com/minio/cli v1.22.0 h1:VTQm7lmXm3quxO917X3p+el1l0Ca5X3S4PM2ruUYO68=
|
||||
github.com/minio/cli v1.22.0/go.mod h1:bYxnK0uS629N3Bq+AOZZ+6lwF77Sodk4+UL9vNuXhOY=
|
||||
github.com/minio/colorjson v1.0.1 h1:+hvfP8C1iMB95AT+ZFDRE+Knn9QPd9lg0CRJY9DRpos=
|
||||
github.com/minio/colorjson v1.0.1/go.mod h1:oPM3zQQY8Gz9NGtgvuBEjQ+gPZLKAGc7T+kjMlwtOgs=
|
||||
github.com/minio/console v0.7.6-0.20210715212137-d8840cc81026 h1:g/J0SaD+xofku31QqkDjCx7//atjsbyBwH9WIAmQoo0=
|
||||
github.com/minio/console v0.7.6-0.20210715212137-d8840cc81026/go.mod h1:ZiDruKgGwP/tSWJxeKAUo8Q3vxaDRVW7fusuAugamX4=
|
||||
github.com/minio/console v0.8.0 h1:W7l5dJku6yANQ4sf9jTDZNF16ap4Q2Sxg9nmYOMLwZs=
|
||||
github.com/minio/console v0.8.0/go.mod h1:HtWrypKxQjhkF4sAgkPpmzEIjwsBv1fw62uU8DxN9iE=
|
||||
github.com/minio/csvparser v1.0.0 h1:xJEHcYK8ZAjeW4hNV9Zu30u+/2o4UyPnYgyjWp8b7ZU=
|
||||
github.com/minio/csvparser v1.0.0/go.mod h1:lKXskSLzPgC5WQyzP7maKH7Sl1cqvANXo9YCto8zbtM=
|
||||
github.com/minio/direct-csi v1.3.5-0.20210601185811-f7776f7961bf h1:wylCc/PdvdTIqYqVNEU9LJAZBanvfGY1TwTnjM3zQaA=
|
||||
github.com/minio/direct-csi v1.3.5-0.20210601185811-f7776f7961bf/go.mod h1:ryRFbKRDMZ1+kGF5EZwuyfF1a/eAu4c9uxZdF0fnmuY=
|
||||
github.com/minio/filepath v1.0.0 h1:fvkJu1+6X+ECRA6G3+JJETj4QeAYO9sV43I79H8ubDY=
|
||||
github.com/minio/filepath v1.0.0/go.mod h1:/nRZA2ldl5z6jT9/KQuvZcQlxZIMQoFFQPvEXx9T/Bw=
|
||||
github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
|
||||
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
github.com/minio/kes v0.11.0/go.mod h1:mTF1Bv8YVEtQqF/B7Felp4tLee44Pp+dgI0rhCvgNg8=
|
||||
@@ -1022,8 +1023,8 @@ github.com/minio/kes v0.14.0 h1:plCGm4LwR++T1P1sXsJbyFRX54CE1WRuo9PAPj6MC3Q=
|
||||
github.com/minio/kes v0.14.0/go.mod h1:OUensXz2BpgMfiogslKxv7Anyx/wj+6bFC6qA7BQcfA=
|
||||
github.com/minio/madmin-go v1.0.6/go.mod h1:BK+z4XRx7Y1v8SFWXsuLNqQqnq5BO/axJ8IDJfgyvfs=
|
||||
github.com/minio/madmin-go v1.0.12/go.mod h1:BK+z4XRx7Y1v8SFWXsuLNqQqnq5BO/axJ8IDJfgyvfs=
|
||||
github.com/minio/madmin-go v1.0.16-0.20210713203601-e2a5b7c4d8ae h1:eRwOOnwEoqHL8CLQR69obOxYYshQoQ3470+qgD9psJw=
|
||||
github.com/minio/madmin-go v1.0.16-0.20210713203601-e2a5b7c4d8ae/go.mod h1:4nl9hvLWFnwCjkLfZSsZXEHgDODa2XSG6xGlIZyQ2oA=
|
||||
github.com/minio/madmin-go v1.0.17 h1:VMEn4nMKf0X3uNH0u+fZcn17KSwVkQGwyER/igG556E=
|
||||
github.com/minio/madmin-go v1.0.17/go.mod h1:4nl9hvLWFnwCjkLfZSsZXEHgDODa2XSG6xGlIZyQ2oA=
|
||||
github.com/minio/mc v0.0.0-20210626002108-cebf3318546f h1:hyFvo5hSFw2K417YvDr/vAKlgCG69uTuhZW/5LNdL0U=
|
||||
github.com/minio/mc v0.0.0-20210626002108-cebf3318546f/go.mod h1:tuaonkPjVApCXkbtKENHBtsqUf7YTV33qmFrC+Pgp5g=
|
||||
github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
|
||||
@@ -1045,8 +1046,6 @@ github.com/minio/pkg v1.0.4/go.mod h1:obU54TZ9QlMv0TRaDgQ/JTzf11ZSXxnSfLrm4tMtBP
|
||||
github.com/minio/pkg v1.0.8/go.mod h1:32x/3OmGB0EOi1N+3ggnp+B5VFkSBBB9svPMVfpnf14=
|
||||
github.com/minio/pkg v1.0.10 h1:fohpAm/0ttQFf4BzmzH5r6A9JUIfg63AyGCPM0f9/9U=
|
||||
github.com/minio/pkg v1.0.10/go.mod h1:32x/3OmGB0EOi1N+3ggnp+B5VFkSBBB9svPMVfpnf14=
|
||||
github.com/minio/rpc v1.0.0 h1:tJCHyLfQF6k6HlMQFpKy2FO/7lc2WP8gLDGMZp18E70=
|
||||
github.com/minio/rpc v1.0.0/go.mod h1:b9xqF7J0xeMXr0cM4pnBlP7Te7PDsG5JrRxl5dG6Ldk=
|
||||
github.com/minio/selfupdate v0.3.1 h1:BWEFSNnrZVMUWXbXIgLDNDjbejkmpAmZvy/nCz1HlEs=
|
||||
github.com/minio/selfupdate v0.3.1/go.mod h1:b8ThJzzH7u2MkF6PcIra7KaXO9Khf6alWPvMSyTDCFM=
|
||||
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
|
||||
@@ -1105,21 +1104,27 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
|
||||
github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
github.com/nats-io/jwt v1.1.0 h1:+vOlgtM0ZsF46GbmUoadq0/2rChNS45gtxHEa3H1gqM=
|
||||
github.com/nats-io/jwt v1.1.0/go.mod h1:n3cvmLfBfnpV4JJRN7lRYCyZnw48ksGsbThGXEk4w9M=
|
||||
github.com/nats-io/jwt v1.2.2 h1:w3GMTO969dFg+UOKTmmyuu7IGdusK+7Ytlt//OYH/uU=
|
||||
github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q=
|
||||
github.com/nats-io/jwt/v2 v2.0.2 h1:ejVCLO8gu6/4bOKIHQpmB5UhhUJfAQw55yvLWpfmKjI=
|
||||
github.com/nats-io/jwt/v2 v2.0.2/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY=
|
||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
||||
github.com/nats-io/nats-server/v2 v2.1.9 h1:Sxr2zpaapgpBT9ElTxTVe62W+qjnhPcKY/8W5cnA/Qk=
|
||||
github.com/nats-io/nats-server/v2 v2.1.9/go.mod h1:9qVyoewoYXzG1ME9ox0HwkkzyYvnlBDugfR4Gg/8uHU=
|
||||
github.com/nats-io/nats-server/v2 v2.3.2 h1:SGJLWrjBHsl0DsdY8PeTR3YKEfiUEYVVq2STw9d8MSY=
|
||||
github.com/nats-io/nats-server/v2 v2.3.2/go.mod h1:dUf7Cm5z5LbciFVwWx54owyCKm8x4/hL6p7rrljhLFY=
|
||||
github.com/nats-io/nats-streaming-server v0.21.2 h1:chyaVdWlPdBcSbLq3cpyCYcuXA+7bVXJmM4yWrdqL/8=
|
||||
github.com/nats-io/nats-streaming-server v0.21.2/go.mod h1:2W8QfNVOtcFpmf0bRiwuLtRb0/hkX4NuOxPOFNOThVQ=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nats.go v1.10.0 h1:L8qnKaofSfNFbXg0C5F71LdjPRnmQwSsA4ukmkt1TvY=
|
||||
github.com/nats-io/nats.go v1.10.0/go.mod h1:AjGArbfyR50+afOUotNX2Xs5SYHf+CoOa5HH1eEl2HE=
|
||||
github.com/nats-io/nats.go v1.11.1-0.20210623165838-4b75fc59ae30 h1:9GqilBhZaR3xYis0JgMlJjNw933WIobdjKhilXm+Vls=
|
||||
github.com/nats-io/nats.go v1.11.1-0.20210623165838-4b75fc59ae30/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nkeys v0.1.4/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=
|
||||
github.com/nats-io/nkeys v0.2.0 h1:WXKF7diOaPU9cJdLD7nuzwasQy9vT1tBqzXZZf3AMJM=
|
||||
github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=
|
||||
github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
|
||||
github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nats-io/stan.go v0.8.3 h1:XyemjL9vAeGHooHn5RQy+ngljd8AVSM2l65Jdnpv4rI=
|
||||
@@ -1534,8 +1539,10 @@ golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWP
|
||||
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
||||
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -1736,8 +1743,9 @@ golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
||||
@@ -21,8 +21,11 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -438,25 +441,51 @@ func (lc Lifecycle) PredictTransitionTime(obj ObjectOpts) (string, time.Time) {
|
||||
return "", time.Time{}
|
||||
}
|
||||
|
||||
var finalTransitionDate time.Time
|
||||
var finalTransitionRuleID string
|
||||
|
||||
// Iterate over all actionable rules and find the earliest
|
||||
// transition date and its associated rule ID.
|
||||
var finalTransitionDate time.Time
|
||||
var finalTransitionRuleID string
|
||||
for _, rule := range lc.FilterActionableRules(obj) {
|
||||
switch {
|
||||
case !rule.Transition.IsDateNull():
|
||||
if finalTransitionDate.IsZero() || finalTransitionDate.After(rule.Transition.Date.Time) {
|
||||
if due, ok := rule.Transition.NextDue(obj); ok {
|
||||
if finalTransitionDate.IsZero() || finalTransitionDate.After(due) {
|
||||
finalTransitionRuleID = rule.ID
|
||||
finalTransitionDate = rule.Transition.Date.Time
|
||||
finalTransitionDate = due
|
||||
}
|
||||
case !rule.Transition.IsDaysNull():
|
||||
expectedTransition := ExpectedExpiryTime(obj.ModTime, int(rule.Expiration.Days))
|
||||
if finalTransitionDate.IsZero() || finalTransitionDate.After(expectedTransition) {
|
||||
}
|
||||
if due, ok := rule.NoncurrentVersionTransition.NextDue(obj); ok {
|
||||
if finalTransitionDate.IsZero() || finalTransitionDate.After(due) {
|
||||
finalTransitionRuleID = rule.ID
|
||||
finalTransitionDate = expectedTransition
|
||||
finalTransitionDate = due
|
||||
}
|
||||
}
|
||||
}
|
||||
return finalTransitionRuleID, finalTransitionDate
|
||||
}
|
||||
|
||||
// SetPredictionHeaders sets time to expiry and transition headers on w for a
|
||||
// given obj.
|
||||
func (lc Lifecycle) SetPredictionHeaders(w http.ResponseWriter, obj ObjectOpts) {
|
||||
if ruleID, expiry := lc.PredictExpiryTime(obj); !expiry.IsZero() {
|
||||
w.Header()[xhttp.AmzExpiration] = []string{
|
||||
fmt.Sprintf(`expiry-date="%s", rule-id="%s"`, expiry.Format(http.TimeFormat), ruleID),
|
||||
}
|
||||
}
|
||||
if ruleID, transition := lc.PredictTransitionTime(obj); !transition.IsZero() {
|
||||
w.Header()[xhttp.MinIOTransition] = []string{
|
||||
fmt.Sprintf(`transition-date="%s", rule-id="%s"`, transition.Format(http.TimeFormat), ruleID),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TransitionTier returns remote tier that applies to obj per ILM rules.
|
||||
func (lc Lifecycle) TransitionTier(obj ObjectOpts) string {
|
||||
for _, rule := range lc.FilterActionableRules(obj) {
|
||||
if obj.IsLatest && rule.Transition.StorageClass != "" {
|
||||
return rule.Transition.StorageClass
|
||||
}
|
||||
if !obj.IsLatest && rule.NoncurrentVersionTransition.StorageClass != "" {
|
||||
return rule.NoncurrentVersionTransition.StorageClass
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -21,8 +21,13 @@ import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
)
|
||||
|
||||
func TestParseAndValidateLifecycleConfig(t *testing.T) {
|
||||
@@ -429,3 +434,122 @@ func TestHasActiveRules(t *testing.T) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPredictionHeaders(t *testing.T) {
|
||||
lc := Lifecycle{
|
||||
Rules: []Rule{
|
||||
{
|
||||
ID: "rule-1",
|
||||
Status: "Enabled",
|
||||
Expiration: Expiration{
|
||||
Days: ExpirationDays(3),
|
||||
set: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "rule-2",
|
||||
Status: "Enabled",
|
||||
Transition: Transition{
|
||||
Days: TransitionDays(3),
|
||||
StorageClass: "TIER-1",
|
||||
set: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "rule-3",
|
||||
Status: "Enabled",
|
||||
NoncurrentVersionTransition: NoncurrentVersionTransition{
|
||||
NoncurrentDays: ExpirationDays(5),
|
||||
StorageClass: "TIER-2",
|
||||
set: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// current version
|
||||
obj1 := ObjectOpts{
|
||||
Name: "obj1",
|
||||
IsLatest: true,
|
||||
}
|
||||
// non-current version
|
||||
obj2 := ObjectOpts{
|
||||
Name: "obj2",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
obj ObjectOpts
|
||||
expRuleID int
|
||||
transRuleID int
|
||||
}{
|
||||
{
|
||||
obj: obj1,
|
||||
expRuleID: 0,
|
||||
transRuleID: 1,
|
||||
},
|
||||
{
|
||||
obj: obj2,
|
||||
expRuleID: 0,
|
||||
transRuleID: 2,
|
||||
},
|
||||
}
|
||||
for i, tc := range tests {
|
||||
w := httptest.NewRecorder()
|
||||
lc.SetPredictionHeaders(w, tc.obj)
|
||||
if expHdrs, ok := w.Header()[xhttp.AmzExpiration]; ok && !strings.Contains(expHdrs[0], lc.Rules[tc.expRuleID].ID) {
|
||||
t.Fatalf("Test %d: Expected %s header", i+1, xhttp.AmzExpiration)
|
||||
}
|
||||
if transHdrs, ok := w.Header()[xhttp.MinIOTransition]; ok {
|
||||
if !strings.Contains(transHdrs[0], lc.Rules[tc.transRuleID].ID) {
|
||||
t.Fatalf("Test %d: Expected %s header", i+1, xhttp.MinIOTransition)
|
||||
}
|
||||
|
||||
if tc.obj.IsLatest {
|
||||
if expectedDue, _ := lc.Rules[tc.transRuleID].Transition.NextDue(tc.obj); !strings.Contains(transHdrs[0], expectedDue.Format(http.TimeFormat)) {
|
||||
t.Fatalf("Test %d: Expected transition time %s", i+1, expectedDue)
|
||||
}
|
||||
} else {
|
||||
if expectedDue, _ := lc.Rules[tc.transRuleID].NoncurrentVersionTransition.NextDue(tc.obj); !strings.Contains(transHdrs[0], expectedDue.Format(http.TimeFormat)) {
|
||||
t.Fatalf("Test %d: Expected transition time %s", i+1, expectedDue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransitionTier(t *testing.T) {
|
||||
lc := Lifecycle{
|
||||
Rules: []Rule{
|
||||
{
|
||||
ID: "rule-1",
|
||||
Status: "Enabled",
|
||||
Transition: Transition{
|
||||
Days: TransitionDays(3),
|
||||
StorageClass: "TIER-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "rule-2",
|
||||
Status: "Enabled",
|
||||
NoncurrentVersionTransition: NoncurrentVersionTransition{
|
||||
NoncurrentDays: ExpirationDays(3),
|
||||
StorageClass: "TIER-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
obj1 := ObjectOpts{
|
||||
Name: "obj1",
|
||||
IsLatest: true,
|
||||
}
|
||||
obj2 := ObjectOpts{
|
||||
Name: "obj2",
|
||||
}
|
||||
if got := lc.TransitionTier(obj1); got != "TIER-1" {
|
||||
t.Fatalf("Expected TIER-1 but got %s", got)
|
||||
}
|
||||
if got := lc.TransitionTier(obj2); got != "TIER-2" {
|
||||
t.Fatalf("Expected TIER-2 but got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package lifecycle
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NoncurrentVersionExpiration - an action for lifecycle configuration rule.
|
||||
@@ -112,3 +113,14 @@ func (n NoncurrentVersionTransition) Validate() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NextDue returns upcoming NoncurrentVersionTransition date for obj if
|
||||
// applicable, returns false otherwise.
|
||||
func (n NoncurrentVersionTransition) NextDue(obj ObjectOpts) (time.Time, bool) {
|
||||
switch {
|
||||
case obj.IsLatest, n.IsDaysNull():
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
return ExpectedExpiryTime(obj.SuccessorModTime, int(n.NoncurrentDays)), true
|
||||
}
|
||||
|
||||
@@ -163,3 +163,20 @@ func (t Transition) IsDateNull() bool {
|
||||
func (t Transition) IsNull() bool {
|
||||
return t.IsDaysNull() && t.IsDateNull()
|
||||
}
|
||||
|
||||
// NextDue returns upcoming transition date for obj and true if applicable,
|
||||
// returns false otherwise.
|
||||
func (t Transition) NextDue(obj ObjectOpts) (time.Time, bool) {
|
||||
if !obj.IsLatest {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
switch {
|
||||
case !t.IsDateNull():
|
||||
return t.Date.Time, true
|
||||
case !t.IsDaysNull():
|
||||
return ExpectedExpiryTime(obj.ModTime, int(t.Days)), true
|
||||
}
|
||||
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ const (
|
||||
EnvArgs = "MINIO_ARGS"
|
||||
EnvDNSWebhook = "MINIO_DNS_WEBHOOK_ENDPOINT"
|
||||
|
||||
EnvMinIOServerURL = "MINIO_SERVER_URL"
|
||||
EnvMinIOBrowserRedirectURL = "MINIO_BROWSER_REDIRECT_URL"
|
||||
EnvRootDiskThresholdSize = "MINIO_ROOTDISK_THRESHOLD_SIZE"
|
||||
|
||||
|
||||
@@ -24,11 +24,15 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/env"
|
||||
)
|
||||
|
||||
@@ -36,6 +40,9 @@ const (
|
||||
defaultLDAPExpiry = time.Hour * 1
|
||||
|
||||
dnDelimiter = ";"
|
||||
|
||||
minLDAPExpiry time.Duration = 15 * time.Minute
|
||||
maxLDAPExpiry time.Duration = 365 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// Config contains AD/LDAP server connectivity information.
|
||||
@@ -290,7 +297,7 @@ func (l *Config) searchForUserGroups(conn *ldap.Conn, username, bindDN string) (
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// LookupUserDN searches for the full DN ang groups of a given username
|
||||
// LookupUserDN searches for the full DN and groups of a given username
|
||||
func (l *Config) LookupUserDN(username string) (string, []string, error) {
|
||||
if !l.isUsingLookupBind {
|
||||
return "", nil, errors.New("current lookup mode does not support searching for User DN")
|
||||
@@ -416,8 +423,22 @@ func (l *Config) Connect() (ldapConn *ldap.Conn, err error) {
|
||||
}
|
||||
|
||||
// GetExpiryDuration - return parsed expiry duration.
|
||||
func (l Config) GetExpiryDuration() time.Duration {
|
||||
return l.stsExpiryDuration
|
||||
func (l Config) GetExpiryDuration(dsecs string) (time.Duration, error) {
|
||||
if dsecs == "" {
|
||||
return l.stsExpiryDuration, nil
|
||||
}
|
||||
|
||||
d, err := strconv.Atoi(dsecs)
|
||||
if err != nil {
|
||||
return 0, auth.ErrInvalidDuration
|
||||
}
|
||||
|
||||
dur := time.Duration(d) * time.Second
|
||||
|
||||
if dur < minLDAPExpiry || dur > maxLDAPExpiry {
|
||||
return 0, auth.ErrInvalidDuration
|
||||
}
|
||||
return dur, nil
|
||||
}
|
||||
|
||||
func (l Config) testConnection() error {
|
||||
@@ -452,6 +473,93 @@ func (l Config) testConnection() error {
|
||||
return fmt.Errorf("LDAP connection test error: %w", err)
|
||||
}
|
||||
|
||||
// IsLDAPUserDN determines if the given string could be a user DN from LDAP.
|
||||
func (l Config) IsLDAPUserDN(user string) bool {
|
||||
return strings.HasSuffix(user, ","+l.UserDNSearchBaseDN)
|
||||
}
|
||||
|
||||
// GetNonExistentUserDistNames - find user accounts (DNs) that are no longer
|
||||
// present in the LDAP server.
|
||||
func (l *Config) GetNonExistentUserDistNames(userDistNames []string) ([]string, error) {
|
||||
if !l.isUsingLookupBind {
|
||||
return nil, errors.New("current LDAP configuration does not permit looking for expired user accounts")
|
||||
}
|
||||
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Bind to the lookup user account
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonExistentUsers := []string{}
|
||||
for _, dn := range userDistNames {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
dn,
|
||||
ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
|
||||
"(objectclass=*)",
|
||||
[]string{}, // only need DN, so no pass no attributes here
|
||||
nil,
|
||||
)
|
||||
|
||||
searchResult, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
// Object does not exist error?
|
||||
if ldap.IsErrorWithCode(err, 32) {
|
||||
nonExistentUsers = append(nonExistentUsers, dn)
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if len(searchResult.Entries) == 0 {
|
||||
// DN was not found - this means this user account is
|
||||
// expired.
|
||||
nonExistentUsers = append(nonExistentUsers, dn)
|
||||
}
|
||||
}
|
||||
return nonExistentUsers, nil
|
||||
}
|
||||
|
||||
// LookupGroupMemberships - for each DN finds the set of LDAP groups they are a
|
||||
// member of.
|
||||
func (l *Config) LookupGroupMemberships(userDistNames []string, userDNToUsernameMap map[string]string) (map[string]set.StringSet, error) {
|
||||
if !l.isUsingLookupBind {
|
||||
return nil, errors.New("current LDAP configuration does not permit this lookup")
|
||||
}
|
||||
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Bind to the lookup user account
|
||||
if err = l.lookupBind(conn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make(map[string]set.StringSet, len(userDistNames))
|
||||
for _, userDistName := range userDistNames {
|
||||
username := userDNToUsernameMap[userDistName]
|
||||
groups, err := l.searchForUserGroups(conn, username, userDistName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res[userDistName] = set.CreateStringSet(groups...)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// EnabledWithLookupBind - checks if ldap IDP is enabled in lookup bind mode.
|
||||
func (l Config) EnabledWithLookupBind() bool {
|
||||
return l.Enabled && l.isUsingLookupBind
|
||||
}
|
||||
|
||||
// Enabled returns if jwks is enabled.
|
||||
func Enabled(kvs config.KVS) bool {
|
||||
return kvs.Get(ServerAddr) != ""
|
||||
@@ -478,12 +586,16 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
|
||||
l.ServerAddr = ldapServer
|
||||
l.stsExpiryDuration = defaultLDAPExpiry
|
||||
if v := env.Get(EnvSTSExpiry, kvs.Get(STSExpiry)); v != "" {
|
||||
logger.Info("DEPRECATION WARNING: Support for configuring the default LDAP credentials expiry duration will be removed in a future release. Please use the `DurationSeconds` parameter in the LDAP STS API instead.")
|
||||
expDur, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return l, errors.New("LDAP expiry time err:" + err.Error())
|
||||
}
|
||||
if expDur <= 0 {
|
||||
return l, errors.New("LDAP expiry time has to be positive")
|
||||
if expDur < minLDAPExpiry {
|
||||
return l, fmt.Errorf("LDAP expiry time must be at least %s", minLDAPExpiry)
|
||||
}
|
||||
if expDur > maxLDAPExpiry {
|
||||
return l, fmt.Errorf("LDAP expiry time may not exceed %s", maxLDAPExpiry)
|
||||
}
|
||||
l.STSExpiryDuration = v
|
||||
l.stsExpiryDuration = expDur
|
||||
|
||||
@@ -30,7 +30,7 @@ var (
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: STSExpiry,
|
||||
Description: `temporary credentials validity duration in s,m,h,d. Default is "1h"`,
|
||||
Description: `[DEPRECATED] temporary credentials validity duration in s,m,h,d. Default is "1h"`,
|
||||
Optional: true,
|
||||
Type: "duration",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user