mirror of
https://github.com/pgsty/minio.git
synced 2026-10-19 16:06:10 +03:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c2c92f7af | |||
| 4c71f1b4ec | |||
| 6cd8a372cb | |||
| 953a3e2bbd | |||
| 7cc0c69228 | |||
| f129fd48f2 | |||
| bc4008ced4 | |||
| 526053339b | |||
| 62a35b3e77 | |||
| 39df134204 | |||
| ad4cbce22d | |||
| 90f5e1e5f6 | |||
| aeabac9181 | |||
| b312f13473 | |||
| 727a803bc0 | |||
| d0e443172d | |||
| 60446e7ac0 | |||
| b8544266e5 | |||
| 437dd4e32a | |||
| 447054b841 | |||
| 9bf43e54cd | |||
| 60f8423157 | |||
| 4355ea3c3f | |||
| e30f1ad7bd | |||
| f00c8c4cce | |||
| 703f51164d | |||
| b8dde47d4e | |||
| 7fa3e39f85 | |||
| 4df7a3aa8f | |||
| 64a8f2e554 | |||
| f4fd4ea66d | |||
| 712fe1a8df | |||
| 4a319bedc9 | |||
| bdb3db6dad | |||
| abb385af41 | |||
| 4ee62606e4 | |||
| 079d64c801 | |||
| dcc000ae2c | |||
| c5d19ecebb | |||
| ed29a525b3 | |||
| 020c46cd3c |
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.23.2
|
||||
go-version: 1.23.5
|
||||
- name: Get official govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
shell: bash
|
||||
|
||||
+10
-1
@@ -21,16 +21,25 @@ linters:
|
||||
- misspell
|
||||
- revive
|
||||
- staticcheck
|
||||
- tenv
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- usetesting
|
||||
- forcetypeassert
|
||||
- whitespace
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
max-issues-per-linter: 100
|
||||
max-same-issues: 100
|
||||
exclude:
|
||||
- "empty-block:"
|
||||
- "unused-parameter:"
|
||||
- "dot-imports:"
|
||||
- should have a package comment
|
||||
- error strings should not be capitalized or end with punctuation or a newline
|
||||
exclude-rules:
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- forcetypeassert
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
FROM golang:1.23-alpine AS build
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG RELEASE
|
||||
|
||||
ENV GOPATH=/go
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
# Install curl and minisign
|
||||
RUN apk add -U --no-cache ca-certificates && \
|
||||
apk add -U --no-cache curl && \
|
||||
go install aead.dev/minisign/cmd/minisign@v0.2.1
|
||||
|
||||
# Download minio binary and signature files
|
||||
RUN curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips -o /go/bin/minio && \
|
||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.minisig -o /go/bin/minio.minisig && \
|
||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.sha256sum -o /go/bin/minio.sha256sum && \
|
||||
chmod +x /go/bin/minio
|
||||
|
||||
# Download mc binary and signature files
|
||||
RUN curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.fips -o /go/bin/mc && \
|
||||
curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.fips.minisig -o /go/bin/mc.minisig && \
|
||||
curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.fips.sha256sum -o /go/bin/mc.sha256sum && \
|
||||
chmod +x /go/bin/mc
|
||||
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
curl -L -s -q https://github.com/moparisthebest/static-curl/releases/latest/download/curl-${TARGETARCH} -o /go/bin/curl; \
|
||||
chmod +x /go/bin/curl; \
|
||||
fi
|
||||
|
||||
# Verify binary signature using public key "RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGavRUN"
|
||||
RUN minisign -Vqm /go/bin/minio -x /go/bin/minio.minisig -P RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav
|
||||
|
||||
FROM registry.access.redhat.com/ubi9/ubi-micro:latest
|
||||
|
||||
ARG RELEASE
|
||||
|
||||
LABEL name="MinIO" \
|
||||
vendor="MinIO Inc <dev@min.io>" \
|
||||
maintainer="MinIO Inc <dev@min.io>" \
|
||||
version="${RELEASE}" \
|
||||
release="${RELEASE}" \
|
||||
summary="MinIO is a High Performance Object Storage, API compatible with Amazon S3 cloud storage service." \
|
||||
description="MinIO object storage is fundamentally different. Designed for performance and the S3 API, it is 100% open-source. MinIO is ideal for large, private cloud environments with stringent security requirements and delivers mission-critical availability across a diverse range of workloads."
|
||||
|
||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
MINIO_SECRET_KEY_FILE=secret_key \
|
||||
MINIO_ROOT_USER_FILE=access_key \
|
||||
MINIO_ROOT_PASSWORD_FILE=secret_key \
|
||||
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
|
||||
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
|
||||
MINIO_CONFIG_ENV_FILE=config.env
|
||||
|
||||
RUN chmod -R 777 /usr/bin
|
||||
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=build /go/bin/minio* /usr/bin/
|
||||
COPY --from=build /go/bin/mc* /usr/bin/
|
||||
COPY --from=build /go/bin/cur* /usr/bin/
|
||||
|
||||
COPY CREDITS /licenses/CREDITS
|
||||
COPY LICENSE /licenses/LICENSE
|
||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
|
||||
|
||||
EXPOSE 9000
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
CMD ["minio"]
|
||||
@@ -2,8 +2,8 @@ PWD := $(shell pwd)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
||||
|
||||
GOARCH := $(shell go env GOARCH)
|
||||
GOOS := $(shell go env GOOS)
|
||||
GOOS ?= $(shell go env GOOS)
|
||||
GOARCH ?= $(shell go env GOARCH)
|
||||
|
||||
VERSION ?= $(shell git describe --tags)
|
||||
REPO ?= quay.io/minio
|
||||
@@ -180,7 +180,7 @@ build-debugging:
|
||||
|
||||
build: checks build-debugging ## builds minio to $(PWD)
|
||||
@echo "Building minio binary to './minio'"
|
||||
@CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||
@CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||
|
||||
hotfix-vars:
|
||||
$(eval LDFLAGS := $(shell MINIO_RELEASE="RELEASE" MINIO_HOTFIX="hotfix.$(shell git rev-parse --short HEAD)" go run buildscripts/gen-ldflags.go $(shell git describe --tags --abbrev=0 | \
|
||||
@@ -188,9 +188,9 @@ hotfix-vars:
|
||||
$(eval VERSION := $(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD))
|
||||
|
||||
hotfix: hotfix-vars clean install ## builds minio binary with hotfix tags
|
||||
@wget -q -c https://github.com/minio/pkger/releases/download/v2.3.1/pkger_2.3.1_linux_amd64.deb
|
||||
@wget -q -c https://raw.githubusercontent.com/minio/minio-service/v1.0.1/linux-systemd/distributed/minio.service
|
||||
@sudo apt install ./pkger_2.3.1_linux_amd64.deb --yes
|
||||
@wget -q -c https://github.com/minio/pkger/releases/download/v2.3.10/pkger_2.3.10_linux_amd64.deb
|
||||
@wget -q -c https://raw.githubusercontent.com/minio/minio-service/v1.1.0/linux-systemd/distributed/minio.service
|
||||
@sudo apt install ./pkger_2.3.10_linux_amd64.deb --yes
|
||||
@mkdir -p minio-release/$(GOOS)-$(GOARCH)/archive
|
||||
@cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio
|
||||
@cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio.$(VERSION)
|
||||
@@ -200,11 +200,11 @@ hotfix: hotfix-vars clean install ## builds minio binary with hotfix tags
|
||||
@pkger -r $(VERSION) --ignore
|
||||
|
||||
hotfix-push: hotfix
|
||||
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-amd64/
|
||||
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-amd64/archive
|
||||
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-amd64/
|
||||
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-amd64/archive
|
||||
@echo "Published new hotfix binaries at https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.$(VERSION)"
|
||||
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-$(GOOS)/
|
||||
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-$(GOOS)/archive
|
||||
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-$(GOOS)/
|
||||
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-$(GOOS)/archive
|
||||
@echo "Published new hotfix binaries at https://dl.min.io/server/minio/hotfixes/linux-$(GOOS)/archive/minio.$(VERSION)"
|
||||
|
||||
docker-hotfix-push: docker-hotfix
|
||||
@docker push -q $(TAG) && echo "Published new container $(TAG)"
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||
"github.com/minio/minio/internal/bucket/versioning"
|
||||
"github.com/minio/minio/internal/event"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/v3/policy"
|
||||
@@ -980,7 +981,6 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
rpt.SetStatus(bucket, "", err)
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
rptData, err := json.Marshal(rpt.BucketMetaImportErrs)
|
||||
@@ -1039,7 +1039,7 @@ func (a adminAPIHandlers) ReplicationDiffHandler(w http.ResponseWriter, r *http.
|
||||
}
|
||||
if len(diffCh) == 0 {
|
||||
// Flush if nothing is queued
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
case <-keepAliveTicker.C:
|
||||
if len(diffCh) > 0 {
|
||||
@@ -1048,7 +1048,7 @@ func (a adminAPIHandlers) ReplicationDiffHandler(w http.ResponseWriter, r *http.
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
@@ -1098,7 +1098,7 @@ func (a adminAPIHandlers) ReplicationMRFHandler(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
if len(mrfCh) == 0 {
|
||||
// Flush if nothing is queued
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
case <-keepAliveTicker.C:
|
||||
if len(mrfCh) > 0 {
|
||||
@@ -1107,7 +1107,7 @@ func (a adminAPIHandlers) ReplicationMRFHandler(w http.ResponseWriter, r *http.R
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
@@ -125,7 +125,6 @@ func addOrUpdateIDPHandler(ctx context.Context, w http.ResponseWriter, r *http.R
|
||||
}
|
||||
|
||||
if err = validateConfig(ctx, cfg, subSys); err != nil {
|
||||
|
||||
var validationErr ldap.Validation
|
||||
if errors.As(err, &validationErr) {
|
||||
// If we got an LDAP validation error, we need to send appropriate
|
||||
@@ -416,7 +415,6 @@ func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
if err = validateConfig(ctx, cfg, subSys); err != nil {
|
||||
|
||||
var validationErr ldap.Validation
|
||||
if errors.As(err, &validationErr) {
|
||||
// If we got an LDAP validation error, we need to send appropriate
|
||||
|
||||
@@ -345,7 +345,7 @@ func (a adminAPIHandlers) AddServiceAccountLDAP(w http.ResponseWriter, r *http.R
|
||||
Name: newCred.Name,
|
||||
Description: newCred.Description,
|
||||
Claims: opts.claims,
|
||||
SessionPolicy: createReq.Policy,
|
||||
SessionPolicy: madmin.SRSessionPolicy(createReq.Policy),
|
||||
Status: auth.AccountOn,
|
||||
Expiration: createReq.Expiration,
|
||||
},
|
||||
|
||||
@@ -259,7 +259,7 @@ func (a adminAPIHandlers) RebalanceStart(w http.ResponseWriter, r *http.Request)
|
||||
if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
|
||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||
if proxyEp.Endpoint.Host == ep.Host {
|
||||
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
||||
if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -330,7 +330,7 @@ func (a adminAPIHandlers) RebalanceStatus(w http.ResponseWriter, r *http.Request
|
||||
if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
|
||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||
if proxyEp.Endpoint.Host == ep.Host {
|
||||
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
||||
if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -384,7 +384,7 @@ func proxyDecommissionRequest(ctx context.Context, defaultEndPoint Endpoint, w h
|
||||
}
|
||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||
if proxyEp.Endpoint.Host == host && !proxyEp.IsLocal {
|
||||
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
||||
if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,7 +783,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
Name: newCred.Name,
|
||||
Description: newCred.Description,
|
||||
Claims: opts.claims,
|
||||
SessionPolicy: createReq.Policy,
|
||||
SessionPolicy: madmin.SRSessionPolicy(createReq.Policy),
|
||||
Status: auth.AccountOn,
|
||||
Expiration: createReq.Expiration,
|
||||
},
|
||||
@@ -907,7 +907,7 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
|
||||
Status: opts.status,
|
||||
Name: opts.name,
|
||||
Description: opts.description,
|
||||
SessionPolicy: updateReq.NewPolicy,
|
||||
SessionPolicy: madmin.SRSessionPolicy(updateReq.NewPolicy),
|
||||
Expiration: updateReq.NewExpiration,
|
||||
},
|
||||
},
|
||||
@@ -1487,8 +1487,8 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
effectivePolicy = globalIAMSys.GetCombinedPolicy(policies...)
|
||||
|
||||
}
|
||||
|
||||
buf, err = json.MarshalIndent(effectivePolicy, "", " ")
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
@@ -2169,7 +2169,7 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
SecretKey: acc.Credentials.SecretKey,
|
||||
Groups: acc.Credentials.Groups,
|
||||
Claims: claims,
|
||||
SessionPolicy: json.RawMessage(policyJSON),
|
||||
SessionPolicy: policyJSON,
|
||||
Status: acc.Credentials.Status,
|
||||
Name: sa.Name,
|
||||
Description: sa.Description,
|
||||
@@ -2279,7 +2279,6 @@ func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiV
|
||||
|
||||
// import policies first
|
||||
{
|
||||
|
||||
f, err := zr.Open(pathJoin(iamAssetsDir, allPoliciesFile))
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
@@ -2362,7 +2361,6 @@ func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiV
|
||||
} else {
|
||||
added.Users = append(added.Users, accessKey)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-16
@@ -829,7 +829,7 @@ func (a adminAPIHandlers) MetricsHandler(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Flush before waiting for next...
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
|
||||
select {
|
||||
case <-ticker.C:
|
||||
@@ -1320,7 +1320,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Analyze the heal token and route the request accordingly
|
||||
token, success := proxyRequestByToken(ctx, w, r, hip.clientToken)
|
||||
token, _, success := proxyRequestByToken(ctx, w, r, hip.clientToken, false)
|
||||
if success {
|
||||
return
|
||||
}
|
||||
@@ -1359,7 +1359,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
case hr := <-respCh:
|
||||
switch hr.apiErr {
|
||||
case noError:
|
||||
@@ -1367,7 +1367,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := w.Write(hr.respBytes); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
} else {
|
||||
writeSuccessResponseJSON(w, hr.respBytes)
|
||||
}
|
||||
@@ -1394,7 +1394,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := w.Write(errorRespJSON); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
break forLoop
|
||||
}
|
||||
@@ -1611,7 +1611,6 @@ func (a adminAPIHandlers) ClientDevNull(w http.ResponseWriter, r *http.Request)
|
||||
if err != nil || ctx.Err() != nil || totalRx > 100*humanize.GiByte {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
@@ -1840,7 +1839,7 @@ func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
case result, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
@@ -1849,7 +1848,7 @@ func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
prevResult = result
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1958,7 +1957,7 @@ func (a adminAPIHandlers) DriveSpeedtestHandler(w http.ResponseWriter, r *http.R
|
||||
if err := enc.Encode(madmin.DriveSpeedTestResult{}); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
case result, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
@@ -1966,7 +1965,7 @@ func (a adminAPIHandlers) DriveSpeedtestHandler(w http.ResponseWriter, r *http.R
|
||||
if err := enc.Encode(result); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2083,7 +2082,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
grid.PutByteBuffer(entry)
|
||||
if len(traceCh) == 0 {
|
||||
// Flush if nothing is queued
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
case <-keepAliveTicker.C:
|
||||
if len(traceCh) > 0 {
|
||||
@@ -2092,7 +2091,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
@@ -2184,7 +2183,7 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
||||
grid.PutByteBuffer(log)
|
||||
if len(logCh) == 0 {
|
||||
// Flush if nothing is queued
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
case <-keepAliveTicker.C:
|
||||
if len(logCh) > 0 {
|
||||
@@ -2193,7 +2192,7 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
@@ -2963,13 +2962,13 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
if len(healthInfoCh) == 0 {
|
||||
// Flush if nothing is queued
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
case <-ticker.C:
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
case <-healthCtx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ type DeletedObject struct {
|
||||
DeleteMarkerMTime DeleteMarkerMTime `xml:"-"`
|
||||
// MinIO extensions to support delete marker replication
|
||||
ReplicationState ReplicationState `xml:"-"`
|
||||
|
||||
found bool // the object was found during deletion
|
||||
}
|
||||
|
||||
// DeleteMarkerMTime is an embedded type containing time.Time for XML marshal
|
||||
|
||||
@@ -2254,6 +2254,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrServerNotInitialized
|
||||
case errBucketMetadataNotInitialized:
|
||||
apiErr = ErrBucketMetadataNotInitialized
|
||||
case hash.ErrInvalidChecksum:
|
||||
apiErr = ErrInvalidChecksum
|
||||
}
|
||||
|
||||
// Compression errors
|
||||
|
||||
+1
-2
@@ -520,7 +520,6 @@ func cleanReservedKeys(metadata map[string]string) map[string]string {
|
||||
}
|
||||
case crypto.SSEC:
|
||||
m[xhttp.AmzServerSideEncryptionCustomerAlgorithm] = xhttp.AmzEncryptionAES
|
||||
|
||||
}
|
||||
|
||||
var toRemove []string
|
||||
@@ -791,7 +790,7 @@ func generateInitiateMultipartUploadResponse(bucket, key, uploadID string) Initi
|
||||
|
||||
// generates CompleteMultipartUploadResponse for given bucket, key, location and ETag.
|
||||
func generateCompleteMultipartUploadResponse(bucket, key, location string, oi ObjectInfo, h http.Header) CompleteMultipartUploadResponse {
|
||||
cs := oi.decryptChecksums(0, h)
|
||||
cs, _ := oi.decryptChecksums(0, h)
|
||||
c := CompleteMultipartUploadResponse{
|
||||
Location: location,
|
||||
Bucket: bucket,
|
||||
|
||||
@@ -162,7 +162,6 @@ func validateAdminSignature(ctx context.Context, r *http.Request, region string)
|
||||
s3Err := ErrAccessDenied
|
||||
if _, ok := r.Header[xhttp.AmzContentSha256]; ok &&
|
||||
getRequestAuthType(r) == authTypeSigned {
|
||||
|
||||
// Get credential information from the request.
|
||||
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||
if s3Err != ErrNone {
|
||||
|
||||
+14
-11
@@ -195,8 +195,8 @@ func (ef BatchJobExpireFilter) Matches(obj ObjectInfo, now time.Time) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(ef.Metadata) > 0 && !obj.DeleteMarker {
|
||||
for _, kv := range ef.Metadata {
|
||||
// Object (version) must match all x-amz-meta and
|
||||
@@ -289,6 +289,16 @@ type BatchJobExpire struct {
|
||||
|
||||
var _ yaml.Unmarshaler = &BatchJobExpire{}
|
||||
|
||||
// RedactSensitive will redact any sensitive information in b.
|
||||
func (r *BatchJobExpire) RedactSensitive() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if r.NotificationCfg.Token != "" {
|
||||
r.NotificationCfg.Token = redactedText
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalYAML - BatchJobExpire extends default unmarshal to extract line, col information.
|
||||
func (r *BatchJobExpire) UnmarshalYAML(val *yaml.Node) error {
|
||||
type expireJob BatchJobExpire
|
||||
@@ -435,14 +445,14 @@ func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo
|
||||
oiCache.Add(od, &exp.ObjectInfo)
|
||||
}
|
||||
|
||||
var done bool
|
||||
// DeleteObject(deletePrefix: true) to expire all versions of an object
|
||||
for _, exp := range toExpireAll {
|
||||
var success bool
|
||||
for attempts := 1; attempts <= retryAttempts; attempts++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
done = true
|
||||
ri.trackMultipleObjectVersions(exp, success)
|
||||
return
|
||||
default:
|
||||
}
|
||||
stopFn := globalBatchJobsMetrics.trace(batchJobMetricExpire, ri.JobID, attempts)
|
||||
@@ -459,14 +469,7 @@ func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo
|
||||
break
|
||||
}
|
||||
}
|
||||
ri.trackMultipleObjectVersions(r.Bucket, exp, success)
|
||||
if done {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if done {
|
||||
return
|
||||
ri.trackMultipleObjectVersions(exp, success)
|
||||
}
|
||||
|
||||
// DeleteMultiple objects
|
||||
|
||||
+37
-12
@@ -61,6 +61,8 @@ var globalBatchConfig batch.Config
|
||||
const (
|
||||
// Keep the completed/failed job stats 3 days before removing it
|
||||
oldJobsExpiration = 3 * 24 * time.Hour
|
||||
|
||||
redactedText = "**REDACTED**"
|
||||
)
|
||||
|
||||
// BatchJobRequest this is an internal data structure not for external consumption.
|
||||
@@ -74,6 +76,29 @@ type BatchJobRequest struct {
|
||||
ctx context.Context `msg:"-"`
|
||||
}
|
||||
|
||||
// RedactSensitive will redact any sensitive information in b.
|
||||
func (j *BatchJobRequest) RedactSensitive() {
|
||||
j.Replicate.RedactSensitive()
|
||||
j.Expire.RedactSensitive()
|
||||
j.KeyRotate.RedactSensitive()
|
||||
}
|
||||
|
||||
// RedactSensitive will redact any sensitive information in b.
|
||||
func (r *BatchJobReplicateV1) RedactSensitive() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if r.Target.Creds.SecretKey != "" {
|
||||
r.Target.Creds.SecretKey = redactedText
|
||||
}
|
||||
if r.Target.Creds.SessionToken != "" {
|
||||
r.Target.Creds.SessionToken = redactedText
|
||||
}
|
||||
}
|
||||
|
||||
// RedactSensitive will redact any sensitive information in b.
|
||||
func (r *BatchJobKeyRotateV1) RedactSensitive() {}
|
||||
|
||||
func notifyEndpoint(ctx context.Context, ri *batchJobInfo, endpoint, token string) error {
|
||||
if endpoint == "" {
|
||||
return nil
|
||||
@@ -597,12 +622,12 @@ func (r BatchJobReplicateV1) writeAsArchive(ctx context.Context, objAPI ObjectLa
|
||||
},
|
||||
}
|
||||
|
||||
opts, err := batchReplicationOpts(ctx, "", gr.ObjInfo)
|
||||
opts, _, err := batchReplicationOpts(ctx, "", gr.ObjInfo)
|
||||
if err != nil {
|
||||
batchLogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: I am not sure we read it back, but we aren't sending whether checksums are single/multipart.
|
||||
for k, vals := range opts.Header() {
|
||||
for _, v := range vals {
|
||||
snowballObj.Headers.Add(k, v)
|
||||
@@ -687,14 +712,14 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
|
||||
return err
|
||||
}
|
||||
|
||||
putOpts, err := batchReplicationOpts(ctx, "", objInfo)
|
||||
putOpts, isMP, err := batchReplicationOpts(ctx, "", objInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
|
||||
putOpts.Internal = miniogo.AdvancedPutOptions{}
|
||||
}
|
||||
if objInfo.isMultipart() {
|
||||
if isMP {
|
||||
if err := replicateObjectWithMultipart(ctx, c, tgtBucket, pathJoin(tgtPrefix, objInfo.Name), rd, objInfo, putOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -971,7 +996,7 @@ func (ri *batchJobInfo) updateAfter(ctx context.Context, api ObjectLayer, durati
|
||||
// Note: to be used only with batch jobs that affect multiple versions through
|
||||
// a single action. e.g batch-expire has an option to expire all versions of an
|
||||
// object which matches the given filters.
|
||||
func (ri *batchJobInfo) trackMultipleObjectVersions(bucket string, info ObjectInfo, success bool) {
|
||||
func (ri *batchJobInfo) trackMultipleObjectVersions(info ObjectInfo, success bool) {
|
||||
if success {
|
||||
ri.Objects += int64(info.NumVersions)
|
||||
} else {
|
||||
@@ -1425,7 +1450,6 @@ func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest,
|
||||
cred = r.Source.Creds
|
||||
remoteBkt = r.Source.Bucket
|
||||
pathStyle = r.Source.Path
|
||||
|
||||
}
|
||||
|
||||
u, err := url.Parse(remoteEp)
|
||||
@@ -1551,11 +1575,11 @@ func (j *BatchJobRequest) load(ctx context.Context, api ObjectLayer, name string
|
||||
return err
|
||||
}
|
||||
|
||||
func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions, err error) {
|
||||
func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions, isMP bool, err error) {
|
||||
// TODO: support custom storage class for remote replication
|
||||
putOpts, err = putReplicationOpts(ctx, "", objInfo, 0)
|
||||
putOpts, isMP, err = putReplicationOpts(ctx, "", objInfo)
|
||||
if err != nil {
|
||||
return putOpts, err
|
||||
return putOpts, isMP, err
|
||||
}
|
||||
putOpts.Internal = miniogo.AdvancedPutOptions{
|
||||
SourceVersionID: objInfo.VersionID,
|
||||
@@ -1563,7 +1587,7 @@ func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (p
|
||||
SourceETag: objInfo.ETag,
|
||||
ReplicationRequest: true,
|
||||
}
|
||||
return putOpts, nil
|
||||
return putOpts, isMP, nil
|
||||
}
|
||||
|
||||
// ListBatchJobs - lists all currently active batch jobs, optionally takes {jobType}
|
||||
@@ -1695,6 +1719,8 @@ func (a adminAPIHandlers) DescribeBatchJob(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
// Remove sensitive fields.
|
||||
req.RedactSensitive()
|
||||
buf, err := yaml.Marshal(req)
|
||||
if err != nil {
|
||||
batchLogIf(ctx, err)
|
||||
@@ -1802,7 +1828,7 @@ func (a adminAPIHandlers) CancelBatchJob(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if _, success := proxyRequestByToken(ctx, w, r, jobID); success {
|
||||
if _, proxied, _ := proxyRequestByToken(ctx, w, r, jobID, true); proxied {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2283,7 +2309,6 @@ func lookupStyle(s string) miniogo.BucketLookupType {
|
||||
lookup = miniogo.BucketLookupDNS
|
||||
default:
|
||||
lookup = miniogo.BucketLookupAuto
|
||||
|
||||
}
|
||||
return lookup
|
||||
}
|
||||
|
||||
+13
-7
@@ -128,14 +128,20 @@ func closeBitrotReaders(rs []io.ReaderAt) {
|
||||
}
|
||||
|
||||
// Close all the writers.
|
||||
func closeBitrotWriters(ws []io.Writer) {
|
||||
for _, w := range ws {
|
||||
if w != nil {
|
||||
if bw, ok := w.(io.Closer); ok {
|
||||
bw.Close()
|
||||
}
|
||||
func closeBitrotWriters(ws []io.Writer) []error {
|
||||
errs := make([]error, len(ws))
|
||||
for i, w := range ws {
|
||||
if w == nil {
|
||||
errs[i] = errDiskNotFound
|
||||
continue
|
||||
}
|
||||
if bw, ok := w.(io.Closer); ok {
|
||||
errs[i] = bw.Close()
|
||||
} else {
|
||||
errs[i] = nil
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Returns hash sum for whole-bitrot, nil for streaming-bitrot.
|
||||
@@ -178,7 +184,7 @@ func bitrotVerify(r io.Reader, wantSize, partSize int64, algo BitrotAlgorithm, w
|
||||
return errFileCorrupt
|
||||
}
|
||||
|
||||
bufp := xioutil.ODirectPoolSmall.Get().(*[]byte)
|
||||
bufp := xioutil.ODirectPoolSmall.Get()
|
||||
defer xioutil.ODirectPoolSmall.Put(bufp)
|
||||
|
||||
for left > 0 {
|
||||
|
||||
@@ -344,11 +344,9 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
Created: dnsRecords[0].CreationDate,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(bucketsInfo, func(i, j int) bool {
|
||||
return bucketsInfo[i].Name < bucketsInfo[j].Name
|
||||
})
|
||||
|
||||
} else {
|
||||
// Invoke the list buckets.
|
||||
var err error
|
||||
@@ -841,7 +839,6 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
|
||||
}
|
||||
apiErr := ErrBucketAlreadyExists
|
||||
if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
|
||||
|
||||
@@ -188,7 +188,6 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
if errorResponse.Code != testCase.errorResponse.Code {
|
||||
t.Errorf("Test %d: %s: Expected the error code to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Code, errorResponse.Code)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test for Anonymous/unsigned http request.
|
||||
@@ -290,7 +289,6 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
|
||||
if recV2.Code != testCase.expectedRespStatus {
|
||||
t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, recV2.Code)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test for Anonymous/unsigned http request.
|
||||
|
||||
@@ -243,26 +243,26 @@ func parseRequestToken(token string) (subToken string, nodeIndex int) {
|
||||
return subToken, nodeIndex
|
||||
}
|
||||
|
||||
func proxyRequestByToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token string) (string, bool) {
|
||||
subToken, nodeIndex := parseRequestToken(token)
|
||||
if nodeIndex >= 0 {
|
||||
return subToken, proxyRequestByNodeIndex(ctx, w, r, nodeIndex)
|
||||
func proxyRequestByToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token string, returnErr bool) (subToken string, proxied bool, success bool) {
|
||||
var nodeIndex int
|
||||
if subToken, nodeIndex = parseRequestToken(token); nodeIndex >= 0 {
|
||||
proxied, success = proxyRequestByNodeIndex(ctx, w, r, nodeIndex, returnErr)
|
||||
}
|
||||
return subToken, false
|
||||
return
|
||||
}
|
||||
|
||||
func proxyRequestByNodeIndex(ctx context.Context, w http.ResponseWriter, r *http.Request, index int) (success bool) {
|
||||
func proxyRequestByNodeIndex(ctx context.Context, w http.ResponseWriter, r *http.Request, index int, returnErr bool) (proxied, success bool) {
|
||||
if len(globalProxyEndpoints) == 0 {
|
||||
return false
|
||||
return
|
||||
}
|
||||
if index < 0 || index >= len(globalProxyEndpoints) {
|
||||
return false
|
||||
return
|
||||
}
|
||||
ep := globalProxyEndpoints[index]
|
||||
if ep.IsLocal {
|
||||
return false
|
||||
return
|
||||
}
|
||||
return proxyRequest(ctx, w, r, ep)
|
||||
return true, proxyRequest(ctx, w, r, ep, returnErr)
|
||||
}
|
||||
|
||||
// ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
|
||||
|
||||
@@ -112,7 +112,6 @@ func (r *ReplicationStats) collectWorkerMetrics(ctx context.Context) {
|
||||
r.wlock.Lock()
|
||||
r.workers.update()
|
||||
r.wlock.Unlock()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+67
-39
@@ -49,6 +49,7 @@ import (
|
||||
"github.com/minio/minio/internal/hash"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
xioutil "github.com/minio/minio/internal/ioutil"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/once"
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
@@ -145,7 +146,7 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
|
||||
if errInt == nil {
|
||||
err = nil
|
||||
} else {
|
||||
err = errInt.(error)
|
||||
err, _ = errInt.(error)
|
||||
}
|
||||
}
|
||||
switch err.(type) {
|
||||
@@ -773,7 +774,7 @@ func (m caseInsensitiveMap) Lookup(key string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, partNum int) (putOpts minio.PutObjectOptions, err error) {
|
||||
func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts minio.PutObjectOptions, isMP bool, err error) {
|
||||
meta := make(map[string]string)
|
||||
isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined)
|
||||
|
||||
@@ -794,23 +795,22 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
||||
meta[k] = v
|
||||
}
|
||||
}
|
||||
isMP = objInfo.isMultipart()
|
||||
if len(objInfo.Checksum) > 0 {
|
||||
// Add encrypted CRC to metadata for SSE-C objects.
|
||||
if isSSEC {
|
||||
meta[ReplicationSsecChecksumHeader] = base64.StdEncoding.EncodeToString(objInfo.Checksum)
|
||||
} else {
|
||||
if objInfo.isMultipart() && partNum > 0 {
|
||||
for _, pi := range objInfo.Parts {
|
||||
if pi.Number == partNum {
|
||||
for k, v := range pi.Checksums { // for PutObjectPart
|
||||
meta[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for k, v := range getCRCMeta(objInfo, 0, nil) { // for PutObject/NewMultipartUpload
|
||||
meta[k] = v
|
||||
}
|
||||
cs, mp := getCRCMeta(objInfo, 0, nil)
|
||||
// Set object checksum.
|
||||
for k, v := range cs {
|
||||
meta[k] = v
|
||||
}
|
||||
isMP = mp
|
||||
if !objInfo.isMultipart() && cs[xhttp.AmzChecksumType] == xhttp.AmzChecksumTypeFullObject {
|
||||
// For objects where checksum is full object, it will be the same.
|
||||
// Therefore, we use the cheaper PutObject replication.
|
||||
isMP = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -841,7 +841,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
||||
if tagTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp]; ok {
|
||||
tagTimestamp, err = time.Parse(time.RFC3339Nano, tagTmstampStr)
|
||||
if err != nil {
|
||||
return putOpts, err
|
||||
return putOpts, false, err
|
||||
}
|
||||
}
|
||||
putOpts.Internal.TaggingTimestamp = tagTimestamp
|
||||
@@ -865,7 +865,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
||||
if retainDateStr, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok {
|
||||
rdate, err := amztime.ISO8601Parse(retainDateStr)
|
||||
if err != nil {
|
||||
return putOpts, err
|
||||
return putOpts, false, err
|
||||
}
|
||||
putOpts.RetainUntilDate = rdate
|
||||
// set retention timestamp in opts
|
||||
@@ -873,7 +873,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
||||
if retainTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok {
|
||||
retTimestamp, err = time.Parse(time.RFC3339Nano, retainTmstampStr)
|
||||
if err != nil {
|
||||
return putOpts, err
|
||||
return putOpts, false, err
|
||||
}
|
||||
}
|
||||
putOpts.Internal.RetentionTimestamp = retTimestamp
|
||||
@@ -885,7 +885,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
||||
if lholdTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp]; ok {
|
||||
lholdTimestamp, err = time.Parse(time.RFC3339Nano, lholdTmstampStr)
|
||||
if err != nil {
|
||||
return putOpts, err
|
||||
return putOpts, false, err
|
||||
}
|
||||
}
|
||||
putOpts.Internal.LegalholdTimestamp = lholdTimestamp
|
||||
@@ -895,9 +895,19 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
||||
}
|
||||
|
||||
if crypto.S3KMS.IsEncrypted(objInfo.UserDefined) {
|
||||
sseEnc, err := encrypt.NewSSEKMS(objInfo.KMSKeyID(), nil)
|
||||
// If KMS key ID replication is enabled (as by default)
|
||||
// we include the object's KMS key ID. In any case, we
|
||||
// always set the SSE-KMS header. If no KMS key ID is
|
||||
// specified, MinIO is supposed to use whatever default
|
||||
// config applies on the site or bucket.
|
||||
var keyID string
|
||||
if kms.ReplicateKeyID() {
|
||||
keyID = objInfo.KMSKeyID()
|
||||
}
|
||||
|
||||
sseEnc, err := encrypt.NewSSEKMS(keyID, nil)
|
||||
if err != nil {
|
||||
return putOpts, err
|
||||
return putOpts, false, err
|
||||
}
|
||||
putOpts.ServerSideEncryption = sseEnc
|
||||
}
|
||||
@@ -958,7 +968,11 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati
|
||||
}
|
||||
|
||||
t, _ := tags.ParseObjectTags(oi1.UserTags)
|
||||
if (oi2.UserTagCount > 0 && !reflect.DeepEqual(oi2.UserTags, t.ToMap())) || (oi2.UserTagCount != len(t.ToMap())) {
|
||||
oi2Map := make(map[string]string)
|
||||
for k, v := range oi2.UserTags {
|
||||
oi2Map[k] = v
|
||||
}
|
||||
if (oi2.UserTagCount > 0 && !reflect.DeepEqual(oi2Map, t.ToMap())) || (oi2.UserTagCount != len(t.ToMap())) {
|
||||
return replicateMetadata
|
||||
}
|
||||
|
||||
@@ -1286,7 +1300,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
||||
// use core client to avoid doing multipart on PUT
|
||||
c := &minio.Core{Client: tgt.Client}
|
||||
|
||||
putOpts, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo, 0)
|
||||
putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo)
|
||||
if err != nil {
|
||||
replLogIf(ctx, fmt.Errorf("failure setting options for replication bucket:%s err:%w", bucket, err))
|
||||
sendEvent(eventArgs{
|
||||
@@ -1318,7 +1332,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
||||
defer cancel()
|
||||
}
|
||||
r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
|
||||
if objInfo.isMultipart() {
|
||||
if isMP {
|
||||
rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts)
|
||||
} else {
|
||||
_, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts)
|
||||
@@ -1447,13 +1461,14 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
||||
}
|
||||
rinfo.Duration = time.Since(startTime)
|
||||
}()
|
||||
|
||||
oi, cerr := tgt.StatObject(ctx, tgt.Bucket, object, minio.StatObjectOptions{
|
||||
sOpts := minio.StatObjectOptions{
|
||||
VersionID: objInfo.VersionID,
|
||||
Internal: minio.AdvancedGetOptions{
|
||||
ReplicationProxyRequest: "false",
|
||||
},
|
||||
})
|
||||
}
|
||||
sOpts.Set(xhttp.AmzTagDirective, "ACCESS")
|
||||
oi, cerr := tgt.StatObject(ctx, tgt.Bucket, object, sOpts)
|
||||
if cerr == nil {
|
||||
rAction = getReplicationAction(objInfo, oi, ri.OpType)
|
||||
rinfo.ReplicationStatus = replication.Completed
|
||||
@@ -1538,19 +1553,30 @@ applyAction:
|
||||
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
|
||||
},
|
||||
}
|
||||
if tagTmStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp]; ok {
|
||||
// default timestamps to ModTime unless present in metadata
|
||||
lkMap := caseInsensitiveMap(objInfo.UserDefined)
|
||||
if _, ok := lkMap.Lookup(xhttp.AmzObjectLockLegalHold); ok {
|
||||
dstOpts.Internal.LegalholdTimestamp = objInfo.ModTime
|
||||
}
|
||||
if _, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok {
|
||||
dstOpts.Internal.RetentionTimestamp = objInfo.ModTime
|
||||
}
|
||||
if objInfo.UserTags != "" {
|
||||
dstOpts.Internal.TaggingTimestamp = objInfo.ModTime
|
||||
}
|
||||
if tagTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + TaggingTimestamp); ok {
|
||||
ondiskTimestamp, err := time.Parse(time.RFC3339, tagTmStr)
|
||||
if err == nil {
|
||||
dstOpts.Internal.TaggingTimestamp = ondiskTimestamp
|
||||
}
|
||||
}
|
||||
if retTmStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok {
|
||||
if retTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp); ok {
|
||||
ondiskTimestamp, err := time.Parse(time.RFC3339, retTmStr)
|
||||
if err == nil {
|
||||
dstOpts.Internal.RetentionTimestamp = ondiskTimestamp
|
||||
}
|
||||
}
|
||||
if lholdTmStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp]; ok {
|
||||
if lholdTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp); ok {
|
||||
ondiskTimestamp, err := time.Parse(time.RFC3339, lholdTmStr)
|
||||
if err == nil {
|
||||
dstOpts.Internal.LegalholdTimestamp = ondiskTimestamp
|
||||
@@ -1561,8 +1587,7 @@ applyAction:
|
||||
replLogIf(ctx, fmt.Errorf("unable to replicate metadata for object %s/%s(%s) to target %s: %w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), rinfo.Err))
|
||||
}
|
||||
} else {
|
||||
var putOpts minio.PutObjectOptions
|
||||
putOpts, err = putReplicationOpts(ctx, tgt.StorageClass, objInfo, 0)
|
||||
putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo)
|
||||
if err != nil {
|
||||
replLogIf(ctx, fmt.Errorf("failed to set replicate options for object %s/%s(%s) (target %s) err:%w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), err))
|
||||
sendEvent(eventArgs{
|
||||
@@ -1593,7 +1618,7 @@ applyAction:
|
||||
defer cancel()
|
||||
}
|
||||
r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
|
||||
if objInfo.isMultipart() {
|
||||
if isMP {
|
||||
rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts)
|
||||
} else {
|
||||
_, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts)
|
||||
@@ -1671,7 +1696,8 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
||||
cHeader := http.Header{}
|
||||
cHeader.Add(xhttp.MinIOSourceReplicationRequest, "true")
|
||||
if !isSSEC {
|
||||
for k, v := range getCRCMeta(objInfo, partInfo.Number, nil) {
|
||||
cs, _ := getCRCMeta(objInfo, partInfo.Number, nil)
|
||||
for k, v := range cs {
|
||||
cHeader.Add(k, v)
|
||||
}
|
||||
}
|
||||
@@ -1716,8 +1742,9 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
||||
defer ccancel()
|
||||
|
||||
if len(objInfo.Checksum) > 0 {
|
||||
for k, v := range getCRCMeta(objInfo, 0, nil) {
|
||||
userMeta[k] = v
|
||||
cs, _ := getCRCMeta(objInfo, 0, nil)
|
||||
for k, v := range cs {
|
||||
userMeta[k] = strings.Split(v, "-")[0]
|
||||
}
|
||||
}
|
||||
_, err = c.CompleteMultipartUpload(cctx, bucket, object, uploadID, uploadedParts, minio.PutObjectOptions{
|
||||
@@ -3766,9 +3793,9 @@ type validateReplicationDestinationOptions struct {
|
||||
checkReadyErr sync.Map
|
||||
}
|
||||
|
||||
func getCRCMeta(oi ObjectInfo, partNum int, h http.Header) map[string]string {
|
||||
func getCRCMeta(oi ObjectInfo, partNum int, h http.Header) (cs map[string]string, isMP bool) {
|
||||
meta := make(map[string]string)
|
||||
cs := oi.decryptChecksums(partNum, h)
|
||||
cs, isMP = oi.decryptChecksums(partNum, h)
|
||||
for k, v := range cs {
|
||||
cksum := hash.NewChecksumString(k, v)
|
||||
if cksum == nil {
|
||||
@@ -3776,8 +3803,9 @@ func getCRCMeta(oi ObjectInfo, partNum int, h http.Header) map[string]string {
|
||||
}
|
||||
if cksum.Valid() {
|
||||
meta[cksum.Type.Key()] = v
|
||||
meta[xhttp.AmzChecksumType] = cs[xhttp.AmzChecksumType]
|
||||
meta[xhttp.AmzChecksumAlgo] = cksum.Type.String()
|
||||
}
|
||||
meta[xhttp.AmzChecksumType] = cksum.Type.ObjType()
|
||||
}
|
||||
return meta
|
||||
return meta, isMP
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func Test_readFromSecret(t *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run("", func(t *testing.T) {
|
||||
tmpfile, err := os.CreateTemp("", "testfile")
|
||||
tmpfile, err := os.CreateTemp(t.TempDir(), "testfile")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -157,7 +157,7 @@ MINIO_ROOT_PASSWORD=minio123`,
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run("", func(t *testing.T) {
|
||||
tmpfile, err := os.CreateTemp("", "testfile")
|
||||
tmpfile, err := os.CreateTemp(t.TempDir(), "testfile")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
+7
-2
@@ -664,6 +664,12 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
||||
into.addChild(h)
|
||||
continue
|
||||
}
|
||||
// Adjust the probability of healing.
|
||||
// This first removes lowest x from the mod check and makes it x times more likely.
|
||||
// So if duudc = 10 and we want heal check every 50 cycles, we check
|
||||
// if (cycle/10) % (50/10) == 0, which would make heal checks run once every 50 cycles,
|
||||
// if the objects are pre-selected as 1:10.
|
||||
folder.objectHealProbDiv = dataUsageUpdateDirCycles
|
||||
}
|
||||
f.updateCurrentPath(folder.name)
|
||||
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
|
||||
@@ -858,8 +864,8 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if compact {
|
||||
stop := globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
|
||||
f.newCache.deleteRecursive(thisHash)
|
||||
@@ -873,7 +879,6 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
||||
}
|
||||
stop(total)
|
||||
}
|
||||
|
||||
}
|
||||
// Compact if too many children...
|
||||
if !into.Compacted {
|
||||
|
||||
@@ -98,7 +98,6 @@ func (dt *dynamicTimeout) logEntry(duration time.Duration) {
|
||||
|
||||
// We leak entries while we copy
|
||||
if entries == dynamicTimeoutLogSize {
|
||||
|
||||
// Make copy on stack in order to call adjust()
|
||||
logCopy := [dynamicTimeoutLogSize]time.Duration{}
|
||||
copy(logCopy[:], dt.log[:])
|
||||
|
||||
@@ -167,7 +167,6 @@ func testDynamicTimeoutAdjust(t *testing.T, timeout *dynamicTimeout, f func() fl
|
||||
const successTimeout = 20 * time.Second
|
||||
|
||||
for i := 0; i < dynamicTimeoutLogSize; i++ {
|
||||
|
||||
rnd := f()
|
||||
duration := time.Duration(float64(successTimeout) * rnd)
|
||||
|
||||
|
||||
@@ -347,8 +347,8 @@ func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byt
|
||||
return errInvalidSSEParameters // AWS returns special error for equal but invalid keys.
|
||||
}
|
||||
return crypto.ErrInvalidCustomerKey // To provide strict AWS S3 compatibility we return: access denied.
|
||||
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && sealedKey.Algorithm == crypto.SealAlgorithm {
|
||||
return nil // don't rotate on equal keys if seal algorithm is latest
|
||||
}
|
||||
@@ -1155,16 +1155,17 @@ func (o *ObjectInfo) metadataEncryptFn(headers http.Header) (objectMetaEncryptFn
|
||||
|
||||
// decryptChecksums will attempt to decode checksums and return it/them if set.
|
||||
// if part > 0, and we have the checksum for the part that will be returned.
|
||||
func (o *ObjectInfo) decryptChecksums(part int, h http.Header) map[string]string {
|
||||
// Returns whether the checksum (main part 0) is a multipart checksum.
|
||||
func (o *ObjectInfo) decryptChecksums(part int, h http.Header) (cs map[string]string, isMP bool) {
|
||||
data := o.Checksum
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
return nil, false
|
||||
}
|
||||
if part > 0 && !crypto.SSEC.IsEncrypted(o.UserDefined) {
|
||||
// already decrypted in ToObjectInfo for multipart objects
|
||||
for _, pi := range o.Parts {
|
||||
if pi.Number == part {
|
||||
return pi.Checksums
|
||||
return pi.Checksums, true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1174,7 +1175,7 @@ func (o *ObjectInfo) decryptChecksums(part int, h http.Header) map[string]string
|
||||
if err != crypto.ErrSecretKeyMismatch {
|
||||
encLogIf(GlobalContext, err)
|
||||
}
|
||||
return nil
|
||||
return nil, part > 0
|
||||
}
|
||||
data = decrypted
|
||||
}
|
||||
|
||||
@@ -362,7 +362,6 @@ func TestGetDecryptedRange(t *testing.T) {
|
||||
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Multipart object tests
|
||||
@@ -538,7 +537,6 @@ func TestGetDecryptedRange(t *testing.T) {
|
||||
i, o, l, skip, sn, ps, oRef, lRef, skipRef, snRef, psRef)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -213,7 +213,6 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||
u.Path = u.Path[1:]
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Only check if the arg is an ip address and ask for scheme since its absent.
|
||||
// localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as
|
||||
@@ -1194,7 +1193,7 @@ func GetProxyEndpointLocalIndex(proxyEps []ProxyEndpoint) int {
|
||||
}
|
||||
|
||||
// GetProxyEndpoints - get all endpoints that can be used to proxy list request.
|
||||
func GetProxyEndpoints(endpointServerPools EndpointServerPools) []ProxyEndpoint {
|
||||
func GetProxyEndpoints(endpointServerPools EndpointServerPools, transport http.RoundTripper) []ProxyEndpoint {
|
||||
var proxyEps []ProxyEndpoint
|
||||
|
||||
proxyEpSet := set.NewStringSet()
|
||||
@@ -1213,7 +1212,7 @@ func GetProxyEndpoints(endpointServerPools EndpointServerPools) []ProxyEndpoint
|
||||
|
||||
proxyEps = append(proxyEps, ProxyEndpoint{
|
||||
Endpoint: endpoint,
|
||||
Transport: globalRemoteTargetTransport,
|
||||
Transport: transport,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,6 @@ func erasureSelfTest() {
|
||||
ok = false
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
|
||||
@@ -304,7 +304,6 @@ func TestListOnlineDisks(t *testing.T) {
|
||||
f.Close()
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount
|
||||
@@ -485,7 +484,6 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
|
||||
f.Close()
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount
|
||||
|
||||
@@ -584,7 +584,6 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
readers[i] = newBitrotReader(disk, copyPartsMetadata[i].Data, bucket, partPath, tillOffset, checksumAlgo,
|
||||
checksumInfo.Hash, erasure.ShardSize())
|
||||
prefer[i] = disk.Hostname() == ""
|
||||
|
||||
}
|
||||
writers := make([]io.Writer, len(outDatedDisks))
|
||||
for i, disk := range outDatedDisks {
|
||||
@@ -609,7 +608,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
// later to the final location.
|
||||
err = erasure.Heal(ctx, writers, readers, partSize, prefer)
|
||||
closeBitrotReaders(readers)
|
||||
closeBitrotWriters(writers)
|
||||
closeErrs := closeBitrotWriters(writers)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
@@ -629,6 +628,13 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
continue
|
||||
}
|
||||
|
||||
// A non-nil stale disk which got error on Close()
|
||||
if closeErrs[i] != nil {
|
||||
outDatedDisks[i] = nil
|
||||
disksToHealCount--
|
||||
continue
|
||||
}
|
||||
|
||||
partsMetadata[i].DataDir = dstDataDir
|
||||
partsMetadata[i].AddObjectPart(partNumber, "", partSize, partActualSize, partModTime, partIdx, partChecksums)
|
||||
if len(inlineBuffers) > 0 && inlineBuffers[i] != nil {
|
||||
@@ -643,9 +649,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
if disksToHealCount == 0 {
|
||||
return result, fmt.Errorf("all drives had write errors, unable to heal %s/%s", bucket, object)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
defer er.deleteAll(context.Background(), minioMetaTmpBucket, tmpID)
|
||||
|
||||
@@ -668,10 +668,13 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
}
|
||||
|
||||
n, err := erasure.Encode(ctx, toEncode, writers, buffer, writeQuorum)
|
||||
closeBitrotWriters(writers)
|
||||
closeErrs := closeBitrotWriters(writers)
|
||||
if err != nil {
|
||||
return pi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
if closeErr := reduceWriteQuorumErrs(ctx, closeErrs, objectOpIgnoredErrs, writeQuorum); closeErr != nil {
|
||||
return pi, toObjectErr(closeErr, bucket, object)
|
||||
}
|
||||
|
||||
// Should return IncompleteBody{} error when reader has fewer bytes
|
||||
// than specified in request header.
|
||||
@@ -1049,7 +1052,6 @@ func readParts(ctx context.Context, disks []StorageAPI, bucket string, partMetaP
|
||||
PartNumber: partNumbers[pidx],
|
||||
}.Error(),
|
||||
}
|
||||
|
||||
}
|
||||
return partInfosInQuorum, nil
|
||||
}
|
||||
|
||||
+34
-10
@@ -27,6 +27,8 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -1176,11 +1178,15 @@ func (er erasureObjects) putMetacacheObject(ctx context.Context, key string, r *
|
||||
}
|
||||
|
||||
n, erasureErr := erasure.Encode(ctx, data, writers, buffer, writeQuorum)
|
||||
closeBitrotWriters(writers)
|
||||
closeErrs := closeBitrotWriters(writers)
|
||||
if erasureErr != nil {
|
||||
return ObjectInfo{}, toObjectErr(erasureErr, minioMetaBucket, key)
|
||||
}
|
||||
|
||||
if closeErr := reduceWriteQuorumErrs(ctx, closeErrs, objectOpIgnoredErrs, writeQuorum); closeErr != nil {
|
||||
return ObjectInfo{}, toObjectErr(closeErr, minioMetaBucket, key)
|
||||
}
|
||||
|
||||
// Should return IncompleteBody{} error when reader has fewer bytes
|
||||
// than specified in request header.
|
||||
if n < data.Size() {
|
||||
@@ -1423,11 +1429,15 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
bugLogIf(ctx, err)
|
||||
}
|
||||
n, erasureErr := erasure.Encode(ctx, toEncode, writers, buffer, writeQuorum)
|
||||
closeBitrotWriters(writers)
|
||||
closeErrs := closeBitrotWriters(writers)
|
||||
if erasureErr != nil {
|
||||
return ObjectInfo{}, toObjectErr(erasureErr, bucket, object)
|
||||
}
|
||||
|
||||
if closeErr := reduceWriteQuorumErrs(ctx, closeErrs, objectOpIgnoredErrs, writeQuorum); closeErr != nil {
|
||||
return ObjectInfo{}, toObjectErr(closeErr, bucket, object)
|
||||
}
|
||||
|
||||
// Should return IncompleteBody{} error when reader has fewer bytes
|
||||
// than specified in request header.
|
||||
if n < data.Size() {
|
||||
@@ -1706,8 +1716,21 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
||||
}
|
||||
|
||||
dedupVersions := make([]FileInfoVersions, 0, len(versionsMap))
|
||||
for _, version := range versionsMap {
|
||||
dedupVersions = append(dedupVersions, version)
|
||||
for _, fivs := range versionsMap {
|
||||
// Removal of existing versions and adding a delete marker in the same
|
||||
// request is supported. At the same time, we cannot allow adding
|
||||
// two delete markers on top of any object. To avoid this situation,
|
||||
// we will sort deletions to execute existing deletion first,
|
||||
// then add only one delete marker if requested
|
||||
sort.SliceStable(fivs.Versions, func(i, j int) bool {
|
||||
return !fivs.Versions[i].Deleted
|
||||
})
|
||||
if idx := slices.IndexFunc(fivs.Versions, func(fi FileInfo) bool {
|
||||
return fi.Deleted
|
||||
}); idx > -1 {
|
||||
fivs.Versions = fivs.Versions[:idx+1]
|
||||
}
|
||||
dedupVersions = append(dedupVersions, fivs)
|
||||
}
|
||||
|
||||
// Initialize list of errors.
|
||||
@@ -1732,12 +1755,6 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
||||
continue
|
||||
}
|
||||
for _, v := range dedupVersions[i].Versions {
|
||||
if err == errFileNotFound || err == errFileVersionNotFound {
|
||||
if !dobjects[v.Idx].DeleteMarker {
|
||||
// Not delete marker, if not found, ok.
|
||||
continue
|
||||
}
|
||||
}
|
||||
delObjErrs[index][v.Idx] = err
|
||||
}
|
||||
}
|
||||
@@ -1757,6 +1774,13 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
||||
}
|
||||
}
|
||||
err := reduceWriteQuorumErrs(ctx, diskErrs, objectOpIgnoredErrs, writeQuorums[objIndex])
|
||||
if err == nil {
|
||||
dobjects[objIndex].found = true
|
||||
} else if isErrVersionNotFound(err) || isErrObjectNotFound(err) {
|
||||
if !dobjects[objIndex].DeleteMarker {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if objects[objIndex].VersionID != "" {
|
||||
errs[objIndex] = toObjectErr(err, bucket, objects[objIndex].ObjectName, objects[objIndex].VersionID)
|
||||
} else {
|
||||
|
||||
@@ -131,6 +131,75 @@ func TestErasureDeleteObjectBasic(t *testing.T) {
|
||||
removeRoots(fsDirs)
|
||||
}
|
||||
|
||||
func TestDeleteObjectsVersionedTwoPools(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
obj, fsDirs, err := prepareErasurePools()
|
||||
if err != nil {
|
||||
t.Fatal("Unable to initialize 'Erasure' object layer.", err)
|
||||
}
|
||||
// Remove all dirs.
|
||||
for _, dir := range fsDirs {
|
||||
defer os.RemoveAll(dir)
|
||||
}
|
||||
|
||||
bucketName := "bucket"
|
||||
objectName := "myobject"
|
||||
err = obj.MakeBucket(ctx, bucketName, MakeBucketOptions{
|
||||
VersioningEnabled: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
z, ok := obj.(*erasureServerPools)
|
||||
if !ok {
|
||||
t.Fatal("unexpected object layer type")
|
||||
}
|
||||
|
||||
versions := make([]string, 2)
|
||||
for i := range z.serverPools {
|
||||
objInfo, err := z.serverPools[i].PutObject(ctx, bucketName, objectName,
|
||||
mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{
|
||||
Versioned: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Erasure Object upload failed: <ERROR> %s", err)
|
||||
}
|
||||
versions[i] = objInfo.VersionID
|
||||
}
|
||||
|
||||
// Remove and check the version in the second pool, then
|
||||
// remove and check the version in the first pool
|
||||
for testIdx, vid := range []string{versions[1], versions[0]} {
|
||||
names := []ObjectToDelete{
|
||||
{
|
||||
ObjectV: ObjectV{
|
||||
ObjectName: objectName,
|
||||
VersionID: vid,
|
||||
},
|
||||
},
|
||||
}
|
||||
_, delErrs := obj.DeleteObjects(ctx, bucketName, names, ObjectOptions{
|
||||
Versioned: true,
|
||||
})
|
||||
for i := range delErrs {
|
||||
if delErrs[i] != nil {
|
||||
t.Errorf("Test %d: Failed to remove object `%v` with the error: `%v`", testIdx, names[i], delErrs[i])
|
||||
}
|
||||
_, statErr := obj.GetObjectInfo(ctx, bucketName, objectName, ObjectOptions{
|
||||
VersionID: names[i].ObjectV.VersionID,
|
||||
})
|
||||
switch statErr.(type) {
|
||||
case VersionNotFound:
|
||||
default:
|
||||
t.Errorf("Test %d: Object %s is not removed", testIdx, objectName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteObjectsVersioned(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -177,7 +246,6 @@ func TestDeleteObjectsVersioned(t *testing.T) {
|
||||
VersionID: objInfo.VersionID,
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
names = append(names, ObjectToDelete{
|
||||
ObjectV: ObjectV{
|
||||
|
||||
@@ -32,9 +32,9 @@ func prepareErasurePools() (ObjectLayer, []string, error) {
|
||||
pools := mustGetPoolEndpoints(0, fsDirs[:16]...)
|
||||
pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...)
|
||||
|
||||
// Everything is fine, should return nil
|
||||
objLayer, err := newErasureServerPools(context.Background(), pools)
|
||||
objLayer, _, err := initObjectLayer(context.Background(), pools)
|
||||
if err != nil {
|
||||
removeRoots(fsDirs)
|
||||
return nil, nil, err
|
||||
}
|
||||
return objLayer, fsDirs, nil
|
||||
|
||||
+27
-74
@@ -1248,89 +1248,42 @@ func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, o
|
||||
ctx = lkctx.Context()
|
||||
defer multiDeleteLock.Unlock(lkctx)
|
||||
|
||||
// Fetch location of up to 10 objects concurrently.
|
||||
poolObjIdxMap := map[int][]ObjectToDelete{}
|
||||
origIndexMap := map[int][]int{}
|
||||
dObjectsByPool := make([][]DeletedObject, len(z.serverPools))
|
||||
dErrsByPool := make([][]error, len(z.serverPools))
|
||||
|
||||
// Always perform 1/10th of the number of objects per delete
|
||||
concurrent := len(objects) / 10
|
||||
if concurrent <= 10 {
|
||||
// if we cannot get 1/10th then choose the number of
|
||||
// objects as concurrent.
|
||||
concurrent = len(objects)
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
eg := errgroup.WithNErrs(len(objects)).WithConcurrency(concurrent)
|
||||
for j, obj := range objects {
|
||||
j := j
|
||||
obj := obj
|
||||
eg := errgroup.WithNErrs(len(z.serverPools)).WithConcurrency(len(z.serverPools))
|
||||
for i, pool := range z.serverPools {
|
||||
i := i
|
||||
pool := pool
|
||||
eg.Go(func() error {
|
||||
pinfo, _, err := z.getPoolInfoExistingWithOpts(ctx, bucket, obj.ObjectName, ObjectOptions{
|
||||
NoLock: true,
|
||||
})
|
||||
if err != nil {
|
||||
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
||||
derrs[j] = err
|
||||
}
|
||||
dobjects[j] = DeletedObject{
|
||||
ObjectName: decodeDirObject(obj.ObjectName),
|
||||
VersionID: obj.VersionID,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete marker already present we are not going to create new delete markers.
|
||||
if pinfo.ObjInfo.DeleteMarker && obj.VersionID == "" {
|
||||
dobjects[j] = DeletedObject{
|
||||
DeleteMarker: pinfo.ObjInfo.DeleteMarker,
|
||||
DeleteMarkerVersionID: pinfo.ObjInfo.VersionID,
|
||||
DeleteMarkerMTime: DeleteMarkerMTime{pinfo.ObjInfo.ModTime},
|
||||
ObjectName: decodeDirObject(pinfo.ObjInfo.Name),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
idx := pinfo.Index
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
poolObjIdxMap[idx] = append(poolObjIdxMap[idx], obj)
|
||||
origIndexMap[idx] = append(origIndexMap[idx], j)
|
||||
dObjectsByPool[i], dErrsByPool[i] = pool.DeleteObjects(ctx, bucket, objects, opts)
|
||||
return nil
|
||||
}, j)
|
||||
}, i)
|
||||
}
|
||||
|
||||
eg.Wait() // wait to check all the pools.
|
||||
|
||||
if len(poolObjIdxMap) > 0 {
|
||||
// Delete concurrently in all server pools.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(z.serverPools))
|
||||
for idx, pool := range z.serverPools {
|
||||
go func(idx int, pool *erasureSets) {
|
||||
defer wg.Done()
|
||||
objs := poolObjIdxMap[idx]
|
||||
if len(objs) == 0 {
|
||||
return
|
||||
}
|
||||
orgIndexes := origIndexMap[idx]
|
||||
deletedObjects, errs := pool.DeleteObjects(ctx, bucket, objs, opts)
|
||||
mu.Lock()
|
||||
for i, derr := range errs {
|
||||
if derr != nil {
|
||||
derrs[orgIndexes[i]] = derr
|
||||
}
|
||||
deletedObjects[i].ObjectName = decodeDirObject(deletedObjects[i].ObjectName)
|
||||
dobjects[orgIndexes[i]] = deletedObjects[i]
|
||||
}
|
||||
mu.Unlock()
|
||||
}(idx, pool)
|
||||
for i := range dobjects {
|
||||
// Iterate over pools
|
||||
for pool := range z.serverPools {
|
||||
if dErrsByPool[pool][i] == nil && dObjectsByPool[pool][i].found {
|
||||
// A fast exit when the object is found and removed
|
||||
dobjects[i] = dObjectsByPool[pool][i]
|
||||
derrs[i] = nil
|
||||
break
|
||||
}
|
||||
if derrs[i] == nil {
|
||||
// No error related to this object is found, assign this pool result
|
||||
// whether it is nil because there is no object found or because of
|
||||
// some other errors such erasure quorum errors.
|
||||
dobjects[i] = dObjectsByPool[pool][i]
|
||||
derrs[i] = dErrsByPool[pool][i]
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
for i := range dobjects {
|
||||
dobjects[i].ObjectName = decodeDirObject(dobjects[i].ObjectName)
|
||||
}
|
||||
return dobjects, derrs
|
||||
}
|
||||
|
||||
|
||||
@@ -367,9 +367,10 @@ func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error)
|
||||
}
|
||||
|
||||
return minio.New(driver.endpoint, &minio.Options{
|
||||
Creds: mcreds,
|
||||
Secure: globalIsTLS,
|
||||
Transport: tr,
|
||||
Creds: mcreds,
|
||||
Secure: globalIsTLS,
|
||||
Transport: tr,
|
||||
TrailingHeaders: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -381,9 +382,10 @@ func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error)
|
||||
}
|
||||
|
||||
return minio.New(driver.endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
|
||||
Secure: globalIsTLS,
|
||||
Transport: tr,
|
||||
Creds: credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
|
||||
Secure: globalIsTLS,
|
||||
Transport: tr,
|
||||
TrailingHeaders: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -552,6 +554,7 @@ func (driver *ftpDriver) PutFile(ctx *ftp.Context, objPath string, data io.Reade
|
||||
info, err := clnt.PutObject(context.Background(), bucket, object, data, -1, minio.PutObjectOptions{
|
||||
ContentType: mimedb.TypeByExtension(path.Ext(object)),
|
||||
DisableContentSha256: true,
|
||||
Checksum: minio.ChecksumFullObjectCRC32C,
|
||||
})
|
||||
n = info.Size
|
||||
return n, err
|
||||
|
||||
+31
-17
@@ -22,11 +22,11 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio-go/v7/pkg/s3utils"
|
||||
@@ -293,12 +293,6 @@ func parseAmzDateHeader(req *http.Request) (time.Time, APIErrorCode) {
|
||||
return time.Time{}, ErrMissingDateHeader
|
||||
}
|
||||
|
||||
// Bad path components to be rejected by the path validity handler.
|
||||
const (
|
||||
dotdotComponent = ".."
|
||||
dotComponent = "."
|
||||
)
|
||||
|
||||
func hasBadHost(host string) error {
|
||||
if globalIsCICD && strings.TrimSpace(host) == "" {
|
||||
// under CI/CD test setups ignore empty hosts as invalid hosts
|
||||
@@ -311,21 +305,41 @@ func hasBadHost(host string) error {
|
||||
// Check if the incoming path has bad path components,
|
||||
// such as ".." and "."
|
||||
func hasBadPathComponent(path string) bool {
|
||||
if len(path) > 4096 {
|
||||
// path cannot be greater than Linux PATH_MAX
|
||||
// this is to avoid a busy loop, that can happen
|
||||
// if the caller sends path of following style
|
||||
// a/a/a/a/a/a/a/a...
|
||||
n := len(path)
|
||||
if n > 32<<10 {
|
||||
// At 32K we are beyond reasonable.
|
||||
return true
|
||||
}
|
||||
path = filepath.ToSlash(strings.TrimSpace(path)) // For windows '\' must be converted to '/'
|
||||
for _, p := range strings.Split(path, SlashSeparator) {
|
||||
switch strings.TrimSpace(p) {
|
||||
case dotdotComponent:
|
||||
i := 0
|
||||
// Skip leading slashes (for sake of Windows \ is included as well)
|
||||
for i < n && (path[i] == SlashSeparatorChar || path[i] == '\\') {
|
||||
i++
|
||||
}
|
||||
|
||||
for i < n {
|
||||
// Find the next segment
|
||||
start := i
|
||||
for i < n && path[i] != SlashSeparatorChar && path[i] != '\\' {
|
||||
i++
|
||||
}
|
||||
|
||||
// Trim whitespace of segment
|
||||
segmentStart, segmentEnd := start, i
|
||||
for segmentStart < segmentEnd && unicode.IsSpace(rune(path[segmentStart])) {
|
||||
segmentStart++
|
||||
}
|
||||
for segmentEnd > segmentStart && unicode.IsSpace(rune(path[segmentEnd-1])) {
|
||||
segmentEnd--
|
||||
}
|
||||
|
||||
// Check for ".." or "."
|
||||
switch {
|
||||
case segmentEnd-segmentStart == 2 && path[segmentStart] == '.' && path[segmentStart+1] == '.':
|
||||
return true
|
||||
case dotComponent:
|
||||
case segmentEnd-segmentStart == 1 && path[segmentStart] == '.':
|
||||
return true
|
||||
}
|
||||
i++
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/internal/crypto"
|
||||
@@ -184,3 +185,27 @@ func TestSSETLSHandler(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_hasBadPathComponent(t *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want bool
|
||||
}{
|
||||
{name: "empty", input: "", want: false},
|
||||
{name: "backslashes", input: `\a\a\ \\ \\\\\\\`, want: false},
|
||||
{name: "long", input: strings.Repeat("a/", 2000), want: false},
|
||||
{name: "long-fail", input: strings.Repeat("a/", 2000) + "../..", want: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(b *testing.B) {
|
||||
b.SetBytes(int64(len(tt.input)))
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if got := hasBadPathComponent(tt.input); got != tt.want {
|
||||
t.Fatalf("hasBadPathComponent() = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +366,6 @@ func maxClients(f http.HandlerFunc) http.HandlerFunc {
|
||||
writeErrorResponse(ctx, w,
|
||||
errorCodes.ToAPIErr(ErrTooManyRequests),
|
||||
r.URL)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@ func getHostName(r *http.Request) (hostName string) {
|
||||
}
|
||||
|
||||
// Proxy any request to an endpoint.
|
||||
func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ep ProxyEndpoint) (success bool) {
|
||||
func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ep ProxyEndpoint, returnErr bool) (success bool) {
|
||||
success = true
|
||||
|
||||
// Make sure we remove any existing headers before
|
||||
@@ -462,7 +462,10 @@ func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, e
|
||||
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
success = false
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
replLogIf(GlobalContext, err)
|
||||
proxyLogIf(GlobalContext, err)
|
||||
}
|
||||
if returnErr {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -493,7 +493,6 @@ func (ies *IAMEtcdStore) watch(ctx context.Context, keyPath string) <-chan iamWa
|
||||
keyPath: string(event.Kv.Key),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +278,6 @@ func (iamOS *IAMObjectStore) loadUserIdentity(ctx context.Context, user string,
|
||||
iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(user, userType, false))
|
||||
}
|
||||
return u, errNoSuchUser
|
||||
|
||||
}
|
||||
u.Credentials.Claims = jwtClaims.Map()
|
||||
}
|
||||
|
||||
+9
-4
@@ -845,7 +845,11 @@ func (store *IAMStoreSys) PolicyDBGet(name string, groups ...string) ([]string,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return val.([]string), nil
|
||||
res, ok := val.([]string)
|
||||
if !ok {
|
||||
return nil, errors.New("unexpected policy type")
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
return getPolicies()
|
||||
}
|
||||
@@ -1218,7 +1222,6 @@ func (store *IAMStoreSys) PolicyDBUpdate(ctx context.Context, name string, isGro
|
||||
cache.iamGroupPolicyMap.Delete(name)
|
||||
}
|
||||
} else {
|
||||
|
||||
if err = store.saveMappedPolicy(ctx, name, userType, isGroup, newPolicyMapping); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -1620,7 +1623,6 @@ func (store *IAMStoreSys) MergePolicies(policyName string) (string, policy.Polic
|
||||
policies = append(policies, policy)
|
||||
toMerge = append(toMerge, p.Policy)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return strings.Join(policies, ","), policy.MergePolicies(toMerge...)
|
||||
@@ -2917,7 +2919,10 @@ func (store *IAMStoreSys) LoadUser(ctx context.Context, accessKey string) error
|
||||
return err
|
||||
}
|
||||
|
||||
newCache := val.(*iamCache)
|
||||
newCache, ok := val.(*iamCache)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
cache := store.lock()
|
||||
defer store.unlock()
|
||||
|
||||
@@ -2286,7 +2286,6 @@ func (sys *IAMSys) IsAllowedSTS(args policy.Args, parentUser string) bool {
|
||||
}
|
||||
policies = policySet.ToSlice()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Defensive code: Do not allow any operation if no policy is found in the session token
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
|
||||
"github.com/minio/minio/internal/event"
|
||||
"github.com/minio/minio/internal/grid"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/pubsub"
|
||||
"github.com/minio/mux"
|
||||
@@ -200,19 +201,19 @@ func (api objectAPIHandlers) ListenNotificationHandler(w http.ResponseWriter, r
|
||||
}
|
||||
if len(mergeCh) == 0 {
|
||||
// Flush if nothing is queued
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
grid.PutByteBuffer(ev)
|
||||
case <-emptyEventTicker:
|
||||
if err := enc.Encode(struct{ Records []event.Event }{}); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
case <-keepAliveTicker:
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
+152
-111
@@ -24,11 +24,20 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/dsync"
|
||||
)
|
||||
|
||||
// Reject new lock requests immediately when this many are queued
|
||||
// for the local lock mutex.
|
||||
// We do not block unlocking or maintenance, but they add to the count.
|
||||
// The limit is set to allow for bursty behavior,
|
||||
// but prevent requests to overload the server completely.
|
||||
// Rejected clients are expected to retry.
|
||||
const lockMutexWaitLimit = 1000
|
||||
|
||||
// lockRequesterInfo stores various info from the client for each lock that is requested.
|
||||
type lockRequesterInfo struct {
|
||||
Name string // name of the resource lock was requested for
|
||||
@@ -52,9 +61,25 @@ func isWriteLock(lri []lockRequesterInfo) bool {
|
||||
//
|
||||
//msgp:ignore localLocker
|
||||
type localLocker struct {
|
||||
mutex sync.Mutex
|
||||
lockMap map[string][]lockRequesterInfo
|
||||
lockUID map[string]string // UUID -> resource map.
|
||||
mutex sync.Mutex
|
||||
waitMutex atomic.Int32
|
||||
lockMap map[string][]lockRequesterInfo
|
||||
lockUID map[string]string // UUID -> resource map.
|
||||
|
||||
// the following are updated on every cleanup defined in lockValidityDuration
|
||||
readers atomic.Int32
|
||||
writers atomic.Int32
|
||||
lastCleanup atomic.Pointer[time.Time]
|
||||
locksOverloaded atomic.Int64
|
||||
}
|
||||
|
||||
// getMutex will lock the mutex.
|
||||
// Call the returned function to unlock.
|
||||
func (l *localLocker) getMutex() func() {
|
||||
l.waitMutex.Add(1)
|
||||
l.mutex.Lock()
|
||||
l.waitMutex.Add(-1)
|
||||
return l.mutex.Unlock
|
||||
}
|
||||
|
||||
func (l *localLocker) String() string {
|
||||
@@ -76,9 +101,16 @@ func (l *localLocker) Lock(ctx context.Context, args dsync.LockArgs) (reply bool
|
||||
return false, fmt.Errorf("internal error: localLocker.Lock called with more than %d resources", maxDeleteList)
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
// If we have too many waiting, reject this at once.
|
||||
if l.waitMutex.Load() > lockMutexWaitLimit {
|
||||
l.locksOverloaded.Add(1)
|
||||
return false, nil
|
||||
}
|
||||
// Wait for mutex
|
||||
defer l.getMutex()()
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
if !l.canTakeLock(args.Resources...) {
|
||||
// Not all locks can be taken on resources,
|
||||
// reject it completely.
|
||||
@@ -117,9 +149,7 @@ func (l *localLocker) Unlock(_ context.Context, args dsync.LockArgs) (reply bool
|
||||
return false, fmt.Errorf("internal error: localLocker.Unlock called with more than %d resources", maxDeleteList)
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
err = nil
|
||||
defer l.getMutex()()
|
||||
|
||||
for _, resource := range args.Resources {
|
||||
lri, ok := l.lockMap[resource]
|
||||
@@ -164,9 +194,17 @@ func (l *localLocker) RLock(ctx context.Context, args dsync.LockArgs) (reply boo
|
||||
if len(args.Resources) != 1 {
|
||||
return false, fmt.Errorf("internal error: localLocker.RLock called with more than one resource")
|
||||
}
|
||||
// If we have too many waiting, reject this at once.
|
||||
if l.waitMutex.Load() > lockMutexWaitLimit {
|
||||
l.locksOverloaded.Add(1)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
// Wait for mutex
|
||||
defer l.getMutex()()
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
resource := args.Resources[0]
|
||||
now := UTCNow()
|
||||
lrInfo := lockRequesterInfo{
|
||||
@@ -200,8 +238,7 @@ func (l *localLocker) RUnlock(_ context.Context, args dsync.LockArgs) (reply boo
|
||||
return false, fmt.Errorf("internal error: localLocker.RUnlock called with more than one resource")
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
defer l.getMutex()()
|
||||
var lri []lockRequesterInfo
|
||||
|
||||
resource := args.Resources[0]
|
||||
@@ -218,35 +255,28 @@ func (l *localLocker) RUnlock(_ context.Context, args dsync.LockArgs) (reply boo
|
||||
}
|
||||
|
||||
type lockStats struct {
|
||||
Total int
|
||||
Writes int
|
||||
Reads int
|
||||
Total int
|
||||
Writes int
|
||||
Reads int
|
||||
LockQueue int
|
||||
LocksAbandoned int
|
||||
LastCleanup *time.Time
|
||||
}
|
||||
|
||||
func (l *localLocker) stats() lockStats {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
st := lockStats{Total: len(l.lockMap)}
|
||||
for _, v := range l.lockMap {
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
}
|
||||
entry := v[0]
|
||||
if entry.Writer {
|
||||
st.Writes++
|
||||
} else {
|
||||
st.Reads += len(v)
|
||||
}
|
||||
return lockStats{
|
||||
Total: len(l.lockMap),
|
||||
Reads: int(l.readers.Load()),
|
||||
Writes: int(l.writers.Load()),
|
||||
LockQueue: int(l.waitMutex.Load()),
|
||||
LastCleanup: l.lastCleanup.Load(),
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
type localLockMap map[string][]lockRequesterInfo
|
||||
|
||||
func (l *localLocker) DupLockMap() localLockMap {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
defer l.getMutex()()
|
||||
|
||||
lockCopy := make(map[string][]lockRequesterInfo, len(l.lockMap))
|
||||
for k, v := range l.lockMap {
|
||||
@@ -274,108 +304,110 @@ func (l *localLocker) IsLocal() bool {
|
||||
}
|
||||
|
||||
func (l *localLocker) ForceUnlock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
default:
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
if len(args.UID) == 0 {
|
||||
for _, resource := range args.Resources {
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Collect uids, so we don't mutate while we delete
|
||||
uids := make([]string, 0, len(lris))
|
||||
for _, lri := range lris {
|
||||
uids = append(uids, lri.UID)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete collected uids:
|
||||
for _, uid := range uids {
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
// Just to be safe, delete uuids.
|
||||
for idx := 0; idx < maxDeleteList; idx++ {
|
||||
mapID := formatUUID(uid, idx)
|
||||
if _, ok := l.lockUID[mapID]; !ok {
|
||||
break
|
||||
}
|
||||
delete(l.lockUID, mapID)
|
||||
}
|
||||
continue
|
||||
}
|
||||
l.removeEntry(resource, dsync.LockArgs{UID: uid}, &lris)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
idx := 0
|
||||
for {
|
||||
mapID := formatUUID(args.UID, idx)
|
||||
resource, ok := l.lockUID[mapID]
|
||||
if !ok {
|
||||
return idx > 0, nil
|
||||
}
|
||||
defer l.getMutex()()
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
if len(args.UID) == 0 {
|
||||
for _, resource := range args.Resources {
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
// Unexpected inconsistency, delete.
|
||||
delete(l.lockUID, mapID)
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
reply = true
|
||||
l.removeEntry(resource, dsync.LockArgs{UID: args.UID}, &lris)
|
||||
idx++
|
||||
// Collect uids, so we don't mutate while we delete
|
||||
uids := make([]string, 0, len(lris))
|
||||
for _, lri := range lris {
|
||||
uids = append(uids, lri.UID)
|
||||
}
|
||||
|
||||
// Delete collected uids:
|
||||
for _, uid := range uids {
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
// Just to be safe, delete uuids.
|
||||
for idx := 0; idx < maxDeleteList; idx++ {
|
||||
mapID := formatUUID(uid, idx)
|
||||
if _, ok := l.lockUID[mapID]; !ok {
|
||||
break
|
||||
}
|
||||
delete(l.lockUID, mapID)
|
||||
}
|
||||
continue
|
||||
}
|
||||
l.removeEntry(resource, dsync.LockArgs{UID: uid}, &lris)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
idx := 0
|
||||
for {
|
||||
mapID := formatUUID(args.UID, idx)
|
||||
resource, ok := l.lockUID[mapID]
|
||||
if !ok {
|
||||
return idx > 0, nil
|
||||
}
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
// Unexpected inconsistency, delete.
|
||||
delete(l.lockUID, mapID)
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
reply = true
|
||||
l.removeEntry(resource, dsync.LockArgs{UID: args.UID}, &lris)
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
func (l *localLocker) Refresh(ctx context.Context, args dsync.LockArgs) (refreshed bool, err error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
default:
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Check whether uid is still active.
|
||||
resource, ok := l.lockUID[formatUUID(args.UID, 0)]
|
||||
defer l.getMutex()()
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
|
||||
// Check whether uid is still active.
|
||||
resource, ok := l.lockUID[formatUUID(args.UID, 0)]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
idx := 0
|
||||
for {
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
return false, nil
|
||||
// Inconsistent. Delete UID.
|
||||
delete(l.lockUID, formatUUID(args.UID, idx))
|
||||
return idx > 0, nil
|
||||
}
|
||||
idx := 0
|
||||
for {
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
// Inconsistent. Delete UID.
|
||||
delete(l.lockUID, formatUUID(args.UID, idx))
|
||||
return idx > 0, nil
|
||||
}
|
||||
now := UTCNow()
|
||||
for i := range lris {
|
||||
if lris[i].UID == args.UID {
|
||||
lris[i].TimeLastRefresh = now.UnixNano()
|
||||
}
|
||||
}
|
||||
idx++
|
||||
resource, ok = l.lockUID[formatUUID(args.UID, idx)]
|
||||
if !ok {
|
||||
// No more resources for UID, but we did update at least one.
|
||||
return true, nil
|
||||
now := UTCNow()
|
||||
for i := range lris {
|
||||
if lris[i].UID == args.UID {
|
||||
lris[i].TimeLastRefresh = now.UnixNano()
|
||||
}
|
||||
}
|
||||
idx++
|
||||
resource, ok = l.lockUID[formatUUID(args.UID, idx)]
|
||||
if !ok {
|
||||
// No more resources for UID, but we did update at least one.
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Similar to removeEntry but only removes an entry only if the lock entry exists in map.
|
||||
// Caller must hold 'l.mutex' lock.
|
||||
func (l *localLocker) expireOldLocks(interval time.Duration) {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
defer l.getMutex()()
|
||||
|
||||
var readers, writers int32
|
||||
for k, lris := range l.lockMap {
|
||||
modified := false
|
||||
for i := 0; i < len(lris); {
|
||||
@@ -393,6 +425,11 @@ func (l *localLocker) expireOldLocks(interval time.Duration) {
|
||||
lris = append(lris[:i], lris[i+1:]...)
|
||||
// Check same i
|
||||
} else {
|
||||
if lri.Writer {
|
||||
writers++
|
||||
} else {
|
||||
readers++
|
||||
}
|
||||
// Move to next
|
||||
i++
|
||||
}
|
||||
@@ -401,6 +438,10 @@ func (l *localLocker) expireOldLocks(interval time.Duration) {
|
||||
l.lockMap[k] = lris
|
||||
}
|
||||
}
|
||||
t := time.Now()
|
||||
l.lastCleanup.Store(&t)
|
||||
l.readers.Store(readers)
|
||||
l.writers.Store(writers)
|
||||
}
|
||||
|
||||
func newLocker() *localLocker {
|
||||
|
||||
+124
-8
@@ -3,6 +3,8 @@ package cmd
|
||||
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
)
|
||||
|
||||
@@ -506,6 +508,36 @@ func (z *lockStats) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err, "Reads")
|
||||
return
|
||||
}
|
||||
case "LockQueue":
|
||||
z.LockQueue, err = dc.ReadInt()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LockQueue")
|
||||
return
|
||||
}
|
||||
case "LocksAbandoned":
|
||||
z.LocksAbandoned, err = dc.ReadInt()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LocksAbandoned")
|
||||
return
|
||||
}
|
||||
case "LastCleanup":
|
||||
if dc.IsNil() {
|
||||
err = dc.ReadNil()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastCleanup")
|
||||
return
|
||||
}
|
||||
z.LastCleanup = nil
|
||||
} else {
|
||||
if z.LastCleanup == nil {
|
||||
z.LastCleanup = new(time.Time)
|
||||
}
|
||||
*z.LastCleanup, err = dc.ReadTime()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastCleanup")
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
err = dc.Skip()
|
||||
if err != nil {
|
||||
@@ -518,10 +550,10 @@ func (z *lockStats) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
}
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z lockStats) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// map header, size 3
|
||||
func (z *lockStats) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// map header, size 6
|
||||
// write "Total"
|
||||
err = en.Append(0x83, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
|
||||
err = en.Append(0x86, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -550,15 +582,52 @@ func (z lockStats) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = msgp.WrapError(err, "Reads")
|
||||
return
|
||||
}
|
||||
// write "LockQueue"
|
||||
err = en.Append(0xa9, 0x4c, 0x6f, 0x63, 0x6b, 0x51, 0x75, 0x65, 0x75, 0x65)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteInt(z.LockQueue)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LockQueue")
|
||||
return
|
||||
}
|
||||
// write "LocksAbandoned"
|
||||
err = en.Append(0xae, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x65, 0x64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteInt(z.LocksAbandoned)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LocksAbandoned")
|
||||
return
|
||||
}
|
||||
// write "LastCleanup"
|
||||
err = en.Append(0xab, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if z.LastCleanup == nil {
|
||||
err = en.WriteNil()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = en.WriteTime(*z.LastCleanup)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastCleanup")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z lockStats) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
func (z *lockStats) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// map header, size 3
|
||||
// map header, size 6
|
||||
// string "Total"
|
||||
o = append(o, 0x83, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
|
||||
o = append(o, 0x86, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
|
||||
o = msgp.AppendInt(o, z.Total)
|
||||
// string "Writes"
|
||||
o = append(o, 0xa6, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73)
|
||||
@@ -566,6 +635,19 @@ func (z lockStats) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
// string "Reads"
|
||||
o = append(o, 0xa5, 0x52, 0x65, 0x61, 0x64, 0x73)
|
||||
o = msgp.AppendInt(o, z.Reads)
|
||||
// string "LockQueue"
|
||||
o = append(o, 0xa9, 0x4c, 0x6f, 0x63, 0x6b, 0x51, 0x75, 0x65, 0x75, 0x65)
|
||||
o = msgp.AppendInt(o, z.LockQueue)
|
||||
// string "LocksAbandoned"
|
||||
o = append(o, 0xae, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x65, 0x64)
|
||||
o = msgp.AppendInt(o, z.LocksAbandoned)
|
||||
// string "LastCleanup"
|
||||
o = append(o, 0xab, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70)
|
||||
if z.LastCleanup == nil {
|
||||
o = msgp.AppendNil(o)
|
||||
} else {
|
||||
o = msgp.AppendTime(o, *z.LastCleanup)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -605,6 +687,35 @@ func (z *lockStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "Reads")
|
||||
return
|
||||
}
|
||||
case "LockQueue":
|
||||
z.LockQueue, bts, err = msgp.ReadIntBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LockQueue")
|
||||
return
|
||||
}
|
||||
case "LocksAbandoned":
|
||||
z.LocksAbandoned, bts, err = msgp.ReadIntBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LocksAbandoned")
|
||||
return
|
||||
}
|
||||
case "LastCleanup":
|
||||
if msgp.IsNil(bts) {
|
||||
bts, err = msgp.ReadNilBytes(bts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
z.LastCleanup = nil
|
||||
} else {
|
||||
if z.LastCleanup == nil {
|
||||
z.LastCleanup = new(time.Time)
|
||||
}
|
||||
*z.LastCleanup, bts, err = msgp.ReadTimeBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastCleanup")
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
@@ -618,7 +729,12 @@ func (z *lockStats) 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 lockStats) Msgsize() (s int) {
|
||||
s = 1 + 6 + msgp.IntSize + 7 + msgp.IntSize + 6 + msgp.IntSize
|
||||
func (z *lockStats) Msgsize() (s int) {
|
||||
s = 1 + 6 + msgp.IntSize + 7 + msgp.IntSize + 6 + msgp.IntSize + 10 + msgp.IntSize + 15 + msgp.IntSize + 12
|
||||
if z.LastCleanup == nil {
|
||||
s += msgp.NilSize
|
||||
} else {
|
||||
s += msgp.TimeSize
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -136,7 +136,6 @@ func TestLocalLockerUnlock(t *testing.T) {
|
||||
}
|
||||
wResources[i] = names
|
||||
wUIDs[i] = uid
|
||||
|
||||
}
|
||||
for i := range rResources {
|
||||
name := mustGetUUID()
|
||||
|
||||
+13
-3
@@ -33,8 +33,12 @@ type lockRESTServer struct {
|
||||
|
||||
// RefreshHandler - refresh the current lock
|
||||
func (l *lockRESTServer) RefreshHandler(args *dsync.LockArgs) (*dsync.LockResp, *grid.RemoteErr) {
|
||||
// Add a timeout similar to what we expect upstream.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dsync.DefaultTimeouts.RefreshCall)
|
||||
defer cancel()
|
||||
|
||||
resp := lockRPCRefresh.NewResponse()
|
||||
refreshed, err := l.ll.Refresh(context.Background(), *args)
|
||||
refreshed, err := l.ll.Refresh(ctx, *args)
|
||||
if err != nil {
|
||||
return l.makeResp(resp, err)
|
||||
}
|
||||
@@ -46,8 +50,11 @@ func (l *lockRESTServer) RefreshHandler(args *dsync.LockArgs) (*dsync.LockResp,
|
||||
|
||||
// LockHandler - Acquires a lock.
|
||||
func (l *lockRESTServer) LockHandler(args *dsync.LockArgs) (*dsync.LockResp, *grid.RemoteErr) {
|
||||
// Add a timeout similar to what we expect upstream.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dsync.DefaultTimeouts.Acquire)
|
||||
defer cancel()
|
||||
resp := lockRPCLock.NewResponse()
|
||||
success, err := l.ll.Lock(context.Background(), *args)
|
||||
success, err := l.ll.Lock(ctx, *args)
|
||||
if err == nil && !success {
|
||||
return l.makeResp(resp, errLockConflict)
|
||||
}
|
||||
@@ -65,8 +72,11 @@ func (l *lockRESTServer) UnlockHandler(args *dsync.LockArgs) (*dsync.LockResp, *
|
||||
|
||||
// RLockHandler - Acquires an RLock.
|
||||
func (l *lockRESTServer) RLockHandler(args *dsync.LockArgs) (*dsync.LockResp, *grid.RemoteErr) {
|
||||
// Add a timeout similar to what we expect upstream.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dsync.DefaultTimeouts.Acquire)
|
||||
defer cancel()
|
||||
resp := lockRPCRLock.NewResponse()
|
||||
success, err := l.ll.RLock(context.Background(), *args)
|
||||
success, err := l.ll.RLock(ctx, *args)
|
||||
if err == nil && !success {
|
||||
err = errLockConflict
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/klauspost/compress/s2"
|
||||
"github.com/minio/minio/internal/bpool"
|
||||
xioutil "github.com/minio/minio/internal/ioutil"
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
@@ -236,7 +237,7 @@ func (w *metacacheWriter) Reset(out io.Writer) {
|
||||
}
|
||||
}
|
||||
|
||||
var s2DecPool = sync.Pool{New: func() interface{} {
|
||||
var s2DecPool = bpool.Pool[*s2.Reader]{New: func() *s2.Reader {
|
||||
// Default alloc block for network transfer.
|
||||
return s2.NewReader(nil, s2.ReaderAllocBlock(16<<10))
|
||||
}}
|
||||
@@ -253,7 +254,7 @@ type metacacheReader struct {
|
||||
// newMetacacheReader creates a new cache reader.
|
||||
// Nothing will be read from the stream yet.
|
||||
func newMetacacheReader(r io.Reader) *metacacheReader {
|
||||
dec := s2DecPool.Get().(*s2.Reader)
|
||||
dec := s2DecPool.Get()
|
||||
dec.Reset(r)
|
||||
mr := msgpNewReader(dec)
|
||||
return &metacacheReader{
|
||||
|
||||
@@ -2474,7 +2474,6 @@ func getReplicationNodeMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
|
||||
}
|
||||
downtimeDuration.Value = float64(dwntime / time.Second)
|
||||
ml = append(ml, downtimeDuration)
|
||||
|
||||
}
|
||||
return ml
|
||||
})
|
||||
|
||||
@@ -177,7 +177,6 @@ func loadClusterUsageBucketMetrics(ctx context.Context, m MetricValues, c *metri
|
||||
for k, v := range usage.ObjectVersionsHistogram {
|
||||
m.Set(usageBucketObjectVersionCountDistribution, float64(v), "range", k, "bucket", bucket)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1184,7 +1184,6 @@ func (sys *NotificationSys) GetPeerOnlineCount() (nodesOnline, nodesOffline int)
|
||||
defer wg.Done()
|
||||
nodesOnlineIndex[idx] = client.restClient.HealthCheckFn()
|
||||
}(idx, client)
|
||||
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
|
||||
@@ -389,7 +389,6 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Formulating the result data set to be expected from ListObjects call inside the tests,
|
||||
@@ -1014,7 +1013,6 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
|
||||
t.Errorf("Test %d: %s: Expected NextMarker to be empty since listing is not truncated, but instead found `%v`", i+1, instanceType, result.NextMarker)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1166,7 +1164,6 @@ func testListObjectVersions(obj ObjectLayer, instanceType string, t1 TestErrHand
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Formualting the result data set to be expected from ListObjects call inside the tests,
|
||||
@@ -1785,12 +1782,10 @@ func testListObjectsContinuation(obj ObjectLayer, instanceType string, t1 TestEr
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Formulating the result data set to be expected from ListObjects call inside the tests,
|
||||
// This will be used in testCases and used for asserting the correctness of ListObjects output in the tests.
|
||||
|
||||
resultCases := []ListObjectsInfo{
|
||||
{
|
||||
Objects: []ObjectInfo{
|
||||
|
||||
@@ -443,7 +443,6 @@ func testListMultipartUploads(obj ObjectLayer, instanceType string, t TestErrHan
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Expected Results set for asserting ListObjectMultipart test.
|
||||
|
||||
@@ -467,13 +467,10 @@ func completeMultipartOpts(ctx context.Context, r *http.Request, bucket, object
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opts.WantChecksum, err = hash.GetContentChecksum(r.Header)
|
||||
if err != nil {
|
||||
return opts, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
Err: fmt.Errorf("invalid/unknown checksum sent: %v", err),
|
||||
}
|
||||
return opts, err
|
||||
}
|
||||
opts.MTime = mtime
|
||||
opts.UserDefined = make(map[string]string)
|
||||
|
||||
@@ -374,7 +374,8 @@ func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, del bool, h htt
|
||||
lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts())
|
||||
}
|
||||
}
|
||||
hash.AddChecksumHeader(w, objInfo.decryptChecksums(0, h))
|
||||
cs, _ := objInfo.decryptChecksums(0, h)
|
||||
hash.AddChecksumHeader(w, cs)
|
||||
}
|
||||
|
||||
func deleteObjectVersions(ctx context.Context, o ObjectLayer, bucket string, toDel []ObjectToDelete, lcEvent lifecycle.Event) {
|
||||
|
||||
+25
-18
@@ -521,7 +521,8 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj
|
||||
|
||||
if r.Header.Get(xhttp.AmzChecksumMode) == "ENABLED" && rs == nil {
|
||||
// AWS S3 silently drops checksums on range requests.
|
||||
hash.AddChecksumHeader(w, objInfo.decryptChecksums(opts.PartNumber, r.Header))
|
||||
cs, _ := objInfo.decryptChecksums(opts.PartNumber, r.Header)
|
||||
hash.AddChecksumHeader(w, cs)
|
||||
}
|
||||
|
||||
if err = setObjectHeaders(ctx, w, objInfo, rs, opts); err != nil {
|
||||
@@ -632,7 +633,7 @@ func (api objectAPIHandlers) getObjectAttributesHandler(ctx context.Context, obj
|
||||
w.Header().Del(xhttp.ContentType)
|
||||
|
||||
if _, ok := opts.ObjectAttributes[xhttp.Checksum]; ok {
|
||||
chkSums := objInfo.decryptChecksums(0, r.Header)
|
||||
chkSums, _ := objInfo.decryptChecksums(0, r.Header)
|
||||
// AWS does not appear to append part number on this API call.
|
||||
if len(chkSums) > 0 {
|
||||
OA.Checksum = &objectAttributesChecksum{
|
||||
@@ -945,7 +946,8 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob
|
||||
|
||||
if r.Header.Get(xhttp.AmzChecksumMode) == "ENABLED" && rs == nil {
|
||||
// AWS S3 silently drops checksums on range requests.
|
||||
hash.AddChecksumHeader(w, objInfo.decryptChecksums(opts.PartNumber, r.Header))
|
||||
cs, _ := objInfo.decryptChecksums(opts.PartNumber, r.Header)
|
||||
hash.AddChecksumHeader(w, cs)
|
||||
}
|
||||
|
||||
// Set standard object headers.
|
||||
@@ -1536,7 +1538,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
srcInfo.UserDefined[xhttp.AmzObjectTagging] = objTags
|
||||
srcInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp] = UTCNow().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
srcInfo.UserDefined = filterReplicationStatusMetadata(srcInfo.UserDefined)
|
||||
@@ -1629,7 +1630,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
srcInfo.metadataOnly && srcOpts.VersionID == "" &&
|
||||
!crypto.Requested(r.Header) &&
|
||||
!crypto.IsSourceEncrypted(srcInfo.UserDefined) {
|
||||
|
||||
// If x-amz-metadata-directive is not set to REPLACE then we need
|
||||
// to error out if source and destination are same.
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopyDest), r.URL)
|
||||
@@ -2229,8 +2229,15 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
|
||||
putObject = objectAPI.PutObject
|
||||
)
|
||||
|
||||
// Check if put is allowed
|
||||
if s3Err = isPutActionAllowed(ctx, rAuthType, bucket, object, r, policy.PutObjectAction); s3Err != ErrNone {
|
||||
var opts untarOptions
|
||||
opts.ignoreDirs = strings.EqualFold(r.Header.Get(xhttp.MinIOSnowballIgnoreDirs), "true")
|
||||
opts.ignoreErrs = strings.EqualFold(r.Header.Get(xhttp.MinIOSnowballIgnoreErrors), "true")
|
||||
opts.prefixAll = r.Header.Get(xhttp.MinIOSnowballPrefix)
|
||||
if opts.prefixAll != "" {
|
||||
opts.prefixAll = trimLeadingSlash(pathJoin(opts.prefixAll, slashSeparator))
|
||||
}
|
||||
// Check if put is allow for specified prefix.
|
||||
if s3Err = isPutActionAllowed(ctx, rAuthType, bucket, opts.prefixAll, r, policy.PutObjectAction); s3Err != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -2298,7 +2305,10 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
|
||||
|
||||
putObjectTar := func(reader io.Reader, info os.FileInfo, object string) error {
|
||||
size := info.Size()
|
||||
|
||||
if s3Err = isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.PutObjectAction); s3Err != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return errors.New(errorCodes.ToAPIErr(s3Err).Code)
|
||||
}
|
||||
metadata := map[string]string{
|
||||
xhttp.AmzStorageClass: sc, // save same storage-class as incoming stream.
|
||||
}
|
||||
@@ -2334,7 +2344,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
|
||||
|
||||
if r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String() {
|
||||
if s3Err = isPutActionAllowed(ctx, getRequestAuthType(r), bucket, object, r, policy.ReplicateObjectAction); s3Err != ErrNone {
|
||||
return err
|
||||
return errors.New(errorCodes.ToAPIErr(s3Err).Code)
|
||||
}
|
||||
metadata[ReservedMetadataPrefixLower+ReplicaStatus] = replication.Replica.String()
|
||||
metadata[ReservedMetadataPrefixLower+ReplicaTimestamp] = UTCNow().Format(time.RFC3339Nano)
|
||||
@@ -2398,7 +2408,6 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
|
||||
if dsc := mustReplicate(ctx, bucket, object, getMustReplicateOptions(metadata, "", "", replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
|
||||
metadata[ReservedMetadataPrefixLower+ReplicationTimestamp] = UTCNow().Format(time.RFC3339Nano)
|
||||
metadata[ReservedMetadataPrefixLower+ReplicationStatus] = dsc.PendingStatus()
|
||||
|
||||
}
|
||||
|
||||
var objectEncryptionKey crypto.ObjectKey
|
||||
@@ -2481,14 +2490,6 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
|
||||
return nil
|
||||
}
|
||||
|
||||
var opts untarOptions
|
||||
opts.ignoreDirs = strings.EqualFold(r.Header.Get(xhttp.MinIOSnowballIgnoreDirs), "true")
|
||||
opts.ignoreErrs = strings.EqualFold(r.Header.Get(xhttp.MinIOSnowballIgnoreErrors), "true")
|
||||
opts.prefixAll = r.Header.Get(xhttp.MinIOSnowballPrefix)
|
||||
if opts.prefixAll != "" {
|
||||
opts.prefixAll = trimLeadingSlash(pathJoin(opts.prefixAll, slashSeparator))
|
||||
}
|
||||
|
||||
if err = untar(ctx, hreader, putObjectTar, opts); err != nil {
|
||||
apiErr := errorCodes.ToAPIErr(s3Err)
|
||||
// If not set, convert or use BadRequest
|
||||
@@ -2880,6 +2881,12 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
// Check permissions to perform this object retention operation
|
||||
if s3Err := checkRequestAuthType(ctx, r, policy.PutObjectRetentionAction, bucket, object); s3Err != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cred, owner, s3Err := validateSignature(getRequestAuthType(r), r)
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
|
||||
@@ -814,7 +814,6 @@ func testAPIGetObjectWithMPHandler(obj ObjectLayer, instanceType, bucketName str
|
||||
caseNumber++
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// HTTP request for testing when `objectLayer` is set to `nil`.
|
||||
@@ -1567,8 +1566,8 @@ func testAPIPutObjectHandler(obj ObjectLayer, instanceType, bucketName string, a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if testCase.expectedRespStatus == http.StatusOK {
|
||||
buffer := new(bytes.Buffer)
|
||||
// Fetch the object to check whether the content is same as the one uploaded via PutObject.
|
||||
@@ -3520,7 +3519,6 @@ func testAPIDeleteObjectHandler(obj ObjectLayer, instanceType, bucketName string
|
||||
t.Errorf("Case %d: MinIO %s: Expected the response status to be `%d`, but instead found `%d`", i+1,
|
||||
instanceType, testCase.expectedRespStatus, recV2.Code)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test for Anonymous/unsigned http request.
|
||||
@@ -4198,7 +4196,6 @@ func testAPIListObjectPartsHandler(obj ObjectLayer, instanceType, bucketName str
|
||||
|
||||
// validate the error response.
|
||||
if test.expectedErr != noAPIErr {
|
||||
|
||||
var errBytes []byte
|
||||
// read the response body.
|
||||
errBytes, err = io.ReadAll(rec.Result().Body)
|
||||
|
||||
@@ -93,8 +93,8 @@ func getLambdaEventData(bucket, object string, cred auth.Credentials, r *http.Re
|
||||
},
|
||||
UserIdentity: levent.Identity{
|
||||
Type: "IAMUser",
|
||||
PrincipalID: cred.AccessKey,
|
||||
AccessKeyID: cred.SecretKey,
|
||||
PrincipalID: cred.ParentUser,
|
||||
AccessKeyID: cred.AccessKey,
|
||||
},
|
||||
}
|
||||
return eventData, nil
|
||||
|
||||
@@ -216,7 +216,7 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
|
||||
checksumType := hash.NewChecksumHeader(r.Header)
|
||||
if checksumType.Is(hash.ChecksumInvalid) {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequestParameter), r.URL)
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidChecksum), r.URL)
|
||||
return
|
||||
} else if checksumType.IsSet() && !checksumType.Is(hash.ChecksumTrailing) {
|
||||
opts.WantChecksum = &hash.Checksum{Type: checksumType}
|
||||
|
||||
@@ -228,7 +228,6 @@ func testMultipleObjectCreation(obj ObjectLayer, instanceType string, t TestErrH
|
||||
if objInfo.Size != int64(len(value)) {
|
||||
t.Errorf("%s: Size mismatch of the GetObject data.", instanceType)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +383,6 @@ func testPaging(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
||||
|
||||
// check results with Marker.
|
||||
{
|
||||
|
||||
result, err = obj.ListObjects(context.Background(), "bucket", "", "newPrefix", "", 3)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: <ERROR> %s", instanceType, err)
|
||||
|
||||
+8
-9
@@ -25,10 +25,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/minio/minio/internal/bpool"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
@@ -106,15 +106,15 @@ const blockSize = 8 << 10 // 8192
|
||||
|
||||
// By default at least 128 entries in single getdents call (1MiB buffer)
|
||||
var (
|
||||
direntPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
direntPool = bpool.Pool[*[]byte]{
|
||||
New: func() *[]byte {
|
||||
buf := make([]byte, blockSize*128)
|
||||
return &buf
|
||||
},
|
||||
}
|
||||
|
||||
direntNamePool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
direntNamePool = bpool.Pool[*[]byte]{
|
||||
New: func() *[]byte {
|
||||
buf := make([]byte, blockSize)
|
||||
return &buf
|
||||
},
|
||||
@@ -183,11 +183,10 @@ func readDirFn(dirPath string, fn func(name string, typ os.FileMode) error) erro
|
||||
}
|
||||
return osErrToFileErr(err)
|
||||
}
|
||||
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
bufp := direntPool.Get().(*[]byte)
|
||||
bufp := direntPool.Get()
|
||||
defer direntPool.Put(bufp)
|
||||
buf := *bufp
|
||||
|
||||
@@ -273,11 +272,11 @@ func readDirWithOpts(dirPath string, opts readDirOpts) (entries []string, err er
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
bufp := direntPool.Get().(*[]byte)
|
||||
bufp := direntPool.Get()
|
||||
defer direntPool.Put(bufp)
|
||||
buf := *bufp
|
||||
|
||||
nameTmp := direntNamePool.Get().(*[]byte)
|
||||
nameTmp := direntNamePool.Get()
|
||||
defer direntNamePool.Put(nameTmp)
|
||||
tmp := *nameTmp
|
||||
|
||||
|
||||
@@ -173,7 +173,6 @@ func readDirWithOpts(dirPath string, opts readDirOpts) (entries []string, err er
|
||||
|
||||
count--
|
||||
entries = append(entries, name)
|
||||
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
|
||||
@@ -1308,6 +1308,7 @@ func (s *peerRESTServer) HeadBucketHandler(mss *grid.MSS) (info *VolInfo, nerr *
|
||||
return &VolInfo{
|
||||
Name: bucketInfo.Name,
|
||||
Created: bucketInfo.Created,
|
||||
Deleted: bucketInfo.Deleted, // needed for site replication
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -393,6 +393,7 @@ func (client *remotePeerS3Client) GetBucketInfo(ctx context.Context, bucket stri
|
||||
return BucketInfo{
|
||||
Name: volInfo.Name,
|
||||
Created: volInfo.Created,
|
||||
Deleted: volInfo.Deleted,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -131,16 +131,17 @@ func sanitizePolicy(r io.Reader) (io.Reader, error) {
|
||||
d := jstream.NewDecoder(r, 0).ObjectAsKVS().MaxDepth(10)
|
||||
sset := set.NewStringSet()
|
||||
for mv := range d.Stream() {
|
||||
var kvs jstream.KVS
|
||||
if mv.ValueType == jstream.Object {
|
||||
// This is a JSON object type (that preserves key order)
|
||||
kvs = mv.Value.(jstream.KVS)
|
||||
for _, kv := range kvs {
|
||||
if sset.Contains(kv.Key) {
|
||||
// Reject duplicate conditions or expiration.
|
||||
return nil, fmt.Errorf("input policy has multiple %s, please fix your client code", kv.Key)
|
||||
kvs, ok := mv.Value.(jstream.KVS)
|
||||
if ok {
|
||||
for _, kv := range kvs {
|
||||
if sset.Contains(kv.Key) {
|
||||
// Reject duplicate conditions or expiration.
|
||||
return nil, fmt.Errorf("input policy has multiple %s, please fix your client code", kv.Key)
|
||||
}
|
||||
sset.Add(kv.Key)
|
||||
}
|
||||
sset.Add(kv.Key)
|
||||
}
|
||||
e.Encode(kvs)
|
||||
}
|
||||
|
||||
+2
-1
@@ -425,9 +425,10 @@ func serverHandleCmdArgs(ctxt serverCtxt) {
|
||||
}
|
||||
|
||||
// allow transport to be HTTP/1.1 for proxying.
|
||||
globalProxyEndpoints = GetProxyEndpoints(globalEndpoints)
|
||||
globalInternodeTransport = NewInternodeHTTPTransport(ctxt.MaxIdleConnsPerHost)()
|
||||
globalRemoteTargetTransport = NewRemoteTargetHTTPTransport(false)()
|
||||
globalProxyEndpoints = GetProxyEndpoints(globalEndpoints, globalRemoteTargetTransport)
|
||||
|
||||
globalForwarder = handlers.NewForwarder(&handlers.Forwarder{
|
||||
PassHost: true,
|
||||
RoundTripper: globalRemoteTargetTransport,
|
||||
|
||||
@@ -127,9 +127,10 @@ func (f *sftpDriver) getMinIOClient() (*minio.Client, error) {
|
||||
tr = forwardForTransport{tr: tr, fwd: f.remoteIP}
|
||||
}
|
||||
return minio.New(f.endpoint, &minio.Options{
|
||||
Creds: mcreds,
|
||||
Secure: globalIsTLS,
|
||||
Transport: tr,
|
||||
TrailingHeaders: true,
|
||||
Creds: mcreds,
|
||||
Secure: globalIsTLS,
|
||||
Transport: tr,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -289,6 +290,7 @@ func (f *sftpDriver) Filewrite(r *sftp.Request) (w io.WriterAt, err error) {
|
||||
oi, err := clnt.PutObject(r.Context(), bucket, object, pr, -1, minio.PutObjectOptions{
|
||||
ContentType: mimedb.TypeByExtension(path.Ext(object)),
|
||||
DisableContentSha256: true,
|
||||
Checksum: minio.ChecksumFullObjectCRC32C,
|
||||
})
|
||||
stopFn(oi.Size, err)
|
||||
pr.CloseWithError(err)
|
||||
|
||||
+17
-12
@@ -62,7 +62,7 @@ var (
|
||||
|
||||
// if the sftp parameter --trusted-user-ca-key is set, then
|
||||
// the final form of the key file will be set as this variable.
|
||||
var caPublicKey ssh.PublicKey
|
||||
var globalSFTPTrustedCAPubkey ssh.PublicKey
|
||||
|
||||
// https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.22.0:ssh/common.go;l=46
|
||||
// preferredKexAlgos specifies the default preference for key-exchange
|
||||
@@ -161,8 +161,8 @@ internalAuth:
|
||||
return nil, errNoSuchUser
|
||||
}
|
||||
|
||||
if caPublicKey != nil && pass == nil {
|
||||
err := validateKey(c, key)
|
||||
if globalSFTPTrustedCAPubkey != nil && pass == nil {
|
||||
err := validateClientKeyIsTrusted(c, key)
|
||||
if err != nil {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
@@ -174,7 +174,6 @@ internalAuth:
|
||||
if subtle.ConstantTimeCompare([]byte(ui.Credentials.SecretKey), pass) != 1 {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
copts := map[string]string{
|
||||
@@ -223,14 +222,11 @@ func processLDAPAuthentication(key ssh.PublicKey, pass []byte, user string) (per
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else if key != nil {
|
||||
|
||||
lookupResult, targetGroups, err = globalIAMSys.LDAPConfig.LookupUserDN(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if lookupResult == nil {
|
||||
@@ -260,9 +256,18 @@ func processLDAPAuthentication(key ssh.PublicKey, pass []byte, user string) (per
|
||||
return nil, errAuthentication
|
||||
}
|
||||
}
|
||||
// Save each attribute to claims.
|
||||
claims[ldapAttribPrefix+attribKey] = attribValue[0]
|
||||
}
|
||||
|
||||
if key != nil {
|
||||
// If a key was provided, we expect the user to have an sshPublicKey
|
||||
// attribute.
|
||||
if _, ok := claims[ldapAttribPrefix+"sshPublicKey"]; !ok {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
}
|
||||
|
||||
expiryDur, err := globalIAMSys.LDAPConfig.GetExpiryDuration("")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -314,8 +319,8 @@ func processLDAPAuthentication(key ssh.PublicKey, pass []byte, user string) (per
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateKey(c ssh.ConnMetadata, clientKey ssh.PublicKey) (err error) {
|
||||
if caPublicKey == nil {
|
||||
func validateClientKeyIsTrusted(c ssh.ConnMetadata, clientKey ssh.PublicKey) (err error) {
|
||||
if globalSFTPTrustedCAPubkey == nil {
|
||||
return errors.New("public key authority validation requested but no ca public key specified.")
|
||||
}
|
||||
|
||||
@@ -335,7 +340,7 @@ func validateKey(c ssh.ConnMetadata, clientKey ssh.PublicKey) (err error) {
|
||||
// and that certificate type is correct.
|
||||
checker := ssh.CertChecker{}
|
||||
checker.IsUserAuthority = func(k ssh.PublicKey) bool {
|
||||
return subtle.ConstantTimeCompare(caPublicKey.Marshal(), k.Marshal()) == 1
|
||||
return subtle.ConstantTimeCompare(globalSFTPTrustedCAPubkey.Marshal(), k.Marshal()) == 1
|
||||
}
|
||||
|
||||
_, err = checker.Authenticate(c, clientKey)
|
||||
@@ -432,7 +437,7 @@ func startSFTPServer(args []string) {
|
||||
allowMACs = filterAlgos(arg, strings.Split(tokens[1], ","), supportedMACs)
|
||||
case "trusted-user-ca-key":
|
||||
userCaKeyFile = tokens[1]
|
||||
case "password-auth":
|
||||
case "disable-password-auth":
|
||||
disablePassAuth, _ = strconv.ParseBool(tokens[1])
|
||||
}
|
||||
}
|
||||
@@ -461,7 +466,7 @@ func startSFTPServer(args []string) {
|
||||
logger.Fatal(fmt.Errorf("invalid arguments passed, trusted user certificate authority public key file is not accessible: %v", err), "unable to start SFTP server")
|
||||
}
|
||||
|
||||
caPublicKey, _, _, _, err = ssh.ParseAuthorizedKey(keyBytes)
|
||||
globalSFTPTrustedCAPubkey, _, _, _, err = ssh.ParseAuthorizedKey(keyBytes)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Errorf("invalid arguments passed, trusted user certificate authority public key file is not parseable: %v", err), "unable to start SFTP server")
|
||||
}
|
||||
|
||||
+68
-17
@@ -91,6 +91,7 @@ func TestSFTPAuthentication(t *testing.T) {
|
||||
|
||||
suite.SFTPPublicKeyAuthentication(c)
|
||||
suite.SFTPFailedPublicKeyAuthenticationInvalidKey(c)
|
||||
suite.SFTPPublicKeyAuthNoPubKey(c)
|
||||
|
||||
suite.TearDownSuite(c)
|
||||
},
|
||||
@@ -146,6 +147,32 @@ func (s *TestSuiteIAM) SFTPPublicKeyAuthentication(c *check) {
|
||||
}
|
||||
}
|
||||
|
||||
// A user without an sshpubkey attribute in LDAP (here: fahim) should not be
|
||||
// able to authenticate.
|
||||
func (s *TestSuiteIAM) SFTPPublicKeyAuthNoPubKey(c *check) {
|
||||
keyBytes, err := os.ReadFile("./testdata/dillon_test_key.pub")
|
||||
if err != nil {
|
||||
c.Fatalf("could not read test key file: %s", err)
|
||||
}
|
||||
|
||||
testKey, _, _, _, err := ssh.ParseAuthorizedKey(keyBytes)
|
||||
if err != nil {
|
||||
c.Fatalf("could not parse test key file: %s", err)
|
||||
}
|
||||
|
||||
newSSHCon := newSSHConnMock("fahim=ldap")
|
||||
_, err = sshPubKeyAuth(newSSHCon, testKey)
|
||||
if err == nil {
|
||||
c.Fatalf("expected error but got none")
|
||||
}
|
||||
|
||||
newSSHCon = newSSHConnMock("fahim")
|
||||
_, err = sshPubKeyAuth(newSSHCon, testKey)
|
||||
if err == nil {
|
||||
c.Fatalf("expected error but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestSuiteIAM) SFTPFailedAuthDueToMissingPolicy(c *check) {
|
||||
newSSHCon := newSSHConnMock("dillon=ldap")
|
||||
_, err := sshPasswordAuth(newSSHCon, []byte("dillon"))
|
||||
@@ -275,24 +302,48 @@ func (s *TestSuiteIAM) SFTPValidLDAPLoginWithPassword(c *check) {
|
||||
c.Fatalf("policy add error: %v", err)
|
||||
}
|
||||
|
||||
userDN := "uid=dillon,ou=people,ou=swengg,dc=min,dc=io"
|
||||
userReq := madmin.PolicyAssociationReq{
|
||||
Policies: []string{policy},
|
||||
User: userDN,
|
||||
}
|
||||
if _, err := s.adm.AttachPolicy(ctx, userReq); err != nil {
|
||||
c.Fatalf("Unable to attach policy: %v", err)
|
||||
}
|
||||
{
|
||||
userDN := "uid=dillon,ou=people,ou=swengg,dc=min,dc=io"
|
||||
userReq := madmin.PolicyAssociationReq{
|
||||
Policies: []string{policy},
|
||||
User: userDN,
|
||||
}
|
||||
if _, err := s.adm.AttachPolicyLDAP(ctx, userReq); err != nil {
|
||||
c.Fatalf("Unable to attach policy: %v", err)
|
||||
}
|
||||
|
||||
newSSHCon := newSSHConnMock("dillon=ldap")
|
||||
_, err = sshPasswordAuth(newSSHCon, []byte("dillon"))
|
||||
if err != nil {
|
||||
c.Fatal("Password authentication failed for user (dillon):", err)
|
||||
}
|
||||
newSSHCon := newSSHConnMock("dillon=ldap")
|
||||
_, err = sshPasswordAuth(newSSHCon, []byte("dillon"))
|
||||
if err != nil {
|
||||
c.Fatal("Password authentication failed for user (dillon):", err)
|
||||
}
|
||||
|
||||
newSSHCon = newSSHConnMock("dillon")
|
||||
_, err = sshPasswordAuth(newSSHCon, []byte("dillon"))
|
||||
if err != nil {
|
||||
c.Fatal("Password authentication failed for user (dillon):", err)
|
||||
newSSHCon = newSSHConnMock("dillon")
|
||||
_, err = sshPasswordAuth(newSSHCon, []byte("dillon"))
|
||||
if err != nil {
|
||||
c.Fatal("Password authentication failed for user (dillon):", err)
|
||||
}
|
||||
}
|
||||
{
|
||||
userDN := "uid=fahim,ou=people,ou=swengg,dc=min,dc=io"
|
||||
userReq := madmin.PolicyAssociationReq{
|
||||
Policies: []string{policy},
|
||||
User: userDN,
|
||||
}
|
||||
if _, err := s.adm.AttachPolicyLDAP(ctx, userReq); err != nil {
|
||||
c.Fatalf("Unable to attach policy: %v", err)
|
||||
}
|
||||
|
||||
newSSHCon := newSSHConnMock("fahim=ldap")
|
||||
_, err = sshPasswordAuth(newSSHCon, []byte("fahim"))
|
||||
if err != nil {
|
||||
c.Fatal("Password authentication failed for user (fahim):", err)
|
||||
}
|
||||
|
||||
newSSHCon = newSSHConnMock("fahim")
|
||||
_, err = sshPasswordAuth(newSSHCon, []byte("fahim"))
|
||||
if err != nil {
|
||||
c.Fatal("Password authentication failed for user (fahim):", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,6 @@ func TestDoesPresignedV2SignatureMatch(t *testing.T) {
|
||||
t.Errorf("(%d) expected to get success, instead got %s", i, niceError(errCode))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -298,7 +298,6 @@ func TestParseSignature(t *testing.T) {
|
||||
t.Errorf("Test %d: Expected the result to be \"%s\", but got \"%s\". ", i+1, testCase.expectedSignStr, actualSignStr)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +342,6 @@ func TestParseSignedHeaders(t *testing.T) {
|
||||
t.Errorf("Test %d: Expected the result to be \"%v\", but got \"%v\". ", i+1, testCase.expectedSignedHeaders, actualSignedHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,7 +512,6 @@ func TestParseSignV4(t *testing.T) {
|
||||
t.Errorf("Test %d: Expected the result to be \"%v\", but got \"%v\". ", i+1, testCase.expectedAuthField, parsedAuthField.SignedHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -880,6 +877,5 @@ func TestParsePreSignV4(t *testing.T) {
|
||||
t.Errorf("Test %d: Expected date to be %v, but got %v", i+1, testCase.expectedPreSignValues.Date.UTC().Format(iso8601Format), parsedPreSign.Date.UTC().Format(iso8601Format))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1037,7 +1037,6 @@ func (c *SiteReplicationSys) PeerBucketConfigureReplHandler(ctx context.Context,
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
|
||||
}
|
||||
// no replication rule for this peer or target ARN missing in bucket targets
|
||||
if targetARN == "" {
|
||||
@@ -1406,7 +1405,6 @@ func (c *SiteReplicationSys) PeerSvcAccChangeHandler(ctx context.Context, change
|
||||
if err := globalIAMSys.DeleteServiceAccount(ctx, change.Delete.AccessKey, true); err != nil {
|
||||
return wrapSRErr(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1430,8 +1428,8 @@ func (c *SiteReplicationSys) PeerPolicyMappingHandler(ctx context.Context, mappi
|
||||
userType := IAMUserType(mapping.UserType)
|
||||
isGroup := mapping.IsGroup
|
||||
entityName := mapping.UserOrGroup
|
||||
if globalIAMSys.GetUsersSysType() == LDAPUsersSysType && userType == stsUser {
|
||||
|
||||
if globalIAMSys.GetUsersSysType() == LDAPUsersSysType && userType == stsUser {
|
||||
// Validate that the user or group exists in LDAP and use the normalized
|
||||
// form of the entityName (which will be an LDAP DN).
|
||||
var err error
|
||||
@@ -1441,7 +1439,7 @@ func (c *SiteReplicationSys) PeerPolicyMappingHandler(ctx context.Context, mappi
|
||||
if foundGroupDN, underBaseDN, err = globalIAMSys.LDAPConfig.GetValidatedGroupDN(nil, entityName); err != nil {
|
||||
iamLogIf(ctx, err)
|
||||
} else if foundGroupDN == nil || !underBaseDN {
|
||||
err = errNoSuchGroup
|
||||
return wrapSRErr(errNoSuchGroup)
|
||||
}
|
||||
entityName = foundGroupDN.NormDN
|
||||
} else {
|
||||
@@ -1449,7 +1447,7 @@ func (c *SiteReplicationSys) PeerPolicyMappingHandler(ctx context.Context, mappi
|
||||
if foundUserDN, err = globalIAMSys.LDAPConfig.GetValidatedDNForUsername(entityName); err != nil {
|
||||
iamLogIf(ctx, err)
|
||||
} else if foundUserDN == nil {
|
||||
err = errNoSuchUser
|
||||
return wrapSRErr(errNoSuchUser)
|
||||
}
|
||||
entityName = foundUserDN.NormDN
|
||||
}
|
||||
@@ -2154,7 +2152,7 @@ func (c *SiteReplicationSys) syncToAllPeers(ctx context.Context, addOpts madmin.
|
||||
SecretKey: acc.Credentials.SecretKey,
|
||||
Groups: acc.Credentials.Groups,
|
||||
Claims: claims,
|
||||
SessionPolicy: json.RawMessage(policyJSON),
|
||||
SessionPolicy: policyJSON,
|
||||
Status: acc.Credentials.Status,
|
||||
Name: acc.Credentials.Name,
|
||||
Description: acc.Credentials.Description,
|
||||
@@ -3062,7 +3060,6 @@ func (c *SiteReplicationSys) siteReplicationStatus(ctx context.Context, objAPI O
|
||||
sum.ReplicatedGroupPolicyMappings++
|
||||
info.StatsSummary[ps.DeploymentID] = sum
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5552,7 +5549,7 @@ func (c *SiteReplicationSys) healUsers(ctx context.Context, objAPI ObjectLayer,
|
||||
SecretKey: creds.SecretKey,
|
||||
Groups: creds.Groups,
|
||||
Claims: claims,
|
||||
SessionPolicy: json.RawMessage(policyJSON),
|
||||
SessionPolicy: policyJSON,
|
||||
Status: creds.Status,
|
||||
Name: creds.Name,
|
||||
Description: creds.Description,
|
||||
|
||||
@@ -111,6 +111,9 @@ type VolInfo struct {
|
||||
|
||||
// total VolInfo counts
|
||||
count int
|
||||
|
||||
// Date and time when the volume was deleted, if Deleted
|
||||
Deleted time.Time
|
||||
}
|
||||
|
||||
// FilesInfo represent a list of files, additionally
|
||||
|
||||
@@ -6504,8 +6504,8 @@ func (z *VolInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if zb0001 != 2 {
|
||||
err = msgp.ArrayError{Wanted: 2, Got: zb0001}
|
||||
if zb0001 != 3 {
|
||||
err = msgp.ArrayError{Wanted: 3, Got: zb0001}
|
||||
return
|
||||
}
|
||||
z.Name, err = dc.ReadString()
|
||||
@@ -6518,13 +6518,18 @@ func (z *VolInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err, "Created")
|
||||
return
|
||||
}
|
||||
z.Deleted, err = dc.ReadTime()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Deleted")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z VolInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// array header, size 2
|
||||
err = en.Append(0x92)
|
||||
// array header, size 3
|
||||
err = en.Append(0x93)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -6538,16 +6543,22 @@ func (z VolInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = msgp.WrapError(err, "Created")
|
||||
return
|
||||
}
|
||||
err = en.WriteTime(z.Deleted)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Deleted")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z VolInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// array header, size 2
|
||||
o = append(o, 0x92)
|
||||
// array header, size 3
|
||||
o = append(o, 0x93)
|
||||
o = msgp.AppendString(o, z.Name)
|
||||
o = msgp.AppendTime(o, z.Created)
|
||||
o = msgp.AppendTime(o, z.Deleted)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6559,8 +6570,8 @@ func (z *VolInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if zb0001 != 2 {
|
||||
err = msgp.ArrayError{Wanted: 2, Got: zb0001}
|
||||
if zb0001 != 3 {
|
||||
err = msgp.ArrayError{Wanted: 3, Got: zb0001}
|
||||
return
|
||||
}
|
||||
z.Name, bts, err = msgp.ReadStringBytes(bts)
|
||||
@@ -6573,13 +6584,18 @@ func (z *VolInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "Created")
|
||||
return
|
||||
}
|
||||
z.Deleted, bts, err = msgp.ReadTimeBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Deleted")
|
||||
return
|
||||
}
|
||||
o = bts
|
||||
return
|
||||
}
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z VolInfo) Msgsize() (s int) {
|
||||
s = 1 + msgp.StringPrefixSize + len(z.Name) + msgp.TimeSize
|
||||
s = 1 + msgp.StringPrefixSize + len(z.Name) + msgp.TimeSize + msgp.TimeSize
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6603,8 +6619,8 @@ func (z *VolsInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err, zb0001)
|
||||
return
|
||||
}
|
||||
if zb0003 != 2 {
|
||||
err = msgp.ArrayError{Wanted: 2, Got: zb0003}
|
||||
if zb0003 != 3 {
|
||||
err = msgp.ArrayError{Wanted: 3, Got: zb0003}
|
||||
return
|
||||
}
|
||||
(*z)[zb0001].Name, err = dc.ReadString()
|
||||
@@ -6617,6 +6633,11 @@ func (z *VolsInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err, zb0001, "Created")
|
||||
return
|
||||
}
|
||||
(*z)[zb0001].Deleted, err = dc.ReadTime()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0001, "Deleted")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -6629,8 +6650,8 @@ func (z VolsInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
return
|
||||
}
|
||||
for zb0004 := range z {
|
||||
// array header, size 2
|
||||
err = en.Append(0x92)
|
||||
// array header, size 3
|
||||
err = en.Append(0x93)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -6644,6 +6665,11 @@ func (z VolsInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = msgp.WrapError(err, zb0004, "Created")
|
||||
return
|
||||
}
|
||||
err = en.WriteTime(z[zb0004].Deleted)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0004, "Deleted")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -6653,10 +6679,11 @@ func (z VolsInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
o = msgp.AppendArrayHeader(o, uint32(len(z)))
|
||||
for zb0004 := range z {
|
||||
// array header, size 2
|
||||
o = append(o, 0x92)
|
||||
// array header, size 3
|
||||
o = append(o, 0x93)
|
||||
o = msgp.AppendString(o, z[zb0004].Name)
|
||||
o = msgp.AppendTime(o, z[zb0004].Created)
|
||||
o = msgp.AppendTime(o, z[zb0004].Deleted)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -6681,8 +6708,8 @@ func (z *VolsInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, zb0001)
|
||||
return
|
||||
}
|
||||
if zb0003 != 2 {
|
||||
err = msgp.ArrayError{Wanted: 2, Got: zb0003}
|
||||
if zb0003 != 3 {
|
||||
err = msgp.ArrayError{Wanted: 3, Got: zb0003}
|
||||
return
|
||||
}
|
||||
(*z)[zb0001].Name, bts, err = msgp.ReadStringBytes(bts)
|
||||
@@ -6695,6 +6722,11 @@ func (z *VolsInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, zb0001, "Created")
|
||||
return
|
||||
}
|
||||
(*z)[zb0001].Deleted, bts, err = msgp.ReadTimeBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0001, "Deleted")
|
||||
return
|
||||
}
|
||||
}
|
||||
o = bts
|
||||
return
|
||||
@@ -6704,7 +6736,7 @@ func (z *VolsInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
func (z VolsInfo) Msgsize() (s int) {
|
||||
s = msgp.ArrayHeaderSize
|
||||
for zb0004 := range z {
|
||||
s += 1 + msgp.StringPrefixSize + len(z[zb0004].Name) + msgp.TimeSize
|
||||
s += 1 + msgp.StringPrefixSize + len(z[zb0004].Name) + msgp.TimeSize + msgp.TimeSize
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/bpool"
|
||||
"github.com/minio/minio/internal/cachevalue"
|
||||
"github.com/minio/minio/internal/grid"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
@@ -508,13 +508,13 @@ func (client *storageRESTClient) RenameData(ctx context.Context, srcVolume, srcP
|
||||
}
|
||||
|
||||
// where we keep old *Readers
|
||||
var readMsgpReaderPool = sync.Pool{New: func() interface{} { return &msgp.Reader{} }}
|
||||
var readMsgpReaderPool = bpool.Pool[*msgp.Reader]{New: func() *msgp.Reader { return &msgp.Reader{} }}
|
||||
|
||||
// mspNewReader returns a *Reader that reads from the provided reader.
|
||||
// The reader will be buffered.
|
||||
// Return with readMsgpReaderPoolPut when done.
|
||||
func msgpNewReader(r io.Reader) *msgp.Reader {
|
||||
p := readMsgpReaderPool.Get().(*msgp.Reader)
|
||||
p := readMsgpReaderPool.Get()
|
||||
if p.R == nil {
|
||||
p.R = xbufio.NewReaderSize(r, 32<<10)
|
||||
} else {
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/bpool"
|
||||
"github.com/minio/minio/internal/grid"
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
|
||||
@@ -831,7 +832,7 @@ func keepHTTPReqResponseAlive(w http.ResponseWriter, r *http.Request) (resp func
|
||||
// Response not ready, write a filler byte.
|
||||
write([]byte{32})
|
||||
if canWrite {
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
case err := <-doneCh:
|
||||
if err != nil {
|
||||
@@ -905,7 +906,7 @@ func keepHTTPResponseAlive(w http.ResponseWriter) func(error) {
|
||||
// Response not ready, write a filler byte.
|
||||
write([]byte{32})
|
||||
if canWrite {
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
case err := <-doneCh:
|
||||
if err != nil {
|
||||
@@ -1025,7 +1026,7 @@ func streamHTTPResponse(w http.ResponseWriter) *httpStreamResponse {
|
||||
// Response not ready, write a filler byte.
|
||||
write([]byte{32})
|
||||
if canWrite {
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
case err := <-doneCh:
|
||||
if err != nil {
|
||||
@@ -1043,7 +1044,7 @@ func streamHTTPResponse(w http.ResponseWriter) *httpStreamResponse {
|
||||
write(tmp[:])
|
||||
write(block)
|
||||
if canWrite {
|
||||
w.(http.Flusher).Flush()
|
||||
xhttp.Flush(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1051,29 +1052,23 @@ func streamHTTPResponse(w http.ResponseWriter) *httpStreamResponse {
|
||||
return &h
|
||||
}
|
||||
|
||||
var poolBuf8k = sync.Pool{
|
||||
New: func() interface{} {
|
||||
var poolBuf8k = bpool.Pool[*[]byte]{
|
||||
New: func() *[]byte {
|
||||
b := make([]byte, 8192)
|
||||
return &b
|
||||
},
|
||||
}
|
||||
|
||||
var poolBuf128k = sync.Pool{
|
||||
New: func() interface{} {
|
||||
b := make([]byte, 128<<10)
|
||||
return b
|
||||
},
|
||||
}
|
||||
|
||||
// waitForHTTPStream will wait for responses where
|
||||
// streamHTTPResponse has been used.
|
||||
// The returned reader contains the payload and must be closed if no error is returned.
|
||||
func waitForHTTPStream(respBody io.ReadCloser, w io.Writer) error {
|
||||
var tmp [1]byte
|
||||
// 8K copy buffer, reused for less allocs...
|
||||
bufp := poolBuf8k.Get().(*[]byte)
|
||||
bufp := poolBuf8k.Get()
|
||||
buf := *bufp
|
||||
defer poolBuf8k.Put(bufp)
|
||||
|
||||
for {
|
||||
_, err := io.ReadFull(respBody, tmp[:])
|
||||
if err != nil {
|
||||
@@ -1438,7 +1433,6 @@ func registerStorageRESTHandlers(router *mux.Router, endpointServerPools Endpoin
|
||||
}
|
||||
}
|
||||
}(endpoint)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ const (
|
||||
ErrSTSMalformedPolicyDocument
|
||||
ErrSTSInsecureConnection
|
||||
ErrSTSInvalidClientCertificate
|
||||
ErrSTSTooManyIntermediateCAs
|
||||
ErrSTSNotInitialized
|
||||
ErrSTSIAMNotInitialized
|
||||
ErrSTSUpstreamError
|
||||
@@ -145,6 +146,11 @@ var stsErrCodes = stsErrorCodeMap{
|
||||
Description: "The provided client certificate is invalid. Retry with a different certificate.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSTooManyIntermediateCAs: {
|
||||
Code: "TooManyIntermediateCAs",
|
||||
Description: "The provided client certificate contains too many intermediate CA certificates",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSNotInitialized: {
|
||||
Code: "STSNotInitialized",
|
||||
Description: "STS API not initialized, please try again.",
|
||||
|
||||
+19
-4
@@ -780,12 +780,26 @@ func (sts *stsAPIHandlers) AssumeRoleWithCertificate(w http.ResponseWriter, r *h
|
||||
// policy mapping would be ambiguous.
|
||||
// However, we can filter all CA certificates and only check
|
||||
// whether they client has sent exactly one (non-CA) leaf certificate.
|
||||
peerCertificates := make([]*x509.Certificate, 0, len(r.TLS.PeerCertificates))
|
||||
const MaxIntermediateCAs = 10
|
||||
var (
|
||||
peerCertificates = make([]*x509.Certificate, 0, len(r.TLS.PeerCertificates))
|
||||
intermediates *x509.CertPool
|
||||
numIntermediates int
|
||||
)
|
||||
for _, cert := range r.TLS.PeerCertificates {
|
||||
if cert.IsCA {
|
||||
continue
|
||||
numIntermediates++
|
||||
if numIntermediates > MaxIntermediateCAs {
|
||||
writeSTSErrorResponse(ctx, w, ErrSTSTooManyIntermediateCAs, fmt.Errorf("client certificate contains more than %d intermediate CAs", MaxIntermediateCAs))
|
||||
return
|
||||
}
|
||||
if intermediates == nil {
|
||||
intermediates = x509.NewCertPool()
|
||||
}
|
||||
intermediates.AddCert(cert)
|
||||
} else {
|
||||
peerCertificates = append(peerCertificates, cert)
|
||||
}
|
||||
peerCertificates = append(peerCertificates, cert)
|
||||
}
|
||||
r.TLS.PeerCertificates = peerCertificates
|
||||
|
||||
@@ -806,7 +820,8 @@ func (sts *stsAPIHandlers) AssumeRoleWithCertificate(w http.ResponseWriter, r *h
|
||||
KeyUsages: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
Roots: globalRootCAs,
|
||||
Intermediates: intermediates,
|
||||
Roots: globalRootCAs,
|
||||
})
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(ctx, w, ErrSTSInvalidClientCertificate, err)
|
||||
|
||||
@@ -18,15 +18,16 @@ func _() {
|
||||
_ = x[ErrSTSMalformedPolicyDocument-7]
|
||||
_ = x[ErrSTSInsecureConnection-8]
|
||||
_ = x[ErrSTSInvalidClientCertificate-9]
|
||||
_ = x[ErrSTSNotInitialized-10]
|
||||
_ = x[ErrSTSIAMNotInitialized-11]
|
||||
_ = x[ErrSTSUpstreamError-12]
|
||||
_ = x[ErrSTSInternalError-13]
|
||||
_ = x[ErrSTSTooManyIntermediateCAs-10]
|
||||
_ = x[ErrSTSNotInitialized-11]
|
||||
_ = x[ErrSTSIAMNotInitialized-12]
|
||||
_ = x[ErrSTSUpstreamError-13]
|
||||
_ = x[ErrSTSInternalError-14]
|
||||
}
|
||||
|
||||
const _STSErrorCode_name = "STSNoneSTSAccessDeniedSTSMissingParameterSTSInvalidParameterValueSTSWebIdentityExpiredTokenSTSClientGrantsExpiredTokenSTSInvalidClientGrantsTokenSTSMalformedPolicyDocumentSTSInsecureConnectionSTSInvalidClientCertificateSTSNotInitializedSTSIAMNotInitializedSTSUpstreamErrorSTSInternalError"
|
||||
const _STSErrorCode_name = "STSNoneSTSAccessDeniedSTSMissingParameterSTSInvalidParameterValueSTSWebIdentityExpiredTokenSTSClientGrantsExpiredTokenSTSInvalidClientGrantsTokenSTSMalformedPolicyDocumentSTSInsecureConnectionSTSInvalidClientCertificateSTSTooManyIntermediateCAsSTSNotInitializedSTSIAMNotInitializedSTSUpstreamErrorSTSInternalError"
|
||||
|
||||
var _STSErrorCode_index = [...]uint16{0, 7, 22, 41, 65, 91, 118, 145, 171, 192, 219, 236, 256, 272, 288}
|
||||
var _STSErrorCode_index = [...]uint16{0, 7, 22, 41, 65, 91, 118, 145, 171, 192, 219, 244, 261, 281, 297, 313}
|
||||
|
||||
func (i STSErrorCode) String() string {
|
||||
if i < 0 || i >= STSErrorCode(len(_STSErrorCode_index)-1) {
|
||||
|
||||
@@ -794,7 +794,6 @@ func assembleStreamingChunks(req *http.Request, body io.ReadSeeker, chunkSize in
|
||||
if n <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewReader(stream))
|
||||
return req, nil
|
||||
|
||||
+5
-9
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/klauspost/compress/s2"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
gzip "github.com/klauspost/pgzip"
|
||||
xioutil "github.com/minio/minio/internal/ioutil"
|
||||
"github.com/pierrec/lz4/v4"
|
||||
)
|
||||
|
||||
@@ -182,7 +183,6 @@ func untar(ctx context.Context, r io.Reader, putObject func(reader io.Reader, in
|
||||
|
||||
header, err := tarReader.Next()
|
||||
switch {
|
||||
|
||||
// if no more files are found return
|
||||
case err == io.EOF:
|
||||
wg.Wait()
|
||||
@@ -226,13 +226,10 @@ func untar(ctx context.Context, r io.Reader, putObject func(reader io.Reader, in
|
||||
|
||||
// Do small files async
|
||||
n++
|
||||
if header.Size <= smallFileThreshold {
|
||||
if header.Size <= xioutil.MediumBlock {
|
||||
asyncWriters <- struct{}{}
|
||||
b := poolBuf128k.Get().([]byte)
|
||||
if cap(b) < int(header.Size) {
|
||||
b = make([]byte, smallFileThreshold)
|
||||
}
|
||||
b = b[:header.Size]
|
||||
bufp := xioutil.ODirectPoolMedium.Get()
|
||||
b := (*bufp)[:header.Size]
|
||||
if _, err := io.ReadFull(tarReader, b); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -243,8 +240,7 @@ func untar(ctx context.Context, r io.Reader, putObject func(reader io.Reader, in
|
||||
rc.Close()
|
||||
<-asyncWriters
|
||||
wg.Done()
|
||||
//nolint:staticcheck // SA6002 we are fine with the tiny alloc
|
||||
poolBuf128k.Put(b)
|
||||
xioutil.ODirectPoolMedium.Put(bufp)
|
||||
}()
|
||||
if err := putObject(&rc, fi, name); err != nil {
|
||||
if o.ignoreErrs {
|
||||
|
||||
+1
-1
@@ -210,7 +210,7 @@ func TestIsKubernetes(t *testing.T) {
|
||||
// Tests if the environment we are running is Helm chart.
|
||||
func TestGetHelmVersion(t *testing.T) {
|
||||
createTempFile := func(content string) string {
|
||||
tmpfile, err := os.CreateTemp("", "helm-testfile-")
|
||||
tmpfile, err := os.CreateTemp(t.TempDir(), "helm-testfile-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create temporary file. %s", err)
|
||||
}
|
||||
|
||||
+4
-2
@@ -271,10 +271,12 @@ func validateLengthAndChecksum(r *http.Request) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !cs.Type.IsSet() {
|
||||
if cs == nil || !cs.Type.IsSet() {
|
||||
return false
|
||||
}
|
||||
r.Body = hash.NewChecker(r.Body, cs.Type.Hasher(), cs.Raw, r.ContentLength)
|
||||
if cs.Valid() && !cs.Type.Trailing() {
|
||||
r.Body = hash.NewChecker(r.Body, cs.Type.Hasher(), cs.Raw, r.ContentLength)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,12 @@ import (
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/google/uuid"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio/internal/bpool"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
"github.com/minio/minio/internal/config/storageclass"
|
||||
@@ -703,18 +703,17 @@ func (j xlMetaV2Object) ToFileInfo(volume, path string, allParts bool) (FileInfo
|
||||
const metaDataReadDefault = 4 << 10
|
||||
|
||||
// Return used metadata byte slices here.
|
||||
var metaDataPool = sync.Pool{New: func() interface{} { return make([]byte, 0, metaDataReadDefault) }}
|
||||
var metaDataPool = bpool.Pool[[]byte]{New: func() []byte { return make([]byte, 0, metaDataReadDefault) }}
|
||||
|
||||
// metaDataPoolGet will return a byte slice with capacity at least metaDataReadDefault.
|
||||
// It will be length 0.
|
||||
func metaDataPoolGet() []byte {
|
||||
return metaDataPool.Get().([]byte)[:0]
|
||||
return metaDataPool.Get()[:0]
|
||||
}
|
||||
|
||||
// metaDataPoolPut will put an unused small buffer back into the pool.
|
||||
func metaDataPoolPut(buf []byte) {
|
||||
if cap(buf) >= metaDataReadDefault && cap(buf) < metaDataReadDefault*4 {
|
||||
//nolint:staticcheck // SA6002 we are fine with the tiny alloc
|
||||
metaDataPool.Put(buf)
|
||||
}
|
||||
}
|
||||
@@ -1982,7 +1981,6 @@ func mergeXLV2Versions(quorum int, strict bool, requestedVersions int, versions
|
||||
if !latest.header.FreeVersion() {
|
||||
nVersions++
|
||||
}
|
||||
|
||||
} else {
|
||||
// Find latest.
|
||||
var latestCount int
|
||||
|
||||
@@ -225,7 +225,6 @@ func compareXLMetaV1(t *testing.T, unMarshalXLMeta, jsoniterXLMeta xlMetaV1Objec
|
||||
if val != jsoniterVal {
|
||||
t.Errorf("Expected the value for Meta data key \"%s\" to be \"%s\", but got \"%s\".", key, val, jsoniterVal)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -2166,10 +2166,10 @@ func (s *xlStorage) writeAllDirect(ctx context.Context, filePath string, fileSiz
|
||||
var bufp *[]byte
|
||||
switch {
|
||||
case fileSize <= xioutil.SmallBlock:
|
||||
bufp = xioutil.ODirectPoolSmall.Get().(*[]byte)
|
||||
bufp = xioutil.ODirectPoolSmall.Get()
|
||||
defer xioutil.ODirectPoolSmall.Put(bufp)
|
||||
default:
|
||||
bufp = xioutil.ODirectPoolLarge.Get().(*[]byte)
|
||||
bufp = xioutil.ODirectPoolLarge.Get()
|
||||
defer xioutil.ODirectPoolLarge.Put(bufp)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,12 +32,4 @@ docker buildx build --push --no-cache \
|
||||
|
||||
docker buildx prune -f
|
||||
|
||||
docker buildx build --push --no-cache \
|
||||
--build-arg RELEASE="${release}" \
|
||||
-t "minio/minio:${release}.fips" \
|
||||
-t "quay.io/minio/minio:${release}.fips" \
|
||||
--platform=linux/amd64 -f Dockerfile.release.fips .
|
||||
|
||||
docker buildx prune -f
|
||||
|
||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||
|
||||
@@ -10,7 +10,7 @@ Replication relies on immutability provided by versioning to sync objects betwee
|
||||
|
||||
If an object meets replication rules as set in the replication configuration, `X-Amz-Replication-Status` is first set to `PENDING` as the PUT operation completes and replication is queued (unless synchronous replication is in place). After replication is performed, the metadata on the source object version changes to `COMPLETED` or `FAILED` depending on whether replication succeeded. The object version on the target shows `X-Amz-Replication-Status` of `REPLICA`
|
||||
|
||||
All replication failures are picked up by the scanner which runs at a one minute frequency, each time scanning upto a sixteenth of the namespace. Object versions marked `PENDING` or `FAILED` are re-queued for replication.
|
||||
All replication failures are picked up by the scanner which runs at a one minute frequency, each time scanning up to a sixteenth of the namespace. Object versions marked `PENDING` or `FAILED` are re-queued for replication.
|
||||
|
||||
Replication speed depends on the cluster load, number of objects in the object store as well as storage speed. In addition, any bandwidth limits set via `mc admin bucket remote add` could also contribute to replication speed. The number of workers used for replication defaults to 100. Based on network bandwidth and system load, the number of workers used in replication can be configured using `mc admin config set alias api` to set the `replication_workers`. The prometheus metrics exposed by MinIO can be used to plan resource allocation and bandwidth management to optimize replication speed.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ To replicate objects in a bucket to a destination bucket on a target site either
|
||||
|
||||
## Highlights
|
||||
|
||||
- Supports source and destination buckets to have the same name unlike AWS S3, addresses variety of usecases such as *Splunk*, *Veeam* site to site DR.
|
||||
- Supports source and destination buckets to have the same name unlike AWS S3, addresses variety of use-cases such as *Splunk*, *Veeam* site to site DR.
|
||||
- Supports object locking/retention across source and destination buckets natively out of the box, unlike AWS S3.
|
||||
- Simpler implementation than [AWS S3 Bucket Replication Config](https://docs.aws.amazon.com/AmazonS3/latest/dev/replication-add-config.html) with requirements such as IAM Role, AccessControlTranslation, Metrics and SourceSelectionCriteria are not needed with MinIO.
|
||||
- Active-Active replication
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Object Lock and Immutablity Guide [](https://slack.min.io)
|
||||
# Object Lock and Immutability Guide [](https://slack.min.io)
|
||||
|
||||
MinIO server allows WORM for specific objects or by configuring a bucket with default object lock configuration that applies default retention mode and retention duration to all objects. This makes objects in the bucket immutable i.e. delete of the version are not allowed until an expiry specified in the bucket's object lock configuration or object retention.
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ Duplicate delete markers are not created on MinIO buckets with versioning, if an
|
||||
|
||||
### Motivation
|
||||
|
||||
**PLEASE READ: This feature is meant for advanced usecases only where the setup is using bucket versioning or with replicated buckets, use this feature to optimize versioning behavior for some specific applications. MinIO experts will evaluate and guide on the benefits for your application, please reach out to us on <https://subnet.min.io>.**
|
||||
**PLEASE READ: This feature is meant for advanced use-cases only where the setup is using bucket versioning or with replicated buckets, use this feature to optimize versioning behavior for some specific applications. MinIO experts will evaluate and guide on the benefits for your application, please reach out to us on <https://subnet.min.io>.**
|
||||
|
||||
Spark/Hadoop workloads which use Hadoop MR Committer v1/v2 algorithm upload objects to a temporary prefix in a bucket. These objects are 'renamed' to a different prefix on Job commit. Object storage admins are forced to configure separate ILM policies to expire these objects and their versions to reclaim space.
|
||||
|
||||
@@ -212,6 +212,6 @@ public class IsVersioningEnabled {
|
||||
## Explore Further
|
||||
|
||||
- [Use `minio-java` SDK with MinIO Server](https://min.io/docs/minio/linux/developers/java/minio-java.html)
|
||||
- [Object Lock and Immutablity Guide](https://min.io/docs/minio/linux/administration/object-management/object-retention.html)
|
||||
- [Object Lock and Immutability Guide](https://min.io/docs/minio/linux/administration/object-management/object-retention.html)
|
||||
- [MinIO Admin Complete Guide](https://min.io/docs/minio/linux/reference/minio-mc-admin.html)
|
||||
- [The MinIO documentation website](https://min.io/docs/minio/linux/index.html)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## HTTP Trace
|
||||
|
||||
HTTP tracing can be enabled by using [`mc admin trace`](https://github.com/minio/mc/blob/master/docs/minio-admin-complete-guide.md#command-trace---display-minio-server-http-trace) command.
|
||||
HTTP tracing can be enabled by using [`mc admin trace`](https://min.io/docs/minio/linux/reference/minio-mc-admin/mc-admin-trace.html) command.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/minio/madmin-go/v3/estream"
|
||||
)
|
||||
@@ -30,18 +33,18 @@ type keepFileErr struct {
|
||||
error
|
||||
}
|
||||
|
||||
func extractInspectV2(pk []byte, r io.Reader, w io.Writer, okMsg string) error {
|
||||
privKey, err := bytesToPrivateKey(pk)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding key returned: %w", err)
|
||||
}
|
||||
|
||||
func extractInspectV2(pks [][]byte, r io.Reader, extractDir string) error {
|
||||
sr, err := estream.NewReader(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sr.SetPrivateKey(privKey)
|
||||
for _, pk := range pks {
|
||||
privKey, err := bytesToPrivateKey(pk)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding key returned: %w", err)
|
||||
}
|
||||
sr.SetPrivateKey(privKey)
|
||||
}
|
||||
sr.ReturnNonDecryptable(true)
|
||||
|
||||
// Debug corrupted streams.
|
||||
@@ -60,33 +63,29 @@ func extractInspectV2(pk []byte, r io.Reader, w io.Writer, okMsg string) error {
|
||||
return errors.New("no data found on stream")
|
||||
}
|
||||
if errors.Is(err, estream.ErrNoKey) {
|
||||
if stream.Name == "inspect.zip" {
|
||||
return errors.New("incorrect private key")
|
||||
}
|
||||
fmt.Println("Skipping", stream.Name, "no private key")
|
||||
if err := stream.Skip(); err != nil {
|
||||
return fmt.Errorf("stream skip: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if extracted {
|
||||
return keepFileErr{fmt.Errorf("next stream: %w", err)}
|
||||
}
|
||||
return fmt.Errorf("next stream: %w", err)
|
||||
}
|
||||
if stream.Name == "inspect.zip" {
|
||||
if extracted {
|
||||
return keepFileErr{errors.New("multiple inspect.zip streams found")}
|
||||
}
|
||||
_, err := io.Copy(w, stream)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading inspect stream: %w", err)
|
||||
}
|
||||
fmt.Println(okMsg)
|
||||
extracted = true
|
||||
continue
|
||||
if strings.Contains(stream.Name, "..") || !utf8.ValidString(stream.Name) {
|
||||
return fmt.Errorf("invalid stream name: %q", stream.Name)
|
||||
}
|
||||
if err := stream.Skip(); err != nil {
|
||||
return fmt.Errorf("stream skip: %w", err)
|
||||
|
||||
dst := filepath.Join(extractDir, stream.Name)
|
||||
os.Mkdir(extractDir, 0o755)
|
||||
w, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating output file: %w", err)
|
||||
}
|
||||
_, err = io.Copy(w, stream)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading inspect stream: %w", err)
|
||||
}
|
||||
fmt.Printf("Extracted: %s\n", dst)
|
||||
extracted = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,15 +51,34 @@ func main() {
|
||||
generateKeys()
|
||||
os.Exit(0)
|
||||
}
|
||||
var privateKey []byte
|
||||
var privateKeys [][]byte
|
||||
if *keyHex == "" {
|
||||
if b, err := os.ReadFile(*privKeyPath); err == nil {
|
||||
privateKey = b
|
||||
fmt.Println("Using private key from", *privKeyPath)
|
||||
// Attempt to load private key(s)
|
||||
n := 1
|
||||
var base, ext string
|
||||
base = *privKeyPath
|
||||
if idx := strings.LastIndexByte(base, '.'); idx != -1 {
|
||||
ext = base[idx:]
|
||||
base = base[:idx]
|
||||
}
|
||||
for {
|
||||
// Automatically read "file.ext", "file-2.ext", "file-3.ext"...
|
||||
fn := base + ext
|
||||
if n > 1 {
|
||||
fn = fmt.Sprintf("%s-%d%s", base, n, ext)
|
||||
}
|
||||
|
||||
if b, err := os.ReadFile(fn); err == nil {
|
||||
privateKeys = append(privateKeys, b)
|
||||
fmt.Println("Added private key from", fn)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
n++
|
||||
}
|
||||
|
||||
// Prompt for decryption key if no --key or --private-key are provided
|
||||
if len(privateKey) == 0 && !*stdin {
|
||||
if len(privateKeys) == 0 && !*stdin {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("Enter Decryption Key: ")
|
||||
|
||||
@@ -91,17 +110,20 @@ func main() {
|
||||
var err error
|
||||
inputs, err = filepathx.Glob(flag.Args()[0])
|
||||
fatalErr(err)
|
||||
if len(inputs) == 0 {
|
||||
fmt.Println("Usage: No input found")
|
||||
}
|
||||
default:
|
||||
flag.Usage()
|
||||
fatalIf(true, "Only 1 file can be decrypted")
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, input := range inputs {
|
||||
processFile(input, privateKey)
|
||||
processFile(input, privateKeys)
|
||||
}
|
||||
}
|
||||
|
||||
func processFile(inputFileName string, privateKey []byte) {
|
||||
func processFile(inputFileName string, privateKeys [][]byte) {
|
||||
// Calculate the output file name
|
||||
var outputFileName string
|
||||
switch {
|
||||
@@ -115,32 +137,31 @@ func processFile(inputFileName string, privateKey []byte) {
|
||||
outputFileName = inputFileName + ".decrypted"
|
||||
}
|
||||
|
||||
// Backup any already existing output file
|
||||
_, err := os.Stat(outputFileName)
|
||||
if err == nil {
|
||||
err := os.Rename(outputFileName, outputFileName+"."+time.Now().Format("20060102150405"))
|
||||
if err != nil {
|
||||
fatalErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Open the input and create the output file
|
||||
input, err := os.Open(inputFileName)
|
||||
fatalErr(err)
|
||||
defer input.Close()
|
||||
output, err := os.Create(outputFileName)
|
||||
fatalErr(err)
|
||||
|
||||
// Decrypt the inspect data
|
||||
msg := fmt.Sprintf("output written to %s", outputFileName)
|
||||
|
||||
switch {
|
||||
case *keyHex != "":
|
||||
// Backup any already existing output file
|
||||
_, err := os.Stat(outputFileName)
|
||||
if err == nil {
|
||||
err := os.Rename(outputFileName, outputFileName+"."+time.Now().Format("20060102150405"))
|
||||
if err != nil {
|
||||
fatalErr(err)
|
||||
}
|
||||
}
|
||||
output, err := os.Create(outputFileName)
|
||||
fatalErr(err)
|
||||
msg := fmt.Sprintf("output written to %s", outputFileName)
|
||||
err = extractInspectV1(*keyHex, input, output, msg)
|
||||
case len(privateKey) != 0:
|
||||
err = extractInspectV2(privateKey, input, output, msg)
|
||||
output.Close()
|
||||
case len(privateKeys) != 0:
|
||||
outputFileName := strings.TrimSuffix(outputFileName, ".zip")
|
||||
err = extractInspectV2(privateKeys, input, outputFileName)
|
||||
}
|
||||
output.Close()
|
||||
if err != nil {
|
||||
|
||||
var keep keepFileErr
|
||||
|
||||
@@ -369,8 +369,8 @@ FLAGS:
|
||||
defer f.Close()
|
||||
r = f
|
||||
}
|
||||
if strings.HasSuffix(file, ".zip") {
|
||||
zr, err := zip.NewReader(r.(io.ReaderAt), sz)
|
||||
if ra, ok := r.(io.ReaderAt); ok && strings.HasSuffix(file, ".zip") {
|
||||
zr, err := zip.NewReader(ra, sz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ Expansion of ellipses and choice of erasure sets based on this expansion is an a
|
||||
|
||||
- Erasure coding used by MinIO is [Reed-Solomon](https://github.com/klauspost/reedsolomon) erasure coding scheme, which has a total shard maximum of 256 i.e 128 data and 128 parity. MinIO design goes beyond this limitation by doing some practical architecture choices.
|
||||
|
||||
- Erasure set is a single erasure coding unit within a MinIO deployment. An object is sharded within an erasure set. Erasure set size is automatically calculated based on the number of drives. MinIO supports unlimited number of drives but each erasure set can be upto 16 drives and a minimum of 2 drives.
|
||||
- Erasure set is a single erasure coding unit within a MinIO deployment. An object is sharded within an erasure set. Erasure set size is automatically calculated based on the number of drives. MinIO supports unlimited number of drives but each erasure set can be up to 16 drives and a minimum of 2 drives.
|
||||
|
||||
- We limited the number of drives to 16 for erasure set because, erasure code shards more than 16 can become chatty and do not have any performance advantages. Additionally since 16 drive erasure set gives you tolerance of 8 drives per object by default which is plenty in any practical scenario.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user