mirror of
https://github.com/pgsty/minio.git
synced 2026-08-01 16:40:43 +03:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 067ebab9d8 | |||
| 6be6c0d2e3 | |||
| aa874010e2 | |||
| 8ec888d13d | |||
| 916f274c83 | |||
| 7ac53c07af | |||
| bc72e4226e | |||
| 5e0776e96a | |||
| 2f1ef02d35 | |||
| db8442584e | |||
| d46cf50760 | |||
| 0357121d17 | |||
| aff236e20e | |||
| cbd70d26b5 | |||
| 5e763b71dc | |||
| 7e4e7a66af | |||
| 906947a285 | |||
| bfc70bc74e | |||
| 6b4f833a12 | |||
| 426c902b87 | |||
| e4b51235f8 | |||
| 4c6498d726 | |||
| 0a3b1ad4eb | |||
| f23f442d33 | |||
| e465c3587b | |||
| 7109b6d414 | |||
| 8c97f3e9bc | |||
| 3795b2c8ba | |||
| 7725425e05 | |||
| b2f4948bbe | |||
| f802d2ba83 | |||
| ce8548a1a2 | |||
| 490dec981a | |||
| 39fd7b0b3b | |||
| e83930333b | |||
| b0d70a0e5e | |||
| ff5a5c1ee0 | |||
| 290a53d735 | |||
| 2393a13f86 | |||
| 7d8c8de827 | |||
| 3faef829c5 | |||
| 7560fb6f9a | |||
| 2fddcc6a11 | |||
| 69bf39f42e | |||
| f4d5c861f3 | |||
| 564a0afae1 | |||
| 1e332f0eb1 | |||
| cab8d3d568 | |||
| be8c4cb24a | |||
| 65166e4ce4 | |||
| 8249cd4406 | |||
| c6ecaf68ed | |||
| d3f89fa6e3 | |||
| ce8397f7d9 | |||
| cae9aeca00 | |||
| f939d1c183 | |||
| 242d06274a | |||
| 957e3ed729 | |||
| ed02ee4ef4 | |||
| ba9691a0ad | |||
| e7eb94de6b | |||
| b6eb8dff64 | |||
| 7da9e3a6f8 | |||
| 876970baea | |||
| e68e76e143 | |||
| 2bc7ca2d34 | |||
| e94eb9af10 | |||
| 3018b21ab8 | |||
| 0b605c3383 | |||
| e7ac1ea54c | |||
| 5ac6d91525 | |||
| 1cd6713e24 | |||
| 785b429737 | |||
| 4aecd8d039 | |||
| 1b339ea062 | |||
| 236ef03dbd | |||
| 53cc561048 | |||
| bb4b143f3b | |||
| 3af41cd37d | |||
| 7e32a17742 | |||
| 6c265534a4 | |||
| 1d42133d44 | |||
| df911c9b9e | |||
| a6f40dd574 | |||
| 688215e787 | |||
| 1cfa2e04bc | |||
| b4f6901903 | |||
| 0149382cdc | |||
| 697c9973a7 | |||
| 788fd3df81 | |||
| 996cac5fed | |||
| 0a8b78cb84 | |||
| b4eb74f5ff | |||
| 57d1f31054 | |||
| 9f02f51b87 | |||
| 911a17b149 | |||
| 3d969bd2b4 | |||
| f800cee4fa | |||
| b49fc33cb3 | |||
| 00e235a1ee | |||
| 37a6b2da67 | |||
| 913e977c8d | |||
| c2ddcb3b40 | |||
| ab9544c0d3 | |||
| 4bfe849409 | |||
| 3bdb92fcad | |||
| cf9e3069f2 | |||
| ed0cbfb31e | |||
| 32b2f6117e | |||
| ae92521310 | |||
| 5802df4365 | |||
| c1901f4e12 |
@@ -0,0 +1,14 @@
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v1
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x, 1.18.x]
|
||||
go-version: [1.18.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
go-version: [1.18.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -48,3 +48,4 @@ jobs:
|
||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||
make verify-healing
|
||||
make verify-healing-inconsistent-versions
|
||||
make verify-healing-with-root-disks
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x, 1.18.x]
|
||||
go-version: [1.18.x]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x, 1.18.x]
|
||||
go-version: [1.18.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
# are turned off - i.e. if ldap="", then ldap server is not enabled for
|
||||
# the tests.
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
go-version: [1.18.x]
|
||||
ldap: ["", "localhost:389"]
|
||||
etcd: ["", "http://localhost:2379"]
|
||||
openid: ["", "http://127.0.0.1:5556/dex"]
|
||||
|
||||
@@ -27,4 +27,4 @@ jobs:
|
||||
npm install -g markdownlint-cli
|
||||
markdownlint --fix '**/*.md' \
|
||||
--config /home/runner/work/minio/minio/.github/markdown-lint-cfg.yaml \
|
||||
--disable MD013 MD040
|
||||
--disable MD013 MD040 MD051
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
go-version: [1.18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
go-version: [1.18.x]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -14,13 +14,9 @@ linters:
|
||||
- govet
|
||||
- revive
|
||||
- ineffassign
|
||||
- gosimple
|
||||
- deadcode
|
||||
- structcheck
|
||||
- gomodguard
|
||||
- gofmt
|
||||
- unused
|
||||
- structcheck
|
||||
- unconvert
|
||||
- varcheck
|
||||
- gocritic
|
||||
|
||||
@@ -39,11 +39,14 @@ lint: ## runs golangci-lint suite of linters
|
||||
check: test
|
||||
test: verifiers build ## builds minio, runs linters, tests
|
||||
@echo "Running unit tests"
|
||||
@CGO_ENABLED=0 go test -tags kqueue ./...
|
||||
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -tags kqueue ./...
|
||||
|
||||
test-decom: install
|
||||
@echo "Running minio decom tests"
|
||||
@env bash $(PWD)/docs/distributed/decom.sh
|
||||
@env bash $(PWD)/docs/distributed/decom-encrypted.sh
|
||||
@env bash $(PWD)/docs/distributed/decom-encrypted-sse-s3.sh
|
||||
@env bash $(PWD)/docs/distributed/decom-compressed-sse-s3.sh
|
||||
|
||||
test-upgrade: build
|
||||
@echo "Running minio upgrade tests"
|
||||
@@ -55,9 +58,9 @@ test-race: verifiers build ## builds minio, runs linters, tests (race)
|
||||
|
||||
test-iam: build ## verify IAM (external IDP, etcd backends)
|
||||
@echo "Running tests for IAM (external IDP, etcd backends)"
|
||||
@CGO_ENABLED=0 go test -tags kqueue -v -run TestIAM* ./cmd
|
||||
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -tags kqueue -v -run TestIAM* ./cmd
|
||||
@echo "Running tests for IAM (external IDP, etcd backends) with -race"
|
||||
@GORACE=history_size=7 CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
|
||||
@MINIO_API_REQUESTS_MAX=10000 GORACE=history_size=7 CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
|
||||
|
||||
test-replication: install ## verify multi site replication
|
||||
@echo "Running tests for replicating three sites"
|
||||
@@ -87,6 +90,11 @@ verify-healing: ## verify healing and replacing disks with minio binary
|
||||
@(env bash $(PWD)/buildscripts/verify-healing.sh)
|
||||
@(env bash $(PWD)/buildscripts/unaligned-healing.sh)
|
||||
|
||||
verify-healing-with-root-disks:
|
||||
@echo "Verify healing with root disks"
|
||||
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||
@(env bash $(PWD)/buildscripts/verify-healing-with-root-disks.sh)
|
||||
|
||||
verify-healing-inconsistent-versions: ## verify resolving inconsistent versions
|
||||
@echo "Verify resolving inconsistent versions build with race"
|
||||
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||
@@ -132,6 +140,8 @@ clean: ## cleanup all generated assets
|
||||
@echo "Cleaning up all the generated files"
|
||||
@find . -name '*.test' | xargs rm -fv
|
||||
@find . -name '*~' | xargs rm -fv
|
||||
@find . -name '.#*#' | xargs rm -fv
|
||||
@find . -name '#*#' | xargs rm -fv
|
||||
@rm -rvf minio
|
||||
@rm -rvf build
|
||||
@rm -rvf release
|
||||
|
||||
+1
-1
@@ -2,6 +2,6 @@
|
||||
|
||||
MinIO creates FIPS builds using a patched version of the Go compiler (that uses BoringCrypto, from BoringSSL, which is [FIPS 140-2 validated](https://csrc.nist.gov/csrc/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp2964.pdf)) published by the Golang Team [here](https://github.com/golang/go/tree/dev.boringcrypto/misc/boring).
|
||||
|
||||
MinIO FIPS executables are available at http://dl.min.io - they are only published for `linux-amd64` architecture as binary files with the suffix `.fips`. We also publish corresponding container images to our official image repositories.
|
||||
MinIO FIPS executables are available at <http://dl.min.io> - they are only published for `linux-amd64` architecture as binary files with the suffix `.fips`. We also publish corresponding container images to our official image repositories.
|
||||
|
||||
We are not making any statements or representations about the suitability of this code or build in relation to the FIPS 140-2 standard. Interested users will have to evaluate for themselves whether this is useful for their own purposes.
|
||||
|
||||
@@ -125,7 +125,7 @@ You can also connect using any S3-compatible tool, such as the MinIO Client `mc`
|
||||
|
||||
## Install from Source
|
||||
|
||||
Use the following commands to compile and run a standalone MinIO server from source. Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://golang.org/doc/install). Minimum version required is [go1.17](https://golang.org/dl/#stable)
|
||||
Use the following commands to compile and run a standalone MinIO server from source. Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://golang.org/doc/install). Minimum version required is [go1.18](https://golang.org/dl/#stable)
|
||||
|
||||
```sh
|
||||
GO111MODULE=on go install github.com/minio/minio@latest
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
set -e
|
||||
|
||||
export GORACE="history_size=7"
|
||||
export MINIO_API_REQUESTS_MAX=10000
|
||||
|
||||
## TODO remove `dsync` from race detector once this is merged and released https://go-review.googlesource.com/c/go/+/333529/
|
||||
for d in $(go list ./... | grep -v dsync); do
|
||||
CGO_ENABLED=1 go test -v -race --timeout 100m "$d"
|
||||
|
||||
@@ -39,7 +39,7 @@ function start_minio_5drive() {
|
||||
"${MINIO[@]}" --address ":$start_port" "${WORK_DIR}/cicd-corpus/disk{1...5}" > "${WORK_DIR}/server1.log" 2>&1 &
|
||||
pid=$!
|
||||
disown $pid
|
||||
sleep 30
|
||||
sleep 5
|
||||
|
||||
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||
echo "server1 log:"
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
set -E
|
||||
set -o pipefail
|
||||
set -x
|
||||
|
||||
|
||||
if [ ! -x "$PWD/minio" ]; then
|
||||
echo "minio executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
||||
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server )
|
||||
|
||||
|
||||
function start_minio() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ROOT_USER=minio
|
||||
export MINIO_ROOT_PASSWORD=minio123
|
||||
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||
unset MINIO_CI_CD
|
||||
unset CI
|
||||
|
||||
args=()
|
||||
for i in $(seq 1 4); do
|
||||
args+=("http://localhost:$[${start_port}+$i]${WORK_DIR}/mnt/disk$i/ ")
|
||||
done
|
||||
|
||||
for i in $(seq 1 4); do
|
||||
"${MINIO[@]}" --address ":$[$start_port+$i]" ${args[@]} 2>&1 >"${WORK_DIR}/server$i.log" &
|
||||
done
|
||||
|
||||
# Wait until all nodes return 403
|
||||
for i in $(seq 1 4); do
|
||||
while [ "$(curl -m 1 -s -o /dev/null -w "%{http_code}" http://localhost:$[$start_port+$i])" -ne "403" ]; do
|
||||
echo -n ".";
|
||||
sleep 1;
|
||||
done
|
||||
done
|
||||
|
||||
}
|
||||
|
||||
# Prepare fake disks with losetup
|
||||
function prepare_block_devices() {
|
||||
mkdir -p ${WORK_DIR}/disks/ ${WORK_DIR}/mnt/
|
||||
for i in 1 2 3 4; do
|
||||
dd if=/dev/zero of=${WORK_DIR}/disks/img.$i bs=1M count=2048
|
||||
mkfs.ext4 -F ${WORK_DIR}/disks/img.$i
|
||||
sudo mknod /dev/minio-loopdisk$i b 7 $[256-$i]
|
||||
sudo losetup /dev/minio-loopdisk$i ${WORK_DIR}/disks/img.$i
|
||||
mkdir -p ${WORK_DIR}/mnt/disk$i/
|
||||
sudo mount /dev/minio-loopdisk$i ${WORK_DIR}/mnt/disk$i/
|
||||
sudo chown "$(id -u):$(id -g)" /dev/minio-loopdisk$i ${WORK_DIR}/mnt/disk$i/
|
||||
done
|
||||
}
|
||||
|
||||
# Start a distributed MinIO setup, unmount one disk and check if it is formatted
|
||||
function main() {
|
||||
start_port=$(shuf -i 10000-65000 -n 1)
|
||||
start_minio ${start_port}
|
||||
|
||||
# Unmount the disk, after the unmount the device id
|
||||
# /tmp/xxx/mnt/disk4 will be the same as '/' and it
|
||||
# will be detected as root disk
|
||||
while [ "$u" != "0" ]; do
|
||||
sudo umount ${WORK_DIR}/mnt/disk4/
|
||||
u=$?
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Wait until MinIO self heal kicks in
|
||||
sleep 60
|
||||
|
||||
if [ -f ${WORK_DIR}/mnt/disk4/.minio.sys/format.json ]; then
|
||||
echo "A root disk is formatted unexpectedely"
|
||||
cat "${WORK_DIR}/server4.log"
|
||||
exit -1
|
||||
fi
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
pkill minio
|
||||
sudo umount ${WORK_DIR}/mnt/disk{1..3}/
|
||||
sudo rm /dev/minio-loopdisk*
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
|
||||
( prepare_block_devices )
|
||||
( main "$@" )
|
||||
rv=$?
|
||||
|
||||
cleanup
|
||||
exit "$rv"
|
||||
|
||||
+2
-2
@@ -80,7 +80,7 @@ func (api objectAPIHandlers) PutBucketACLHandler(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// Before proceeding validate if bucket exists.
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket)
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -142,7 +142,7 @@ func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// Before proceeding validate if bucket exists.
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket)
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
|
||||
@@ -63,14 +63,13 @@ func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SetBucketQuotaAdminAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := pathClean(vars["bucket"])
|
||||
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -126,14 +125,13 @@ func (a adminAPIHandlers) GetBucketQuotaConfigHandler(w http.ResponseWriter, r *
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.GetBucketQuotaAdminAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := pathClean(vars["bucket"])
|
||||
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -171,12 +169,11 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
||||
// Get current object layer instance.
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SetBucketTargetAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -300,12 +297,11 @@ func (a adminAPIHandlers) ListRemoteTargetsHandler(w http.ResponseWriter, r *htt
|
||||
// Get current object layer instance.
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.GetBucketTargetAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
if bucket != "" {
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -340,12 +336,11 @@ func (a adminAPIHandlers) RemoveRemoteTargetHandler(w http.ResponseWriter, r *ht
|
||||
// Get current object layer instance.
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SetBucketTargetAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -386,22 +381,22 @@ func (a adminAPIHandlers) ExportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
// Get current object layer instance.
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ExportBucketMetadataAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
buckets []BucketInfo
|
||||
err error
|
||||
)
|
||||
if bucket != "" {
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
buckets = append(buckets, BucketInfo{Name: bucket})
|
||||
} else {
|
||||
buckets, err = objectAPI.ListBuckets(ctx)
|
||||
buckets, err = objectAPI.ListBuckets(ctx, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
@@ -642,7 +637,6 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
// Get current object layer instance.
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ImportBucketMetadataAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
@@ -690,7 +684,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
return
|
||||
}
|
||||
if _, ok := bucketMap[bucket]; !ok {
|
||||
opts := BucketOptions{
|
||||
opts := MakeBucketOptions{
|
||||
LockEnabled: config.ObjectLockEnabled == "Enabled",
|
||||
}
|
||||
err = objectAPI.MakeBucketWithLocation(ctx, bucket, opts)
|
||||
@@ -756,7 +750,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
return
|
||||
}
|
||||
if _, ok := bucketMap[bucket]; !ok {
|
||||
err = objectAPI.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
err = objectAPI.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
if _, ok := err.(BucketExists); !ok {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
@@ -823,7 +817,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
}
|
||||
// create bucket if it does not exist yet.
|
||||
if _, ok := bucketMap[bucket]; !ok {
|
||||
err = objectAPI.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
err = objectAPI.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
if _, ok := err.(BucketExists); !ok {
|
||||
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||
@@ -953,7 +947,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
kmsKey := encConfig.KeyID()
|
||||
if kmsKey != "" {
|
||||
kmsContext := kms.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
|
||||
_, err := GlobalKMS.GenerateKey(kmsKey, kmsContext)
|
||||
_, err := GlobalKMS.GenerateKey(ctx, kmsKey, kmsContext)
|
||||
if err != nil {
|
||||
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||
writeErrorResponse(ctx, w, importError(ctx, errKMSKeyNotFound, file.Name, bucket), r.URL)
|
||||
@@ -1067,3 +1061,79 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReplicationDiffHandler - POST returns info on unreplicated versions for a remote target ARN
|
||||
// to the connected HTTP client. This is a MinIO only extension
|
||||
func (a adminAPIHandlers) ReplicationDiffHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ReplicationDiff")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
if globalIsGateway {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
// check if user has permissions to perform this operation
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketVersionsAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
opts := extractReplicateDiffOpts(r.Form)
|
||||
if opts.ARN != "" {
|
||||
tgt := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, opts.ARN)
|
||||
if tgt.Empty() {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, fmt.Errorf("invalid arn : '%s'", opts.ARN)), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
|
||||
defer keepAliveTicker.Stop()
|
||||
|
||||
diffCh, err := getReplicationDiff(ctx, objectAPI, bucket, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
enc := json.NewEncoder(w)
|
||||
for {
|
||||
select {
|
||||
case entry, ok := <-diffCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := enc.Encode(entry); err != nil {
|
||||
return
|
||||
}
|
||||
if len(diffCh) == 0 {
|
||||
// Flush if nothing is queued
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
case <-keepAliveTicker.C:
|
||||
if len(diffCh) > 0 {
|
||||
continue
|
||||
}
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ import (
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
)
|
||||
|
||||
// validateAdminReq will validate request against and return whether it is allowed.
|
||||
// If any of the supplied actions are allowed it will be successful.
|
||||
// If nil ObjectLayer is returned, the operation is not permitted.
|
||||
// When nil ObjectLayer has been returned an error has always been sent to w.
|
||||
func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request, actions ...iampolicy.AdminAction) (ObjectLayer, auth.Credentials) {
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
@@ -41,11 +45,16 @@ func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
||||
for _, action := range actions {
|
||||
// Validate request signature.
|
||||
cred, adminAPIErr := checkAdminRequestAuth(ctx, r, action, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
switch adminAPIErr {
|
||||
case ErrNone:
|
||||
return objectAPI, cred
|
||||
case ErrAccessDenied:
|
||||
// Try another
|
||||
continue
|
||||
default:
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
|
||||
return nil, cred
|
||||
}
|
||||
return objectAPI, cred
|
||||
}
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
return nil, auth.Credentials{}
|
||||
|
||||
@@ -24,6 +24,8 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/madmin-go"
|
||||
@@ -116,19 +118,38 @@ func (a adminAPIHandlers) SRPeerBucketOps(w http.ResponseWriter, r *http.Request
|
||||
_, isLockEnabled := r.Form["lockEnabled"]
|
||||
_, isVersioningEnabled := r.Form["versioningEnabled"]
|
||||
_, isForceCreate := r.Form["forceCreate"]
|
||||
opts := BucketOptions{
|
||||
createdAtStr := strings.TrimSpace(r.Form.Get("createdAt"))
|
||||
createdAt, cerr := time.Parse(time.RFC3339Nano, createdAtStr)
|
||||
if cerr != nil {
|
||||
createdAt = timeSentinel
|
||||
}
|
||||
|
||||
opts := MakeBucketOptions{
|
||||
Location: r.Form.Get("location"),
|
||||
LockEnabled: isLockEnabled,
|
||||
VersioningEnabled: isVersioningEnabled,
|
||||
ForceCreate: isForceCreate,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
err = globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(ctx, bucket, opts)
|
||||
case madmin.ConfigureReplBktOp:
|
||||
err = globalSiteReplicationSys.PeerBucketConfigureReplHandler(ctx, bucket)
|
||||
case madmin.DeleteBucketBktOp:
|
||||
err = globalSiteReplicationSys.PeerBucketDeleteHandler(ctx, bucket, false)
|
||||
_, noRecreate := r.Form["noRecreate"]
|
||||
err = globalSiteReplicationSys.PeerBucketDeleteHandler(ctx, bucket, DeleteBucketOptions{
|
||||
Force: false,
|
||||
NoRecreate: noRecreate,
|
||||
SRDeleteOp: getSRBucketDeleteOp(true),
|
||||
})
|
||||
case madmin.ForceDeleteBucketBktOp:
|
||||
err = globalSiteReplicationSys.PeerBucketDeleteHandler(ctx, bucket, true)
|
||||
_, noRecreate := r.Form["noRecreate"]
|
||||
err = globalSiteReplicationSys.PeerBucketDeleteHandler(ctx, bucket, DeleteBucketOptions{
|
||||
Force: true,
|
||||
NoRecreate: noRecreate,
|
||||
SRDeleteOp: getSRBucketDeleteOp(true),
|
||||
})
|
||||
case madmin.PurgeDeletedBucketOp:
|
||||
globalSiteReplicationSys.purgeDeletedBucket(ctx, objectAPI, bucket)
|
||||
}
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -436,6 +457,7 @@ func getSRStatusOptions(r *http.Request) (opts madmin.SRStatusOptions) {
|
||||
opts.Users = q.Get("users") == "true"
|
||||
opts.Entity = madmin.GetSREntityType(q.Get("entity"))
|
||||
opts.EntityValue = q.Get("entityvalue")
|
||||
opts.ShowDeleted = q.Get("showDeleted") == "true"
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+14
-17
@@ -1177,7 +1177,7 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
||||
return buckets[i].Name < buckets[j].Name
|
||||
})
|
||||
} else {
|
||||
buckets, err = objectAPI.ListBuckets(ctx)
|
||||
buckets, err = objectAPI.ListBuckets(ctx, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
@@ -1543,6 +1543,7 @@ const (
|
||||
groupPolicyMappingsFile = "group_mappings.json"
|
||||
stsUserPolicyMappingsFile = "stsuser_mappings.json"
|
||||
stsGroupPolicyMappingsFile = "stsgroup_mappings.json"
|
||||
iamAssetsDir = "iam-assets"
|
||||
)
|
||||
|
||||
// ExportIAMHandler - exports all iam info as a zipped file
|
||||
@@ -1553,7 +1554,6 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
// Get current object layer instance.
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ExportIAMAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
// Initialize a zip writer which will provide a zipped content
|
||||
@@ -1595,8 +1595,9 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
stsUserPolicyMappingsFile,
|
||||
stsGroupPolicyMappingsFile,
|
||||
}
|
||||
for _, iamFile := range iamFiles {
|
||||
switch iamFile {
|
||||
for _, f := range iamFiles {
|
||||
iamFile := pathJoin(iamAssetsDir, f)
|
||||
switch f {
|
||||
case allPoliciesFile:
|
||||
allPolicies, err := globalIAMSys.ListPolicies(ctx, "")
|
||||
if err != nil {
|
||||
@@ -1674,11 +1675,6 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
svcAccts := make(map[string]madmin.SRSvcAccCreate)
|
||||
for user, acc := range serviceAccounts {
|
||||
if user == siteReplicatorSvcAcc {
|
||||
// skip the site replicate svc account as it should be created automatically if
|
||||
// site replication is enabled.
|
||||
continue
|
||||
}
|
||||
claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
@@ -1827,7 +1823,8 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
// import policies first
|
||||
{
|
||||
f, err := zr.Open(allPoliciesFile)
|
||||
|
||||
f, err := zr.Open(pathJoin(iamAssetsDir, allPoliciesFile))
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
@@ -1862,7 +1859,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// import users
|
||||
{
|
||||
f, err := zr.Open(allUsersFile)
|
||||
f, err := zr.Open(pathJoin(iamAssetsDir, allUsersFile))
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
@@ -1939,7 +1936,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// import groups
|
||||
{
|
||||
f, err := zr.Open(allGroupsFile)
|
||||
f, err := zr.Open(pathJoin(iamAssetsDir, allGroupsFile))
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
@@ -1977,7 +1974,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// import service accounts
|
||||
{
|
||||
f, err := zr.Open(allSvcAcctsFile)
|
||||
f, err := zr.Open(pathJoin(iamAssetsDir, allSvcAcctsFile))
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
@@ -2073,7 +2070,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// import user policy mappings
|
||||
{
|
||||
f, err := zr.Open(userPolicyMappingsFile)
|
||||
f, err := zr.Open(pathJoin(iamAssetsDir, userPolicyMappingsFile))
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
@@ -2112,7 +2109,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// import group policy mappings
|
||||
{
|
||||
f, err := zr.Open(groupPolicyMappingsFile)
|
||||
f, err := zr.Open(pathJoin(iamAssetsDir, groupPolicyMappingsFile))
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
@@ -2141,7 +2138,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// import sts user policy mappings
|
||||
{
|
||||
f, err := zr.Open(stsUserPolicyMappingsFile)
|
||||
f, err := zr.Open(pathJoin(iamAssetsDir, stsUserPolicyMappingsFile))
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
@@ -2180,7 +2177,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// import sts group policy mappings
|
||||
{
|
||||
f, err := zr.Open(stsGroupPolicyMappingsFile)
|
||||
f, err := zr.Open(pathJoin(iamAssetsDir, stsGroupPolicyMappingsFile))
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case err != nil:
|
||||
|
||||
+185
-52
@@ -71,16 +71,6 @@ const (
|
||||
mgmtForceStop = "forceStop"
|
||||
)
|
||||
|
||||
func updateServer(u *url.URL, sha256Sum []byte, lrTime time.Time, releaseInfo string, mode string) (us madmin.ServerUpdateStatus, err error) {
|
||||
if err = doUpdate(u, lrTime, sha256Sum, releaseInfo, mode); err != nil {
|
||||
return us, err
|
||||
}
|
||||
|
||||
us.CurrentVersion = Version
|
||||
us.UpdatedVersion = lrTime.Format(minioReleaseTagTimeLayout)
|
||||
return us, nil
|
||||
}
|
||||
|
||||
// ServerUpdateHandler - POST /minio/admin/v3/update?updateURL={updateURL}
|
||||
// ----------
|
||||
// updates all minio servers and restarts them gracefully.
|
||||
@@ -152,7 +142,16 @@ func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
for _, nerr := range globalNotificationSys.ServerUpdate(ctx, u, sha256Sum, lrTime, releaseInfo) {
|
||||
// Download Binary Once
|
||||
reader, err := downloadBinary(u, mode)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("server update failed with %w", err))
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Push binary to other servers
|
||||
for _, nerr := range globalNotificationSys.VerifyBinary(ctx, u, sha256Sum, releaseInfo, reader) {
|
||||
if nerr.Err != nil {
|
||||
err := AdminError{
|
||||
Code: AdminUpdateApplyFailure,
|
||||
@@ -166,13 +165,39 @@ func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus, err := updateServer(u, sha256Sum, lrTime, releaseInfo, mode)
|
||||
err = verifyBinary(u, sha256Sum, releaseInfo, mode, reader)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("server update failed with %w", err))
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
for _, nerr := range globalNotificationSys.CommitBinary(ctx) {
|
||||
if nerr.Err != nil {
|
||||
err := AdminError{
|
||||
Code: AdminUpdateApplyFailure,
|
||||
Message: nerr.Err.Error(),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, fmt.Errorf("server update failed with %w", err))
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = commitBinary()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("server update failed with %w", err))
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
updateStatus := madmin.ServerUpdateStatus{
|
||||
CurrentVersion: Version,
|
||||
UpdatedVersion: lrTime.Format(minioReleaseTagTimeLayout),
|
||||
}
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(updateStatus)
|
||||
if err != nil {
|
||||
@@ -1149,17 +1174,17 @@ func (a adminAPIHandlers) NetperfHandler(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
// SpeedtestHandler - Deprecated. See ObjectSpeedtestHandler
|
||||
func (a adminAPIHandlers) SpeedtestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
a.ObjectSpeedtestHandler(w, r)
|
||||
// SpeedtestHandler - Deprecated. See ObjectSpeedTestHandler
|
||||
func (a adminAPIHandlers) SpeedTestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
a.ObjectSpeedTestHandler(w, r)
|
||||
}
|
||||
|
||||
// ObjectSpeedtestHandler - reports maximum speed of a cluster by performing PUT and
|
||||
// ObjectSpeedTestHandler - reports maximum speed of a cluster by performing PUT and
|
||||
// GET operations on the server, supports auto tuning by default by automatically
|
||||
// increasing concurrency and stopping when we have reached the limits on the
|
||||
// system.
|
||||
func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ObjectSpeedtestHandler")
|
||||
func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ObjectSpeedTestHandler")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
@@ -1176,8 +1201,9 @@ func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.
|
||||
sizeStr := r.Form.Get(peerRESTSize)
|
||||
durationStr := r.Form.Get(peerRESTDuration)
|
||||
concurrentStr := r.Form.Get(peerRESTConcurrent)
|
||||
storageClass := strings.TrimSpace(r.Form.Get(peerRESTStorageClass))
|
||||
customBucket := strings.TrimSpace(r.Form.Get(peerRESTBucket))
|
||||
autotune := r.Form.Get("autotune") == "true"
|
||||
storageClass := r.Form.Get("storage-class")
|
||||
|
||||
size, err := strconv.Atoi(sizeStr)
|
||||
if err != nil {
|
||||
@@ -1189,16 +1215,14 @@ func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.
|
||||
concurrent = 32
|
||||
}
|
||||
|
||||
if runtime.GOMAXPROCS(0) < concurrent {
|
||||
concurrent = runtime.GOMAXPROCS(0)
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(durationStr)
|
||||
if err != nil {
|
||||
duration = time.Second * 10
|
||||
}
|
||||
|
||||
sufficientCapacity, canAutotune, capacityErrMsg := validateObjPerfOptions(ctx, objectAPI, concurrent, size, autotune)
|
||||
storageInfo, _ := objectAPI.StorageInfo(ctx)
|
||||
|
||||
sufficientCapacity, canAutotune, capacityErrMsg := validateObjPerfOptions(ctx, storageInfo, concurrent, size, autotune)
|
||||
if !sufficientCapacity {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, AdminError{
|
||||
Code: "XMinioSpeedtestInsufficientCapacity",
|
||||
@@ -1212,15 +1236,23 @@ func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.
|
||||
autotune = false
|
||||
}
|
||||
|
||||
bucketExists, err := makeObjectPerfBucket(ctx, objectAPI)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
if customBucket == "" {
|
||||
customBucket = globalObjectPerfBucket
|
||||
|
||||
bucketExists, err := makeObjectPerfBucket(ctx, objectAPI, customBucket)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if !bucketExists {
|
||||
defer deleteObjectPerfBucket(objectAPI)
|
||||
}
|
||||
}
|
||||
|
||||
if !bucketExists {
|
||||
defer deleteObjectPerfBucket(objectAPI)
|
||||
}
|
||||
defer objectAPI.DeleteObject(ctx, customBucket, speedTest+SlashSeparator, ObjectOptions{
|
||||
DeletePrefix: true,
|
||||
})
|
||||
|
||||
// Freeze all incoming S3 API calls before running speedtest.
|
||||
globalNotificationSys.ServiceFreeze(ctx, true)
|
||||
@@ -1232,7 +1264,14 @@ func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.
|
||||
defer keepAliveTicker.Stop()
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
ch := objectSpeedTest(ctx, speedTestOpts{size, concurrent, duration, autotune, storageClass})
|
||||
ch := objectSpeedTest(ctx, speedTestOpts{
|
||||
objectSize: size,
|
||||
concurrencyStart: concurrent,
|
||||
duration: duration,
|
||||
autotune: autotune,
|
||||
storageClass: storageClass,
|
||||
bucketName: customBucket,
|
||||
})
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -1255,9 +1294,8 @@ func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.
|
||||
}
|
||||
}
|
||||
|
||||
func makeObjectPerfBucket(ctx context.Context, objectAPI ObjectLayer) (bucketExists bool, err error) {
|
||||
err = objectAPI.MakeBucketWithLocation(ctx, globalObjectPerfBucket, BucketOptions{})
|
||||
if err != nil {
|
||||
func makeObjectPerfBucket(ctx context.Context, objectAPI ObjectLayer, bucketName string) (bucketExists bool, err error) {
|
||||
if err = objectAPI.MakeBucketWithLocation(ctx, bucketName, MakeBucketOptions{}); err != nil {
|
||||
if _, ok := err.(BucketExists); !ok {
|
||||
// Only BucketExists error can be ignored.
|
||||
return false, err
|
||||
@@ -1271,11 +1309,11 @@ func deleteObjectPerfBucket(objectAPI ObjectLayer) {
|
||||
objectAPI.DeleteBucket(context.Background(), globalObjectPerfBucket, DeleteBucketOptions{
|
||||
Force: true,
|
||||
NoRecreate: true,
|
||||
SRDeleteOp: getSRBucketDeleteOp(globalSiteReplicationSys.isEnabled()),
|
||||
})
|
||||
}
|
||||
|
||||
func validateObjPerfOptions(ctx context.Context, objectAPI ObjectLayer, concurrent int, size int, autotune bool) (sufficientCapacity bool, canAutotune bool, capacityErrMsg string) {
|
||||
storageInfo, _ := objectAPI.StorageInfo(ctx)
|
||||
func validateObjPerfOptions(ctx context.Context, storageInfo madmin.StorageInfo, concurrent int, size int, autotune bool) (bool, bool, string) {
|
||||
capacityNeeded := uint64(concurrent * size)
|
||||
capacity := GetTotalUsableCapacityFree(storageInfo.Disks, storageInfo)
|
||||
|
||||
@@ -1600,7 +1638,7 @@ func (a adminAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
if err := GlobalKMS.CreateKey(r.Form.Get("key-id")); err != nil {
|
||||
if err := GlobalKMS.CreateKey(ctx, r.Form.Get("key-id")); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1622,7 +1660,7 @@ func (a adminAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
stat, err := GlobalKMS.Stat()
|
||||
stat, err := GlobalKMS.Stat(ctx)
|
||||
if err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||
return
|
||||
@@ -1661,7 +1699,7 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
stat, err := GlobalKMS.Stat()
|
||||
stat, err := GlobalKMS.Stat(ctx)
|
||||
if err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||
return
|
||||
@@ -1677,7 +1715,7 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
|
||||
|
||||
kmsContext := kms.Context{"MinIO admin API": "KMSKeyStatusHandler"} // Context for a test key operation
|
||||
// 1. Generate a new key using the KMS.
|
||||
key, err := GlobalKMS.GenerateKey(keyID, kmsContext)
|
||||
key, err := GlobalKMS.GenerateKey(ctx, keyID, kmsContext)
|
||||
if err != nil {
|
||||
response.EncryptionErr = err.Error()
|
||||
resp, err := json.Marshal(response)
|
||||
@@ -2184,15 +2222,13 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
getAndWriteObjPerfInfo := func() {
|
||||
if query.Get(string(madmin.HealthDataTypePerfObj)) == "true" {
|
||||
concurrent := 32
|
||||
if runtime.GOMAXPROCS(0) < concurrent {
|
||||
concurrent = runtime.GOMAXPROCS(0)
|
||||
}
|
||||
|
||||
storageInfo, _ := objectAPI.StorageInfo(ctx)
|
||||
|
||||
size := 64 * humanize.MiByte
|
||||
autotune := true
|
||||
|
||||
sufficientCapacity, canAutotune, capacityErrMsg := validateObjPerfOptions(ctx, objectAPI, concurrent, size, autotune)
|
||||
|
||||
sufficientCapacity, canAutotune, capacityErrMsg := validateObjPerfOptions(ctx, storageInfo, concurrent, size, autotune)
|
||||
if !sufficientCapacity {
|
||||
healthInfo.Perf.Error = capacityErrMsg
|
||||
partialWrite(healthInfo)
|
||||
@@ -2203,9 +2239,9 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
autotune = false
|
||||
}
|
||||
|
||||
bucketExists, err := makeObjectPerfBucket(ctx, objectAPI)
|
||||
bucketExists, err := makeObjectPerfBucket(ctx, objectAPI, globalObjectPerfBucket)
|
||||
if err != nil {
|
||||
healthInfo.Perf.Error = "Could not make object perf bucket: " + err.Error()
|
||||
healthInfo.Perf.Error = "Unable to create bucket: " + err.Error()
|
||||
partialWrite(healthInfo)
|
||||
return
|
||||
}
|
||||
@@ -2215,7 +2251,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
opts := speedTestOpts{
|
||||
throughputSize: size,
|
||||
objectSize: size,
|
||||
concurrencyStart: concurrent,
|
||||
duration: 10 * time.Second,
|
||||
autotune: autotune,
|
||||
@@ -2527,7 +2563,7 @@ func fetchKMSStatus() madmin.KMS {
|
||||
return kmsStat
|
||||
}
|
||||
|
||||
stat, err := GlobalKMS.Stat()
|
||||
stat, err := GlobalKMS.Stat(context.Background())
|
||||
if err != nil {
|
||||
kmsStat.Status = string(madmin.ItemOffline)
|
||||
return kmsStat
|
||||
@@ -2540,7 +2576,7 @@ func fetchKMSStatus() madmin.KMS {
|
||||
|
||||
kmsContext := kms.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
|
||||
// 1. Generate a new key using the KMS.
|
||||
key, err := GlobalKMS.GenerateKey("", kmsContext)
|
||||
key, err := GlobalKMS.GenerateKey(context.Background(), "", kmsContext)
|
||||
if err != nil {
|
||||
kmsStat.Encrypt = fmt.Sprintf("Encryption failed: %v", err)
|
||||
} else {
|
||||
@@ -2621,6 +2657,100 @@ func checkConnection(endpointStr string, timeout time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type clusterInfo struct {
|
||||
DeploymentID string `json:"deployment_id"`
|
||||
ClusterName string `json:"cluster_name"`
|
||||
UsedCapacity uint64 `json:"used_capacity"`
|
||||
Info struct {
|
||||
MinioVersion string `json:"minio_version"`
|
||||
PoolsCount int `json:"pools_count"`
|
||||
ServersCount int `json:"servers_count"`
|
||||
DrivesCount int `json:"drives_count"`
|
||||
BucketsCount uint64 `json:"buckets_count"`
|
||||
ObjectsCount uint64 `json:"objects_count"`
|
||||
TotalDriveSpace uint64 `json:"total_drive_space"`
|
||||
UsedDriveSpace uint64 `json:"used_drive_space"`
|
||||
} `json:"info"`
|
||||
}
|
||||
|
||||
func embedFileInZip(zipWriter *zip.Writer, name string, data []byte) error {
|
||||
// Send profiling data to zip as file
|
||||
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
||||
name: name,
|
||||
size: int64(len(data)),
|
||||
mode: 0o600,
|
||||
modTime: UTCNow(),
|
||||
isDir: false,
|
||||
sys: nil,
|
||||
})
|
||||
if zerr != nil {
|
||||
return zerr
|
||||
}
|
||||
header.Method = zip.Deflate
|
||||
zwriter, zerr := zipWriter.CreateHeader(header)
|
||||
if zerr != nil {
|
||||
return zerr
|
||||
}
|
||||
_, err := io.Copy(zwriter, bytes.NewReader(data))
|
||||
return err
|
||||
}
|
||||
|
||||
// appendClusterMetaInfoToZip gets information of the current cluster and embedded
|
||||
// it in the passed zipwriter, This is not a critical function and it is allowed
|
||||
// to fail with a ten seconds timeout.
|
||||
func appendClusterMetaInfoToZip(ctx context.Context, zipWriter *zip.Writer) {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add a ten seconds timeout because getting profiling data
|
||||
// is critical for debugging, in contrary to getting cluster info
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resultCh := make(chan clusterInfo)
|
||||
go func() {
|
||||
ci := clusterInfo{}
|
||||
ci.Info.PoolsCount = len(globalEndpoints)
|
||||
ci.Info.ServersCount = globalEndpoints.NEndpoints()
|
||||
ci.Info.MinioVersion = Version
|
||||
|
||||
si, _ := objectAPI.StorageInfo(ctx)
|
||||
|
||||
ci.Info.DrivesCount = len(si.Disks)
|
||||
for _, disk := range si.Disks {
|
||||
ci.Info.TotalDriveSpace += disk.TotalSpace
|
||||
ci.Info.UsedDriveSpace += disk.UsedSpace
|
||||
}
|
||||
|
||||
dataUsageInfo, _ := loadDataUsageFromBackend(ctx, objectAPI)
|
||||
|
||||
ci.UsedCapacity = dataUsageInfo.ObjectsTotalSize
|
||||
ci.Info.BucketsCount = dataUsageInfo.BucketsCount
|
||||
ci.Info.ObjectsCount = dataUsageInfo.ObjectsTotalCount
|
||||
|
||||
ci.DeploymentID = globalDeploymentID
|
||||
ci.ClusterName = fmt.Sprintf("%d-servers-%d-disks-%s", ci.Info.ServersCount, ci.Info.DrivesCount, ci.Info.MinioVersion)
|
||||
resultCh <- ci
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case ci := <-resultCh:
|
||||
out, err := json.MarshalIndent(ci, "", " ")
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
err = embedFileInZip(zipWriter, "cluster.info", out)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getRawDataer provides an interface for getting raw FS files.
|
||||
type getRawDataer interface {
|
||||
GetRawData(ctx context.Context, volume, file string, fn func(r io.Reader, host string, disk string, filename string, info StatInfo) error) error
|
||||
@@ -2640,7 +2770,8 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
o, ok := newObjectLayerFn().(getRawDataer)
|
||||
objLayer := newObjectLayerFn()
|
||||
o, ok := objLayer.(getRawDataer)
|
||||
if !ok {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
@@ -2763,6 +2894,8 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
|
||||
}); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
|
||||
appendClusterMetaInfoToZip(ctx, zipWriter)
|
||||
}
|
||||
|
||||
func createHostAnonymizerForFSMode() map[string]string {
|
||||
|
||||
@@ -822,7 +822,7 @@ func (h *healSequence) healBuckets(objAPI ObjectLayer, bucketsOnly bool) error {
|
||||
return h.healBucket(objAPI, h.bucket, bucketsOnly)
|
||||
}
|
||||
|
||||
buckets, err := objAPI.ListBuckets(h.ctx)
|
||||
buckets, err := objAPI.ListBuckets(h.ctx, BucketOptions{})
|
||||
if err != nil {
|
||||
return errFnHealFromAPIErr(h.ctx, err)
|
||||
}
|
||||
|
||||
+5
-2
@@ -202,6 +202,9 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
// RemoveRemoteTargetHandler
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.RemoveRemoteTargetHandler))).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
|
||||
// ReplicationDiff - MinIO extension API
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/replication/diff").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.ReplicationDiffHandler))).Queries("bucket", "{bucket:.*}")
|
||||
|
||||
// Bucket migration operations
|
||||
// ExportBucketMetaHandler
|
||||
@@ -244,8 +247,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
Queries("paths", "{paths:.*}").HandlerFunc(gz(httpTraceHdrs(adminAPI.ForceUnlockHandler)))
|
||||
}
|
||||
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest").HandlerFunc(httpTraceHdrs(adminAPI.SpeedtestHandler))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/object").HandlerFunc(httpTraceHdrs(adminAPI.ObjectSpeedtestHandler))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest").HandlerFunc(httpTraceHdrs(adminAPI.SpeedTestHandler))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/object").HandlerFunc(httpTraceHdrs(adminAPI.ObjectSpeedTestHandler))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/drive").HandlerFunc(httpTraceHdrs(adminAPI.DriveSpeedtestHandler))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/net").HandlerFunc(httpTraceHdrs(adminAPI.NetperfHandler))
|
||||
|
||||
|
||||
@@ -199,5 +199,12 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
||||
lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts())
|
||||
}
|
||||
|
||||
if v, ok := objInfo.UserDefined[ReservedMetadataPrefix+"compression"]; ok {
|
||||
if i := strings.LastIndexByte(v, '/'); i >= 0 {
|
||||
v = v[i+1:]
|
||||
}
|
||||
w.Header()[xhttp.MinIOCompressed] = []string{v}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func loadHealingTracker(ctx context.Context, disk StorageAPI) (*healingTracker,
|
||||
return nil, err
|
||||
}
|
||||
b, err := disk.ReadAll(ctx, minioMetaBucket,
|
||||
pathJoin(bucketMetaPrefix, slashSeparator, healingTrackerFilename))
|
||||
pathJoin(bucketMetaPrefix, healingTrackerFilename))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -155,15 +155,19 @@ func (h *healingTracker) save(ctx context.Context) error {
|
||||
}
|
||||
globalBackgroundHealState.updateHealStatus(h)
|
||||
return h.disk.WriteAll(ctx, minioMetaBucket,
|
||||
pathJoin(bucketMetaPrefix, slashSeparator, healingTrackerFilename),
|
||||
pathJoin(bucketMetaPrefix, healingTrackerFilename),
|
||||
htrackerBytes)
|
||||
}
|
||||
|
||||
// delete the tracker on disk.
|
||||
func (h *healingTracker) delete(ctx context.Context) error {
|
||||
return h.disk.Delete(ctx, minioMetaBucket,
|
||||
pathJoin(bucketMetaPrefix, slashSeparator, healingTrackerFilename),
|
||||
false)
|
||||
pathJoin(bucketMetaPrefix, healingTrackerFilename),
|
||||
DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (h *healingTracker) isHealed(bucket string) bool {
|
||||
@@ -314,8 +318,7 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
||||
ctx = lkctx.Context()
|
||||
defer locker.Unlock(lkctx.Cancel)
|
||||
|
||||
buckets, _ := z.ListBuckets(ctx)
|
||||
|
||||
buckets, _ := z.ListBuckets(ctx, BucketOptions{})
|
||||
// Buckets data are dispersed in multiple zones/sets, make
|
||||
// sure to heal all bucket metadata configuration.
|
||||
buckets = append(buckets, BucketInfo{
|
||||
@@ -361,12 +364,15 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
||||
}
|
||||
|
||||
// Start or resume healing of this erasure set
|
||||
err = z.serverPools[poolIdx].sets[setIdx].healErasureSet(ctx, tracker.QueuedBuckets, tracker)
|
||||
if err != nil {
|
||||
if err = z.serverPools[poolIdx].sets[setIdx].healErasureSet(ctx, tracker.QueuedBuckets, tracker); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Healing disk '%s' is complete (healed: %d, failed: %d).", disk, tracker.ItemsHealed, tracker.ItemsFailed)
|
||||
if tracker.ItemsFailed > 0 {
|
||||
logger.Info("Healing disk '%s' failed (healed: %d, failed: %d).", disk, tracker.ItemsHealed, tracker.ItemsFailed)
|
||||
} else {
|
||||
logger.Info("Healing disk '%s' complete (healed: %d, failed: %d).", disk, tracker.ItemsHealed, tracker.ItemsFailed)
|
||||
}
|
||||
|
||||
if serverDebugLog {
|
||||
tracker.printTo(os.Stdout)
|
||||
@@ -374,6 +380,7 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
||||
}
|
||||
|
||||
logger.LogIf(ctx, tracker.delete(ctx))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -390,12 +397,11 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-diskCheckTimer.C:
|
||||
|
||||
healDisks := globalBackgroundHealState.getHealLocalDiskEndpoints()
|
||||
if len(healDisks) == 0 {
|
||||
// Reset for next interval.
|
||||
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
||||
break
|
||||
continue
|
||||
}
|
||||
|
||||
// Reformat disks immediately
|
||||
@@ -404,7 +410,7 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools) {
|
||||
logger.LogIf(ctx, err)
|
||||
// Reset for next interval.
|
||||
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
||||
break
|
||||
continue
|
||||
}
|
||||
|
||||
for _, disk := range healDisks {
|
||||
|
||||
@@ -35,7 +35,7 @@ func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
||||
object := getRandomObjectName()
|
||||
|
||||
// create bucket.
|
||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -196,7 +196,7 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err := obj.MakeBucketWithLocation(context.Background(), bucket, BucketOptions{})
|
||||
err := obj.MakeBucketWithLocation(context.Background(), bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -152,10 +152,10 @@ func (b *streamingBitrotReader) ReadAt(buf []byte, offset int64) (int, error) {
|
||||
streamOffset := (offset/b.shardSize)*int64(b.h.Size()) + offset
|
||||
if len(b.data) == 0 && b.tillOffset != streamOffset {
|
||||
b.rc, err = b.disk.ReadFileStream(context.TODO(), b.volume, b.filePath, streamOffset, b.tillOffset-streamOffset)
|
||||
if err != nil {
|
||||
if err != nil && err != errDiskNotFound {
|
||||
logger.LogIf(GlobalContext,
|
||||
fmt.Errorf("Error(%w) reading erasure shards at (%s: %s/%s), will attempt to reconstruct if we have quorum",
|
||||
err, b.disk, b.volume, b.filePath))
|
||||
fmt.Errorf("Reading erasure shards at (%s: %s/%s) returned '%w', will attempt to reconstruct if we have quorum",
|
||||
b.disk, b.volume, b.filePath, err))
|
||||
}
|
||||
} else {
|
||||
b.rc = io.NewSectionReader(bytes.NewReader(b.data), streamOffset, b.tillOffset-streamOffset)
|
||||
|
||||
+7
-8
@@ -20,17 +20,11 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testBitrotReaderWriterAlgo(t *testing.T, bitrotAlgo BitrotAlgorithm) {
|
||||
tmpDir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
volume := "testvol"
|
||||
filePath := "testfile"
|
||||
@@ -60,7 +54,9 @@ func testBitrotReaderWriterAlgo(t *testing.T, bitrotAlgo BitrotAlgorithm) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writer.(io.Closer).Close()
|
||||
if bw, ok := writer.(io.Closer); ok {
|
||||
bw.Close()
|
||||
}
|
||||
|
||||
reader := newBitrotReader(disk, nil, volume, filePath, 35, bitrotAlgo, bitrotWriterSum(writer), 10)
|
||||
b := make([]byte, 10)
|
||||
@@ -76,6 +72,9 @@ func testBitrotReaderWriterAlgo(t *testing.T, bitrotAlgo BitrotAlgorithm) {
|
||||
if _, err = reader.ReadAt(b[:5], 30); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if br, ok := reader.(io.Closer); ok {
|
||||
br.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllBitrotAlgorithms(t *testing.T) {
|
||||
|
||||
@@ -65,7 +65,7 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
||||
kmsKey := encConfig.KeyID()
|
||||
if kmsKey != "" {
|
||||
kmsContext := kms.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
|
||||
_, err := GlobalKMS.GenerateKey(kmsKey, kmsContext)
|
||||
_, err := GlobalKMS.GenerateKey(ctx, kmsKey, kmsContext)
|
||||
if err != nil {
|
||||
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errKMSKeyNotFound), r.URL)
|
||||
@@ -155,7 +155,7 @@ func (api objectAPIHandlers) GetBucketEncryptionHandler(w http.ResponseWriter, r
|
||||
|
||||
// Check if bucket exists
|
||||
var err error
|
||||
if _, err = objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err = objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -198,7 +198,7 @@ func (api objectAPIHandlers) DeleteBucketEncryptionHandler(w http.ResponseWriter
|
||||
|
||||
// Check if bucket exists
|
||||
var err error
|
||||
if _, err = objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err = objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
+26
-15
@@ -201,7 +201,7 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
|
||||
|
||||
getBucketInfo := objectAPI.GetBucketInfo
|
||||
|
||||
if _, err := getBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := getBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -332,7 +332,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
} else {
|
||||
// Invoke the list buckets.
|
||||
var err error
|
||||
bucketsInfo, err = listBuckets(ctx)
|
||||
bucketsInfo, err = listBuckets(ctx, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -447,7 +447,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, "")
|
||||
|
||||
// Before proceeding validate if bucket exists.
|
||||
_, err := objectAPI.GetBucketInfo(ctx, bucket)
|
||||
_, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -649,6 +649,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
dv := DeletedObjectReplicationInfo{
|
||||
DeletedObject: dobj,
|
||||
Bucket: bucket,
|
||||
EventType: ReplicateIncomingDelete,
|
||||
}
|
||||
scheduleReplicationDelete(ctx, dv, objectAPI)
|
||||
}
|
||||
@@ -745,7 +746,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
opts := BucketOptions{
|
||||
opts := MakeBucketOptions{
|
||||
Location: location,
|
||||
LockEnabled: objectLockEnabled,
|
||||
ForceCreate: forceCreate,
|
||||
@@ -764,7 +765,11 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if err = globalDNSConfig.Put(bucket); err != nil {
|
||||
objectAPI.DeleteBucket(context.Background(), bucket, DeleteBucketOptions{Force: false, NoRecreate: true})
|
||||
objectAPI.DeleteBucket(context.Background(), bucket, DeleteBucketOptions{
|
||||
Force: false,
|
||||
NoRecreate: true,
|
||||
SRDeleteOp: getSRBucketDeleteOp(globalSiteReplicationSys.isEnabled()),
|
||||
})
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -864,7 +869,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := crypto.IsRequested(r.Header); !objectAPI.IsEncryptionSupported() && ok {
|
||||
if crypto.Requested(r.Header) && !objectAPI.IsEncryptionSupported() {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1040,7 +1045,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if _, ok := crypto.IsRequested(formValues); ok && !HasSuffix(object, SlashSeparator) { // handle SSE requests
|
||||
if crypto.Requested(formValues) && !HasSuffix(object, SlashSeparator) { // handle SSE requests
|
||||
if crypto.SSECopy.IsRequested(r.Header) {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParameters), r.URL)
|
||||
return
|
||||
@@ -1066,7 +1071,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
}
|
||||
reader, objectEncryptionKey, err = newEncryptReader(hashReader, kind, keyID, key, bucket, object, metadata, kmsCtx)
|
||||
reader, objectEncryptionKey, err = newEncryptReader(ctx, hashReader, kind, keyID, key, bucket, object, metadata, kmsCtx)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -1115,9 +1120,12 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
Host: handlers.GetSourceIP(r),
|
||||
})
|
||||
|
||||
if successRedirect != "" {
|
||||
// Replace raw query params..
|
||||
redirectURL.RawQuery = getRedirectPostRawQuery(objInfo)
|
||||
if redirectURL != nil { // success_action_redirect is valid and set.
|
||||
v := redirectURL.Query()
|
||||
v.Add("bucket", objInfo.Bucket)
|
||||
v.Add("key", objInfo.Name)
|
||||
v.Add("etag", "\""+objInfo.ETag+"\"")
|
||||
redirectURL.RawQuery = v.Encode()
|
||||
writeRedirectSeeOther(w, redirectURL.String())
|
||||
return
|
||||
}
|
||||
@@ -1161,7 +1169,7 @@ func (api objectAPIHandlers) GetBucketPolicyStatusHandler(w http.ResponseWriter,
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1224,7 +1232,7 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
|
||||
|
||||
getBucketInfo := objectAPI.GetBucketInfo
|
||||
|
||||
if _, err := getBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := getBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
|
||||
return
|
||||
}
|
||||
@@ -1301,7 +1309,10 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
deleteBucket := objectAPI.DeleteBucket
|
||||
|
||||
// Attempt to delete bucket.
|
||||
if err := deleteBucket(ctx, bucket, DeleteBucketOptions{Force: forceDelete}); err != nil {
|
||||
if err := deleteBucket(ctx, bucket, DeleteBucketOptions{
|
||||
Force: forceDelete,
|
||||
SRDeleteOp: getSRBucketDeleteOp(globalSiteReplicationSys.isEnabled()),
|
||||
}); err != nil {
|
||||
apiErr := toAPIError(ctx, err)
|
||||
if _, ok := err.(BucketNotEmpty); ok {
|
||||
if globalBucketVersioningSys.Enabled(bucket) || globalBucketVersioningSys.Suspended(bucket) {
|
||||
@@ -1468,7 +1479,7 @@ func (api objectAPIHandlers) PutBucketTaggingHandler(w http.ResponseWriter, r *h
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func (api objectAPIHandlers) GetBucketLifecycleHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func (api objectAPIHandlers) DeleteBucketLifecycleHandler(w http.ResponseWriter,
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -560,7 +560,7 @@ func (r *RestoreObjectRequest) validate(ctx context.Context, objAPI ObjectLayer)
|
||||
}
|
||||
// Check if bucket exists.
|
||||
if !r.OutputLocation.IsEmpty() {
|
||||
if _, err := objAPI.GetBucketInfo(ctx, r.OutputLocation.S3.BucketName); err != nil {
|
||||
if _, err := objAPI.GetBucketInfo(ctx, r.OutputLocation.S3.BucketName, BucketOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.OutputLocation.S3.Prefix == "" {
|
||||
|
||||
@@ -136,7 +136,7 @@ func (sys *BucketMetadataSys) Update(ctx context.Context, bucket string, configF
|
||||
meta.ReplicationConfigXML = configData
|
||||
meta.ReplicationConfigUpdatedAt = updatedAt
|
||||
case bucketTargetsFile:
|
||||
meta.BucketTargetsConfigJSON, meta.BucketTargetsConfigMetaJSON, err = encryptBucketMetadata(meta.Name, configData, kms.Context{
|
||||
meta.BucketTargetsConfigJSON, meta.BucketTargetsConfigMetaJSON, err = encryptBucketMetadata(ctx, meta.Name, configData, kms.Context{
|
||||
bucket: meta.Name,
|
||||
bucketTargetsFile: bucketTargetsFile,
|
||||
})
|
||||
|
||||
+21
-9
@@ -105,10 +105,8 @@ type BucketMetadata struct {
|
||||
|
||||
// newBucketMetadata creates BucketMetadata with the supplied name and Created to Now.
|
||||
func newBucketMetadata(name string) BucketMetadata {
|
||||
now := UTCNow()
|
||||
return BucketMetadata{
|
||||
Name: name,
|
||||
Created: now,
|
||||
Name: name,
|
||||
notificationConfig: &event.Config{
|
||||
XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/",
|
||||
},
|
||||
@@ -121,6 +119,17 @@ func newBucketMetadata(name string) BucketMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
// SetCreatedAt preserves the CreatedAt time for bucket across sites in site replication. It defaults to
|
||||
// creation time of bucket on this cluster in all other cases.
|
||||
func (b *BucketMetadata) SetCreatedAt(createdAt time.Time) {
|
||||
if b.Created.IsZero() {
|
||||
b.Created = UTCNow()
|
||||
}
|
||||
if !createdAt.IsZero() {
|
||||
b.Created = createdAt.UTC()
|
||||
}
|
||||
}
|
||||
|
||||
// Load - loads the metadata of bucket by name from ObjectLayer api.
|
||||
// If an error is returned the returned metadata will be default initialized.
|
||||
func (b *BucketMetadata) Load(ctx context.Context, api ObjectLayer, name string) error {
|
||||
@@ -160,17 +169,19 @@ func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket strin
|
||||
if err != nil && !errors.Is(err, errConfigNotFound) {
|
||||
return b, err
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
b.defaultTimestamps()
|
||||
}
|
||||
// Old bucket without bucket metadata. Hence we migrate existing settings.
|
||||
if err := b.convertLegacyConfigs(ctx, objectAPI); err != nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
// migrate unencrypted remote targets
|
||||
if err = b.migrateTargetConfig(ctx, objectAPI); err != nil {
|
||||
if err := b.migrateTargetConfig(ctx, objectAPI); err != nil {
|
||||
return b, err
|
||||
}
|
||||
b.defaultTimestamps()
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
@@ -345,6 +356,7 @@ func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI Obj
|
||||
b.BucketTargetsConfigJSON = configData
|
||||
}
|
||||
}
|
||||
b.defaultTimestamps()
|
||||
|
||||
if err := b.Save(ctx, objectAPI); err != nil {
|
||||
return err
|
||||
@@ -438,7 +450,7 @@ func (b *BucketMetadata) migrateTargetConfig(ctx context.Context, objectAPI Obje
|
||||
return nil
|
||||
}
|
||||
|
||||
encBytes, metaBytes, err := encryptBucketMetadata(b.Name, b.BucketTargetsConfigJSON, kms.Context{b.Name: b.Name, bucketTargetsFile: bucketTargetsFile})
|
||||
encBytes, metaBytes, err := encryptBucketMetadata(ctx, b.Name, b.BucketTargetsConfigJSON, kms.Context{b.Name: b.Name, bucketTargetsFile: bucketTargetsFile})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -449,14 +461,14 @@ func (b *BucketMetadata) migrateTargetConfig(ctx context.Context, objectAPI Obje
|
||||
}
|
||||
|
||||
// encrypt bucket metadata if kms is configured.
|
||||
func encryptBucketMetadata(bucket string, input []byte, kmsContext kms.Context) (output, metabytes []byte, err error) {
|
||||
func encryptBucketMetadata(ctx context.Context, bucket string, input []byte, kmsContext kms.Context) (output, metabytes []byte, err error) {
|
||||
if GlobalKMS == nil {
|
||||
output = input
|
||||
return
|
||||
}
|
||||
|
||||
metadata := make(map[string]string)
|
||||
key, err := GlobalKMS.GenerateKey("", kmsContext)
|
||||
key, err := GlobalKMS.GenerateKey(ctx, "", kmsContext)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucketName)
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucketName, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -132,7 +132,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
_, err := objectAPI.GetBucketInfo(ctx, bucketName)
|
||||
_, err := objectAPI.GetBucketInfo(ctx, bucketName, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
|
||||
@@ -61,7 +61,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -145,7 +145,7 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -191,7 +191,7 @@ func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ func testCreateBucket(obj ObjectLayer, instanceType, bucketName string, apiRoute
|
||||
defer wg.Done()
|
||||
// Sync start.
|
||||
<-start
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName1, BucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName1, MakeBucketOptions{}); err != nil {
|
||||
if _, ok := err.(BucketExists); !ok {
|
||||
t.Logf("unexpected error: %T: %v", err, err)
|
||||
return
|
||||
@@ -163,7 +163,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
||||
credentials auth.Credentials, t *testing.T,
|
||||
) {
|
||||
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName1, BucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName1, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWr
|
||||
return
|
||||
}
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func (api objectAPIHandlers) GetBucketReplicationConfigHandler(w http.ResponseWr
|
||||
return
|
||||
}
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func (api objectAPIHandlers) DeleteBucketReplicationConfigHandler(w http.Respons
|
||||
return
|
||||
}
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -198,7 +198,7 @@ func (api objectAPIHandlers) GetBucketReplicationMetricsHandler(w http.ResponseW
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -267,7 +267,7 @@ func (api objectAPIHandlers) ResetBucketReplicationStartHandler(w http.ResponseW
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -356,7 +356,7 @@ func (api objectAPIHandlers) ResetBucketReplicationStatusHandler(w http.Response
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,12 +21,14 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
)
|
||||
@@ -503,6 +505,8 @@ func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) Repl
|
||||
}
|
||||
var dsc ReplicateDecision
|
||||
var tgtStatuses map[string]replication.StatusType
|
||||
var purgeStatuses map[string]VersionPurgeStatusType
|
||||
|
||||
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
|
||||
dsc = checkReplicateDelete(GlobalContext, oi.Bucket, ObjectToDelete{
|
||||
ObjectV: ObjectV{
|
||||
@@ -516,15 +520,17 @@ func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) Repl
|
||||
}, replication.HealReplicationType, ObjectOptions{}))
|
||||
}
|
||||
tgtStatuses = replicationStatusesMap(oi.ReplicationStatusInternal)
|
||||
|
||||
purgeStatuses = versionPurgeStatusesMap(oi.VersionPurgeStatusInternal)
|
||||
existingObjResync := rcfg.Resync(GlobalContext, oi, &dsc, tgtStatuses)
|
||||
|
||||
tm, _ := time.Parse(time.RFC3339Nano, oi.UserDefined[ReservedMetadataPrefixLower+ReplicationTimestamp])
|
||||
return ReplicateObjectInfo{
|
||||
ObjectInfo: oi,
|
||||
OpType: replication.HealReplicationType,
|
||||
Dsc: dsc,
|
||||
ExistingObjResync: existingObjResync,
|
||||
TargetStatuses: tgtStatuses,
|
||||
ObjectInfo: oi,
|
||||
OpType: replication.HealReplicationType,
|
||||
Dsc: dsc,
|
||||
ExistingObjResync: existingObjResync,
|
||||
TargetStatuses: tgtStatuses,
|
||||
TargetPurgeStatuses: purgeStatuses,
|
||||
ReplicationTimestamp: tm,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,3 +730,10 @@ func parseSizeFromContentRange(h http.Header) (sz int64, err error) {
|
||||
}
|
||||
return int64(usz), nil
|
||||
}
|
||||
|
||||
func extractReplicateDiffOpts(q url.Values) (opts madmin.ReplDiffOpts) {
|
||||
opts.Verbose = q.Get("verbose") == "true"
|
||||
opts.ARN = q.Get("arn")
|
||||
opts.Prefix = q.Get("prefix")
|
||||
return
|
||||
}
|
||||
|
||||
+132
-29
@@ -347,7 +347,7 @@ func checkReplicateDelete(ctx context.Context, bucket string, dobj ObjectToDelet
|
||||
// target cluster, the object version is marked deleted on the source and hidden from listing. It is permanently
|
||||
// deleted from the source when the VersionPurgeStatus changes to "Complete", i.e after replication succeeds
|
||||
// on target.
|
||||
func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, objectAPI ObjectLayer, trigger string) {
|
||||
func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, objectAPI ObjectLayer) {
|
||||
var replicationStatus replication.StatusType
|
||||
bucket := dobj.Bucket
|
||||
versionID := dobj.DeleteMarkerVersionID
|
||||
@@ -358,7 +358,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
||||
defer func() {
|
||||
replStatus := string(replicationStatus)
|
||||
auditLogInternal(context.Background(), bucket, dobj.ObjectName, AuditLogOptions{
|
||||
Trigger: trigger,
|
||||
Event: dobj.EventType,
|
||||
APIName: ReplicateDeleteAPI,
|
||||
VersionID: versionID,
|
||||
Status: replStatus,
|
||||
@@ -479,6 +479,10 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
||||
eventName = event.ObjectReplicationFailed
|
||||
}
|
||||
drs := getReplicationState(rinfos, dobj.ReplicationState, dobj.VersionID)
|
||||
if replicationStatus != prevStatus {
|
||||
drs.ReplicationTimeStamp = UTCNow()
|
||||
}
|
||||
|
||||
dobjInfo, err := objectAPI.DeleteObject(ctx, bucket, dobj.ObjectName, ObjectOptions{
|
||||
VersionID: versionID,
|
||||
MTime: dobj.DeleteMarkerMTime.Time,
|
||||
@@ -857,7 +861,7 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati
|
||||
|
||||
// replicateObject replicates the specified version of the object to destination bucket
|
||||
// The source object is then updated to reflect the replication status.
|
||||
func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI ObjectLayer, trigger string) {
|
||||
func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI ObjectLayer) {
|
||||
var replicationStatus replication.StatusType
|
||||
defer func() {
|
||||
if replicationStatus.Empty() {
|
||||
@@ -868,7 +872,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
|
||||
replicationStatus = ri.ReplicationStatus
|
||||
}
|
||||
auditLogInternal(ctx, ri.Bucket, ri.Name, AuditLogOptions{
|
||||
Trigger: trigger,
|
||||
Event: ri.EventType,
|
||||
APIName: ReplicateObjectAPI,
|
||||
VersionID: ri.VersionID,
|
||||
Status: replicationStatus.String(),
|
||||
@@ -990,6 +994,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
|
||||
// the target site is down. Leave it to scanner to catch up instead.
|
||||
if rinfos.ReplicationStatus() != replication.Completed && ri.RetryCount < 1 {
|
||||
ri.OpType = replication.HealReplicationType
|
||||
ri.EventType = ReplicateMRF
|
||||
ri.ReplicationStatusInternal = rinfos.ReplicationStatusInternal()
|
||||
ri.RetryCount++
|
||||
globalReplicationPool.queueReplicaFailedTask(ri)
|
||||
@@ -1291,6 +1296,7 @@ func filterReplicationStatusMetadata(metadata map[string]string) map[string]stri
|
||||
type DeletedObjectReplicationInfo struct {
|
||||
DeletedObject
|
||||
Bucket string
|
||||
EventType string
|
||||
OpType replication.Type
|
||||
ResetID string
|
||||
TargetArn string
|
||||
@@ -1313,12 +1319,15 @@ const (
|
||||
|
||||
// ReplicateMRF - audit trail for replication from Most Recent Failures (MRF) queue
|
||||
ReplicateMRF = "replicate:mrf"
|
||||
// ReplicateIncoming - audit trail indicating replication started [could be from incoming/existing/heal activity]
|
||||
// ReplicateIncoming - audit trail of inline replication
|
||||
ReplicateIncoming = "replicate:incoming"
|
||||
// ReplicateIncomingDelete - audit trail of inline replication of deletes.
|
||||
ReplicateIncomingDelete = "replicate:incoming:delete"
|
||||
|
||||
// ReplicateHeal - audit trail for healing of failed/pending replications
|
||||
ReplicateHeal = "replicate:heal"
|
||||
// ReplicateDelete - audit trail for delete replication
|
||||
ReplicateDelete = "replicate:delete"
|
||||
// ReplicateHealDelete - audit trail of healing of failed/pending delete replications.
|
||||
ReplicateHealDelete = "replicate:heal:delete"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -1364,13 +1373,14 @@ func NewReplicationPool(ctx context.Context, o ObjectLayer, opts replicationPool
|
||||
pool.ResizeWorkers(opts.Workers)
|
||||
pool.ResizeFailedWorkers(opts.FailedWorkers)
|
||||
go pool.AddExistingObjectReplicateWorker()
|
||||
go pool.periodicResyncMetaSave(ctx, o)
|
||||
go pool.updateResyncStatus(ctx, o)
|
||||
return pool
|
||||
}
|
||||
|
||||
// AddMRFWorker adds a pending/failed replication worker to handle requests that could not be queued
|
||||
// to the other workers
|
||||
func (p *ReplicationPool) AddMRFWorker() {
|
||||
defer p.mrfWorkerWg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
@@ -1379,7 +1389,7 @@ func (p *ReplicationPool) AddMRFWorker() {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
replicateObject(p.ctx, oi, p.objLayer, ReplicateMRF)
|
||||
replicateObject(p.ctx, oi, p.objLayer)
|
||||
case <-p.mrfWorkerKillCh:
|
||||
return
|
||||
}
|
||||
@@ -1397,12 +1407,12 @@ func (p *ReplicationPool) AddWorker() {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
replicateObject(p.ctx, oi, p.objLayer, ReplicateIncoming)
|
||||
replicateObject(p.ctx, oi, p.objLayer)
|
||||
case doi, ok := <-p.replicaDeleteCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
replicateDelete(p.ctx, doi, p.objLayer, ReplicateDelete)
|
||||
replicateDelete(p.ctx, doi, p.objLayer)
|
||||
case <-p.workerKillCh:
|
||||
return
|
||||
}
|
||||
@@ -1419,12 +1429,12 @@ func (p *ReplicationPool) AddExistingObjectReplicateWorker() {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
replicateObject(p.ctx, oi, p.objLayer, ReplicateExisting)
|
||||
replicateObject(p.ctx, oi, p.objLayer)
|
||||
case doi, ok := <-p.existingReplicaDeleteCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
replicateDelete(p.ctx, doi, p.objLayer, ReplicateExistingDelete)
|
||||
replicateDelete(p.ctx, doi, p.objLayer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1482,7 +1492,7 @@ func (p *ReplicationPool) queueReplicaFailedTask(ri ReplicateObjectInfo) {
|
||||
})
|
||||
case p.mrfReplicaCh <- ri:
|
||||
default:
|
||||
logger.LogOnceIf(GlobalContext, fmt.Errorf("WARNING: Replication failed workers could not keep up with healing failures - consider increasing number of replication failed workers with `mc admin config set api replication_failed_workers=%d`", p.suggestedWorkers(true)), replicationSubsystem)
|
||||
logger.LogOnceIf(GlobalContext, fmt.Errorf("WARNING: Unable to keep up retrying failed replication - we recommend increasing number of replication failed workers with `mc admin config set api replication_failed_workers=%d`", p.suggestedWorkers(true)), string(replicationSubsystem))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1508,7 +1518,7 @@ func (p *ReplicationPool) queueReplicaTask(ri ReplicateObjectInfo) {
|
||||
})
|
||||
case ch <- ri:
|
||||
default:
|
||||
logger.LogOnceIf(GlobalContext, fmt.Errorf("WARNING: Replication workers could not keep up with incoming traffic - consider increasing number of replication workers with `mc admin config set api replication_workers=%d`", p.suggestedWorkers(false)), replicationSubsystem)
|
||||
logger.LogOnceIf(GlobalContext, fmt.Errorf("WARNING: Unable to keep up with incoming traffic - we recommend increasing number of replicate object workers with `mc admin config set api replication_workers=%d`", p.suggestedWorkers(false)), string(replicationSubsystem))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1545,7 +1555,7 @@ func (p *ReplicationPool) queueReplicaDeleteTask(doi DeletedObjectReplicationInf
|
||||
})
|
||||
case ch <- doi:
|
||||
default:
|
||||
logger.LogOnceIf(GlobalContext, fmt.Errorf("WARNING: Replication workers could not keep up with incoming traffic - consider increasing number of replication workers with `mc admin config set api replication_workers=%d`", p.suggestedWorkers(false)), replicationSubsystem)
|
||||
logger.LogOnceIf(GlobalContext, fmt.Errorf("WARNING: Unable to keep up with incoming deletes - we recommend increasing number of replicate workers with `mc admin config set api replication_workers=%d`", p.suggestedWorkers(false)), string(replicationSubsystem))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1722,10 +1732,11 @@ func proxyHeadToReplicationTarget(ctx context.Context, bucket, object string, rs
|
||||
}
|
||||
|
||||
func scheduleReplication(ctx context.Context, objInfo ObjectInfo, o ObjectLayer, dsc ReplicateDecision, opType replication.Type) {
|
||||
ri := ReplicateObjectInfo{ObjectInfo: objInfo, OpType: opType, Dsc: dsc, EventType: ReplicateIncoming}
|
||||
if dsc.Synchronous() {
|
||||
replicateObject(ctx, ReplicateObjectInfo{ObjectInfo: objInfo, OpType: opType, Dsc: dsc}, o, ReplicateIncoming)
|
||||
replicateObject(ctx, ri, o)
|
||||
} else {
|
||||
globalReplicationPool.queueReplicaTask(ReplicateObjectInfo{ObjectInfo: objInfo, OpType: opType, Dsc: dsc})
|
||||
globalReplicationPool.queueReplicaTask(ri)
|
||||
}
|
||||
if sz, err := objInfo.GetActualSize(); err == nil {
|
||||
for arn := range dsc.targetsMap {
|
||||
@@ -1794,6 +1805,7 @@ func (c replicationConfig) Resync(ctx context.Context, oi ObjectInfo, dsc *Repli
|
||||
objInfo.VersionPurgeStatusInternal = ""
|
||||
objInfo.ReplicationStatus = ""
|
||||
objInfo.VersionPurgeStatus = ""
|
||||
delete(objInfo.UserDefined, xhttp.AmzBucketReplicationStatus)
|
||||
resyncdsc := mustReplicate(ctx, oi.Bucket, oi.Name, getMustReplicateOptions(objInfo, replication.ExistingObjectReplicationType, ObjectOptions{}))
|
||||
dsc = &resyncdsc
|
||||
return c.resync(oi, dsc, tgtStatuses)
|
||||
@@ -1956,10 +1968,10 @@ func getLatestReplicationStats(bucket string, u BucketUsageInfo) (s BucketReplic
|
||||
return calculateBucketReplicationStats(bucket, u, bucketStats)
|
||||
}
|
||||
|
||||
const resyncTimeInterval = time.Minute * 10
|
||||
const resyncTimeInterval = time.Minute * 1
|
||||
|
||||
// periodicResyncMetaSave saves in-memory resync meta stats to disk in periodic intervals
|
||||
func (p *ReplicationPool) periodicResyncMetaSave(ctx context.Context, objectAPI ObjectLayer) {
|
||||
// updateResyncStatus persists in-memory resync metadata stats to disk at periodic intervals
|
||||
func (p *ReplicationPool) updateResyncStatus(ctx context.Context, objectAPI ObjectLayer) {
|
||||
resyncTimer := time.NewTimer(resyncTimeInterval)
|
||||
defer resyncTimer.Stop()
|
||||
|
||||
@@ -2085,13 +2097,15 @@ func resyncBucket(ctx context.Context, bucket, arn string, heal bool, objectAPI
|
||||
DeleteMarkerMTime: DeleteMarkerMTime{roi.ModTime},
|
||||
DeleteMarker: roi.DeleteMarker,
|
||||
},
|
||||
Bucket: roi.Bucket,
|
||||
OpType: replication.ExistingObjectReplicationType,
|
||||
Bucket: roi.Bucket,
|
||||
OpType: replication.ExistingObjectReplicationType,
|
||||
EventType: ReplicateExistingDelete,
|
||||
}
|
||||
replicateDelete(ctx, doi, objectAPI, ReplicateDelete)
|
||||
replicateDelete(ctx, doi, objectAPI)
|
||||
} else {
|
||||
roi.OpType = replication.ExistingObjectReplicationType
|
||||
replicateObject(ctx, roi, objectAPI, ReplicateExisting)
|
||||
roi.EventType = ReplicateExisting
|
||||
replicateObject(ctx, roi, objectAPI)
|
||||
}
|
||||
_, err = tgt.StatObject(ctx, tgt.Bucket, roi.Name, miniogo.StatObjectOptions{
|
||||
VersionID: roi.VersionID,
|
||||
@@ -2205,18 +2219,21 @@ func (p *ReplicationPool) loadResync(ctx context.Context, buckets []BucketInfo,
|
||||
for index := range buckets {
|
||||
meta, err := loadBucketResyncMetadata(ctx, buckets[index].Name, objAPI)
|
||||
if err != nil {
|
||||
if errors.Is(err, errVolumeNotFound) {
|
||||
meta = newBucketResyncStatus(buckets[index].Name)
|
||||
} else {
|
||||
if !errors.Is(err, errVolumeNotFound) {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
p.resyncState.Lock()
|
||||
p.resyncState.statusMap[buckets[index].Name] = meta
|
||||
p.resyncState.Unlock()
|
||||
}
|
||||
for index := range buckets {
|
||||
bucket := buckets[index].Name
|
||||
p.resyncState.RLock()
|
||||
m, ok := p.resyncState.statusMap[bucket]
|
||||
p.resyncState.RUnlock()
|
||||
|
||||
if ok {
|
||||
for arn, st := range m.TargetsMap {
|
||||
if st.ResyncStatus == ResyncFailed || st.ResyncStatus == ResyncStarted {
|
||||
@@ -2283,3 +2300,89 @@ func saveResyncStatus(ctx context.Context, bucket string, brs BucketReplicationR
|
||||
configFile := path.Join(bucketMetaPrefix, bucket, replicationDir, resyncFileName)
|
||||
return saveConfig(ctx, objectAPI, configFile, buf)
|
||||
}
|
||||
|
||||
// getReplicationDiff returns unreplicated objects in a channel
|
||||
func getReplicationDiff(ctx context.Context, objAPI ObjectLayer, bucket string, opts madmin.ReplDiffOpts) (diffCh chan madmin.DiffInfo, err error) {
|
||||
objInfoCh := make(chan ObjectInfo)
|
||||
if err := objAPI.Walk(ctx, bucket, opts.Prefix, objInfoCh, ObjectOptions{}); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return diffCh, err
|
||||
}
|
||||
cfg, err := getReplicationConfig(ctx, bucket)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return diffCh, err
|
||||
}
|
||||
tgts, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return diffCh, err
|
||||
}
|
||||
rcfg := replicationConfig{
|
||||
Config: cfg,
|
||||
remotes: tgts,
|
||||
}
|
||||
diffCh = make(chan madmin.DiffInfo, 4000)
|
||||
go func() {
|
||||
defer close(diffCh)
|
||||
for obj := range objInfoCh {
|
||||
// Ignore object prefixes which are excluded
|
||||
// from versioning via the MinIO bucket versioning extension.
|
||||
if globalBucketVersioningSys.PrefixSuspended(bucket, obj.Name) {
|
||||
continue
|
||||
}
|
||||
roi := getHealReplicateObjectInfo(obj, rcfg)
|
||||
switch roi.ReplicationStatus {
|
||||
case replication.Completed, replication.Replica:
|
||||
if !opts.Verbose {
|
||||
continue
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
// ignore pre-existing objects that don't satisfy replication rule(s)
|
||||
if roi.ReplicationStatus.Empty() && !roi.ExistingObjResync.mustResync() {
|
||||
continue
|
||||
}
|
||||
tgtsMap := make(map[string]madmin.TgtDiffInfo)
|
||||
for arn, st := range roi.TargetStatuses {
|
||||
if opts.ARN == "" || opts.ARN == arn {
|
||||
if !opts.Verbose && (st == replication.Completed || st == replication.Replica) {
|
||||
continue
|
||||
}
|
||||
tgtsMap[arn] = madmin.TgtDiffInfo{
|
||||
ReplicationStatus: st.String(),
|
||||
}
|
||||
}
|
||||
}
|
||||
for arn, st := range roi.TargetPurgeStatuses {
|
||||
if opts.ARN == "" || opts.ARN == arn {
|
||||
if !opts.Verbose && st == Complete {
|
||||
continue
|
||||
}
|
||||
t, ok := tgtsMap[arn]
|
||||
if !ok {
|
||||
t = madmin.TgtDiffInfo{}
|
||||
}
|
||||
t.DeleteReplicationStatus = string(st)
|
||||
tgtsMap[arn] = t
|
||||
}
|
||||
}
|
||||
select {
|
||||
case diffCh <- madmin.DiffInfo{
|
||||
Object: obj.Name,
|
||||
VersionID: obj.VersionID,
|
||||
LastModified: obj.ModTime,
|
||||
IsDeleteMarker: obj.DeleteMarker,
|
||||
ReplicationStatus: string(roi.ReplicationStatus),
|
||||
DeleteReplicationStatus: string(roi.VersionPurgeStatus),
|
||||
ReplicationTimestamp: roi.ReplicationTimestamp,
|
||||
Targets: tgtsMap,
|
||||
}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return diffCh, nil
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ func (api objectAPIHandlers) GetBucketVersioningHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
+31
-24
@@ -48,6 +48,7 @@ import (
|
||||
"github.com/inconshreveable/mousetrap"
|
||||
dns2 "github.com/miekg/dns"
|
||||
"github.com/minio/cli"
|
||||
consoleoauth2 "github.com/minio/console/pkg/auth/idp/oauth2"
|
||||
consoleCerts "github.com/minio/console/pkg/certs"
|
||||
"github.com/minio/console/restapi"
|
||||
"github.com/minio/console/restapi/operations"
|
||||
@@ -205,28 +206,6 @@ func minioConfigToConsoleFeatures() {
|
||||
if globalLDAPConfig.Enabled {
|
||||
os.Setenv("CONSOLE_LDAP_ENABLED", config.EnableOn)
|
||||
}
|
||||
// if IDP is enabled, set IDP environment variables
|
||||
if globalOpenIDConfig.ProviderCfgs[config.Default] != nil {
|
||||
os.Setenv("CONSOLE_IDP_URL", globalOpenIDConfig.ProviderCfgs[config.Default].URL.String())
|
||||
os.Setenv("CONSOLE_IDP_CLIENT_ID", globalOpenIDConfig.ProviderCfgs[config.Default].ClientID)
|
||||
os.Setenv("CONSOLE_IDP_SECRET", globalOpenIDConfig.ProviderCfgs[config.Default].ClientSecret)
|
||||
os.Setenv("CONSOLE_IDP_HMAC_SALT", globalDeploymentID)
|
||||
os.Setenv("CONSOLE_IDP_HMAC_PASSPHRASE", globalOpenIDConfig.ProviderCfgs[config.Default].ClientID)
|
||||
os.Setenv("CONSOLE_IDP_SCOPES", strings.Join(globalOpenIDConfig.ProviderCfgs[config.Default].DiscoveryDoc.ScopesSupported, ","))
|
||||
if globalOpenIDConfig.ProviderCfgs[config.Default].ClaimUserinfo {
|
||||
os.Setenv("CONSOLE_IDP_USERINFO", config.EnableOn)
|
||||
}
|
||||
if globalOpenIDConfig.ProviderCfgs[config.Default].RedirectURIDynamic {
|
||||
// Enable dynamic redirect-uri's based on incoming 'host' header,
|
||||
// Overrides any other callback URL.
|
||||
os.Setenv("CONSOLE_IDP_CALLBACK_DYNAMIC", config.EnableOn)
|
||||
}
|
||||
if globalOpenIDConfig.ProviderCfgs[config.Default].RedirectURI != "" {
|
||||
os.Setenv("CONSOLE_IDP_CALLBACK", globalOpenIDConfig.ProviderCfgs[config.Default].RedirectURI)
|
||||
} else {
|
||||
os.Setenv("CONSOLE_IDP_CALLBACK", getConsoleEndpoints()[0]+"/oauth_callback")
|
||||
}
|
||||
}
|
||||
os.Setenv("CONSOLE_MINIO_REGION", globalSite.Region)
|
||||
os.Setenv("CONSOLE_CERT_PASSWD", env.Get("MINIO_CERT_PASSWD", ""))
|
||||
if globalSubnetConfig.License != "" {
|
||||
@@ -240,6 +219,29 @@ func minioConfigToConsoleFeatures() {
|
||||
}
|
||||
}
|
||||
|
||||
func buildOpenIDConsoleConfig() consoleoauth2.OpenIDPCfg {
|
||||
m := make(map[string]consoleoauth2.ProviderConfig, len(globalOpenIDConfig.ProviderCfgs))
|
||||
for name, cfg := range globalOpenIDConfig.ProviderCfgs {
|
||||
callback := getConsoleEndpoints()[0] + "/oauth_callback"
|
||||
if cfg.RedirectURI != "" {
|
||||
callback = cfg.RedirectURI
|
||||
}
|
||||
m[name] = consoleoauth2.ProviderConfig{
|
||||
URL: cfg.URL.String(),
|
||||
DisplayName: cfg.DisplayName,
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
HMACSalt: globalDeploymentID,
|
||||
HMACPassphrase: cfg.ClientID,
|
||||
Scopes: strings.Join(cfg.DiscoveryDoc.ScopesSupported, ","),
|
||||
Userinfo: cfg.ClaimUserinfo,
|
||||
RedirectCallbackDynamic: cfg.RedirectURIDynamic,
|
||||
RedirectCallback: callback,
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func initConsoleServer() (*restapi.Server, error) {
|
||||
// unset all console_ environment variables.
|
||||
for _, cenv := range env.List(consolePrefix) {
|
||||
@@ -273,6 +275,12 @@ func initConsoleServer() (*restapi.Server, error) {
|
||||
api.Logger = noLog
|
||||
}
|
||||
|
||||
// Pass in console application config. This needs to happen before the
|
||||
// ConfigureAPI() call.
|
||||
restapi.GlobalMinIOConfig = restapi.MinIOConfig{
|
||||
OpenIDProviders: buildOpenIDConsoleConfig(),
|
||||
}
|
||||
|
||||
server := restapi.NewServer(api)
|
||||
// register all APIs
|
||||
server.ConfigureAPI()
|
||||
@@ -430,7 +438,6 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
if err != nil {
|
||||
logger.FatalIf(err, "Unable to get free port for console on the host")
|
||||
}
|
||||
globalMinioConsolePortAuto = true
|
||||
consoleAddr = net.JoinHostPort("", p.String())
|
||||
}
|
||||
|
||||
@@ -869,7 +876,7 @@ func handleKMSConfig() {
|
||||
// This implicitly checks that we can communicate to KES. We don't treat
|
||||
// a policy error as failure condition since MinIO may not have the permission
|
||||
// to create keys - just to generate/decrypt data encryption keys.
|
||||
if err = KMS.CreateKey(defaultKeyID); err != nil && !errors.Is(err, kes.ErrKeyExists) && !errors.Is(err, kes.ErrNotAllowed) {
|
||||
if err = KMS.CreateKey(context.Background(), defaultKeyID); err != nil && !errors.Is(err, kes.ErrKeyExists) && !errors.Is(err, kes.ErrNotAllowed) {
|
||||
logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment")
|
||||
}
|
||||
GlobalKMS = KMS
|
||||
|
||||
@@ -70,7 +70,7 @@ func migrateIAMConfigsEtcdToEncrypted(ctx context.Context, client *etcd.Client)
|
||||
}
|
||||
|
||||
if encrypted && GlobalKMS != nil {
|
||||
stat, err := GlobalKMS.Stat()
|
||||
stat, err := GlobalKMS.Stat(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, encrypted bool) error {
|
||||
return nil
|
||||
}
|
||||
if encrypted && GlobalKMS != nil {
|
||||
stat, err := GlobalKMS.Stat()
|
||||
stat, err := GlobalKMS.Stat(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -37,11 +37,7 @@ func TestServerConfigMigrateV1(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
rootPath := t.TempDir()
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
|
||||
globalObjLayerMutex.Lock()
|
||||
@@ -74,13 +70,7 @@ func TestServerConfigMigrateV1(t *testing.T) {
|
||||
// Test if all migrate code returns nil when config file does not
|
||||
// exist
|
||||
func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
globalConfigDir = &ConfigDir{path: t.TempDir()}
|
||||
|
||||
if err := migrateV2ToV3(); err != nil {
|
||||
t.Fatal("migrate v2 to v3 should succeed when no config file is found")
|
||||
@@ -164,11 +154,7 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
||||
|
||||
// Test if a config migration from v2 to v33 is successfully done
|
||||
func TestServerConfigMigrateV2toV33(t *testing.T) {
|
||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
rootPath := t.TempDir()
|
||||
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
|
||||
@@ -234,11 +220,7 @@ func TestServerConfigMigrateV2toV33(t *testing.T) {
|
||||
|
||||
// Test if all migrate code returns error with corrupted config files
|
||||
func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
rootPath := t.TempDir()
|
||||
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
configPath := rootPath + SlashSeparator + minioConfigFile
|
||||
@@ -331,35 +313,31 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
||||
|
||||
// Test if all migrate code returns error with corrupted config files
|
||||
func TestServerConfigMigrateCorruptedConfig(t *testing.T) {
|
||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
rootPath := t.TempDir()
|
||||
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
configPath := rootPath + SlashSeparator + minioConfigFile
|
||||
|
||||
for i := 3; i <= 17; i++ {
|
||||
// Create a corrupted config file
|
||||
if err = ioutil.WriteFile(configPath, []byte(fmt.Sprintf("{ \"version\":\"%d\", \"credential\": { \"accessKey\": 1 } }", i)),
|
||||
if err := ioutil.WriteFile(configPath, []byte(fmt.Sprintf("{ \"version\":\"%d\", \"credential\": { \"accessKey\": 1 } }", i)),
|
||||
0o644); err != nil {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
// Test different migrate versions and be sure they are returning an error
|
||||
if err = migrateConfig(); err == nil {
|
||||
if err := migrateConfig(); err == nil {
|
||||
t.Fatal("migrateConfig() should fail with a corrupted json")
|
||||
}
|
||||
}
|
||||
|
||||
// Create a corrupted config file for version '2'.
|
||||
if err = ioutil.WriteFile(configPath, []byte("{ \"version\":\"2\", \"credentials\": { \"accessKeyId\": 1 } }"), 0o644); err != nil {
|
||||
if err := ioutil.WriteFile(configPath, []byte("{ \"version\":\"2\", \"credentials\": { \"accessKeyId\": 1 } }"), 0o644); err != nil {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
// Test different migrate versions and be sure they are returning an error
|
||||
if err = migrateConfig(); err == nil {
|
||||
if err := migrateConfig(); err == nil {
|
||||
t.Fatal("migrateConfig() should fail with a corrupted json")
|
||||
}
|
||||
}
|
||||
|
||||
+22
-19
@@ -48,12 +48,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
dataScannerSleepPerFolder = time.Millisecond // Time to wait between folders.
|
||||
dataUsageUpdateDirCycles = 16 // Visit all folders every n cycles.
|
||||
dataScannerCompactLeastObject = 500 // Compact when there is less than this many objects in a branch.
|
||||
dataScannerCompactAtChildren = 10000 // Compact when there are this many children in a branch.
|
||||
dataScannerCompactAtFolders = dataScannerCompactAtChildren / 4 // Compact when this many subfolders in a single folder.
|
||||
dataScannerStartDelay = 1 * time.Minute // Time to wait on startup and between cycles.
|
||||
dataScannerSleepPerFolder = time.Millisecond // Time to wait between folders.
|
||||
dataUsageUpdateDirCycles = 16 // Visit all folders every n cycles.
|
||||
dataScannerCompactLeastObject = 500 // Compact when there is less than this many objects in a branch.
|
||||
dataScannerCompactAtChildren = 10000 // Compact when there are this many children in a branch.
|
||||
dataScannerCompactAtFolders = dataScannerCompactAtChildren / 4 // Compact when this many subfolders in a single folder.
|
||||
dataScannerForceCompactAtFolders = 1_000_000 // Compact when this many subfolders in a single folder (even top level).
|
||||
dataScannerStartDelay = 1 * time.Minute // Time to wait on startup and between cycles.
|
||||
|
||||
healDeleteDangling = true
|
||||
healFolderIncludeProb = 32 // Include a clean folder one in n cycles.
|
||||
@@ -566,9 +567,11 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
||||
}
|
||||
|
||||
// If we have many subfolders, compact ourself.
|
||||
if !into.Compacted &&
|
||||
f.newCache.Info.Name != folder.name &&
|
||||
len(existingFolders)+len(newFolders) >= dataScannerCompactAtFolders {
|
||||
shouldCompact := f.newCache.Info.Name != folder.name &&
|
||||
len(existingFolders)+len(newFolders) >= dataScannerCompactAtFolders ||
|
||||
len(existingFolders)+len(newFolders) >= dataScannerForceCompactAtFolders
|
||||
|
||||
if !into.Compacted && shouldCompact {
|
||||
into.Compacted = true
|
||||
newFolders = append(newFolders, existingFolders...)
|
||||
existingFolders = nil
|
||||
@@ -734,11 +737,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
||||
}
|
||||
},
|
||||
// Some disks have data for this.
|
||||
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
|
||||
if f.dataUsageScannerDebug {
|
||||
console.Debugf(healObjectsPrefix+" got partial, %d agreed, errs: %v\n", nAgreed, errs)
|
||||
}
|
||||
|
||||
partial: func(entries metaCacheEntries, errs []error) {
|
||||
entry, ok := entries.resolve(&resolver)
|
||||
if !ok {
|
||||
// check if we can get one entry atleast
|
||||
@@ -1279,12 +1278,14 @@ func (i *scannerItem) healReplication(ctx context.Context, o ObjectLayer, oi Obj
|
||||
|
||||
switch oi.ReplicationStatus {
|
||||
case replication.Pending, replication.Failed:
|
||||
roi.EventType = ReplicateHeal
|
||||
globalReplicationPool.queueReplicaTask(roi)
|
||||
return
|
||||
case replication.Replica:
|
||||
sizeS.replicaSize += oi.Size
|
||||
}
|
||||
if roi.ExistingObjResync.mustResync() {
|
||||
roi.EventType = ReplicateExisting
|
||||
globalReplicationPool.queueReplicaTask(roi)
|
||||
}
|
||||
}
|
||||
@@ -1310,11 +1311,13 @@ func (i *scannerItem) healReplicationDeletes(ctx context.Context, o ObjectLayer,
|
||||
DeleteMarkerMTime: DeleteMarkerMTime{roi.ModTime},
|
||||
DeleteMarker: roi.DeleteMarker,
|
||||
},
|
||||
Bucket: roi.Bucket,
|
||||
OpType: replication.HealReplicationType,
|
||||
Bucket: roi.Bucket,
|
||||
OpType: replication.HealReplicationType,
|
||||
EventType: ReplicateHealDelete,
|
||||
}
|
||||
if roi.ExistingObjResync.mustResync() {
|
||||
doi.OpType = replication.ExistingObjectReplicationType
|
||||
doi.EventType = ReplicateExistingDelete
|
||||
queueReplicateDeletesWrapper(doi, roi.ExistingObjResync)
|
||||
return
|
||||
}
|
||||
@@ -1477,9 +1480,9 @@ const (
|
||||
ILMTransition = " ilm:transition"
|
||||
)
|
||||
|
||||
func auditLogLifecycle(ctx context.Context, oi ObjectInfo, trigger string) {
|
||||
func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string) {
|
||||
var apiName string
|
||||
switch trigger {
|
||||
switch event {
|
||||
case ILMExpiry:
|
||||
apiName = "ILMExpiry"
|
||||
case ILMFreeVersionDelete:
|
||||
@@ -1488,7 +1491,7 @@ func auditLogLifecycle(ctx context.Context, oi ObjectInfo, trigger string) {
|
||||
apiName = "ILMTransition"
|
||||
}
|
||||
auditLogInternal(ctx, oi.Bucket, oi.Name, AuditLogOptions{
|
||||
Trigger: trigger,
|
||||
Event: event,
|
||||
APIName: apiName,
|
||||
VersionID: oi.VersionID,
|
||||
})
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
@@ -100,15 +99,11 @@ func TestDataUpdateTracker(t *testing.T) {
|
||||
|
||||
dut.Current.bf = dut.newBloomFilter()
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "TestDataUpdateTracker")
|
||||
tmpDir := t.TempDir()
|
||||
err := os.MkdirAll(filepath.Dir(filepath.Join(tmpDir, dataUpdateTrackerFilename)), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.MkdirAll(filepath.Dir(filepath.Join(tmpDir, dataUpdateTrackerFilename)), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
dut.start(ctx, tmpDir)
|
||||
|
||||
+3
-15
@@ -35,12 +35,8 @@ type usageTestFile struct {
|
||||
}
|
||||
|
||||
func TestDataUsageUpdate(t *testing.T) {
|
||||
base, err := ioutil.TempDir("", "TestDataUsageUpdate")
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
base := t.TempDir()
|
||||
const bucket = "bucket"
|
||||
defer os.RemoveAll(base)
|
||||
files := []usageTestFile{
|
||||
{name: "rootfile", size: 10000},
|
||||
{name: "rootfile2", size: 10000},
|
||||
@@ -251,12 +247,8 @@ func TestDataUsageUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDataUsageUpdatePrefix(t *testing.T) {
|
||||
base, err := ioutil.TempDir("", "TestDataUpdateUsagePrefix")
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
base := t.TempDir()
|
||||
scannerSleeper.Update(0, 0)
|
||||
defer os.RemoveAll(base)
|
||||
files := []usageTestFile{
|
||||
{name: "bucket/rootfile", size: 10000},
|
||||
{name: "bucket/rootfile2", size: 10000},
|
||||
@@ -537,12 +529,8 @@ func generateUsageTestFiles(t *testing.T, base, bucket string, nFolders, nFiles,
|
||||
}
|
||||
|
||||
func TestDataUsageCacheSerialize(t *testing.T) {
|
||||
base, err := ioutil.TempDir("", "TestDataUsageCacheSerialize")
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
base := t.TempDir()
|
||||
const bucket = "abucket"
|
||||
defer os.RemoveAll(base)
|
||||
files := []usageTestFile{
|
||||
{name: "rootfile", size: 10000},
|
||||
{name: "rootfile2", size: 10000},
|
||||
|
||||
@@ -797,8 +797,8 @@ func (c *diskCache) bitrotWriteToCache(cachePath, fileName string, reader io.Rea
|
||||
return bytesWritten, base64.StdEncoding.EncodeToString(md5sumCurr), nil
|
||||
}
|
||||
|
||||
func newCacheEncryptReader(content io.Reader, bucket, object string, metadata map[string]string) (r io.Reader, err error) {
|
||||
objectEncryptionKey, err := newCacheEncryptMetadata(bucket, object, metadata)
|
||||
func newCacheEncryptReader(ctx context.Context, content io.Reader, bucket, object string, metadata map[string]string) (r io.Reader, err error) {
|
||||
objectEncryptionKey, err := newCacheEncryptMetadata(ctx, bucket, object, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -810,12 +810,12 @@ func newCacheEncryptReader(content io.Reader, bucket, object string, metadata ma
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func newCacheEncryptMetadata(bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||
func newCacheEncryptMetadata(ctx context.Context, bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||
var sealedKey crypto.SealedKey
|
||||
if globalCacheKMS == nil {
|
||||
return nil, errKMSNotConfigured
|
||||
}
|
||||
key, err := globalCacheKMS.GenerateKey("", kms.Context{bucket: pathJoin(bucket, object)})
|
||||
key, err := globalCacheKMS.GenerateKey(ctx, "", kms.Context{bucket: pathJoin(bucket, object)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -891,7 +891,7 @@ func (c *diskCache) put(ctx context.Context, bucket, object string, data io.Read
|
||||
reader := data
|
||||
actualSize := uint64(size)
|
||||
if globalCacheKMS != nil {
|
||||
reader, err = newCacheEncryptReader(data, bucket, object, metadata)
|
||||
reader, err = newCacheEncryptReader(ctx, data, bucket, object, metadata)
|
||||
if err != nil {
|
||||
removeAll(cachePath)
|
||||
return oi, err
|
||||
@@ -948,7 +948,7 @@ func (c *diskCache) putRange(ctx context.Context, bucket, object string, data io
|
||||
// objSize is the actual size of object (with encryption overhead if any)
|
||||
objSize := uint64(size)
|
||||
if globalCacheKMS != nil {
|
||||
reader, err = newCacheEncryptReader(data, bucket, object, metadata)
|
||||
reader, err = newCacheEncryptReader(ctx, data, bucket, object, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1301,7 +1301,7 @@ func (c *diskCache) NewMultipartUpload(ctx context.Context, bucket, object, uID
|
||||
m.Stat.ModTime = UTCNow()
|
||||
if globalCacheKMS != nil {
|
||||
m.Meta[ReservedMetadataPrefix+"Encrypted-Multipart"] = ""
|
||||
if _, err := newCacheEncryptMetadata(bucket, object, m.Meta); err != nil {
|
||||
if _, err := newCacheEncryptMetadata(ctx, bucket, object, m.Meta); err != nil {
|
||||
return uploadID, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (api objectAPIHandlers) GetBucketWebsiteHandler(w http.ResponseWriter, r *h
|
||||
}
|
||||
|
||||
// Validate if bucket exists, before proceeding further...
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket)
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -84,7 +84,7 @@ func (api objectAPIHandlers) GetBucketAccelerateHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Validate if bucket exists, before proceeding further...
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket)
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -117,7 +117,7 @@ func (api objectAPIHandlers) GetBucketRequestPaymentHandler(w http.ResponseWrite
|
||||
}
|
||||
|
||||
// Validate if bucket exists, before proceeding further...
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket)
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -151,7 +151,7 @@ func (api objectAPIHandlers) GetBucketLoggingHandler(w http.ResponseWriter, r *h
|
||||
}
|
||||
|
||||
// Validate if bucket exists, before proceeding further...
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket)
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
@@ -189,7 +189,7 @@ func (api objectAPIHandlers) GetBucketCorsHandler(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// Validate if bucket exists, before proceeding further...
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket)
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
|
||||
+20
-20
@@ -255,7 +255,7 @@ func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
|
||||
}
|
||||
|
||||
// This function rotates old to new key.
|
||||
func rotateKey(oldKey []byte, newKeyID string, newKey []byte, bucket, object string, metadata map[string]string, ctx kms.Context) error {
|
||||
func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byte, bucket, object string, metadata map[string]string, cryptoCtx kms.Context) error {
|
||||
kind, _ := crypto.IsEncrypted(metadata)
|
||||
switch kind {
|
||||
case crypto.S3:
|
||||
@@ -275,7 +275,7 @@ func rotateKey(oldKey []byte, newKeyID string, newKey []byte, bucket, object str
|
||||
return err
|
||||
}
|
||||
|
||||
newKey, err := GlobalKMS.GenerateKey("", kms.Context{bucket: path.Join(bucket, object)})
|
||||
newKey, err := GlobalKMS.GenerateKey(ctx, "", kms.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -291,8 +291,8 @@ func rotateKey(oldKey []byte, newKeyID string, newKey []byte, bucket, object str
|
||||
return err
|
||||
}
|
||||
|
||||
if len(ctx) == 0 {
|
||||
_, _, _, ctx, err = crypto.S3KMS.ParseMetadata(metadata)
|
||||
if len(cryptoCtx) == 0 {
|
||||
_, _, _, cryptoCtx, err = crypto.S3KMS.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -305,19 +305,19 @@ func rotateKey(oldKey []byte, newKeyID string, newKey []byte, bucket, object str
|
||||
// of the client provided context and add the bucket
|
||||
// key, if not present.
|
||||
kmsCtx := kms.Context{}
|
||||
for k, v := range ctx {
|
||||
for k, v := range cryptoCtx {
|
||||
kmsCtx[k] = v
|
||||
}
|
||||
if _, ok := kmsCtx[bucket]; !ok {
|
||||
kmsCtx[bucket] = path.Join(bucket, object)
|
||||
}
|
||||
newKey, err := GlobalKMS.GenerateKey(newKeyID, kmsCtx)
|
||||
newKey, err := GlobalKMS.GenerateKey(ctx, newKeyID, kmsCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sealedKey := objectKey.Seal(newKey.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3KMS.String(), bucket, object)
|
||||
crypto.S3KMS.CreateMetadata(metadata, newKey.KeyID, newKey.Ciphertext, sealedKey, ctx)
|
||||
crypto.S3KMS.CreateMetadata(metadata, newKey.KeyID, newKey.Ciphertext, sealedKey, cryptoCtx)
|
||||
return nil
|
||||
case crypto.SSEC:
|
||||
sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
|
||||
@@ -344,14 +344,14 @@ func rotateKey(oldKey []byte, newKeyID string, newKey []byte, bucket, object str
|
||||
}
|
||||
}
|
||||
|
||||
func newEncryptMetadata(kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, ctx kms.Context) (crypto.ObjectKey, error) {
|
||||
func newEncryptMetadata(ctx context.Context, kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, cryptoCtx kms.Context) (crypto.ObjectKey, error) {
|
||||
var sealedKey crypto.SealedKey
|
||||
switch kind {
|
||||
case crypto.S3:
|
||||
if GlobalKMS == nil {
|
||||
return crypto.ObjectKey{}, errKMSNotConfigured
|
||||
}
|
||||
key, err := GlobalKMS.GenerateKey("", kms.Context{bucket: path.Join(bucket, object)})
|
||||
key, err := GlobalKMS.GenerateKey(ctx, "", kms.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return crypto.ObjectKey{}, err
|
||||
}
|
||||
@@ -372,13 +372,13 @@ func newEncryptMetadata(kind crypto.Type, keyID string, key []byte, bucket, obje
|
||||
// of the client provided context and add the bucket
|
||||
// key, if not present.
|
||||
kmsCtx := kms.Context{}
|
||||
for k, v := range ctx {
|
||||
for k, v := range cryptoCtx {
|
||||
kmsCtx[k] = v
|
||||
}
|
||||
if _, ok := kmsCtx[bucket]; !ok {
|
||||
kmsCtx[bucket] = path.Join(bucket, object)
|
||||
}
|
||||
key, err := GlobalKMS.GenerateKey(keyID, kmsCtx)
|
||||
key, err := GlobalKMS.GenerateKey(ctx, keyID, kmsCtx)
|
||||
if err != nil {
|
||||
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||
return crypto.ObjectKey{}, errKMSKeyNotFound
|
||||
@@ -388,7 +388,7 @@ func newEncryptMetadata(kind crypto.Type, keyID string, key []byte, bucket, obje
|
||||
|
||||
objectKey := crypto.GenerateKey(key.Plaintext, rand.Reader)
|
||||
sealedKey = objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3KMS.String(), bucket, object)
|
||||
crypto.S3KMS.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey, ctx)
|
||||
crypto.S3KMS.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey, cryptoCtx)
|
||||
return objectKey, nil
|
||||
case crypto.SSEC:
|
||||
objectKey := crypto.GenerateKey(key, rand.Reader)
|
||||
@@ -400,8 +400,8 @@ func newEncryptMetadata(kind crypto.Type, keyID string, key []byte, bucket, obje
|
||||
}
|
||||
}
|
||||
|
||||
func newEncryptReader(content io.Reader, kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, ctx kms.Context) (io.Reader, crypto.ObjectKey, error) {
|
||||
objectEncryptionKey, err := newEncryptMetadata(kind, keyID, key, bucket, object, metadata, ctx)
|
||||
func newEncryptReader(ctx context.Context, content io.Reader, kind crypto.Type, keyID string, key []byte, bucket, object string, metadata map[string]string, cryptoCtx kms.Context) (io.Reader, crypto.ObjectKey, error) {
|
||||
objectEncryptionKey, err := newEncryptMetadata(ctx, kind, keyID, key, bucket, object, metadata, cryptoCtx)
|
||||
if err != nil {
|
||||
return nil, crypto.ObjectKey{}, err
|
||||
}
|
||||
@@ -418,9 +418,9 @@ func newEncryptReader(content io.Reader, kind crypto.Type, keyID string, key []b
|
||||
// SSE-S3
|
||||
func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[string]string) (err error) {
|
||||
var (
|
||||
key []byte
|
||||
keyID string
|
||||
ctx kms.Context
|
||||
key []byte
|
||||
keyID string
|
||||
kmsCtx kms.Context
|
||||
)
|
||||
kind, _ := crypto.IsRequested(r.Header)
|
||||
switch kind {
|
||||
@@ -430,12 +430,12 @@ func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[
|
||||
return err
|
||||
}
|
||||
case crypto.S3KMS:
|
||||
keyID, ctx, err = crypto.S3KMS.ParseHTTP(r.Header)
|
||||
keyID, kmsCtx, err = crypto.S3KMS.ParseHTTP(r.Header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = newEncryptMetadata(kind, keyID, key, bucket, object, metadata, ctx)
|
||||
_, err = newEncryptMetadata(r.Context(), kind, keyID, key, bucket, object, metadata, kmsCtx)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -468,7 +468,7 @@ func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, m
|
||||
return nil, crypto.ObjectKey{}, err
|
||||
}
|
||||
}
|
||||
return newEncryptReader(content, kind, keyID, key, bucket, object, metadata, ctx)
|
||||
return newEncryptReader(r.Context(), content, kind, keyID, key, bucket, object, metadata, ctx)
|
||||
}
|
||||
|
||||
func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||
|
||||
@@ -292,6 +292,19 @@ func (l EndpointServerPools) LocalDisksPaths() []string {
|
||||
return disks
|
||||
}
|
||||
|
||||
// NLocalDisksPathsPerPool returns the disk paths of the local disks per pool
|
||||
func (l EndpointServerPools) NLocalDisksPathsPerPool() []int {
|
||||
localDisksCount := make([]int, len(l))
|
||||
for i, ep := range l {
|
||||
for _, endpoint := range ep.Endpoints {
|
||||
if endpoint.IsLocal {
|
||||
localDisksCount[i]++
|
||||
}
|
||||
}
|
||||
}
|
||||
return localDisksCount
|
||||
}
|
||||
|
||||
// FirstLocal returns true if the first endpoint is local.
|
||||
func (l EndpointServerPools) FirstLocal() bool {
|
||||
return l[0].Endpoints[0].IsLocal
|
||||
|
||||
+60
-6
@@ -35,7 +35,7 @@ var bucketMetadataOpIgnoredErrs = append(bucketOpIgnoredErrs, errVolumeNotFound)
|
||||
// Bucket operations
|
||||
|
||||
// MakeBucket - make a bucket.
|
||||
func (er erasureObjects) MakeBucketWithLocation(ctx context.Context, bucket string, opts BucketOptions) error {
|
||||
func (er erasureObjects) MakeBucketWithLocation(ctx context.Context, bucket string, opts MakeBucketOptions) error {
|
||||
defer NSUpdated(bucket, slashSeparator)
|
||||
|
||||
// Verify if bucket is valid.
|
||||
@@ -94,7 +94,7 @@ func undoDeleteBucket(storageDisks []StorageAPI, bucket string) {
|
||||
}
|
||||
|
||||
// getBucketInfo - returns the BucketInfo from one of the load balanced disks.
|
||||
func (er erasureObjects) getBucketInfo(ctx context.Context, bucketName string) (bucketInfo BucketInfo, err error) {
|
||||
func (er erasureObjects) getBucketInfo(ctx context.Context, bucketName string, opts BucketOptions) (bucketInfo BucketInfo, err error) {
|
||||
storageDisks := er.getDisks()
|
||||
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
@@ -108,9 +108,17 @@ func (er erasureObjects) getBucketInfo(ctx context.Context, bucketName string) (
|
||||
}
|
||||
volInfo, err := storageDisks[index].StatVol(ctx, bucketName)
|
||||
if err != nil {
|
||||
if opts.Deleted {
|
||||
dvi, derr := storageDisks[index].StatVol(ctx, pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, bucketName))
|
||||
if derr != nil {
|
||||
return err
|
||||
}
|
||||
bucketsInfo[index] = BucketInfo{Name: bucketName, Deleted: dvi.Created}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
bucketsInfo[index] = BucketInfo(volInfo)
|
||||
bucketsInfo[index] = BucketInfo{Name: volInfo.Name, Created: volInfo.Created}
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
@@ -131,8 +139,8 @@ func (er erasureObjects) getBucketInfo(ctx context.Context, bucketName string) (
|
||||
}
|
||||
|
||||
// GetBucketInfo - returns BucketInfo for a bucket.
|
||||
func (er erasureObjects) GetBucketInfo(ctx context.Context, bucket string) (bi BucketInfo, e error) {
|
||||
bucketInfo, err := er.getBucketInfo(ctx, bucket)
|
||||
func (er erasureObjects) GetBucketInfo(ctx context.Context, bucket string, opts BucketOptions) (bi BucketInfo, e error) {
|
||||
bucketInfo, err := er.getBucketInfo(ctx, bucket, opts)
|
||||
if err != nil {
|
||||
return bi, toObjectErr(err, bucket)
|
||||
}
|
||||
@@ -191,12 +199,58 @@ func (er erasureObjects) DeleteBucket(ctx context.Context, bucket string, opts D
|
||||
if purgedDangling {
|
||||
err = nil
|
||||
}
|
||||
|
||||
if opts.SRDeleteOp == MarkDelete {
|
||||
er.markDelete(ctx, minioMetaBucket, pathJoin(bucketMetaPrefix, deletedBucketsPrefix, bucket))
|
||||
}
|
||||
}
|
||||
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// markDelete creates a vol entry in .minio.sys/buckets/.deleted until site replication
|
||||
// syncs the delete to peers
|
||||
func (er erasureObjects) markDelete(ctx context.Context, bucket, prefix string) error {
|
||||
storageDisks := er.getDisks()
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
// Make a volume entry on all underlying storage disks.
|
||||
for index := range storageDisks {
|
||||
index := index
|
||||
if storageDisks[index] == nil {
|
||||
continue
|
||||
}
|
||||
g.Go(func() error {
|
||||
if err := storageDisks[index].MakeVol(ctx, pathJoin(bucket, prefix)); err != nil {
|
||||
if errors.Is(err, errVolumeExists) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
err := reduceWriteQuorumErrs(ctx, g.Wait(), bucketOpIgnoredErrs, er.defaultWQuorum())
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// purgeDelete deletes vol entry in .minio.sys/buckets/.deleted after site replication
|
||||
// syncs the delete to peers OR on a new MakeBucket call.
|
||||
func (er erasureObjects) purgeDelete(ctx context.Context, bucket, prefix string) error {
|
||||
storageDisks := er.getDisks()
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
// Make a volume entry on all underlying storage disks.
|
||||
for index := range storageDisks {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if storageDisks[index] != nil {
|
||||
return storageDisks[index].DeleteVol(ctx, pathJoin(bucket, prefix), true)
|
||||
}
|
||||
return errDiskNotFound
|
||||
}, index)
|
||||
}
|
||||
err := reduceWriteQuorumErrs(ctx, g.Wait(), bucketOpIgnoredErrs, er.defaultWQuorum())
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// IsNotificationSupported returns whether bucket notification is applicable for this layer.
|
||||
func (er erasureObjects) IsNotificationSupported() bool {
|
||||
return true
|
||||
|
||||
@@ -19,7 +19,11 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/sync/errgroup"
|
||||
)
|
||||
|
||||
func (er erasureObjects) getOnlineDisks() (newDisks []StorageAPI) {
|
||||
@@ -119,3 +123,84 @@ func (er erasureObjects) getLoadBalancedDisks(optimized bool) []StorageAPI {
|
||||
// Return disks which have maximum disk usage common.
|
||||
return newDisks[max]
|
||||
}
|
||||
|
||||
// readMultipleFiles Reads raw data from all specified files from all disks.
|
||||
func readMultipleFiles(ctx context.Context, disks []StorageAPI, req ReadMultipleReq, readQuorum int) ([]ReadMultipleResp, error) {
|
||||
resps := make([]chan ReadMultipleResp, len(disks))
|
||||
for i := range resps {
|
||||
resps[i] = make(chan ReadMultipleResp, len(req.Files))
|
||||
}
|
||||
g := errgroup.WithNErrs(len(disks))
|
||||
// Read files in parallel across disks.
|
||||
for index := range disks {
|
||||
index := index
|
||||
g.Go(func() (err error) {
|
||||
if disks[index] == nil {
|
||||
return errDiskNotFound
|
||||
}
|
||||
return disks[index].ReadMultiple(ctx, req, resps[index])
|
||||
}, index)
|
||||
}
|
||||
|
||||
dataArray := make([]ReadMultipleResp, 0, len(req.Files))
|
||||
// Merge results. They should come in order from each.
|
||||
for _, wantFile := range req.Files {
|
||||
quorum := 0
|
||||
toAdd := ReadMultipleResp{
|
||||
Bucket: req.Bucket,
|
||||
Prefix: req.Prefix,
|
||||
File: wantFile,
|
||||
}
|
||||
for i := range resps {
|
||||
if disks[i] == nil {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case gotFile, ok := <-resps[i]:
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if gotFile.Error != "" || !gotFile.Exists {
|
||||
continue
|
||||
}
|
||||
if gotFile.File != wantFile || gotFile.Bucket != req.Bucket || gotFile.Prefix != req.Prefix {
|
||||
continue
|
||||
}
|
||||
quorum++
|
||||
if toAdd.Modtime.After(gotFile.Modtime) || len(gotFile.Data) < len(toAdd.Data) {
|
||||
// Pick latest, or largest to avoid possible truncated entries.
|
||||
continue
|
||||
}
|
||||
toAdd = gotFile
|
||||
}
|
||||
}
|
||||
if quorum < readQuorum {
|
||||
toAdd.Exists = false
|
||||
toAdd.Error = errErasureReadQuorum.Error()
|
||||
toAdd.Data = nil
|
||||
}
|
||||
dataArray = append(dataArray, toAdd)
|
||||
}
|
||||
|
||||
errs := g.Wait()
|
||||
for index, err := range errs {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if !IsErr(err, []error{
|
||||
errFileNotFound,
|
||||
errVolumeNotFound,
|
||||
errFileVersionNotFound,
|
||||
errDiskNotFound,
|
||||
errUnformattedDisk,
|
||||
}...) {
|
||||
logger.LogOnceIf(ctx, fmt.Errorf("Drive %s, path (%s/%s) returned an error (%w)",
|
||||
disks[index], req.Bucket, req.Prefix, err),
|
||||
disks[index].String())
|
||||
}
|
||||
}
|
||||
|
||||
// Return all the metadata.
|
||||
return dataArray, nil
|
||||
}
|
||||
|
||||
@@ -85,19 +85,17 @@ var erasureDecodeTests = []struct {
|
||||
|
||||
func TestErasureDecode(t *testing.T) {
|
||||
for i, test := range erasureDecodeTests {
|
||||
setup, err := newErasureTestSetup(test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
setup, err := newErasureTestSetup(t, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
||||
}
|
||||
erasure, err := NewErasure(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||
}
|
||||
disks := setup.disks
|
||||
data := make([]byte, test.data)
|
||||
if _, err = io.ReadFull(crand.Reader, data); err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
|
||||
}
|
||||
|
||||
@@ -113,11 +111,9 @@ func TestErasureDecode(t *testing.T) {
|
||||
n, err := erasure.Encode(context.Background(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1)
|
||||
closeBitrotWriters(writers)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create erasure test file: %v", i, err)
|
||||
}
|
||||
if n != test.data {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create erasure test file", i)
|
||||
}
|
||||
for i, w := range writers {
|
||||
@@ -195,7 +191,6 @@ func TestErasureDecode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
setup.Remove()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,12 +205,11 @@ func TestErasureDecodeRandomOffsetLength(t *testing.T) {
|
||||
dataBlocks := 7
|
||||
parityBlocks := 7
|
||||
blockSize := int64(1 * humanize.MiByte)
|
||||
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
|
||||
setup, err := newErasureTestSetup(t, dataBlocks, parityBlocks, blockSize)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer setup.Remove()
|
||||
disks := setup.disks
|
||||
erasure, err := NewErasure(context.Background(), dataBlocks, parityBlocks, blockSize)
|
||||
if err != nil {
|
||||
@@ -288,11 +282,10 @@ func TestErasureDecodeRandomOffsetLength(t *testing.T) {
|
||||
// Benchmarks
|
||||
|
||||
func benchmarkErasureDecode(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
|
||||
setup, err := newErasureTestSetup(data, parity, blockSizeV2)
|
||||
setup, err := newErasureTestSetup(b, data, parity, blockSizeV2)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create test setup: %v", err)
|
||||
}
|
||||
defer setup.Remove()
|
||||
disks := setup.disks
|
||||
erasure, err := NewErasure(context.Background(), data, parity, blockSizeV2)
|
||||
if err != nil {
|
||||
|
||||
@@ -87,21 +87,19 @@ var erasureEncodeTests = []struct {
|
||||
|
||||
func TestErasureEncode(t *testing.T) {
|
||||
for i, test := range erasureEncodeTests {
|
||||
setup, err := newErasureTestSetup(test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
setup, err := newErasureTestSetup(t, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
||||
}
|
||||
disks := setup.disks
|
||||
erasure, err := NewErasure(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||
}
|
||||
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
||||
|
||||
data := make([]byte, test.data)
|
||||
if _, err = io.ReadFull(rand.Reader, data); err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
|
||||
}
|
||||
writers := make([]io.Writer, len(disks))
|
||||
@@ -160,18 +158,16 @@ func TestErasureEncode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
setup.Remove()
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmarks
|
||||
|
||||
func benchmarkErasureEncode(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
|
||||
setup, err := newErasureTestSetup(data, parity, blockSizeV2)
|
||||
setup, err := newErasureTestSetup(b, data, parity, blockSizeV2)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create test setup: %v", err)
|
||||
}
|
||||
defer setup.Remove()
|
||||
erasure, err := NewErasure(context.Background(), data, parity, blockSizeV2)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create ErasureStorage: %v", err)
|
||||
@@ -196,7 +192,10 @@ func benchmarkErasureEncode(data, parity, dataDown, parityDown int, size int64,
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
disk.Delete(context.Background(), "testbucket", "object", false)
|
||||
disk.Delete(context.Background(), "testbucket", "object", DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", erasure.ShardFileSize(size), DefaultBitrotAlgorithm, erasure.ShardSize())
|
||||
}
|
||||
_, err := erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1)
|
||||
|
||||
@@ -70,19 +70,17 @@ func TestErasureHeal(t *testing.T) {
|
||||
}
|
||||
|
||||
// create some test data
|
||||
setup, err := newErasureTestSetup(test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
|
||||
setup, err := newErasureTestSetup(t, test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to setup Erasure environment: %v", i, err)
|
||||
}
|
||||
disks := setup.disks
|
||||
erasure, err := NewErasure(context.Background(), test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||
}
|
||||
data := make([]byte, test.size)
|
||||
if _, err = io.ReadFull(rand.Reader, data); err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
||||
}
|
||||
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
||||
@@ -93,7 +91,6 @@ func TestErasureHeal(t *testing.T) {
|
||||
_, err = erasure.Encode(context.Background(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1)
|
||||
closeBitrotWriters(writers)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
||||
}
|
||||
|
||||
@@ -156,6 +153,5 @@ func TestErasureHeal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
setup.Remove()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,9 +180,15 @@ func listOnlineDisks(disks []StorageAPI, partsMetadata []FileInfo, errs []error)
|
||||
}
|
||||
|
||||
// Returns the latest updated FileInfo files and error in case of failure.
|
||||
func getLatestFileInfo(ctx context.Context, partsMetadata []FileInfo, errs []error) (FileInfo, error) {
|
||||
func getLatestFileInfo(ctx context.Context, partsMetadata []FileInfo, defaultParityCount int, errs []error) (FileInfo, error) {
|
||||
// There should be atleast half correct entries, if not return failure
|
||||
reducedErr := reduceReadQuorumErrs(ctx, errs, objectOpIgnoredErrs, len(partsMetadata)/2)
|
||||
expectedRQuorum := len(partsMetadata) / 2
|
||||
if defaultParityCount == 0 {
|
||||
// if parity count is '0', we expected all entries to be present.
|
||||
expectedRQuorum = len(partsMetadata)
|
||||
}
|
||||
|
||||
reducedErr := reduceReadQuorumErrs(ctx, errs, objectOpIgnoredErrs, expectedRQuorum)
|
||||
if reducedErr != nil {
|
||||
return FileInfo{}, reducedErr
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ func TestListOnlineDisks(t *testing.T) {
|
||||
}
|
||||
|
||||
bucket := "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket %v", err)
|
||||
}
|
||||
@@ -194,7 +194,7 @@ func TestListOnlineDisks(t *testing.T) {
|
||||
}
|
||||
|
||||
partsMetadata, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
fi, err := getLatestFileInfo(ctx, partsMetadata, errs)
|
||||
fi, err := getLatestFileInfo(ctx, partsMetadata, z.serverPools[0].sets[0].defaultParityCount, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo %v", err)
|
||||
}
|
||||
@@ -218,7 +218,10 @@ func TestListOnlineDisks(t *testing.T) {
|
||||
// and check if that disk
|
||||
// appears in outDatedDisks.
|
||||
tamperedIndex = index
|
||||
dErr := erasureDisks[index].Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
||||
dErr := erasureDisks[index].Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
if dErr != nil {
|
||||
t.Fatalf("Failed to delete %s - %v", filepath.Join(object, "part.1"), dErr)
|
||||
}
|
||||
@@ -348,7 +351,7 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
|
||||
}
|
||||
|
||||
bucket := "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket %v", err)
|
||||
}
|
||||
@@ -367,7 +370,7 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
|
||||
}
|
||||
|
||||
partsMetadata, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", true)
|
||||
_, err = getLatestFileInfo(ctx, partsMetadata, errs)
|
||||
_, err = getLatestFileInfo(ctx, partsMetadata, z.serverPools[0].sets[0].defaultParityCount, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo %v", err)
|
||||
}
|
||||
@@ -395,7 +398,10 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
|
||||
// and check if that disk
|
||||
// appears in outDatedDisks.
|
||||
tamperedIndex = index
|
||||
dErr := erasureDisks[index].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||
dErr := erasureDisks[index].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
if dErr != nil {
|
||||
t.Fatalf("Failed to delete %s - %v", pathJoin(object, xlStorageFormatFile), dErr)
|
||||
}
|
||||
@@ -423,7 +429,7 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
|
||||
|
||||
}
|
||||
partsMetadata, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", true)
|
||||
fi, err := getLatestFileInfo(ctx, partsMetadata, errs)
|
||||
fi, err := getLatestFileInfo(ctx, partsMetadata, z.serverPools[0].sets[0].defaultParityCount, errs)
|
||||
if !errors.Is(err, errErasureReadQuorum) {
|
||||
t.Fatalf("Failed to getLatestFileInfo, expected %v, got %v", errErasureReadQuorum, err)
|
||||
}
|
||||
@@ -466,7 +472,7 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
z := obj.(*erasureServerPools)
|
||||
s := z.serverPools[0].sets[0]
|
||||
erasureDisks := s.getDisks()
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket %v", err)
|
||||
}
|
||||
@@ -489,7 +495,7 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
t.Fatalf("Failed to read xl meta data %v", err)
|
||||
}
|
||||
|
||||
fi, err := getLatestFileInfo(ctx, partsMetadata, errs)
|
||||
fi, err := getLatestFileInfo(ctx, partsMetadata, s.defaultParityCount, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get quorum consistent fileInfo %v", err)
|
||||
}
|
||||
|
||||
@@ -500,7 +500,9 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
for partIndex := 0; partIndex < len(latestMeta.Parts); partIndex++ {
|
||||
partSize := latestMeta.Parts[partIndex].Size
|
||||
partActualSize := latestMeta.Parts[partIndex].ActualSize
|
||||
partModTime := latestMeta.Parts[partIndex].ModTime
|
||||
partNumber := latestMeta.Parts[partIndex].Number
|
||||
partIdx := latestMeta.Parts[partIndex].Index
|
||||
tillOffset := erasure.ShardFileOffset(0, partSize, partSize)
|
||||
readers := make([]io.ReaderAt, len(latestDisks))
|
||||
checksumAlgo := erasureInfo.GetChecksumInfo(partNumber).Algorithm
|
||||
@@ -550,7 +552,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
}
|
||||
|
||||
partsMetadata[i].DataDir = dstDataDir
|
||||
partsMetadata[i].AddObjectPart(partNumber, "", partSize, partActualSize)
|
||||
partsMetadata[i].AddObjectPart(partNumber, "", partSize, partActualSize, partModTime, partIdx)
|
||||
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{
|
||||
PartNumber: partNumber,
|
||||
Algorithm: checksumAlgo,
|
||||
@@ -655,7 +657,10 @@ func (er erasureObjects) healObjectDir(ctx context.Context, bucket, object strin
|
||||
wg.Add(1)
|
||||
go func(index int, disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
_ = disk.Delete(ctx, bucket, object, false)
|
||||
_ = disk.Delete(ctx, bucket, object, DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
}(index, disk)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
+63
-30
@@ -236,7 +236,7 @@ func TestHealing(t *testing.T) {
|
||||
er := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -359,7 +359,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
||||
object := getRandomObjectName()
|
||||
data := bytes.Repeat([]byte("a"), 128*1024)
|
||||
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -554,7 +554,7 @@ func TestHealCorrectQuorum(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("a"), 5*1024*1024)
|
||||
var opts ObjectOptions
|
||||
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -580,7 +580,7 @@ func TestHealCorrectQuorum(t *testing.T) {
|
||||
|
||||
_, err = objLayer.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to complete multipart upload - %v", err)
|
||||
t.Fatalf("Failed to complete multipart upload - got: %v", err)
|
||||
}
|
||||
|
||||
cfgFile := pathJoin(bucketMetaPrefix, bucket, ".test.bin")
|
||||
@@ -601,7 +601,7 @@ func TestHealCorrectQuorum(t *testing.T) {
|
||||
erasureDisks := er.getDisks()
|
||||
|
||||
fileInfos, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
nfi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||
nfi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
|
||||
if errors.Is(err, errFileNotFound) {
|
||||
continue
|
||||
}
|
||||
@@ -610,7 +610,10 @@ func TestHealCorrectQuorum(t *testing.T) {
|
||||
}
|
||||
|
||||
for i := 0; i < nfi.Erasure.ParityBlocks; i++ {
|
||||
erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||
erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Try healing now, it should heal the content properly.
|
||||
@@ -625,7 +628,7 @@ func TestHealCorrectQuorum(t *testing.T) {
|
||||
}
|
||||
|
||||
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, minioMetaBucket, cfgFile, "", false)
|
||||
nfi, err = getLatestFileInfo(ctx, fileInfos, errs)
|
||||
nfi, err = getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
|
||||
if errors.Is(err, errFileNotFound) {
|
||||
continue
|
||||
}
|
||||
@@ -634,7 +637,10 @@ func TestHealCorrectQuorum(t *testing.T) {
|
||||
}
|
||||
|
||||
for i := 0; i < nfi.Erasure.ParityBlocks; i++ {
|
||||
erasureDisks[i].Delete(context.Background(), minioMetaBucket, pathJoin(cfgFile, xlStorageFormatFile), false)
|
||||
erasureDisks[i].Delete(context.Background(), minioMetaBucket, pathJoin(cfgFile, xlStorageFormatFile), DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Try healing now, it should heal the content properly.
|
||||
@@ -679,7 +685,7 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("a"), 5*1024*1024)
|
||||
var opts ObjectOptions
|
||||
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -714,7 +720,10 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
||||
er := set.sets[0]
|
||||
erasureDisks := er.getDisks()
|
||||
firstDisk := erasureDisks[0]
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete a file - %v", err)
|
||||
}
|
||||
@@ -725,7 +734,7 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
||||
}
|
||||
|
||||
fileInfos, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
fi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||
fi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
@@ -734,7 +743,10 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
||||
t.Errorf("Expected xl.meta file to be present but stat failed - %v", err)
|
||||
}
|
||||
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Failure during deleting part.1 - %v", err)
|
||||
}
|
||||
@@ -750,7 +762,7 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
||||
}
|
||||
|
||||
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
nfi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||
nfi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
@@ -761,7 +773,10 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
||||
t.Fatalf("FileInfo not equal after healing: %v != %v", fi, nfi)
|
||||
}
|
||||
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Failure during deleting part.1 - %v", err)
|
||||
}
|
||||
@@ -778,7 +793,7 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
||||
}
|
||||
|
||||
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
nfi, err = getLatestFileInfo(ctx, fileInfos, errs)
|
||||
nfi, err = getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
@@ -792,7 +807,10 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
||||
// Test 4: checks if HealObject returns an error when xl.meta is not found
|
||||
// in more than read quorum number of disks, to create a corrupted situation.
|
||||
for i := 0; i <= nfi.Erasure.DataBlocks; i++ {
|
||||
erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||
erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Try healing now, expect to receive errFileNotFound.
|
||||
@@ -843,7 +861,7 @@ func TestHealObjectCorruptedXLMeta(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("a"), 5*1024*1024)
|
||||
var opts ObjectOptions
|
||||
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -879,12 +897,15 @@ func TestHealObjectCorruptedXLMeta(t *testing.T) {
|
||||
|
||||
// Test 1: Remove the object backend files from the first disk.
|
||||
fileInfos, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
fi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||
fi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete a file - %v", err)
|
||||
}
|
||||
@@ -899,7 +920,7 @@ func TestHealObjectCorruptedXLMeta(t *testing.T) {
|
||||
}
|
||||
|
||||
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
nfi1, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||
nfi1, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
@@ -922,7 +943,7 @@ func TestHealObjectCorruptedXLMeta(t *testing.T) {
|
||||
}
|
||||
|
||||
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
nfi2, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||
nfi2, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
@@ -936,7 +957,10 @@ func TestHealObjectCorruptedXLMeta(t *testing.T) {
|
||||
// Test 3: checks if HealObject returns an error when xl.meta is not found
|
||||
// in more than read quorum number of disks, to create a corrupted situation.
|
||||
for i := 0; i <= nfi2.Erasure.DataBlocks; i++ {
|
||||
erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||
erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Try healing now, expect to receive errFileNotFound.
|
||||
@@ -980,7 +1004,7 @@ func TestHealObjectCorruptedParts(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("a"), 5*1024*1024)
|
||||
var opts ObjectOptions
|
||||
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -1017,7 +1041,7 @@ func TestHealObjectCorruptedParts(t *testing.T) {
|
||||
secondDisk := erasureDisks[1]
|
||||
|
||||
fileInfos, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||
fi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||
fi, err := getLatestFileInfo(ctx, fileInfos, er.defaultParityCount, errs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||
}
|
||||
@@ -1033,7 +1057,10 @@ func TestHealObjectCorruptedParts(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test 1, remove part.1
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete a file - %v", err)
|
||||
}
|
||||
@@ -1078,7 +1105,10 @@ func TestHealObjectCorruptedParts(t *testing.T) {
|
||||
t.Fatalf("Failed to write a file - %v", err)
|
||||
}
|
||||
|
||||
err = secondDisk.Delete(context.Background(), bucket, object, true)
|
||||
err = secondDisk.Delete(context.Background(), bucket, object, DeleteOptions{
|
||||
Recursive: true,
|
||||
Force: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete a file - %v", err)
|
||||
}
|
||||
@@ -1131,7 +1161,7 @@ func TestHealObjectErasure(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("a"), 5*1024*1024)
|
||||
var opts ObjectOptions
|
||||
|
||||
err = obj.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -1166,7 +1196,10 @@ func TestHealObjectErasure(t *testing.T) {
|
||||
}
|
||||
|
||||
// Delete the whole object folder
|
||||
err = firstDisk.Delete(context.Background(), bucket, object, true)
|
||||
err = firstDisk.Delete(context.Background(), bucket, object, DeleteOptions{
|
||||
Recursive: true,
|
||||
Force: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete a file - %v", err)
|
||||
}
|
||||
@@ -1225,7 +1258,7 @@ func TestHealEmptyDirectoryErasure(t *testing.T) {
|
||||
object := "empty-dir/"
|
||||
var opts ObjectOptions
|
||||
|
||||
err = obj.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -1326,7 +1359,7 @@ func TestHealLastDataShard(t *testing.T) {
|
||||
}
|
||||
var opts ObjectOptions
|
||||
|
||||
err = obj.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
|
||||
@@ -237,12 +237,14 @@ func objectPartIndex(parts []ObjectPartInfo, partNumber int) int {
|
||||
}
|
||||
|
||||
// AddObjectPart - add a new object part in order.
|
||||
func (fi *FileInfo) AddObjectPart(partNumber int, partETag string, partSize int64, actualSize int64) {
|
||||
func (fi *FileInfo) AddObjectPart(partNumber int, partETag string, partSize, actualSize int64, modTime time.Time, idx []byte) {
|
||||
partInfo := ObjectPartInfo{
|
||||
Number: partNumber,
|
||||
ETag: partETag,
|
||||
Size: partSize,
|
||||
ActualSize: actualSize,
|
||||
ModTime: modTime,
|
||||
Index: idx,
|
||||
}
|
||||
|
||||
// Update part info if it already exists.
|
||||
@@ -398,17 +400,17 @@ func writeUniqueFileInfo(ctx context.Context, disks []StorageAPI, bucket, prefix
|
||||
// readQuorum is the min required disks to read data.
|
||||
// writeQuorum is the min required disks to write data.
|
||||
func objectQuorumFromMeta(ctx context.Context, partsMetaData []FileInfo, errs []error, defaultParityCount int) (objectReadQuorum, objectWriteQuorum int, err error) {
|
||||
if defaultParityCount == 0 {
|
||||
return 1, 1, nil
|
||||
}
|
||||
|
||||
// get the latest updated Metadata and a count of all the latest updated FileInfo(s)
|
||||
latestFileInfo, err := getLatestFileInfo(ctx, partsMetaData, errs)
|
||||
latestFileInfo, err := getLatestFileInfo(ctx, partsMetaData, defaultParityCount, errs)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if latestFileInfo.Deleted {
|
||||
// special case when parity is '0'
|
||||
if defaultParityCount == 0 {
|
||||
return len(partsMetaData), len(partsMetaData), nil
|
||||
}
|
||||
// For delete markers do not use 'defaultParityCount' as it is not expected to be the case.
|
||||
// Use maximum allowed read quorum instead, writeQuorum+1 is returned for compatibility sake
|
||||
// but there are no callers that shall be using this.
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
const ActualSize = 1000
|
||||
@@ -58,7 +58,7 @@ func TestAddObjectPart(t *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
if testCase.expectedIndex > -1 {
|
||||
partNumString := strconv.Itoa(testCase.partNum)
|
||||
fi.AddObjectPart(testCase.partNum, "etag."+partNumString, int64(testCase.partNum+humanize.MiByte), ActualSize)
|
||||
fi.AddObjectPart(testCase.partNum, "etag."+partNumString, int64(testCase.partNum+humanize.MiByte), ActualSize, UTCNow(), nil)
|
||||
}
|
||||
|
||||
if index := objectPartIndex(fi.Parts, testCase.partNum); index != testCase.expectedIndex {
|
||||
@@ -91,7 +91,7 @@ func TestObjectPartIndex(t *testing.T) {
|
||||
// Add some parts for testing.
|
||||
for _, testCase := range testCases {
|
||||
partNumString := strconv.Itoa(testCase.partNum)
|
||||
fi.AddObjectPart(testCase.partNum, "etag."+partNumString, int64(testCase.partNum+humanize.MiByte), ActualSize)
|
||||
fi.AddObjectPart(testCase.partNum, "etag."+partNumString, int64(testCase.partNum+humanize.MiByte), ActualSize, UTCNow(), nil)
|
||||
}
|
||||
|
||||
// Add failure test case.
|
||||
@@ -121,7 +121,7 @@ func TestObjectToPartOffset(t *testing.T) {
|
||||
// Total size of all parts is 5,242,899 bytes.
|
||||
for _, partNum := range []int{1, 2, 4, 5, 7} {
|
||||
partNumString := strconv.Itoa(partNum)
|
||||
fi.AddObjectPart(partNum, "etag."+partNumString, int64(partNum+humanize.MiByte), ActualSize)
|
||||
fi.AddObjectPart(partNum, "etag."+partNumString, int64(partNum+humanize.MiByte), ActualSize, UTCNow(), nil)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
@@ -160,7 +160,7 @@ func TestObjectToPartOffset(t *testing.T) {
|
||||
func TestFindFileInfoInQuorum(t *testing.T) {
|
||||
getNFInfo := func(n int, quorum int, t int64, dataDir string) []FileInfo {
|
||||
fi := newFileInfo("test", 8, 8)
|
||||
fi.AddObjectPart(1, "etag", 100, 100)
|
||||
fi.AddObjectPart(1, "etag", 100, 100, UTCNow(), nil)
|
||||
fi.ModTime = time.Unix(t, 0)
|
||||
fi.DataDir = dataDir
|
||||
fis := make([]FileInfo, n)
|
||||
|
||||
+252
-98
@@ -19,6 +19,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -48,7 +49,7 @@ func (er erasureObjects) getMultipartSHADir(bucket, object string) string {
|
||||
// checkUploadIDExists - verify if a given uploadID exists and is valid.
|
||||
func (er erasureObjects) checkUploadIDExists(ctx context.Context, bucket, object, uploadID string) (err error) {
|
||||
defer func() {
|
||||
if err == errFileNotFound {
|
||||
if errors.Is(err, errFileNotFound) || errors.Is(err, errVolumeNotFound) {
|
||||
err = errUploadIDNotFound
|
||||
}
|
||||
}()
|
||||
@@ -75,6 +76,30 @@ func (er erasureObjects) checkUploadIDExists(ctx context.Context, bucket, object
|
||||
return err
|
||||
}
|
||||
|
||||
// Removes part.meta given by partName belonging to a mulitpart upload from minioMetaBucket
|
||||
func (er erasureObjects) removePartMeta(bucket, object, uploadID, dataDir string, partNumber int) {
|
||||
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
|
||||
curpartPath := pathJoin(uploadIDPath, dataDir, fmt.Sprintf("part.%d", partNumber))
|
||||
storageDisks := er.getDisks()
|
||||
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
for index, disk := range storageDisks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
_ = storageDisks[index].Delete(context.TODO(), minioMetaMultipartBucket, curpartPath+".meta", DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
g.Wait()
|
||||
}
|
||||
|
||||
// Removes part given by partName belonging to a mulitpart upload from minioMetaBucket
|
||||
func (er erasureObjects) removeObjectPart(bucket, object, uploadID, dataDir string, partNumber int) {
|
||||
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
|
||||
@@ -91,7 +116,15 @@ func (er erasureObjects) removeObjectPart(bucket, object, uploadID, dataDir stri
|
||||
// Ignoring failure to remove parts that weren't present in CompleteMultipartUpload
|
||||
// requests. xl.meta is the authoritative source of truth on which parts constitute
|
||||
// the object. The presence of parts that don't belong in the object doesn't affect correctness.
|
||||
_ = storageDisks[index].Delete(context.TODO(), minioMetaMultipartBucket, curpartPath, false)
|
||||
_ = storageDisks[index].Delete(context.TODO(), minioMetaMultipartBucket, curpartPath, DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
_ = storageDisks[index].Delete(context.TODO(), minioMetaMultipartBucket, curpartPath+".meta", DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
@@ -138,7 +171,10 @@ func (er erasureObjects) deleteAll(ctx context.Context, bucket, prefix string) {
|
||||
wg.Add(1)
|
||||
go func(disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
disk.Delete(ctx, bucket, prefix, true)
|
||||
disk.Delete(ctx, bucket, prefix, DeleteOptions{
|
||||
Recursive: true,
|
||||
Force: false,
|
||||
})
|
||||
}(disk)
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -202,10 +238,10 @@ func (er erasureObjects) ListMultipartUploads(ctx context.Context, bucket, objec
|
||||
for _, disk = range er.getLoadBalancedDisks(true) {
|
||||
uploadIDs, err = disk.ListDir(ctx, minioMetaMultipartBucket, er.getMultipartSHADir(bucket, object), -1)
|
||||
if err != nil {
|
||||
if err == errDiskNotFound {
|
||||
if errors.Is(err, errDiskNotFound) {
|
||||
continue
|
||||
}
|
||||
if err == errFileNotFound {
|
||||
if errors.Is(err, errFileNotFound) || errors.Is(err, errVolumeNotFound) {
|
||||
return result, nil
|
||||
}
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -230,7 +266,11 @@ func (er erasureObjects) ListMultipartUploads(ctx context.Context, bucket, objec
|
||||
}
|
||||
fi, err := disk.ReadVersion(ctx, minioMetaMultipartBucket, pathJoin(er.getUploadIDDir(bucket, object, uploadID)), "", false)
|
||||
if err != nil {
|
||||
return result, toObjectErr(err, bucket, object)
|
||||
if !IsErrIgnored(err, errFileNotFound, errDiskNotFound) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
// Ignore this invalid upload-id since we are listing here
|
||||
continue
|
||||
}
|
||||
populatedUploadIds.Add(uploadID)
|
||||
uploads = append(uploads, MultipartInfo{
|
||||
@@ -442,6 +482,47 @@ func renamePart(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, ds
|
||||
return evalDisks(disks, errs), err
|
||||
}
|
||||
|
||||
// writeAllDisks - writes 'b' to all provided disks.
|
||||
// If write cannot reach quorum, the files will be deleted from all disks.
|
||||
func writeAllDisks(ctx context.Context, disks []StorageAPI, dstBucket, dstEntry string, b []byte, writeQuorum int) ([]StorageAPI, error) {
|
||||
g := errgroup.WithNErrs(len(disks))
|
||||
|
||||
// Write file to all underlying storage disks.
|
||||
for index := range disks {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if disks[index] == nil {
|
||||
return errDiskNotFound
|
||||
}
|
||||
return disks[index].WriteAll(ctx, dstBucket, dstEntry, b)
|
||||
}, index)
|
||||
}
|
||||
|
||||
// Wait for all renames to finish.
|
||||
errs := g.Wait()
|
||||
|
||||
// We can safely allow RenameFile errors up to len(er.getDisks()) - writeQuorum
|
||||
// otherwise return failure. Cleanup successful renames.
|
||||
err := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||
if err == errErasureWriteQuorum {
|
||||
// Remove all written
|
||||
g := errgroup.WithNErrs(len(disks))
|
||||
for index := range disks {
|
||||
if disks[index] == nil || errs[index] != nil {
|
||||
continue
|
||||
}
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
return disks[index].Delete(ctx, dstBucket, dstEntry, DeleteOptions{Force: true})
|
||||
}, index)
|
||||
}
|
||||
// Ignore these errors.
|
||||
g.WaitErr()
|
||||
}
|
||||
|
||||
return evalDisks(disks, errs), err
|
||||
}
|
||||
|
||||
// PutObjectPart - reads incoming stream and internally erasure codes
|
||||
// them. This call is similar to single put operation but it is part
|
||||
// of the multipart transaction.
|
||||
@@ -468,11 +549,7 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
return PartInfo{}, err
|
||||
}
|
||||
rctx := rlkctx.Context()
|
||||
defer func() {
|
||||
if uploadIDRLock != nil {
|
||||
uploadIDRLock.RUnlock(rlkctx.Cancel)
|
||||
}
|
||||
}()
|
||||
defer uploadIDRLock.RUnlock(rlkctx.Cancel)
|
||||
|
||||
data := r.Reader
|
||||
// Validate input data size and it can never be less than zero.
|
||||
@@ -496,10 +573,6 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
partsMetadata, errs = readAllFileInfo(rctx, storageDisks, minioMetaMultipartBucket,
|
||||
uploadIDPath, "", false)
|
||||
|
||||
// Unlock upload id locks before, so others can get it.
|
||||
uploadIDRLock.RUnlock(rlkctx.Cancel)
|
||||
uploadIDRLock = nil
|
||||
|
||||
// get Quorum for this object
|
||||
_, writeQuorum, err := objectQuorumFromMeta(pctx, partsMetadata, errs, er.defaultParityCount)
|
||||
if err != nil {
|
||||
@@ -606,79 +679,50 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire write lock to update metadata.
|
||||
uploadIDWLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
wlkctx, err := uploadIDWLock.GetLock(pctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return PartInfo{}, err
|
||||
}
|
||||
wctx := wlkctx.Context()
|
||||
defer uploadIDWLock.Unlock(wlkctx.Cancel)
|
||||
|
||||
// Validates if upload ID exists.
|
||||
if err = er.checkUploadIDExists(wctx, bucket, object, uploadID); err != nil {
|
||||
return pi, toObjectErr(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
// Rename temporary part file to its final location.
|
||||
partPath := pathJoin(uploadIDPath, fi.DataDir, partSuffix)
|
||||
onlineDisks, err = renamePart(wctx, onlineDisks, minioMetaTmpBucket, tmpPartPath, minioMetaMultipartBucket, partPath, writeQuorum)
|
||||
onlineDisks, err = renamePart(ctx, onlineDisks, minioMetaTmpBucket, tmpPartPath, minioMetaMultipartBucket, partPath, writeQuorum)
|
||||
if err != nil {
|
||||
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
||||
}
|
||||
|
||||
// Read metadata again because it might be updated with parallel upload of another part.
|
||||
partsMetadata, errs = readAllFileInfo(wctx, onlineDisks, minioMetaMultipartBucket, uploadIDPath, "", false)
|
||||
reducedErr = reduceWriteQuorumErrs(wctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||
if reducedErr == errErasureWriteQuorum {
|
||||
return pi, toObjectErr(reducedErr, bucket, object)
|
||||
}
|
||||
|
||||
// Get current highest version based on re-read partsMetadata.
|
||||
onlineDisks, modTime = listOnlineDisks(onlineDisks, partsMetadata, errs)
|
||||
|
||||
// Pick one from the first valid metadata.
|
||||
fi, err = pickValidFileInfo(wctx, partsMetadata, modTime, writeQuorum)
|
||||
if err != nil {
|
||||
return pi, err
|
||||
}
|
||||
|
||||
// Once part is successfully committed, proceed with updating erasure metadata.
|
||||
fi.ModTime = UTCNow()
|
||||
|
||||
md5hex := r.MD5CurrentHexString()
|
||||
|
||||
// Add the current part.
|
||||
fi.AddObjectPart(partID, md5hex, n, data.ActualSize())
|
||||
|
||||
for i, disk := range onlineDisks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
partsMetadata[i].Size = fi.Size
|
||||
partsMetadata[i].ModTime = fi.ModTime
|
||||
partsMetadata[i].Parts = fi.Parts
|
||||
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{
|
||||
PartNumber: partID,
|
||||
Algorithm: DefaultBitrotAlgorithm,
|
||||
Hash: bitrotWriterSum(writers[i]),
|
||||
})
|
||||
if opts.PreserveETag != "" {
|
||||
md5hex = opts.PreserveETag
|
||||
}
|
||||
|
||||
// Writes update `xl.meta` format for each disk.
|
||||
if _, err = writeUniqueFileInfo(wctx, onlineDisks, minioMetaMultipartBucket, uploadIDPath, partsMetadata, writeQuorum); err != nil {
|
||||
return pi, toObjectErr(err, minioMetaMultipartBucket, uploadIDPath)
|
||||
var index []byte
|
||||
if opts.IndexCB != nil {
|
||||
index = opts.IndexCB()
|
||||
}
|
||||
|
||||
online = countOnlineDisks(onlineDisks)
|
||||
part := ObjectPartInfo{
|
||||
Number: partID,
|
||||
ETag: md5hex,
|
||||
Size: n,
|
||||
ActualSize: data.ActualSize(),
|
||||
ModTime: UTCNow(),
|
||||
Index: index,
|
||||
}
|
||||
|
||||
partMsg, err := part.MarshalMsg(nil)
|
||||
if err != nil {
|
||||
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
||||
}
|
||||
|
||||
// Write part metadata to all disks.
|
||||
onlineDisks, err = writeAllDisks(ctx, onlineDisks, minioMetaMultipartBucket, partPath+".meta", partMsg, writeQuorum)
|
||||
if err != nil {
|
||||
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
||||
}
|
||||
|
||||
// Return success.
|
||||
return PartInfo{
|
||||
PartNumber: partID,
|
||||
ETag: md5hex,
|
||||
LastModified: fi.ModTime,
|
||||
Size: n,
|
||||
ActualSize: data.ActualSize(),
|
||||
PartNumber: part.Number,
|
||||
ETag: part.ETag,
|
||||
LastModified: part.ModTime,
|
||||
Size: part.Size,
|
||||
ActualSize: part.ActualSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -686,6 +730,7 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
// by callers to verify object states
|
||||
// - encrypted
|
||||
// - compressed
|
||||
// Does not contain currently uploaded parts by design.
|
||||
func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (MultipartInfo, error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
@@ -777,7 +822,7 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
||||
return result, toObjectErr(reducedErr, minioMetaMultipartBucket, uploadIDPath)
|
||||
}
|
||||
|
||||
_, modTime := listOnlineDisks(storageDisks, partsMetadata, errs)
|
||||
onlineDisks, modTime := listOnlineDisks(storageDisks, partsMetadata, errs)
|
||||
|
||||
// Pick one from the first valid metadata.
|
||||
fi, err := pickValidFileInfo(ctx, partsMetadata, modTime, writeQuorum)
|
||||
@@ -785,6 +830,41 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
||||
return result, err
|
||||
}
|
||||
|
||||
if maxParts == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if partNumberMarker < 0 {
|
||||
partNumberMarker = 0
|
||||
}
|
||||
|
||||
// Limit output to maxPartsList.
|
||||
if maxParts > maxPartsList-partNumberMarker {
|
||||
maxParts = maxPartsList - partNumberMarker
|
||||
}
|
||||
|
||||
// Read Part info for all parts
|
||||
partPath := pathJoin(uploadIDPath, fi.DataDir) + "/"
|
||||
req := ReadMultipleReq{
|
||||
Bucket: minioMetaMultipartBucket,
|
||||
Prefix: partPath,
|
||||
MaxSize: 1 << 20, // Each part should realistically not be > 1MiB.
|
||||
MaxResults: maxParts + 1,
|
||||
}
|
||||
|
||||
start := partNumberMarker + 1
|
||||
end := start + maxParts
|
||||
|
||||
// Parts are 1 based, so index 0 is part one, etc.
|
||||
for i := start; i <= end; i++ {
|
||||
req.Files = append(req.Files, fmt.Sprintf("part.%d.meta", i))
|
||||
}
|
||||
|
||||
partInfoFiles, err := readMultipleFiles(ctx, onlineDisks, req, writeQuorum)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Populate the result stub.
|
||||
result.Bucket = bucket
|
||||
result.Object = object
|
||||
@@ -794,34 +874,49 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
||||
result.UserDefined = cloneMSS(fi.Metadata)
|
||||
|
||||
// For empty number of parts or maxParts as zero, return right here.
|
||||
if len(fi.Parts) == 0 || maxParts == 0 {
|
||||
if len(partInfoFiles) == 0 || maxParts == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Limit output to maxPartsList.
|
||||
if maxParts > maxPartsList {
|
||||
maxParts = maxPartsList
|
||||
var partI ObjectPartInfo
|
||||
for i, part := range partInfoFiles {
|
||||
partN := i + partNumberMarker + 1
|
||||
if part.Error != "" || !part.Exists {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := partI.UnmarshalMsg(part.Data)
|
||||
if err != nil {
|
||||
// Maybe crash or similar.
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if partN != partI.Number {
|
||||
logger.LogIf(ctx, fmt.Errorf("part.%d.meta has incorrect corresponding part number: expected %d, got %d", i+1, i+1, partI.Number))
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the current part.
|
||||
fi.AddObjectPart(partI.Number, partI.ETag, partI.Size, partI.ActualSize, partI.ModTime, partI.Index)
|
||||
}
|
||||
|
||||
// Only parts with higher part numbers will be listed.
|
||||
partIdx := objectPartIndex(fi.Parts, partNumberMarker)
|
||||
parts := fi.Parts
|
||||
if partIdx != -1 {
|
||||
parts = fi.Parts[partIdx+1:]
|
||||
}
|
||||
count := maxParts
|
||||
result.Parts = make([]PartInfo, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
result.Parts = append(result.Parts, PartInfo{
|
||||
PartNumber: part.Number,
|
||||
ETag: part.ETag,
|
||||
LastModified: fi.ModTime,
|
||||
LastModified: part.ModTime,
|
||||
ActualSize: part.ActualSize,
|
||||
Size: part.Size,
|
||||
})
|
||||
count--
|
||||
if count == 0 {
|
||||
if len(result.Parts) >= maxParts {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If listed entries are more than maxParts, we set IsTruncated as true.
|
||||
if len(parts) > len(result.Parts) {
|
||||
result.IsTruncated = true
|
||||
@@ -842,17 +937,17 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
||||
func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, parts []CompletePart, opts ObjectOptions) (oi ObjectInfo, err error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
// Hold read-locks to verify uploaded parts, also disallows
|
||||
// parallel part uploads as well.
|
||||
// Hold write locks to verify uploaded parts, also disallows any
|
||||
// parallel PutObjectPart() requests.
|
||||
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
rlkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
|
||||
wlkctx, err := uploadIDLock.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return oi, err
|
||||
}
|
||||
rctx := rlkctx.Context()
|
||||
defer uploadIDLock.RUnlock(rlkctx.Cancel)
|
||||
wctx := wlkctx.Context()
|
||||
defer uploadIDLock.Unlock(wlkctx.Cancel)
|
||||
|
||||
if err = er.checkUploadIDExists(rctx, bucket, object, uploadID); err != nil {
|
||||
if err = er.checkUploadIDExists(wctx, bucket, object, uploadID); err != nil {
|
||||
return oi, toObjectErr(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
@@ -861,15 +956,15 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
storageDisks := er.getDisks()
|
||||
|
||||
// Read metadata associated with the object from all disks.
|
||||
partsMetadata, errs := readAllFileInfo(rctx, storageDisks, minioMetaMultipartBucket, uploadIDPath, "", false)
|
||||
partsMetadata, errs := readAllFileInfo(wctx, storageDisks, minioMetaMultipartBucket, uploadIDPath, "", false)
|
||||
|
||||
// get Quorum for this object
|
||||
_, writeQuorum, err := objectQuorumFromMeta(rctx, partsMetadata, errs, er.defaultParityCount)
|
||||
_, writeQuorum, err := objectQuorumFromMeta(wctx, partsMetadata, errs, er.defaultParityCount)
|
||||
if err != nil {
|
||||
return oi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
reducedErr := reduceWriteQuorumErrs(rctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||
reducedErr := reduceWriteQuorumErrs(wctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||
if reducedErr == errErasureWriteQuorum {
|
||||
return oi, toObjectErr(reducedErr, bucket, object)
|
||||
}
|
||||
@@ -877,11 +972,63 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
onlineDisks, modTime := listOnlineDisks(storageDisks, partsMetadata, errs)
|
||||
|
||||
// Pick one from the first valid metadata.
|
||||
fi, err := pickValidFileInfo(rctx, partsMetadata, modTime, writeQuorum)
|
||||
fi, err := pickValidFileInfo(wctx, partsMetadata, modTime, writeQuorum)
|
||||
if err != nil {
|
||||
return oi, err
|
||||
}
|
||||
|
||||
// Read Part info for all parts
|
||||
partPath := pathJoin(uploadIDPath, fi.DataDir) + "/"
|
||||
req := ReadMultipleReq{
|
||||
Bucket: minioMetaMultipartBucket,
|
||||
Prefix: partPath,
|
||||
MaxSize: 1 << 20, // Each part should realistically not be > 1MiB.
|
||||
Files: make([]string, 0, len(parts)),
|
||||
AbortOn404: true,
|
||||
}
|
||||
for _, part := range parts {
|
||||
req.Files = append(req.Files, fmt.Sprintf("part.%d.meta", part.PartNumber))
|
||||
}
|
||||
partInfoFiles, err := readMultipleFiles(ctx, onlineDisks, req, writeQuorum)
|
||||
if err != nil {
|
||||
return oi, err
|
||||
}
|
||||
if len(partInfoFiles) != len(parts) {
|
||||
// Should only happen through internal error
|
||||
err := fmt.Errorf("unexpected part result count: %d, want %d", len(partInfoFiles), len(parts))
|
||||
logger.LogIf(ctx, err)
|
||||
return oi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
var partI ObjectPartInfo
|
||||
for i, part := range partInfoFiles {
|
||||
partID := parts[i].PartNumber
|
||||
if part.Error != "" || !part.Exists {
|
||||
return oi, InvalidPart{
|
||||
PartNumber: partID,
|
||||
}
|
||||
}
|
||||
|
||||
_, err := partI.UnmarshalMsg(part.Data)
|
||||
if err != nil {
|
||||
// Maybe crash or similar.
|
||||
logger.LogIf(ctx, err)
|
||||
return oi, InvalidPart{
|
||||
PartNumber: partID,
|
||||
}
|
||||
}
|
||||
|
||||
if partID != partI.Number {
|
||||
logger.LogIf(ctx, fmt.Errorf("part.%d.meta has incorrect corresponding part number: expected %d, got %d", partID, partID, partI.Number))
|
||||
return oi, InvalidPart{
|
||||
PartNumber: partID,
|
||||
}
|
||||
}
|
||||
|
||||
// Add the current part.
|
||||
fi.AddObjectPart(partI.Number, partI.ETag, partI.Size, partI.ActualSize, partI.ModTime, partI.Index)
|
||||
}
|
||||
|
||||
// Calculate full object size.
|
||||
var objectSize int64
|
||||
|
||||
@@ -921,7 +1068,7 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
return oi, invp
|
||||
}
|
||||
|
||||
// All parts except the last part has to be atleast 5MB.
|
||||
// All parts except the last part has to be at least 5MB.
|
||||
if (i < len(parts)-1) && !isMinAllowedPartSize(currentFI.Parts[partIdx].ActualSize) {
|
||||
return oi, PartTooSmall{
|
||||
PartNumber: part.PartNumber,
|
||||
@@ -941,6 +1088,8 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
Number: part.PartNumber,
|
||||
Size: currentFI.Parts[partIdx].Size,
|
||||
ActualSize: currentFI.Parts[partIdx].ActualSize,
|
||||
ModTime: currentFI.Parts[partIdx].ModTime,
|
||||
Index: currentFI.Parts[partIdx].Index,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -999,6 +1148,11 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
}
|
||||
}
|
||||
|
||||
// Remove part.meta which is not needed anymore.
|
||||
for _, part := range fi.Parts {
|
||||
er.removePartMeta(bucket, object, uploadID, fi.DataDir, part.Number)
|
||||
}
|
||||
|
||||
// Rename the multipart object to final location.
|
||||
if onlineDisks, err = renameData(ctx, onlineDisks, minioMetaMultipartBucket, uploadIDPath,
|
||||
partsMetadata, bucket, object, writeQuorum); err != nil {
|
||||
|
||||
+39
-29
@@ -534,6 +534,11 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
|
||||
return metaFileInfos, errs
|
||||
}
|
||||
|
||||
versionID := lfi.VersionID
|
||||
if versionID == "" {
|
||||
versionID = nullVersionID
|
||||
}
|
||||
|
||||
for index := range metadataArray {
|
||||
if metadataArray[index] == nil {
|
||||
continue
|
||||
@@ -541,21 +546,15 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
|
||||
|
||||
// make sure to preserve this for diskmtime based healing bugfix.
|
||||
diskMTime := metaFileInfos[index].DiskMTime
|
||||
metaFileInfos[index], errs[index] = metadataArray[index].ToFileInfo(bucket, object, "")
|
||||
metaFileInfos[index], errs[index] = metadataArray[index].ToFileInfo(bucket, object, versionID)
|
||||
if errs[index] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if metaFileInfos[index].IsValid() && metaFileInfos[index].ModTime.Equal(lfi.ModTime) {
|
||||
versionID := metaFileInfos[index].VersionID
|
||||
if versionID == "" {
|
||||
versionID = nullVersionID
|
||||
}
|
||||
if readData {
|
||||
metaFileInfos[index].Data = metadataArray[index].data.find(versionID)
|
||||
}
|
||||
metaFileInfos[index].DiskMTime = diskMTime
|
||||
if readData {
|
||||
metaFileInfos[index].Data = metadataArray[index].data.find(versionID)
|
||||
}
|
||||
metaFileInfos[index].DiskMTime = diskMTime
|
||||
}
|
||||
|
||||
// Return all the metadata.
|
||||
@@ -815,6 +814,12 @@ func (er erasureObjects) putMetacacheObject(ctx context.Context, key string, r *
|
||||
if n < data.Size() {
|
||||
return ObjectInfo{}, IncompleteBody{Bucket: minioMetaBucket, Object: key}
|
||||
}
|
||||
var index []byte
|
||||
if opts.IndexCB != nil {
|
||||
index = opts.IndexCB()
|
||||
}
|
||||
|
||||
modTime := UTCNow()
|
||||
|
||||
for i, w := range writers {
|
||||
if w == nil {
|
||||
@@ -823,7 +828,7 @@ func (er erasureObjects) putMetacacheObject(ctx context.Context, key string, r *
|
||||
continue
|
||||
}
|
||||
partsMetadata[i].Data = inlineBuffers[i].Bytes()
|
||||
partsMetadata[i].AddObjectPart(1, "", n, data.ActualSize())
|
||||
partsMetadata[i].AddObjectPart(1, "", n, data.ActualSize(), modTime, index)
|
||||
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{
|
||||
PartNumber: 1,
|
||||
Algorithm: DefaultBitrotAlgorithm,
|
||||
@@ -831,8 +836,6 @@ func (er erasureObjects) putMetacacheObject(ctx context.Context, key string, r *
|
||||
})
|
||||
}
|
||||
|
||||
modTime := UTCNow()
|
||||
|
||||
// Fill all the necessary metadata.
|
||||
// Update `xl.meta` content on each disks.
|
||||
for index := range partsMetadata {
|
||||
@@ -972,14 +975,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
switch size := data.Size(); {
|
||||
case size == 0:
|
||||
buffer = make([]byte, 1) // Allocate atleast a byte to reach EOF
|
||||
case size == -1:
|
||||
if size := data.ActualSize(); size > 0 && size < fi.Erasure.BlockSize {
|
||||
buffer = make([]byte, data.ActualSize()+256, data.ActualSize()*2+512)
|
||||
} else {
|
||||
buffer = er.bp.Get()
|
||||
defer er.bp.Put(buffer)
|
||||
}
|
||||
case size >= fi.Erasure.BlockSize:
|
||||
case size >= fi.Erasure.BlockSize || size == -1:
|
||||
buffer = er.bp.Get()
|
||||
defer er.bp.Put(buffer)
|
||||
case size < fi.Erasure.BlockSize:
|
||||
@@ -1071,6 +1067,10 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
return ObjectInfo{}, IncompleteBody{Bucket: bucket, Object: object}
|
||||
}
|
||||
|
||||
var compIndex []byte
|
||||
if opts.IndexCB != nil {
|
||||
compIndex = opts.IndexCB()
|
||||
}
|
||||
if !opts.NoLock {
|
||||
lk := er.NewNSLock(bucket, object)
|
||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||
@@ -1081,6 +1081,11 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
defer lk.Unlock(lkctx.Cancel)
|
||||
}
|
||||
|
||||
modTime := opts.MTime
|
||||
if opts.MTime.IsZero() {
|
||||
modTime = UTCNow()
|
||||
}
|
||||
|
||||
for i, w := range writers {
|
||||
if w == nil {
|
||||
onlineDisks[i] = nil
|
||||
@@ -1091,15 +1096,19 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
} else {
|
||||
partsMetadata[i].Data = nil
|
||||
}
|
||||
partsMetadata[i].AddObjectPart(1, "", n, data.ActualSize())
|
||||
partsMetadata[i].AddObjectPart(1, "", n, data.ActualSize(), modTime, compIndex)
|
||||
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{
|
||||
PartNumber: 1,
|
||||
Algorithm: DefaultBitrotAlgorithm,
|
||||
Hash: bitrotWriterSum(w),
|
||||
})
|
||||
}
|
||||
|
||||
if userDefined["etag"] == "" {
|
||||
userDefined["etag"] = r.MD5CurrentHexString()
|
||||
if opts.PreserveETag != "" {
|
||||
userDefined["etag"] = opts.PreserveETag
|
||||
}
|
||||
}
|
||||
|
||||
// Guess content-type from the extension if possible.
|
||||
@@ -1107,11 +1116,6 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
userDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
|
||||
}
|
||||
|
||||
modTime := opts.MTime
|
||||
if opts.MTime.IsZero() {
|
||||
modTime = UTCNow()
|
||||
}
|
||||
|
||||
// Fill all the necessary metadata.
|
||||
// Update `xl.meta` content on each disks.
|
||||
for index := range partsMetadata {
|
||||
@@ -1367,8 +1371,14 @@ func (er erasureObjects) deletePrefix(ctx context.Context, bucket, prefix string
|
||||
// Deletes
|
||||
// - The prefix and its children
|
||||
// - The prefix__XLDIR__
|
||||
defer disks[index].Delete(ctx, bucket, dirPrefix, true)
|
||||
return disks[index].Delete(ctx, bucket, prefix, true)
|
||||
defer disks[index].Delete(ctx, bucket, dirPrefix, DeleteOptions{
|
||||
Recursive: true,
|
||||
Force: true,
|
||||
})
|
||||
return disks[index].Delete(ctx, bucket, prefix, DeleteOptions{
|
||||
Recursive: true,
|
||||
Force: true,
|
||||
})
|
||||
}, index)
|
||||
}
|
||||
for _, err := range g.Wait() {
|
||||
|
||||
+24
-21
@@ -53,7 +53,7 @@ func TestRepeatPutObjectPart(t *testing.T) {
|
||||
defer objLayer.Shutdown(context.Background())
|
||||
defer removeRoots(disks)
|
||||
|
||||
err = objLayer.MakeBucketWithLocation(ctx, "bucket1", BucketOptions{})
|
||||
err = objLayer.MakeBucketWithLocation(ctx, "bucket1", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func TestErasureDeleteObjectBasic(t *testing.T) {
|
||||
}
|
||||
defer xl.Shutdown(context.Background())
|
||||
|
||||
err = xl.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
|
||||
err = xl.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -155,7 +155,7 @@ func TestDeleteObjectsVersioned(t *testing.T) {
|
||||
{bucketName, "dir/obj1"},
|
||||
}
|
||||
|
||||
err = obj.MakeBucketWithLocation(ctx, bucketName, BucketOptions{
|
||||
err = obj.MakeBucketWithLocation(ctx, bucketName, MakeBucketOptions{
|
||||
VersioningEnabled: true,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -244,7 +244,7 @@ func TestErasureDeleteObjectsErasureSet(t *testing.T) {
|
||||
{bucketName, "obj_4"},
|
||||
}
|
||||
|
||||
err := erasureSets.MakeBucketWithLocation(ctx, bucketName, BucketOptions{})
|
||||
err := erasureSets.MakeBucketWithLocation(ctx, bucketName, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -305,7 +305,7 @@ func TestErasureDeleteObjectDiskNotFound(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -374,7 +374,7 @@ func TestErasureDeleteObjectDiskNotFoundErasure4(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -434,7 +434,7 @@ func TestErasureDeleteObjectDiskNotFoundErr(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -505,7 +505,7 @@ func TestGetObjectNoQuorum(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -528,7 +528,10 @@ func TestGetObjectNoQuorum(t *testing.T) {
|
||||
files, _ := disk.ListDir(ctx, bucket, object, -1)
|
||||
for _, file := range files {
|
||||
if file != "xl.meta" {
|
||||
disk.Delete(ctx, bucket, pathJoin(object, file), true)
|
||||
disk.Delete(ctx, bucket, pathJoin(object, file), DeleteOptions{
|
||||
Recursive: true,
|
||||
Force: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -611,7 +614,7 @@ func TestHeadObjectNoQuorum(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -629,7 +632,10 @@ func TestHeadObjectNoQuorum(t *testing.T) {
|
||||
files, _ := disk.ListDir(ctx, bucket, object, -1)
|
||||
for _, file := range files {
|
||||
if file != "xl.meta" {
|
||||
disk.Delete(ctx, bucket, pathJoin(object, file), true)
|
||||
disk.Delete(ctx, bucket, pathJoin(object, file), DeleteOptions{
|
||||
Recursive: true,
|
||||
Force: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -686,7 +692,7 @@ func TestPutObjectNoQuorum(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -749,7 +755,7 @@ func TestPutObjectNoQuorumSmall(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -818,7 +824,7 @@ func TestPutObjectSmallInlineData(t *testing.T) {
|
||||
object := "object"
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -886,7 +892,7 @@ func testObjectQuorumFromMeta(obj ObjectLayer, instanceType string, dirs []strin
|
||||
ctx, cancel := context.WithCancel(GlobalContext)
|
||||
defer cancel()
|
||||
|
||||
err := obj.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
err := obj.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket %v", err)
|
||||
}
|
||||
@@ -1067,10 +1073,7 @@ func TestGetObjectInlineNotInline(t *testing.T) {
|
||||
// Create a backend with 4 disks named disk{1...4}, this name convention
|
||||
// because we will unzip some object data from a sample archive.
|
||||
const numDisks = 4
|
||||
path, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := t.TempDir()
|
||||
|
||||
var fsDirs []string
|
||||
for i := 1; i <= numDisks; i++ {
|
||||
@@ -1088,7 +1091,7 @@ func TestGetObjectInlineNotInline(t *testing.T) {
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
// Create a testbucket
|
||||
err = objLayer.MakeBucketWithLocation(ctx, "testbucket", BucketOptions{})
|
||||
err = objLayer.MakeBucketWithLocation(ctx, "testbucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1154,7 +1157,7 @@ func TestGetObjectWithOutdatedDisks(t *testing.T) {
|
||||
|
||||
for i, testCase := range testCases {
|
||||
// Step 1: create a bucket
|
||||
err = z.MakeBucketWithLocation(ctx, testCase.bucket, BucketOptions{VersioningEnabled: testCase.versioned})
|
||||
err = z.MakeBucketWithLocation(ctx, testCase.bucket, MakeBucketOptions{VersioningEnabled: testCase.versioned})
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to create a bucket: %v", i+1, err)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -30,6 +31,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/hash"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/console"
|
||||
@@ -523,14 +526,26 @@ func (z *erasureServerPools) Init(ctx context.Context) error {
|
||||
}
|
||||
if globalEndpoints[idx].Endpoints[0].IsLocal {
|
||||
go func(pool PoolStatus) {
|
||||
switch err := z.Decommission(ctx, pool.ID); err {
|
||||
case nil:
|
||||
// we already started decommission
|
||||
case errDecommissionAlreadyRunning:
|
||||
// A previous decommission running found restart it.
|
||||
z.doDecommissionInRoutine(ctx, idx)
|
||||
default:
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to resume decommission of pool %v: %w", pool, err))
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for {
|
||||
if err := z.Decommission(ctx, pool.ID); err != nil {
|
||||
switch err {
|
||||
// we already started decommission
|
||||
case errDecommissionAlreadyRunning:
|
||||
// A previous decommission running found restart it.
|
||||
z.doDecommissionInRoutine(ctx, idx)
|
||||
return
|
||||
default:
|
||||
if configRetriableErrors(err) {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to resume decommission of pool %v: %w: retrying..", pool, err))
|
||||
time.Sleep(time.Second + time.Duration(r.Float64()*float64(5*time.Second)))
|
||||
continue
|
||||
}
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to resume decommission of pool %v: %w", pool, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}(pool)
|
||||
}
|
||||
@@ -566,6 +581,11 @@ func (z *erasureServerPools) decommissionObject(ctx context.Context, bucket stri
|
||||
auditLogDecom(ctx, "DecomCopyData", objInfo.Bucket, objInfo.Name, objInfo.VersionID, err)
|
||||
}()
|
||||
|
||||
actualSize, err := objInfo.GetActualSize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if objInfo.isMultipart() {
|
||||
uploadID, err := z.NewMultipartUpload(ctx, bucket, objInfo.Name, ObjectOptions{
|
||||
VersionID: objInfo.VersionID,
|
||||
@@ -573,21 +593,26 @@ func (z *erasureServerPools) decommissionObject(ctx context.Context, bucket stri
|
||||
UserDefined: objInfo.UserDefined,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("decommissionObject: NewMultipartUpload() %w", err)
|
||||
}
|
||||
defer z.AbortMultipartUpload(ctx, bucket, objInfo.Name, uploadID, ObjectOptions{})
|
||||
parts := make([]CompletePart, len(objInfo.Parts))
|
||||
for i, part := range objInfo.Parts {
|
||||
hr, err := hash.NewReader(gr, part.Size, "", "", part.Size)
|
||||
hr, err := hash.NewReader(gr, part.Size, "", "", part.ActualSize)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("decommissionObject: hash.NewReader() %w", err)
|
||||
}
|
||||
pi, err := z.PutObjectPart(ctx, bucket, objInfo.Name, uploadID,
|
||||
part.Number,
|
||||
NewPutObjReader(hr),
|
||||
ObjectOptions{})
|
||||
ObjectOptions{
|
||||
PreserveETag: part.ETag, // Preserve original ETag to ensure same metadata.
|
||||
IndexCB: func() []byte {
|
||||
return part.Index // Preserve part Index to ensure decompression works.
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("decommissionObject: PutObjectPart() %w", err)
|
||||
}
|
||||
parts[i] = CompletePart{
|
||||
ETag: pi.ETag,
|
||||
@@ -597,21 +622,32 @@ func (z *erasureServerPools) decommissionObject(ctx context.Context, bucket stri
|
||||
_, err = z.CompleteMultipartUpload(ctx, bucket, objInfo.Name, uploadID, parts, ObjectOptions{
|
||||
MTime: objInfo.ModTime,
|
||||
})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("decommissionObject: CompleteMultipartUpload() %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
hr, err := hash.NewReader(gr, objInfo.Size, "", "", objInfo.Size)
|
||||
|
||||
hr, err := hash.NewReader(gr, objInfo.Size, "", "", actualSize)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("decommissionObject: hash.NewReader() %w", err)
|
||||
}
|
||||
_, err = z.PutObject(ctx,
|
||||
bucket,
|
||||
objInfo.Name,
|
||||
NewPutObjReader(hr),
|
||||
ObjectOptions{
|
||||
VersionID: objInfo.VersionID,
|
||||
MTime: objInfo.ModTime,
|
||||
UserDefined: objInfo.UserDefined,
|
||||
VersionID: objInfo.VersionID,
|
||||
MTime: objInfo.ModTime,
|
||||
UserDefined: objInfo.UserDefined,
|
||||
PreserveETag: objInfo.ETag, // Preserve original ETag to ensure same metadata.
|
||||
IndexCB: func() []byte {
|
||||
return objInfo.Parts[0].Index // Preserve part Index to ensure decompression works.
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("decommissionObject: PutObject() %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -647,6 +683,33 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
}
|
||||
|
||||
vc, _ := globalBucketVersioningSys.Get(bi.Name)
|
||||
|
||||
// Check if the current bucket has a configured lifecycle policy
|
||||
lc, _ := globalLifecycleSys.Get(bi.Name)
|
||||
|
||||
// Check if bucket is object locked.
|
||||
lr, _ := globalBucketObjectLockSys.Get(bi.Name)
|
||||
|
||||
filterLifecycle := func(bucket, object string, fi FileInfo) bool {
|
||||
if lc == nil {
|
||||
return false
|
||||
}
|
||||
versioned := vc != nil && vc.Versioned(object)
|
||||
objInfo := fi.ToObjectInfo(bucket, object, versioned)
|
||||
action := evalActionFromLifecycle(ctx, *lc, lr, objInfo, false)
|
||||
switch action {
|
||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
||||
globalExpiryState.enqueueByDays(objInfo, false, action == lifecycle.DeleteVersionAction)
|
||||
// Skip this entry.
|
||||
return true
|
||||
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||
globalExpiryState.enqueueByDays(objInfo, true, action == lifecycle.DeleteRestoredVersionAction)
|
||||
// Skip this entry.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
decommissionEntry := func(entry metaCacheEntry) {
|
||||
defer func() {
|
||||
<-parallelWorkers
|
||||
@@ -673,6 +736,13 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
logger.LogIf(ctx, fmt.Errorf("found %s/%s transitioned object, transitioned object won't be decommissioned", bi.Name, version.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply lifecycle rules on the objects that are expired.
|
||||
if filterLifecycle(bi.Name, version.Name, version) {
|
||||
logger.LogIf(ctx, fmt.Errorf("found %s/%s (%s) expired object based on ILM rules, skipping and scheduled for deletion", bi.Name, version.Name, version.VersionID))
|
||||
continue
|
||||
}
|
||||
|
||||
// We will skip decommissioning delete markers
|
||||
// with single version, its as good as there
|
||||
// is no data associated with the object.
|
||||
@@ -680,16 +750,18 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
logger.LogIf(ctx, fmt.Errorf("found %s/%s delete marked object with no other versions, skipping since there is no content left", bi.Name, version.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
if version.Deleted {
|
||||
_, err := z.DeleteObject(ctx,
|
||||
bi.Name,
|
||||
version.Name,
|
||||
ObjectOptions{
|
||||
Versioned: vc.PrefixEnabled(version.Name),
|
||||
VersionID: version.VersionID,
|
||||
MTime: version.ModTime,
|
||||
DeleteReplication: version.ReplicationState,
|
||||
DeleteMarker: true, // make sure we create a delete marker
|
||||
Versioned: vc.PrefixEnabled(version.Name),
|
||||
VersionID: version.VersionID,
|
||||
MTime: version.ModTime,
|
||||
DeleteReplication: version.ReplicationState,
|
||||
DeleteMarker: true, // make sure we create a delete marker
|
||||
SkipDecommissioned: true, // make sure we skip the decommissioned pool
|
||||
})
|
||||
var failure bool
|
||||
if err != nil {
|
||||
@@ -716,7 +788,8 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
http.Header{},
|
||||
noLock, // all mutations are blocked reads are safe without locks.
|
||||
ObjectOptions{
|
||||
VersionID: version.VersionID,
|
||||
VersionID: version.VersionID,
|
||||
NoDecryption: true,
|
||||
})
|
||||
if isErrObjectNotFound(err) {
|
||||
// object deleted by the application, nothing to do here we move on.
|
||||
@@ -791,7 +864,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
wg.Add(1)
|
||||
go decommissionEntry(entry)
|
||||
},
|
||||
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
|
||||
partial: func(entries metaCacheEntries, _ []error) {
|
||||
entry, ok := entries.resolve(&resolver)
|
||||
if ok {
|
||||
parallelWorkers <- struct{}{}
|
||||
@@ -912,15 +985,9 @@ func (z *erasureServerPools) getDecommissionPoolSpaceInfo(idx int) (pi poolSpace
|
||||
if idx+1 > len(z.serverPools) {
|
||||
return pi, errInvalidArgument
|
||||
}
|
||||
|
||||
info, _ := z.serverPools[idx].StorageInfo(context.Background())
|
||||
info.Backend = z.BackendInfo()
|
||||
for _, disk := range info.Disks {
|
||||
if disk.Healing {
|
||||
return pi, decomError{
|
||||
Err: fmt.Sprintf("%s drive is healing, decommission will not be started", disk.Endpoint),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usableTotal := int64(GetTotalUsableCapacity(info.Disks, info))
|
||||
usableFree := int64(GetTotalUsableCapacityFree(info.Disks, info))
|
||||
@@ -984,7 +1051,9 @@ func (z *erasureServerPools) DecommissionCancel(ctx context.Context, idx int) (e
|
||||
defer z.poolMetaMutex.Unlock()
|
||||
|
||||
if z.poolMeta.DecommissionCancel(idx) {
|
||||
defer z.decommissionCancelers[idx]() // cancel any active thread.
|
||||
if fn := z.decommissionCancelers[idx]; fn != nil {
|
||||
defer fn() // cancel any active thread.
|
||||
}
|
||||
if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1006,7 +1075,9 @@ func (z *erasureServerPools) DecommissionFailed(ctx context.Context, idx int) (e
|
||||
defer z.poolMetaMutex.Unlock()
|
||||
|
||||
if z.poolMeta.DecommissionFailed(idx) {
|
||||
defer z.decommissionCancelers[idx]() // cancel any active thread.
|
||||
if fn := z.decommissionCancelers[idx]; fn != nil {
|
||||
defer fn() // cancel any active thread.
|
||||
}
|
||||
if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1028,7 +1099,9 @@ func (z *erasureServerPools) CompleteDecommission(ctx context.Context, idx int)
|
||||
defer z.poolMetaMutex.Unlock()
|
||||
|
||||
if z.poolMeta.DecommissionComplete(idx) {
|
||||
defer z.decommissionCancelers[idx]() // cancel any active thread.
|
||||
if fn := z.decommissionCancelers[idx]; fn != nil {
|
||||
defer fn() // cancel any active thread.
|
||||
}
|
||||
if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1046,11 +1119,18 @@ func (z *erasureServerPools) StartDecommission(ctx context.Context, idx int) (er
|
||||
return errInvalidArgument
|
||||
}
|
||||
|
||||
buckets, err := z.ListBuckets(ctx)
|
||||
buckets, err := z.ListBuckets(ctx, BucketOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure to heal the buckets to ensure the new
|
||||
// pool has the new buckets, this is to avoid
|
||||
// failures later.
|
||||
for _, bucket := range buckets {
|
||||
z.HealBucket(ctx, bucket.Name, madmin.HealOpts{})
|
||||
}
|
||||
|
||||
decomBuckets := make([]decomBucketInfo, len(buckets))
|
||||
for i := range buckets {
|
||||
decomBuckets[i] = decomBucketInfo{
|
||||
@@ -1077,7 +1157,7 @@ func (z *erasureServerPools) StartDecommission(ctx context.Context, idx int) (er
|
||||
pathJoin(minioMetaBucket, bucketMetaPrefix),
|
||||
} {
|
||||
var bucketExists BucketExists
|
||||
if err = z.MakeBucketWithLocation(ctx, metaBucket, BucketOptions{}); err != nil {
|
||||
if err = z.MakeBucketWithLocation(ctx, metaBucket, MakeBucketOptions{}); err != nil {
|
||||
if !errors.As(err, &bucketExists) {
|
||||
return err
|
||||
}
|
||||
@@ -1132,7 +1212,7 @@ func auditLogDecom(ctx context.Context, apiName, bucket, object, versionID strin
|
||||
errStr = err.Error()
|
||||
}
|
||||
auditLogInternal(ctx, bucket, object, AuditLogOptions{
|
||||
Trigger: "decommissioning",
|
||||
Event: "decommission",
|
||||
APIName: apiName,
|
||||
VersionID: versionID,
|
||||
Error: errStr,
|
||||
|
||||
+86
-37
@@ -427,8 +427,9 @@ func (z *erasureServerPools) getPoolIdxExistingWithOpts(ctx context.Context, buc
|
||||
})
|
||||
|
||||
for _, pinfo := range poolObjInfos {
|
||||
// skip all objects from suspended pools for mutating calls.
|
||||
if z.IsSuspended(pinfo.PoolIndex) && opts.Mutate {
|
||||
// skip all objects from suspended pools if asked by the
|
||||
// caller.
|
||||
if z.IsSuspended(pinfo.PoolIndex) && opts.SkipDecommissioned {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -461,8 +462,8 @@ func (z *erasureServerPools) getPoolIdxExistingWithOpts(ctx context.Context, buc
|
||||
// The check is skipped if there is only one pool, and 0, nil is always returned in that case.
|
||||
func (z *erasureServerPools) getPoolIdxExistingNoLock(ctx context.Context, bucket, object string) (idx int, err error) {
|
||||
return z.getPoolIdxExistingWithOpts(ctx, bucket, object, ObjectOptions{
|
||||
NoLock: true,
|
||||
Mutate: true,
|
||||
NoLock: true,
|
||||
SkipDecommissioned: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -486,7 +487,7 @@ func (z *erasureServerPools) getPoolIdxNoLock(ctx context.Context, bucket, objec
|
||||
// if none are found falls back to most available space pool, this function is
|
||||
// designed to be only used by PutObject, CopyObject (newObject creation) and NewMultipartUpload.
|
||||
func (z *erasureServerPools) getPoolIdx(ctx context.Context, bucket, object string, size int64) (idx int, err error) {
|
||||
idx, err = z.getPoolIdxExistingWithOpts(ctx, bucket, object, ObjectOptions{Mutate: true})
|
||||
idx, err = z.getPoolIdxExistingWithOpts(ctx, bucket, object, ObjectOptions{SkipDecommissioned: true})
|
||||
if err != nil && !isErrObjectNotFound(err) {
|
||||
return idx, err
|
||||
}
|
||||
@@ -532,9 +533,11 @@ func (z *erasureServerPools) BackendInfo() (b madmin.BackendInfo) {
|
||||
rrSCParity := globalStorageClass.GetParityForSC(storageclass.RRS)
|
||||
|
||||
// Data blocks can vary per pool, but parity is same.
|
||||
for _, setDriveCount := range z.SetDriveCounts() {
|
||||
for i, setDriveCount := range z.SetDriveCounts() {
|
||||
b.StandardSCData = append(b.StandardSCData, setDriveCount-scParity)
|
||||
b.RRSCData = append(b.RRSCData, setDriveCount-rrSCParity)
|
||||
b.DrivesPerSet = append(b.DrivesPerSet, setDriveCount)
|
||||
b.TotalSets = append(b.TotalSets, z.serverPools[i].setCount)
|
||||
}
|
||||
|
||||
b.StandardSCParity = scParity
|
||||
@@ -612,7 +615,7 @@ func (z *erasureServerPools) NSScanner(ctx context.Context, bf *bloomFilter, upd
|
||||
var results []dataUsageCache
|
||||
var firstErr error
|
||||
|
||||
allBuckets, err := z.ListBuckets(ctx)
|
||||
allBuckets, err := z.ListBuckets(ctx, BucketOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -717,7 +720,7 @@ func (z *erasureServerPools) NSScanner(ctx context.Context, bf *bloomFilter, upd
|
||||
// MakeBucketWithLocation - creates a new bucket across all serverPools simultaneously
|
||||
// even if one of the sets fail to create buckets, we proceed all the successful
|
||||
// operations.
|
||||
func (z *erasureServerPools) MakeBucketWithLocation(ctx context.Context, bucket string, opts BucketOptions) error {
|
||||
func (z *erasureServerPools) MakeBucketWithLocation(ctx context.Context, bucket string, opts MakeBucketOptions) error {
|
||||
g := errgroup.WithNErrs(len(z.serverPools))
|
||||
|
||||
// Lock the bucket name before creating.
|
||||
@@ -757,6 +760,7 @@ func (z *erasureServerPools) MakeBucketWithLocation(ctx context.Context, bucket
|
||||
|
||||
// If it doesn't exist we get a new, so ignore errors
|
||||
meta := newBucketMetadata(bucket)
|
||||
meta.SetCreatedAt(opts.CreatedAt)
|
||||
if opts.LockEnabled {
|
||||
meta.VersioningConfigXML = enabledBucketVersioningConfig
|
||||
meta.ObjectLockConfigXML = enabledBucketObjectLockConfig
|
||||
@@ -976,13 +980,8 @@ func (z *erasureServerPools) PutObject(ctx context.Context, bucket string, objec
|
||||
}
|
||||
|
||||
func (z *erasureServerPools) deletePrefix(ctx context.Context, bucket string, prefix string) error {
|
||||
for idx, pool := range z.serverPools {
|
||||
if z.IsSuspended(idx) {
|
||||
logger.LogIf(ctx, fmt.Errorf("pool %d is suspended, all writes are suspended", idx+1))
|
||||
continue
|
||||
}
|
||||
_, err := pool.DeleteObject(ctx, bucket, prefix, ObjectOptions{DeletePrefix: true})
|
||||
if err != nil {
|
||||
for _, pool := range z.serverPools {
|
||||
if _, err := pool.DeleteObject(ctx, bucket, prefix, ObjectOptions{DeletePrefix: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1325,7 +1324,10 @@ func (z *erasureServerPools) ListMultipartUploads(ctx context.Context, bucket, p
|
||||
poolResult.KeyMarker = keyMarker
|
||||
poolResult.Prefix = prefix
|
||||
poolResult.Delimiter = delimiter
|
||||
for _, pool := range z.serverPools {
|
||||
for idx, pool := range z.serverPools {
|
||||
if z.IsSuspended(idx) {
|
||||
continue
|
||||
}
|
||||
result, err := pool.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker,
|
||||
delimiter, maxUploads)
|
||||
if err != nil {
|
||||
@@ -1350,6 +1352,9 @@ func (z *erasureServerPools) NewMultipartUpload(ctx context.Context, bucket, obj
|
||||
}
|
||||
|
||||
for idx, pool := range z.serverPools {
|
||||
if z.IsSuspended(idx) {
|
||||
continue
|
||||
}
|
||||
result, err := pool.ListMultipartUploads(ctx, bucket, object, "", "", "", maxUploadsList)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -1392,7 +1397,10 @@ func (z *erasureServerPools) PutObjectPart(ctx context.Context, bucket, object,
|
||||
return z.serverPools[0].PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
|
||||
}
|
||||
|
||||
for _, pool := range z.serverPools {
|
||||
for idx, pool := range z.serverPools {
|
||||
if z.IsSuspended(idx) {
|
||||
continue
|
||||
}
|
||||
_, err := pool.GetMultipartInfo(ctx, bucket, object, uploadID, opts)
|
||||
if err == nil {
|
||||
return pool.PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
|
||||
@@ -1421,7 +1429,10 @@ func (z *erasureServerPools) GetMultipartInfo(ctx context.Context, bucket, objec
|
||||
if z.SinglePool() {
|
||||
return z.serverPools[0].GetMultipartInfo(ctx, bucket, object, uploadID, opts)
|
||||
}
|
||||
for _, pool := range z.serverPools {
|
||||
for idx, pool := range z.serverPools {
|
||||
if z.IsSuspended(idx) {
|
||||
continue
|
||||
}
|
||||
mi, err := pool.GetMultipartInfo(ctx, bucket, object, uploadID, opts)
|
||||
if err == nil {
|
||||
return mi, nil
|
||||
@@ -1450,7 +1461,10 @@ func (z *erasureServerPools) ListObjectParts(ctx context.Context, bucket, object
|
||||
if z.SinglePool() {
|
||||
return z.serverPools[0].ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||
}
|
||||
for _, pool := range z.serverPools {
|
||||
for idx, pool := range z.serverPools {
|
||||
if z.IsSuspended(idx) {
|
||||
continue
|
||||
}
|
||||
_, err := pool.GetMultipartInfo(ctx, bucket, object, uploadID, opts)
|
||||
if err == nil {
|
||||
return pool.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||
@@ -1478,7 +1492,10 @@ func (z *erasureServerPools) AbortMultipartUpload(ctx context.Context, bucket, o
|
||||
return z.serverPools[0].AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
|
||||
}
|
||||
|
||||
for _, pool := range z.serverPools {
|
||||
for idx, pool := range z.serverPools {
|
||||
if z.IsSuspended(idx) {
|
||||
continue
|
||||
}
|
||||
_, err := pool.GetMultipartInfo(ctx, bucket, object, uploadID, opts)
|
||||
if err == nil {
|
||||
return pool.AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
|
||||
@@ -1507,7 +1524,10 @@ func (z *erasureServerPools) CompleteMultipartUpload(ctx context.Context, bucket
|
||||
return z.serverPools[0].CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||
}
|
||||
|
||||
for _, pool := range z.serverPools {
|
||||
for idx, pool := range z.serverPools {
|
||||
if z.IsSuspended(idx) {
|
||||
continue
|
||||
}
|
||||
_, err := pool.GetMultipartInfo(ctx, bucket, object, uploadID, opts)
|
||||
if err == nil {
|
||||
return pool.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||
@@ -1522,9 +1542,9 @@ func (z *erasureServerPools) CompleteMultipartUpload(ctx context.Context, bucket
|
||||
}
|
||||
|
||||
// GetBucketInfo - returns bucket info from one of the erasure coded serverPools.
|
||||
func (z *erasureServerPools) GetBucketInfo(ctx context.Context, bucket string) (bucketInfo BucketInfo, err error) {
|
||||
func (z *erasureServerPools) GetBucketInfo(ctx context.Context, bucket string, opts BucketOptions) (bucketInfo BucketInfo, err error) {
|
||||
if z.SinglePool() {
|
||||
bucketInfo, err = z.serverPools[0].GetBucketInfo(ctx, bucket)
|
||||
bucketInfo, err = z.serverPools[0].GetBucketInfo(ctx, bucket, opts)
|
||||
if err != nil {
|
||||
return bucketInfo, err
|
||||
}
|
||||
@@ -1535,7 +1555,7 @@ func (z *erasureServerPools) GetBucketInfo(ctx context.Context, bucket string) (
|
||||
return bucketInfo, nil
|
||||
}
|
||||
for _, pool := range z.serverPools {
|
||||
bucketInfo, err = pool.GetBucketInfo(ctx, bucket)
|
||||
bucketInfo, err = pool.GetBucketInfo(ctx, bucket, opts)
|
||||
if err != nil {
|
||||
if isErrBucketNotFound(err) {
|
||||
continue
|
||||
@@ -1609,7 +1629,11 @@ func (z *erasureServerPools) DeleteBucket(ctx context.Context, bucket string, op
|
||||
|
||||
// Purge the entire bucket metadata entirely.
|
||||
z.renameAll(context.Background(), minioMetaBucket, pathJoin(bucketMetaPrefix, bucket))
|
||||
|
||||
// If site replication is configured, hold on to deleted bucket state until sites sync
|
||||
switch opts.SRDeleteOp {
|
||||
case MarkDelete:
|
||||
z.markDelete(context.Background(), minioMetaBucket, pathJoin(bucketMetaPrefix, deletedBucketsPrefix, bucket))
|
||||
}
|
||||
// Success.
|
||||
return nil
|
||||
}
|
||||
@@ -1627,6 +1651,27 @@ func (z *erasureServerPools) renameAll(ctx context.Context, bucket, prefix strin
|
||||
}
|
||||
}
|
||||
|
||||
// markDelete will create a directory of deleted bucket in .minio.sys/buckets/.deleted across all disks
|
||||
// in situations where the deleted bucket needs to be held on to until all sites are in sync for
|
||||
// site replication
|
||||
func (z *erasureServerPools) markDelete(ctx context.Context, bucket, prefix string) {
|
||||
for _, servers := range z.serverPools {
|
||||
for _, set := range servers.sets {
|
||||
set.markDelete(ctx, bucket, prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// purgeDelete deletes vol entry in .minio.sys/buckets/.deleted after site replication
|
||||
// syncs the delete to peers.
|
||||
func (z *erasureServerPools) purgeDelete(ctx context.Context, bucket, prefix string) {
|
||||
for _, servers := range z.serverPools {
|
||||
for _, set := range servers.sets {
|
||||
set.purgeDelete(ctx, bucket, prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function is used to undo a successful DeleteBucket operation.
|
||||
func undoDeleteBucketServerPools(ctx context.Context, bucket string, serverPools []*erasureSets, errs []error) {
|
||||
g := errgroup.WithNErrs(len(serverPools))
|
||||
@@ -1636,7 +1681,7 @@ func undoDeleteBucketServerPools(ctx context.Context, bucket string, serverPools
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if errs[index] == nil {
|
||||
return serverPools[index].MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
return serverPools[index].MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
}
|
||||
return nil
|
||||
}, index)
|
||||
@@ -1648,12 +1693,15 @@ func undoDeleteBucketServerPools(ctx context.Context, bucket string, serverPools
|
||||
// List all buckets from one of the serverPools, we are not doing merge
|
||||
// sort here just for simplification. As per design it is assumed
|
||||
// that all buckets are present on all serverPools.
|
||||
func (z *erasureServerPools) ListBuckets(ctx context.Context) (buckets []BucketInfo, err error) {
|
||||
func (z *erasureServerPools) ListBuckets(ctx context.Context, opts BucketOptions) (buckets []BucketInfo, err error) {
|
||||
if z.SinglePool() {
|
||||
buckets, err = z.serverPools[0].ListBuckets(ctx)
|
||||
buckets, err = z.serverPools[0].ListBuckets(ctx, opts)
|
||||
} else {
|
||||
for _, pool := range z.serverPools {
|
||||
buckets, err = pool.ListBuckets(ctx)
|
||||
for idx, pool := range z.serverPools {
|
||||
if z.IsSuspended(idx) {
|
||||
continue
|
||||
}
|
||||
buckets, err = pool.ListBuckets(ctx, opts)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
@@ -1665,9 +1713,9 @@ func (z *erasureServerPools) ListBuckets(ctx context.Context) (buckets []BucketI
|
||||
return nil, err
|
||||
}
|
||||
for i := range buckets {
|
||||
meta, err := globalBucketMetadataSys.Get(buckets[i].Name)
|
||||
createdAt, err := globalBucketMetadataSys.CreatedAt(buckets[i].Name)
|
||||
if err == nil {
|
||||
buckets[i].Created = meta.Created
|
||||
buckets[i].Created = createdAt
|
||||
}
|
||||
}
|
||||
return buckets, nil
|
||||
@@ -1822,7 +1870,7 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
|
||||
minDisks: 1,
|
||||
reportNotFound: false,
|
||||
agreed: loadEntry,
|
||||
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
|
||||
partial: func(entries metaCacheEntries, _ []error) {
|
||||
entry, ok := entries.resolve(&resolver)
|
||||
if !ok {
|
||||
// check if we can get one entry atleast
|
||||
@@ -1889,7 +1937,7 @@ func listAndHeal(ctx context.Context, bucket, prefix string, set *erasureObjects
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
|
||||
partial: func(entries metaCacheEntries, _ []error) {
|
||||
entry, ok := entries.resolve(&resolver)
|
||||
if !ok {
|
||||
// check if we can get one entry atleast
|
||||
@@ -2256,7 +2304,6 @@ func (z *erasureServerPools) GetObjectTags(ctx context.Context, bucket, object s
|
||||
return z.serverPools[0].GetObjectTags(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
opts.Mutate = false
|
||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2272,7 +2319,8 @@ func (z *erasureServerPools) TransitionObject(ctx context.Context, bucket, objec
|
||||
return z.serverPools[0].TransitionObject(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
opts.Mutate = true // Avoid transitioning an object from a pool being decommissioned.
|
||||
// Avoid transitioning an object from a pool being decommissioned.
|
||||
opts.SkipDecommissioned = true
|
||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -2288,7 +2336,8 @@ func (z *erasureServerPools) RestoreTransitionedObject(ctx context.Context, buck
|
||||
return z.serverPools[0].RestoreTransitionedObject(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
opts.Mutate = true // Avoid restoring object from a pool being decommissioned.
|
||||
// Avoid restoring object from a pool being decommissioned.
|
||||
opts.SkipDecommissioned = true
|
||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+83
-43
@@ -20,7 +20,6 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
@@ -28,6 +27,7 @@ import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -549,29 +549,13 @@ func (s *erasureSets) cleanupStaleUploads(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
const objectErasureMapKey = "objectErasureMap"
|
||||
|
||||
type auditObjectOp struct {
|
||||
Name string `json:"name"`
|
||||
Pool int `json:"poolId"`
|
||||
Set int `json:"setId"`
|
||||
Disks []string `json:"disks"`
|
||||
}
|
||||
|
||||
type auditObjectErasureMap struct {
|
||||
sync.Map
|
||||
}
|
||||
|
||||
// Define how to marshal auditObjectErasureMap so it can be
|
||||
// printed in the audit webhook notification request.
|
||||
func (a *auditObjectErasureMap) MarshalJSON() ([]byte, error) {
|
||||
mapCopy := make(map[string]auditObjectOp)
|
||||
a.Range(func(k, v interface{}) bool {
|
||||
mapCopy[k.(string)] = v.(auditObjectOp)
|
||||
return true
|
||||
})
|
||||
return json.Marshal(mapCopy)
|
||||
}
|
||||
|
||||
// Add erasure set information to the current context
|
||||
func auditObjectErasureSet(ctx context.Context, object string, set *erasureObjects) {
|
||||
if len(logger.AuditTargets()) == 0 {
|
||||
@@ -579,33 +563,20 @@ func auditObjectErasureSet(ctx context.Context, object string, set *erasureObjec
|
||||
}
|
||||
|
||||
object = decodeDirObject(object)
|
||||
|
||||
var disksEndpoints []string
|
||||
for _, endpoint := range set.getEndpoints() {
|
||||
endpoints := set.getEndpoints()
|
||||
disksEndpoints := make([]string, 0, len(endpoints))
|
||||
for _, endpoint := range endpoints {
|
||||
disksEndpoints = append(disksEndpoints, endpoint.String())
|
||||
}
|
||||
|
||||
op := auditObjectOp{
|
||||
Name: object,
|
||||
Pool: set.poolIndex + 1,
|
||||
Set: set.setIndex + 1,
|
||||
Disks: disksEndpoints,
|
||||
}
|
||||
|
||||
var objectErasureSetTag *auditObjectErasureMap
|
||||
reqInfo := logger.GetReqInfo(ctx)
|
||||
for _, kv := range reqInfo.GetTags() {
|
||||
if kv.Key == objectErasureMapKey {
|
||||
objectErasureSetTag = kv.Val.(*auditObjectErasureMap)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if objectErasureSetTag == nil {
|
||||
objectErasureSetTag = &auditObjectErasureMap{}
|
||||
}
|
||||
|
||||
objectErasureSetTag.Store(object, op)
|
||||
reqInfo.SetTags(objectErasureMapKey, objectErasureSetTag)
|
||||
logger.GetReqInfo(ctx).AppendTags("objectLocation", op)
|
||||
}
|
||||
|
||||
// NewNSLock - initialize a new namespace RWLocker instance.
|
||||
@@ -719,7 +690,7 @@ func (s *erasureSets) Shutdown(ctx context.Context) error {
|
||||
|
||||
// MakeBucketLocation - creates a new bucket across all sets simultaneously,
|
||||
// then return the first encountered error
|
||||
func (s *erasureSets) MakeBucketWithLocation(ctx context.Context, bucket string, opts BucketOptions) error {
|
||||
func (s *erasureSets) MakeBucketWithLocation(ctx context.Context, bucket string, opts MakeBucketOptions) error {
|
||||
g := errgroup.WithNErrs(len(s.sets))
|
||||
|
||||
// Create buckets in parallel across all sets.
|
||||
@@ -790,8 +761,8 @@ func (s *erasureSets) getHashedSet(input string) (set *erasureObjects) {
|
||||
}
|
||||
|
||||
// GetBucketInfo - returns bucket info from one of the erasure coded set.
|
||||
func (s *erasureSets) GetBucketInfo(ctx context.Context, bucket string) (bucketInfo BucketInfo, err error) {
|
||||
return s.getHashedSet("").GetBucketInfo(ctx, bucket)
|
||||
func (s *erasureSets) GetBucketInfo(ctx context.Context, bucket string, opts BucketOptions) (bucketInfo BucketInfo, err error) {
|
||||
return s.getHashedSet("").GetBucketInfo(ctx, bucket, opts)
|
||||
}
|
||||
|
||||
// IsNotificationSupported returns whether bucket notification is applicable for this layer.
|
||||
@@ -855,7 +826,7 @@ func undoDeleteBucketSets(ctx context.Context, bucket string, sets []*erasureObj
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if errs[index] == nil {
|
||||
return sets[index].MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||
return sets[index].MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
}
|
||||
return nil
|
||||
}, index)
|
||||
@@ -867,7 +838,7 @@ func undoDeleteBucketSets(ctx context.Context, bucket string, sets []*erasureObj
|
||||
// List all buckets from one of the set, we are not doing merge
|
||||
// sort here just for simplification. As per design it is assumed
|
||||
// that all buckets are present on all sets.
|
||||
func (s *erasureSets) ListBuckets(ctx context.Context) (buckets []BucketInfo, err error) {
|
||||
func (s *erasureSets) ListBuckets(ctx context.Context, opts BucketOptions) (buckets []BucketInfo, err error) {
|
||||
var listBuckets []BucketInfo
|
||||
healBuckets := map[string]VolInfo{}
|
||||
for _, set := range s.sets {
|
||||
@@ -877,8 +848,34 @@ func (s *erasureSets) ListBuckets(ctx context.Context) (buckets []BucketInfo, er
|
||||
}
|
||||
}
|
||||
|
||||
// include deleted buckets in listBuckets output
|
||||
deletedBuckets := map[string]VolInfo{}
|
||||
|
||||
if opts.Deleted {
|
||||
for _, set := range s.sets {
|
||||
// lists all unique buckets across drives.
|
||||
if err := listDeletedBuckets(ctx, set.getDisks(), deletedBuckets, s.defaultParityCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, v := range healBuckets {
|
||||
listBuckets = append(listBuckets, BucketInfo(v))
|
||||
bi := BucketInfo{
|
||||
Name: v.Name,
|
||||
Created: v.Created,
|
||||
}
|
||||
if vi, ok := deletedBuckets[v.Name]; ok {
|
||||
bi.Deleted = vi.Created
|
||||
}
|
||||
listBuckets = append(listBuckets, bi)
|
||||
}
|
||||
for _, v := range deletedBuckets {
|
||||
if _, ok := healBuckets[v.Name]; !ok {
|
||||
listBuckets = append(listBuckets, BucketInfo{
|
||||
Name: v.Name,
|
||||
Deleted: v.Created,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(listBuckets, func(i, j int) bool {
|
||||
@@ -888,6 +885,45 @@ func (s *erasureSets) ListBuckets(ctx context.Context) (buckets []BucketInfo, er
|
||||
return listBuckets, nil
|
||||
}
|
||||
|
||||
// listDeletedBuckets lists deleted buckets from all disks.
|
||||
func listDeletedBuckets(ctx context.Context, storageDisks []StorageAPI, delBuckets map[string]VolInfo, readQuorum int) error {
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
var mu sync.Mutex
|
||||
for index := range storageDisks {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if storageDisks[index] == nil {
|
||||
// we ignore disk not found errors
|
||||
return nil
|
||||
}
|
||||
volsInfo, err := storageDisks[index].ListDir(ctx, minioMetaBucket, pathJoin(bucketMetaPrefix, deletedBucketsPrefix), -1)
|
||||
if err != nil {
|
||||
if err == errFileNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
for _, volName := range volsInfo {
|
||||
mu.Lock()
|
||||
if _, ok := delBuckets[volName]; !ok {
|
||||
vi, err := storageDisks[index].StatVol(ctx, pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, volName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bkt := strings.TrimPrefix(vi.Name, pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix))
|
||||
bkt = strings.TrimPrefix(bkt, slashSeparator)
|
||||
bkt = strings.TrimSuffix(bkt, slashSeparator)
|
||||
vi.Name = bkt
|
||||
delBuckets[bkt] = vi
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
return reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, readQuorum)
|
||||
}
|
||||
|
||||
// --- Object Operations ---
|
||||
|
||||
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||
@@ -1195,8 +1231,12 @@ func markRootDisksAsDown(storageDisks []StorageAPI, errs []error) {
|
||||
// Do nothing
|
||||
return
|
||||
}
|
||||
infos, _ := getHealDiskInfos(storageDisks, errs)
|
||||
infos, ierrs := getHealDiskInfos(storageDisks, errs)
|
||||
for i := range storageDisks {
|
||||
if ierrs[i] != nil && ierrs[i] != errUnformattedDisk {
|
||||
storageDisks[i] = nil
|
||||
continue
|
||||
}
|
||||
if storageDisks[i] != nil && infos[i].RootDisk {
|
||||
// We should not heal on root disk. i.e in a situation where the minio-administrator has unmounted a
|
||||
// defective drive we should not heal a path on the root disk.
|
||||
|
||||
+132
-21
@@ -108,16 +108,40 @@ func newErasureSingle(ctx context.Context, storageDisk StorageAPI, format *forma
|
||||
// List all buckets from one of the set, we are not doing merge
|
||||
// sort here just for simplification. As per design it is assumed
|
||||
// that all buckets are present on all sets.
|
||||
func (es *erasureSingle) ListBuckets(ctx context.Context) (buckets []BucketInfo, err error) {
|
||||
func (es *erasureSingle) ListBuckets(ctx context.Context, opts BucketOptions) (buckets []BucketInfo, err error) {
|
||||
var listBuckets []BucketInfo
|
||||
healBuckets := map[string]VolInfo{}
|
||||
// lists all unique buckets across drives.
|
||||
if err := listAllBuckets(ctx, []StorageAPI{es.disk}, healBuckets, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// include deleted buckets in listBuckets output
|
||||
deletedBuckets := map[string]VolInfo{}
|
||||
|
||||
if opts.Deleted {
|
||||
// lists all deleted buckets across drives.
|
||||
if err := listDeletedBuckets(ctx, []StorageAPI{es.disk}, deletedBuckets, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, v := range healBuckets {
|
||||
listBuckets = append(listBuckets, BucketInfo(v))
|
||||
bi := BucketInfo{
|
||||
Name: v.Name,
|
||||
Created: v.Created,
|
||||
}
|
||||
if vi, ok := deletedBuckets[v.Name]; ok {
|
||||
bi.Deleted = vi.Created
|
||||
}
|
||||
listBuckets = append(listBuckets, bi)
|
||||
}
|
||||
|
||||
for _, v := range deletedBuckets {
|
||||
if _, ok := healBuckets[v.Name]; !ok {
|
||||
listBuckets = append(listBuckets, BucketInfo{
|
||||
Name: v.Name,
|
||||
Deleted: v.Created,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(listBuckets, func(i, j int) bool {
|
||||
@@ -256,7 +280,7 @@ type renameAllStorager interface {
|
||||
|
||||
// Bucket operations
|
||||
// MakeBucket - make a bucket.
|
||||
func (es *erasureSingle) MakeBucketWithLocation(ctx context.Context, bucket string, opts BucketOptions) error {
|
||||
func (es *erasureSingle) MakeBucketWithLocation(ctx context.Context, bucket string, opts MakeBucketOptions) error {
|
||||
defer NSUpdated(bucket, slashSeparator)
|
||||
|
||||
// Lock the bucket name before creating.
|
||||
@@ -289,6 +313,7 @@ func (es *erasureSingle) MakeBucketWithLocation(ctx context.Context, bucket stri
|
||||
|
||||
// If it doesn't exist we get a new, so ignore errors
|
||||
meta := newBucketMetadata(bucket)
|
||||
meta.SetCreatedAt(opts.CreatedAt)
|
||||
if opts.LockEnabled {
|
||||
meta.VersioningConfigXML = enabledBucketVersioningConfig
|
||||
meta.ObjectLockConfigXML = enabledBucketObjectLockConfig
|
||||
@@ -308,12 +333,17 @@ func (es *erasureSingle) MakeBucketWithLocation(ctx context.Context, bucket stri
|
||||
}
|
||||
|
||||
// GetBucketInfo - returns BucketInfo for a bucket.
|
||||
func (es *erasureSingle) GetBucketInfo(ctx context.Context, bucket string) (bi BucketInfo, e error) {
|
||||
func (es *erasureSingle) GetBucketInfo(ctx context.Context, bucket string, opts BucketOptions) (bi BucketInfo, e error) {
|
||||
volInfo, err := es.disk.StatVol(ctx, bucket)
|
||||
if err != nil {
|
||||
if opts.Deleted {
|
||||
if dvi, derr := es.disk.StatVol(ctx, pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, bucket)); derr == nil {
|
||||
return BucketInfo{Name: bucket, Deleted: dvi.Created}, nil
|
||||
}
|
||||
}
|
||||
return bi, toObjectErr(err, bucket)
|
||||
}
|
||||
return BucketInfo(volInfo), nil
|
||||
return BucketInfo{Name: volInfo.Name, Created: volInfo.Created}, nil
|
||||
}
|
||||
|
||||
// DeleteBucket - deletes a bucket.
|
||||
@@ -322,6 +352,32 @@ func (es *erasureSingle) DeleteBucket(ctx context.Context, bucket string, opts D
|
||||
defer NSUpdated(bucket, slashSeparator)
|
||||
|
||||
err := es.disk.DeleteVol(ctx, bucket, opts.Force)
|
||||
// Purge the entire bucket metadata entirely.
|
||||
deleteBucketMetadata(ctx, es, bucket)
|
||||
globalBucketMetadataSys.Remove(bucket)
|
||||
|
||||
if err == nil || errors.Is(err, errVolumeNotFound) {
|
||||
if opts.SRDeleteOp == MarkDelete {
|
||||
es.markDelete(ctx, minioMetaBucket, pathJoin(bucketMetaPrefix, deletedBucketsPrefix, bucket))
|
||||
}
|
||||
}
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// markDelete creates a vol entry in .minio.sys/buckets/.deleted until site replication
|
||||
// syncs the delete to peers
|
||||
func (es *erasureSingle) markDelete(ctx context.Context, bucket, prefix string) error {
|
||||
err := es.disk.MakeVol(ctx, pathJoin(bucket, prefix))
|
||||
if err != nil && errors.Is(err, errVolumeExists) {
|
||||
return nil
|
||||
}
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// purgeDelete deletes vol entry in .minio.sys/buckets/.deleted after site replication
|
||||
// syncs the delete to peers OR on a new MakeBucket call.
|
||||
func (es *erasureSingle) purgeDelete(ctx context.Context, bucket, prefix string) error {
|
||||
err := es.disk.DeleteVol(ctx, pathJoin(bucket, prefix), true)
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
@@ -849,6 +905,13 @@ func (es *erasureSingle) putMetacacheObject(ctx context.Context, key string, r *
|
||||
return ObjectInfo{}, IncompleteBody{Bucket: minioMetaBucket, Object: key}
|
||||
}
|
||||
|
||||
var index []byte
|
||||
if opts.IndexCB != nil {
|
||||
index = opts.IndexCB()
|
||||
}
|
||||
|
||||
modTime := UTCNow()
|
||||
|
||||
for i, w := range writers {
|
||||
if w == nil {
|
||||
// Make sure to avoid writing to disks which we couldn't complete in erasure.Encode()
|
||||
@@ -856,7 +919,7 @@ func (es *erasureSingle) putMetacacheObject(ctx context.Context, key string, r *
|
||||
continue
|
||||
}
|
||||
partsMetadata[i].Data = inlineBuffers[i].Bytes()
|
||||
partsMetadata[i].AddObjectPart(1, "", n, data.ActualSize())
|
||||
partsMetadata[i].AddObjectPart(1, "", n, data.ActualSize(), modTime, index)
|
||||
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{
|
||||
PartNumber: 1,
|
||||
Algorithm: DefaultBitrotAlgorithm,
|
||||
@@ -864,8 +927,6 @@ func (es *erasureSingle) putMetacacheObject(ctx context.Context, key string, r *
|
||||
})
|
||||
}
|
||||
|
||||
modTime := UTCNow()
|
||||
|
||||
// Fill all the necessary metadata.
|
||||
// Update `xl.meta` content on each disks.
|
||||
for index := range partsMetadata {
|
||||
@@ -1082,6 +1143,16 @@ func (es *erasureSingle) putObject(ctx context.Context, bucket string, object st
|
||||
defer lk.Unlock(lkctx.Cancel)
|
||||
}
|
||||
|
||||
var index []byte
|
||||
if opts.IndexCB != nil {
|
||||
index = opts.IndexCB()
|
||||
}
|
||||
|
||||
modTime := opts.MTime
|
||||
if opts.MTime.IsZero() {
|
||||
modTime = UTCNow()
|
||||
}
|
||||
|
||||
for i, w := range writers {
|
||||
if w == nil {
|
||||
onlineDisks[i] = nil
|
||||
@@ -1092,7 +1163,7 @@ func (es *erasureSingle) putObject(ctx context.Context, bucket string, object st
|
||||
} else {
|
||||
partsMetadata[i].Data = nil
|
||||
}
|
||||
partsMetadata[i].AddObjectPart(1, "", n, data.ActualSize())
|
||||
partsMetadata[i].AddObjectPart(1, "", n, data.ActualSize(), modTime, index)
|
||||
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{
|
||||
PartNumber: 1,
|
||||
Algorithm: DefaultBitrotAlgorithm,
|
||||
@@ -1108,11 +1179,6 @@ func (es *erasureSingle) putObject(ctx context.Context, bucket string, object st
|
||||
opts.UserDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
|
||||
}
|
||||
|
||||
modTime := opts.MTime
|
||||
if opts.MTime.IsZero() {
|
||||
modTime = UTCNow()
|
||||
}
|
||||
|
||||
// Fill all the necessary metadata.
|
||||
// Update `xl.meta` content on each disks.
|
||||
for index := range partsMetadata {
|
||||
@@ -1321,8 +1387,14 @@ func (es *erasureSingle) DeleteObjects(ctx context.Context, bucket string, objec
|
||||
|
||||
func (es *erasureSingle) deletePrefix(ctx context.Context, bucket, prefix string) error {
|
||||
dirPrefix := encodeDirObject(prefix)
|
||||
defer es.disk.Delete(ctx, bucket, dirPrefix, true)
|
||||
return es.disk.Delete(ctx, bucket, prefix, true)
|
||||
defer es.disk.Delete(ctx, bucket, dirPrefix, DeleteOptions{
|
||||
Recursive: true,
|
||||
Force: true,
|
||||
})
|
||||
return es.disk.Delete(ctx, bucket, prefix, DeleteOptions{
|
||||
Recursive: true,
|
||||
Force: true,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteObject - deletes an object, this call doesn't necessary reply
|
||||
@@ -1916,7 +1988,10 @@ func (es *erasureSingle) removeObjectPart(bucket, object, uploadID, dataDir stri
|
||||
// Ignoring failure to remove parts that weren't present in CompleteMultipartUpload
|
||||
// requests. xl.meta is the authoritative source of truth on which parts constitute
|
||||
// the object. The presence of parts that don't belong in the object doesn't affect correctness.
|
||||
_ = storageDisks[index].Delete(context.TODO(), minioMetaMultipartBucket, curpartPath, false)
|
||||
_ = storageDisks[index].Delete(context.TODO(), minioMetaMultipartBucket, curpartPath, DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
@@ -1954,7 +2029,10 @@ func (es *erasureSingle) cleanupStaleUploadsOnDisk(ctx context.Context, disk Sto
|
||||
}
|
||||
wait := es.deletedCleanupSleeper.Timer(ctx)
|
||||
if now.Sub(vi.Created) > expiry {
|
||||
disk.Delete(ctx, minioMetaTmpBucket, tmpDir, true)
|
||||
disk.Delete(ctx, minioMetaTmpBucket, tmpDir, DeleteOptions{
|
||||
Recursive: true,
|
||||
Force: false,
|
||||
})
|
||||
}
|
||||
wait()
|
||||
return nil
|
||||
@@ -2357,8 +2435,13 @@ func (es *erasureSingle) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
|
||||
md5hex := r.MD5CurrentHexString()
|
||||
|
||||
var index []byte
|
||||
if opts.IndexCB != nil {
|
||||
index = opts.IndexCB()
|
||||
}
|
||||
|
||||
// Add the current part.
|
||||
fi.AddObjectPart(partID, md5hex, n, data.ActualSize())
|
||||
fi.AddObjectPart(partID, md5hex, n, data.ActualSize(), fi.ModTime, index)
|
||||
|
||||
for i, disk := range onlineDisks {
|
||||
if disk == OfflineDisk {
|
||||
@@ -2656,6 +2739,7 @@ func (es *erasureSingle) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
Number: part.PartNumber,
|
||||
Size: currentFI.Parts[partIdx].Size,
|
||||
ActualSize: currentFI.Parts[partIdx].ActualSize,
|
||||
Index: currentFI.Parts[partIdx].Index,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2984,7 +3068,7 @@ func (es *erasureSingle) Walk(ctx context.Context, bucket, prefix string, result
|
||||
minDisks: 1,
|
||||
reportNotFound: false,
|
||||
agreed: loadEntry,
|
||||
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
|
||||
partial: func(entries metaCacheEntries, _ []error) {
|
||||
entry, ok := entries.resolve(&resolver)
|
||||
if !ok {
|
||||
// check if we can get one entry atleast
|
||||
@@ -3008,6 +3092,33 @@ func (es *erasureSingle) Walk(ctx context.Context, bucket, prefix string, result
|
||||
return nil
|
||||
}
|
||||
|
||||
// Health - returns current status of the object layer health, for single drive
|
||||
// its as simple as returning healthy as long as drive is accessible.
|
||||
func (es *erasureSingle) Health(ctx context.Context, opts HealthOptions) HealthResult {
|
||||
_, err := es.disk.DiskInfo(ctx)
|
||||
if err != nil {
|
||||
return HealthResult{}
|
||||
}
|
||||
if opts.Maintenance {
|
||||
// Single drive cannot be put under maintenance.
|
||||
return HealthResult{
|
||||
Healthy: false,
|
||||
WriteQuorum: 1,
|
||||
}
|
||||
}
|
||||
return HealthResult{
|
||||
Healthy: true,
|
||||
WriteQuorum: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// ReadHealth - returns current status of the object layer health for reads,
|
||||
// for single drive its as simple as returning healthy as long as drive is accessible.
|
||||
func (es *erasureSingle) ReadHealth(ctx context.Context) bool {
|
||||
res := es.Health(ctx, HealthOptions{})
|
||||
return res.Healthy
|
||||
}
|
||||
|
||||
// nsScanner will start scanning buckets and send updated totals as they are traversed.
|
||||
// Updates are sent on a regular basis and the caller *must* consume them.
|
||||
func (es *erasureSingle) nsScanner(ctx context.Context, buckets []BucketInfo, bf *bloomFilter, wantCycle uint32, updates chan<- dataUsageCache, healScanMode madmin.HealScanMode) error {
|
||||
@@ -3203,7 +3314,7 @@ func (es *erasureSingle) NSScanner(ctx context.Context, bf *bloomFilter, updates
|
||||
results := make([]dataUsageCache, 1)
|
||||
var firstErr error
|
||||
|
||||
allBuckets, err := es.ListBuckets(ctx)
|
||||
allBuckets, err := es.ListBuckets(ctx, BucketOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+2
-10
@@ -22,7 +22,6 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -118,20 +117,13 @@ type erasureTestSetup struct {
|
||||
disks []StorageAPI
|
||||
}
|
||||
|
||||
// Removes the temporary disk directories.
|
||||
func (e erasureTestSetup) Remove() {
|
||||
for _, path := range e.diskPaths {
|
||||
os.RemoveAll(path)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an initialized setup for erasure tests.
|
||||
func newErasureTestSetup(dataBlocks int, parityBlocks int, blockSize int64) (*erasureTestSetup, error) {
|
||||
func newErasureTestSetup(tb testing.TB, dataBlocks int, parityBlocks int, blockSize int64) (*erasureTestSetup, error) {
|
||||
diskPaths := make([]string, dataBlocks+parityBlocks)
|
||||
disks := make([]StorageAPI, len(diskPaths))
|
||||
var err error
|
||||
for i := range diskPaths {
|
||||
disks[i], diskPaths[i], err = newXLStorageTestSetup()
|
||||
disks[i], diskPaths[i], err = newXLStorageTestSetup(tb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -359,7 +359,7 @@ func migrateCacheData(ctx context.Context, c *diskCache, bucket, object, oldfile
|
||||
|
||||
actualSize := uint64(st.Size())
|
||||
if globalCacheKMS != nil {
|
||||
reader, err = newCacheEncryptReader(readCloser, bucket, object, metadata)
|
||||
reader, err = newCacheEncryptReader(ctx, readCloser, bucket, object, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -367,7 +367,10 @@ func saveFormatErasure(disk StorageAPI, format *formatErasureV3, heal bool) erro
|
||||
tmpFormat := mustGetUUID()
|
||||
|
||||
// Purge any existing temporary file, okay to ignore errors here.
|
||||
defer disk.Delete(context.TODO(), minioMetaBucket, tmpFormat, false)
|
||||
defer disk.Delete(context.TODO(), minioMetaBucket, tmpFormat, DeleteOptions{
|
||||
Recursive: false,
|
||||
Force: false,
|
||||
})
|
||||
|
||||
// write to unique file.
|
||||
if err = disk.WriteAll(context.TODO(), minioMetaBucket, tmpFormat, formatBytes); err != nil {
|
||||
@@ -643,9 +646,8 @@ func saveFormatErasureAll(ctx context.Context, storageDisks []StorageAPI, format
|
||||
}, index)
|
||||
}
|
||||
|
||||
writeQuorum := (len(storageDisks) + 1/2)
|
||||
// Wait for the routines to finish.
|
||||
return reduceWriteQuorumErrs(ctx, g.Wait(), nil, writeQuorum)
|
||||
return reduceWriteQuorumErrs(ctx, g.Wait(), nil, len(storageDisks))
|
||||
}
|
||||
|
||||
// relinquishes the underlying connection for all storage disks.
|
||||
|
||||
@@ -104,11 +104,7 @@ func TestFormatErasureEmpty(t *testing.T) {
|
||||
// Tests xl format migration.
|
||||
func TestFormatErasureMigrate(t *testing.T) {
|
||||
// Get test root.
|
||||
rootPath, err := getTestRoot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
rootPath := t.TempDir()
|
||||
|
||||
m := &formatErasureV1{}
|
||||
m.Format = formatBackendErasure
|
||||
|
||||
+10
-26
@@ -30,11 +30,10 @@ import (
|
||||
|
||||
func TestFSRenameFile(t *testing.T) {
|
||||
// create xlStorage test setup
|
||||
_, path, err := newXLStorageTestSetup()
|
||||
_, path, err := newXLStorageTestSetup(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create xlStorage test setup, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
if err = fsMkdir(GlobalContext, pathJoin(path, "testvolume1")); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -55,11 +54,10 @@ func TestFSRenameFile(t *testing.T) {
|
||||
|
||||
func TestFSStats(t *testing.T) {
|
||||
// create xlStorage test setup
|
||||
_, path, err := newXLStorageTestSetup()
|
||||
_, path, err := newXLStorageTestSetup(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create xlStorage test setup, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
// Setup test environment.
|
||||
|
||||
@@ -183,11 +181,10 @@ func TestFSStats(t *testing.T) {
|
||||
|
||||
func TestFSCreateAndOpen(t *testing.T) {
|
||||
// Setup test environment.
|
||||
_, path, err := newXLStorageTestSetup()
|
||||
_, path, err := newXLStorageTestSetup(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create xlStorage test setup, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
if err = fsMkdir(GlobalContext, pathJoin(path, "success-vol")); err != nil {
|
||||
t.Fatalf("Unable to create directory, %s", err)
|
||||
@@ -248,11 +245,10 @@ func TestFSCreateAndOpen(t *testing.T) {
|
||||
|
||||
func TestFSDeletes(t *testing.T) {
|
||||
// create xlStorage test setup
|
||||
_, path, err := newXLStorageTestSetup()
|
||||
_, path, err := newXLStorageTestSetup(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create xlStorage test setup, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
// Setup test environment.
|
||||
if err = fsMkdir(GlobalContext, pathJoin(path, "success-vol")); err != nil {
|
||||
@@ -350,11 +346,10 @@ func TestFSDeletes(t *testing.T) {
|
||||
|
||||
func BenchmarkFSDeleteFile(b *testing.B) {
|
||||
// create xlStorage test setup
|
||||
_, path, err := newXLStorageTestSetup()
|
||||
_, path, err := newXLStorageTestSetup(b)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to create xlStorage test setup, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
// Setup test environment.
|
||||
if err = fsMkdir(GlobalContext, pathJoin(path, "benchmark")); err != nil {
|
||||
@@ -384,11 +379,10 @@ func BenchmarkFSDeleteFile(b *testing.B) {
|
||||
// Tests fs removes.
|
||||
func TestFSRemoves(t *testing.T) {
|
||||
// create xlStorage test setup
|
||||
_, path, err := newXLStorageTestSetup()
|
||||
_, path, err := newXLStorageTestSetup(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create xlStorage test setup, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
// Setup test environment.
|
||||
if err = fsMkdir(GlobalContext, pathJoin(path, "success-vol")); err != nil {
|
||||
@@ -501,11 +495,10 @@ func TestFSRemoves(t *testing.T) {
|
||||
|
||||
func TestFSRemoveMeta(t *testing.T) {
|
||||
// create xlStorage test setup
|
||||
_, fsPath, err := newXLStorageTestSetup()
|
||||
_, fsPath, err := newXLStorageTestSetup(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create xlStorage test setup, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(fsPath)
|
||||
|
||||
// Setup test environment.
|
||||
if err = fsMkdir(GlobalContext, pathJoin(fsPath, "success-vol")); err != nil {
|
||||
@@ -529,10 +522,7 @@ func TestFSRemoveMeta(t *testing.T) {
|
||||
|
||||
defer rwPool.Close(filePath)
|
||||
|
||||
tmpDir, tmpErr := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if tmpErr != nil {
|
||||
t.Fatal(tmpErr)
|
||||
}
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
if err := fsRemoveMeta(GlobalContext, fsPath, filePath, tmpDir); err != nil {
|
||||
t.Fatalf("Unable to remove file, %s", err)
|
||||
@@ -548,15 +538,9 @@ func TestFSRemoveMeta(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFSIsFile(t *testing.T) {
|
||||
dirPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create tmp directory %s", err)
|
||||
}
|
||||
defer os.RemoveAll(dirPath)
|
||||
filePath := pathJoin(t.TempDir(), "tmpfile")
|
||||
|
||||
filePath := pathJoin(dirPath, "tmpfile")
|
||||
|
||||
if err = ioutil.WriteFile(filePath, nil, 0o777); err != nil {
|
||||
if err := ioutil.WriteFile(filePath, nil, 0o777); err != nil {
|
||||
t.Fatalf("Unable to create file %s", filePath)
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestReadFSMetadata(t *testing.T) {
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
if _, err := obj.PutObject(GlobalContext, bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{}); err != nil {
|
||||
@@ -92,7 +92,7 @@ func TestWriteFSMetadata(t *testing.T) {
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
if _, err := obj.PutObject(GlobalContext, bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{}); err != nil {
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
|
||||
|
||||
// Create a context we can cancel.
|
||||
ctx, cancel := context.WithCancel(GlobalContext)
|
||||
obj.MakeBucketWithLocation(ctx, bucketName, BucketOptions{})
|
||||
obj.MakeBucketWithLocation(ctx, bucketName, MakeBucketOptions{})
|
||||
|
||||
uploadID, err := obj.NewMultipartUpload(ctx, bucketName, objectName, ObjectOptions{})
|
||||
if err != nil {
|
||||
@@ -99,7 +99,7 @@ func TestNewMultipartUploadFaultyDisk(t *testing.T) {
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal("Cannot create bucket, err: ", err)
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ func TestPutObjectPartFaultyDisk(t *testing.T) {
|
||||
data := []byte("12345")
|
||||
dataLen := int64(len(data))
|
||||
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal("Cannot create bucket, err: ", err)
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ func TestCompleteMultipartUploadFaultyDisk(t *testing.T) {
|
||||
objectName := "object"
|
||||
data := []byte("12345")
|
||||
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal("Cannot create bucket, err: ", err)
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ func TestCompleteMultipartUpload(t *testing.T) {
|
||||
objectName := "object"
|
||||
data := []byte("12345")
|
||||
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal("Cannot create bucket, err: ", err)
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ func TestAbortMultipartUpload(t *testing.T) {
|
||||
objectName := "object"
|
||||
data := []byte("12345")
|
||||
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal("Cannot create bucket, err: ", err)
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ func TestListMultipartUploadsFaultyDisk(t *testing.T) {
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal("Cannot create bucket, err: ", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,8 @@ func (fsi *fsIOPool) Create(path string) (wlk *lock.LockedFile, err error) {
|
||||
return nil, errFileAccessDenied
|
||||
case isSysErrIsDir(err):
|
||||
return nil, errIsNotRegular
|
||||
case isSysErrNotDir(err):
|
||||
return nil, errFileAccessDenied
|
||||
case isSysErrPathNotFound(err):
|
||||
return nil, errFileAccessDenied
|
||||
default:
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
@@ -48,11 +47,10 @@ func TestRWPoolLongPath(t *testing.T) {
|
||||
// Tests all RWPool methods.
|
||||
func TestRWPool(t *testing.T) {
|
||||
// create xlStorage test setup
|
||||
_, path, err := newXLStorageTestSetup()
|
||||
_, path, err := newXLStorageTestSetup(t)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create xlStorage test setup, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
rwPool := &fsIOPool{
|
||||
readersMap: make(map[string]*lock.RLockedFile),
|
||||
|
||||
+4
-4
@@ -254,7 +254,7 @@ func (fs *FSObjects) NSScanner(ctx context.Context, bf *bloomFilter, updates cha
|
||||
return err
|
||||
}
|
||||
totalCache.Info.Name = dataUsageRoot
|
||||
buckets, err := fs.ListBuckets(ctx)
|
||||
buckets, err := fs.ListBuckets(ctx, BucketOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -453,7 +453,7 @@ func (fs *FSObjects) statBucketDir(ctx context.Context, bucket string) (os.FileI
|
||||
}
|
||||
|
||||
// MakeBucketWithLocation - create a new bucket, returns if it already exists.
|
||||
func (fs *FSObjects) MakeBucketWithLocation(ctx context.Context, bucket string, opts BucketOptions) error {
|
||||
func (fs *FSObjects) MakeBucketWithLocation(ctx context.Context, bucket string, opts MakeBucketOptions) error {
|
||||
if opts.LockEnabled || opts.VersioningEnabled {
|
||||
return NotImplemented{}
|
||||
}
|
||||
@@ -524,7 +524,7 @@ func (fs *FSObjects) DeleteBucketPolicy(ctx context.Context, bucket string) erro
|
||||
}
|
||||
|
||||
// GetBucketInfo - fetch bucket metadata info.
|
||||
func (fs *FSObjects) GetBucketInfo(ctx context.Context, bucket string) (bi BucketInfo, e error) {
|
||||
func (fs *FSObjects) GetBucketInfo(ctx context.Context, bucket string, opts BucketOptions) (bi BucketInfo, e error) {
|
||||
st, err := fs.statBucketDir(ctx, bucket)
|
||||
if err != nil {
|
||||
return bi, toObjectErr(err, bucket)
|
||||
@@ -543,7 +543,7 @@ func (fs *FSObjects) GetBucketInfo(ctx context.Context, bucket string) (bi Bucke
|
||||
}
|
||||
|
||||
// ListBuckets - list all s3 compatible buckets (directories) at fsPath.
|
||||
func (fs *FSObjects) ListBuckets(ctx context.Context) ([]BucketInfo, error) {
|
||||
func (fs *FSObjects) ListBuckets(ctx context.Context, opts BucketOptions) ([]BucketInfo, error) {
|
||||
if err := checkPathLength(fs.fsPath); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
|
||||
+13
-13
@@ -62,7 +62,7 @@ func TestFSShutdown(t *testing.T) {
|
||||
fs := obj.(*FSObjects)
|
||||
|
||||
objectContent := "12345"
|
||||
obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{})
|
||||
obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{})
|
||||
obj.PutObject(GlobalContext, bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), ObjectOptions{})
|
||||
return fs, disk
|
||||
}
|
||||
@@ -95,18 +95,18 @@ func TestFSGetBucketInfo(t *testing.T) {
|
||||
fs := obj.(*FSObjects)
|
||||
bucketName := "bucket"
|
||||
|
||||
err := obj.MakeBucketWithLocation(GlobalContext, "a", BucketOptions{})
|
||||
err := obj.MakeBucketWithLocation(GlobalContext, "a", MakeBucketOptions{})
|
||||
if !isSameType(err, BucketNameInvalid{}) {
|
||||
t.Fatal("BucketNameInvalid error not returned")
|
||||
}
|
||||
|
||||
err = obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{})
|
||||
err = obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test with valid parameters
|
||||
info, err := fs.GetBucketInfo(GlobalContext, bucketName)
|
||||
info, err := fs.GetBucketInfo(GlobalContext, bucketName, BucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -115,7 +115,7 @@ func TestFSGetBucketInfo(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test with non-existent bucket
|
||||
_, err = fs.GetBucketInfo(GlobalContext, "a")
|
||||
_, err = fs.GetBucketInfo(GlobalContext, "a", BucketOptions{})
|
||||
if !isSameType(err, BucketNotFound{}) {
|
||||
t.Fatal("BucketNotFound error not returned")
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func TestFSGetBucketInfo(t *testing.T) {
|
||||
// Check for buckets and should get disk not found.
|
||||
os.RemoveAll(disk)
|
||||
|
||||
if _, err = fs.GetBucketInfo(GlobalContext, bucketName); err != nil {
|
||||
if _, err = fs.GetBucketInfo(GlobalContext, bucketName, BucketOptions{}); err != nil {
|
||||
if !isSameType(err, BucketNotFound{}) {
|
||||
t.Fatal("BucketNotFound error not returned")
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func TestFSPutObject(t *testing.T) {
|
||||
bucketName := "bucket"
|
||||
objectName := "1/2/3/4/object"
|
||||
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func TestFSDeleteObject(t *testing.T) {
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
|
||||
obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{})
|
||||
obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{})
|
||||
obj.PutObject(GlobalContext, bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{})
|
||||
|
||||
// Test with invalid bucket name
|
||||
@@ -223,7 +223,7 @@ func TestFSDeleteBucket(t *testing.T) {
|
||||
fs := obj.(*FSObjects)
|
||||
bucketName := "bucket"
|
||||
|
||||
err := obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{})
|
||||
err := obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
@@ -242,7 +242,7 @@ func TestFSDeleteBucket(t *testing.T) {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{})
|
||||
obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{})
|
||||
|
||||
// Delete bucket should get error disk not found.
|
||||
os.RemoveAll(disk)
|
||||
@@ -264,7 +264,7 @@ func TestFSListBuckets(t *testing.T) {
|
||||
fs := obj.(*FSObjects)
|
||||
|
||||
bucketName := "bucket"
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, BucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ func TestFSListBuckets(t *testing.T) {
|
||||
f.Close()
|
||||
|
||||
// Test list buckets to have only one entry.
|
||||
buckets, err := fs.ListBuckets(GlobalContext)
|
||||
buckets, err := fs.ListBuckets(GlobalContext, BucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
@@ -289,7 +289,7 @@ func TestFSListBuckets(t *testing.T) {
|
||||
|
||||
// Test ListBuckets with disk not found.
|
||||
os.RemoveAll(disk)
|
||||
if _, err := fs.ListBuckets(GlobalContext); err != nil {
|
||||
if _, err := fs.ListBuckets(GlobalContext, BucketOptions{}); err != nil {
|
||||
if err != errDiskNotFound {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
+2
-2
@@ -310,7 +310,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
go globalIAMSys.Init(GlobalContext, newObject, globalEtcdClient, globalRefreshIAMInterval)
|
||||
|
||||
if gatewayName == NASBackendGateway {
|
||||
buckets, err := newObject.ListBuckets(GlobalContext)
|
||||
buckets, err := newObject.ListBuckets(GlobalContext, BucketOptions{})
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to list buckets")
|
||||
}
|
||||
@@ -332,7 +332,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
|
||||
// Populate existing buckets to the etcd backend
|
||||
if globalDNSConfig != nil {
|
||||
buckets, err := newObject.ListBuckets(GlobalContext)
|
||||
buckets, err := newObject.ListBuckets(GlobalContext, BucketOptions{})
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to list buckets")
|
||||
}
|
||||
|
||||
@@ -48,12 +48,6 @@ func printGatewayStartupMessage(apiEndPoints []string, backendType string) {
|
||||
|
||||
// Prints documentation message.
|
||||
printObjectAPIMsg()
|
||||
|
||||
if globalMinioConsolePortAuto && globalBrowserEnabled {
|
||||
msg := fmt.Sprintf("\nWARNING: Console endpoint is listening on a dynamic port (%s), please use --console-address \":PORT\" to choose a static port.",
|
||||
globalMinioConsolePort)
|
||||
logger.Info(color.RedBold(msg))
|
||||
}
|
||||
}
|
||||
|
||||
// Prints common server startup message. Prints credential, region and browser access.
|
||||
|
||||
@@ -734,7 +734,7 @@ func (l *s3EncObjects) cleanupStaleEncMultipartUploads(ctx context.Context, clea
|
||||
|
||||
// cleanupStaleUploads removes old custom encryption multipart uploads on backend
|
||||
func (l *s3EncObjects) cleanupStaleUploads(ctx context.Context, expiry time.Duration) {
|
||||
buckets, err := l.s3Objects.ListBuckets(ctx)
|
||||
buckets, err := l.s3Objects.ListBuckets(ctx, minio.BucketOptions{})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
|
||||
@@ -325,7 +325,7 @@ func (l *s3Objects) StorageInfo(ctx context.Context) (si minio.StorageInfo, _ []
|
||||
}
|
||||
|
||||
// MakeBucket creates a new container on S3 backend.
|
||||
func (l *s3Objects) MakeBucketWithLocation(ctx context.Context, bucket string, opts minio.BucketOptions) error {
|
||||
func (l *s3Objects) MakeBucketWithLocation(ctx context.Context, bucket string, opts minio.MakeBucketOptions) error {
|
||||
if opts.LockEnabled || opts.VersioningEnabled {
|
||||
return minio.NotImplemented{}
|
||||
}
|
||||
@@ -348,7 +348,7 @@ func (l *s3Objects) MakeBucketWithLocation(ctx context.Context, bucket string, o
|
||||
}
|
||||
|
||||
// GetBucketInfo gets bucket metadata..
|
||||
func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.BucketInfo, e error) {
|
||||
func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string, opts minio.BucketOptions) (bi minio.BucketInfo, e error) {
|
||||
buckets, err := l.Client.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
// Listbuckets may be disallowed, proceed to check if
|
||||
@@ -381,7 +381,7 @@ func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.
|
||||
}
|
||||
|
||||
// ListBuckets lists all S3 buckets
|
||||
func (l *s3Objects) ListBuckets(ctx context.Context) ([]minio.BucketInfo, error) {
|
||||
func (l *s3Objects) ListBuckets(ctx context.Context, opts minio.BucketOptions) ([]minio.BucketInfo, error) {
|
||||
buckets, err := l.Client.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
return nil, minio.ErrorRespToObjectError(err)
|
||||
|
||||
+1
-1
@@ -316,7 +316,7 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
||||
minDisks: 1,
|
||||
reportNotFound: false,
|
||||
agreed: healEntry,
|
||||
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
|
||||
partial: func(entries metaCacheEntries, _ []error) {
|
||||
entry, ok := entries.resolve(&resolver)
|
||||
if !ok {
|
||||
// check if we can get one entry atleast
|
||||
|
||||
+6
-3
@@ -27,6 +27,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/restapi"
|
||||
minio "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/bucket/bandwidth"
|
||||
"github.com/minio/minio/internal/config"
|
||||
@@ -173,9 +174,8 @@ var (
|
||||
globalMinioAddr = ""
|
||||
|
||||
// MinIO default port, can be changed through command line.
|
||||
globalMinioPort = GlobalMinioDefaultPort
|
||||
globalMinioConsolePort = "13333"
|
||||
globalMinioConsolePortAuto = false
|
||||
globalMinioPort = GlobalMinioDefaultPort
|
||||
globalMinioConsolePort = "13333"
|
||||
|
||||
// Holds the host that was passed using --address
|
||||
globalMinioHost = ""
|
||||
@@ -377,6 +377,9 @@ var (
|
||||
// MinIO version unix timestamp
|
||||
globalVersionUnix uint64
|
||||
|
||||
// MinIO client
|
||||
globalMinioClient *minio.Client
|
||||
|
||||
// Add new variable global values here.
|
||||
)
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -197,16 +196,6 @@ func extractMetadataFromMime(ctx context.Context, v textproto.MIMEHeader, m map[
|
||||
return nil
|
||||
}
|
||||
|
||||
// The Query string for the redirect URL the client is
|
||||
// redirected on successful upload.
|
||||
func getRedirectPostRawQuery(objInfo ObjectInfo) string {
|
||||
redirectValues := make(url.Values)
|
||||
redirectValues.Set("bucket", objInfo.Bucket)
|
||||
redirectValues.Set("key", objInfo.Name)
|
||||
redirectValues.Set("etag", "\""+objInfo.ETag+"\"")
|
||||
return redirectValues.Encode()
|
||||
}
|
||||
|
||||
// Returns access credentials in the request Authorization header.
|
||||
func getReqAccessCred(r *http.Request, region string) (cred auth.Credentials) {
|
||||
cred, _, _ = getReqAccessKeyV4(r, region, serviceS3)
|
||||
|
||||
+1
-25
@@ -337,33 +337,16 @@ func (c *iamCache) policyDBGet(mode UsersSysType, name string, isGroup bool) ([]
|
||||
return c.iamGroupPolicyMap[name].toSlice(), c.iamGroupPolicyMap[name].UpdatedAt, nil
|
||||
}
|
||||
|
||||
if name == globalActiveCred.AccessKey {
|
||||
return []string{"consoleAdmin"}, time.Time{}, nil
|
||||
}
|
||||
|
||||
// When looking for a user's policies, we also check if the user
|
||||
// and the groups they are member of are enabled.
|
||||
var parentName string
|
||||
u, ok := c.iamUsersMap[name]
|
||||
if ok {
|
||||
if !u.Credentials.IsValid() {
|
||||
return nil, time.Time{}, nil
|
||||
}
|
||||
parentName = u.Credentials.ParentUser
|
||||
}
|
||||
|
||||
mp, ok := c.iamUserPolicyMap[name]
|
||||
if !ok {
|
||||
// Service accounts with root credentials, inherit parent permissions
|
||||
if parentName == globalActiveCred.AccessKey && u.Credentials.IsServiceAccount() {
|
||||
// even if this is set, the claims present in the service
|
||||
// accounts apply the final permissions if any.
|
||||
return []string{"consoleAdmin"}, mp.UpdatedAt, nil
|
||||
}
|
||||
if parentName != "" {
|
||||
mp = c.iamUserPolicyMap[parentName]
|
||||
}
|
||||
}
|
||||
mp := c.iamUserPolicyMap[name]
|
||||
|
||||
// returned policy could be empty
|
||||
policies := mp.toSlice()
|
||||
@@ -900,7 +883,6 @@ func (store *IAMStoreSys) PolicyDBSet(ctx context.Context, name, policy string,
|
||||
mp := newMappedPolicy(policy)
|
||||
for _, p := range mp.toSlice() {
|
||||
if _, found := cache.iamPolicyDocsMap[p]; !found {
|
||||
logger.LogIf(GlobalContext, fmt.Errorf("%w: (%s)", errNoSuchPolicy, p))
|
||||
return updatedAt, errNoSuchPolicy
|
||||
}
|
||||
}
|
||||
@@ -1002,12 +984,6 @@ func (store *IAMStoreSys) DeletePolicy(ctx context.Context, policy string) error
|
||||
}
|
||||
}
|
||||
if len(users) != 0 || len(groups) != 0 {
|
||||
// error out when a policy could not be deleted as it was in use.
|
||||
loggedErr := fmt.Errorf("policy could not be deleted as it is use (users=%s; groups=%s)",
|
||||
fmt.Sprintf("[%s]", strings.Join(users, ",")),
|
||||
fmt.Sprintf("[%s]", strings.Join(groups, ",")),
|
||||
)
|
||||
logger.LogIf(GlobalContext, loggedErr)
|
||||
return errPolicyInUse
|
||||
}
|
||||
|
||||
|
||||
+85
-46
@@ -1458,7 +1458,7 @@ const sessionPolicyNameExtracted = iampolicy.SessionPolicyName + "-extracted"
|
||||
// IsAllowedServiceAccount - checks if the given service account is allowed to perform
|
||||
// actions. The permission of the parent user is checked first
|
||||
func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser string) bool {
|
||||
// Now check if we have a subject claim
|
||||
// Verify if the parent claim matches the parentUser.
|
||||
p, ok := args.Claims[parentClaim]
|
||||
if ok {
|
||||
parentInClaim, ok := p.(string)
|
||||
@@ -1478,41 +1478,56 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
|
||||
return false
|
||||
}
|
||||
|
||||
// Check policy for parent user of service account.
|
||||
svcPolicies, err := sys.PolicyDBGet(parentUser, false, args.Groups...)
|
||||
if err != nil {
|
||||
logger.LogIf(GlobalContext, err)
|
||||
return false
|
||||
}
|
||||
isOwnerDerived := parentUser == globalActiveCred.AccessKey
|
||||
|
||||
if len(svcPolicies) == 0 {
|
||||
// If parent user has no policies, check for OpenID
|
||||
// claims/RoleARN in case it exists.
|
||||
roleArn := args.GetRoleArn()
|
||||
if roleArn != "" {
|
||||
arn, err := arn.Parse(roleArn)
|
||||
if err != nil {
|
||||
logger.LogIf(GlobalContext, fmt.Errorf("error parsing role ARN %s: %v", roleArn, err))
|
||||
return false
|
||||
}
|
||||
svcPolicies = newMappedPolicy(sys.rolesMap[arn]).toSlice()
|
||||
} else {
|
||||
// If there is no roleArn claim, check the OpenID
|
||||
// provider's policy claim.
|
||||
var err error
|
||||
var svcPolicies []string
|
||||
roleArn := args.GetRoleArn()
|
||||
|
||||
switch {
|
||||
case isOwnerDerived:
|
||||
// All actions are allowed by default and no policy evaluation is
|
||||
// required.
|
||||
|
||||
case roleArn != "":
|
||||
arn, err := arn.Parse(roleArn)
|
||||
if err != nil {
|
||||
logger.LogIf(GlobalContext, fmt.Errorf("error parsing role ARN %s: %v", roleArn, err))
|
||||
return false
|
||||
}
|
||||
svcPolicies = newMappedPolicy(sys.rolesMap[arn]).toSlice()
|
||||
|
||||
default:
|
||||
// Check policy for parent user of service account.
|
||||
svcPolicies, err = sys.PolicyDBGet(parentUser, false, args.Groups...)
|
||||
if err != nil {
|
||||
logger.LogIf(GlobalContext, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Finally, if there is no parent policy, check if a policy claim is
|
||||
// present.
|
||||
if len(svcPolicies) == 0 {
|
||||
policySet, _ := iampolicy.GetPoliciesFromClaims(args.Claims, iamPolicyClaimNameOpenID())
|
||||
svcPolicies = policySet.ToSlice()
|
||||
}
|
||||
if len(svcPolicies) == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Policies were found, evaluate all of them.
|
||||
availablePoliciesStr, combinedPolicy := sys.store.FilterPolicies(strings.Join(svcPolicies, ","), "")
|
||||
if availablePoliciesStr == "" {
|
||||
// Defensive code: Do not allow any operation if no policy is found.
|
||||
if !isOwnerDerived && len(svcPolicies) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
var combinedPolicy iampolicy.Policy
|
||||
// Policies were found, evaluate all of them.
|
||||
if !isOwnerDerived {
|
||||
availablePoliciesStr, c := sys.store.FilterPolicies(strings.Join(svcPolicies, ","), "")
|
||||
if availablePoliciesStr == "" {
|
||||
return false
|
||||
}
|
||||
combinedPolicy = c
|
||||
}
|
||||
|
||||
parentArgs := args
|
||||
parentArgs.AccountName = parentUser
|
||||
// These are dynamic values set them appropriately.
|
||||
@@ -1532,7 +1547,7 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
|
||||
}
|
||||
|
||||
if saPolicyClaimStr == inheritedPolicyType {
|
||||
return combinedPolicy.IsAllowed(parentArgs)
|
||||
return isOwnerDerived || combinedPolicy.IsAllowed(parentArgs)
|
||||
}
|
||||
|
||||
// Now check if we have a sessionPolicy.
|
||||
@@ -1558,37 +1573,51 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
|
||||
|
||||
// This can only happen if policy was set but with an empty JSON.
|
||||
if subPolicy.Version == "" && len(subPolicy.Statements) == 0 {
|
||||
return combinedPolicy.IsAllowed(parentArgs)
|
||||
return isOwnerDerived || combinedPolicy.IsAllowed(parentArgs)
|
||||
}
|
||||
|
||||
if subPolicy.Version == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return combinedPolicy.IsAllowed(parentArgs) && subPolicy.IsAllowed(parentArgs)
|
||||
return subPolicy.IsAllowed(parentArgs) && (isOwnerDerived || combinedPolicy.IsAllowed(parentArgs))
|
||||
}
|
||||
|
||||
// IsAllowedSTS is meant for STS based temporary credentials,
|
||||
// which implements claims validation and verification other than
|
||||
// applying policies.
|
||||
func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
|
||||
// 1. Determine mapped policies
|
||||
|
||||
isOwnerDerived := parentUser == globalActiveCred.AccessKey
|
||||
var policies []string
|
||||
roleArn := args.GetRoleArn()
|
||||
if roleArn != "" {
|
||||
|
||||
switch {
|
||||
case isOwnerDerived:
|
||||
// All actions are allowed by default and no policy evaluation is
|
||||
// required.
|
||||
|
||||
case roleArn != "":
|
||||
// If a roleARN is present, the role policy is applied.
|
||||
arn, err := arn.Parse(roleArn)
|
||||
if err != nil {
|
||||
logger.LogIf(GlobalContext, fmt.Errorf("error parsing role ARN %s: %v", roleArn, err))
|
||||
return false
|
||||
}
|
||||
policies = newMappedPolicy(sys.rolesMap[arn]).toSlice()
|
||||
} else {
|
||||
// Lookup the parent user's mapping if there's no role-ARN.
|
||||
|
||||
default:
|
||||
// Otherwise, inherit parent user's policy
|
||||
var err error
|
||||
policies, err = sys.store.PolicyDBGet(parentUser, false, args.Groups...)
|
||||
if err != nil {
|
||||
logger.LogIf(GlobalContext, fmt.Errorf("error fetching policies on %s: %v", parentUser, err))
|
||||
return false
|
||||
}
|
||||
|
||||
// Finally, if there is no parent policy, check if a policy claim is
|
||||
// present in the session token.
|
||||
if len(policies) == 0 {
|
||||
// If there is no parent policy mapping, we fall back to
|
||||
// using policy claim from JWT.
|
||||
@@ -1599,27 +1628,37 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
|
||||
}
|
||||
policies = policySet.ToSlice()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Defensive code: Do not allow any operation if no policy is found in the session token
|
||||
if len(policies) == 0 {
|
||||
if !isOwnerDerived && len(policies) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
combinedPolicy, err := sys.store.GetPolicy(strings.Join(policies, ","))
|
||||
if err == errNoSuchPolicy {
|
||||
for _, pname := range policies {
|
||||
_, err := sys.store.GetPolicy(pname)
|
||||
if err == errNoSuchPolicy {
|
||||
// all policies presented in the claim should exist
|
||||
logger.LogIf(GlobalContext, fmt.Errorf("expected policy (%s) missing from the JWT claim %s, rejecting the request", pname, iamPolicyClaimNameOpenID()))
|
||||
return false
|
||||
// 2. Combine the mapped policies into a single combined policy.
|
||||
|
||||
var combinedPolicy iampolicy.Policy
|
||||
if !isOwnerDerived {
|
||||
var err error
|
||||
combinedPolicy, err = sys.store.GetPolicy(strings.Join(policies, ","))
|
||||
if err == errNoSuchPolicy {
|
||||
for _, pname := range policies {
|
||||
_, err := sys.store.GetPolicy(pname)
|
||||
if err == errNoSuchPolicy {
|
||||
// all policies presented in the claim should exist
|
||||
logger.LogIf(GlobalContext, fmt.Errorf("expected policy (%s) missing from the JWT claim %s, rejecting the request", pname, iamPolicyClaimNameOpenID()))
|
||||
return false
|
||||
}
|
||||
}
|
||||
logger.LogIf(GlobalContext, fmt.Errorf("all policies were unexpectedly present!"))
|
||||
return false
|
||||
}
|
||||
logger.LogIf(GlobalContext, fmt.Errorf("all policies were unexpectedly present!"))
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// 3. If an inline session-policy is present, evaluate it.
|
||||
|
||||
// These are dynamic values set them appropriately.
|
||||
args.ConditionValues["username"] = []string{parentUser}
|
||||
args.ConditionValues["userid"] = []string{parentUser}
|
||||
@@ -1627,12 +1666,12 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
|
||||
// Now check if we have a sessionPolicy.
|
||||
hasSessionPolicy, isAllowedSP := isAllowedBySessionPolicy(args)
|
||||
if hasSessionPolicy {
|
||||
return isAllowedSP && combinedPolicy.IsAllowed(args)
|
||||
return isAllowedSP && (isOwnerDerived || combinedPolicy.IsAllowed(args))
|
||||
}
|
||||
|
||||
// Sub policy not set, this is most common since subPolicy
|
||||
// is optional, use the inherited policies.
|
||||
return combinedPolicy.IsAllowed(args)
|
||||
return isOwnerDerived || combinedPolicy.IsAllowed(args)
|
||||
}
|
||||
|
||||
func isAllowedBySessionPolicy(args iampolicy.Args) (hasSessionPolicy bool, isAllowed bool) {
|
||||
|
||||
@@ -113,7 +113,7 @@ func (api objectAPIHandlers) ListenNotificationHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
if bucketName != "" {
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucketName); err != nil {
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucketName, BucketOptions{}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -667,8 +667,11 @@ func filterLifeCycle(ctx context.Context, bucket string, lc lifecycle.Lifecycle,
|
||||
action := evalActionFromLifecycle(ctx, lc, lr, objInfo, false)
|
||||
switch action {
|
||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
||||
fallthrough
|
||||
globalExpiryState.enqueueByDays(objInfo, false, action == lifecycle.DeleteVersionAction)
|
||||
// Skip this entry.
|
||||
continue
|
||||
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||
globalExpiryState.enqueueByDays(objInfo, true, action == lifecycle.DeleteRestoredVersionAction)
|
||||
// Skip this entry.
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -760,7 +760,7 @@ func (es *erasureSingle) listPathInner(ctx context.Context, o listPathOptions, r
|
||||
case results <- entry:
|
||||
}
|
||||
},
|
||||
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
|
||||
partial: func(entries metaCacheEntries, errs []error) {
|
||||
// Results Disagree :-(
|
||||
entry, ok := entries.resolve(&resolver)
|
||||
if ok {
|
||||
@@ -829,7 +829,7 @@ func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, resul
|
||||
case results <- entry:
|
||||
}
|
||||
},
|
||||
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
|
||||
partial: func(entries metaCacheEntries, errs []error) {
|
||||
// Results Disagree :-(
|
||||
entry, ok := entries.resolve(&resolver)
|
||||
if ok {
|
||||
@@ -1145,7 +1145,7 @@ type listPathRawOptions struct {
|
||||
// partial will be called when there is disagreement between disks.
|
||||
// if disk did not return any result, but also haven't errored
|
||||
// the entry will be empty and errs will
|
||||
partial func(entries metaCacheEntries, nAgreed int, errs []error)
|
||||
partial func(entries metaCacheEntries, errs []error)
|
||||
|
||||
// finished will be called when all streams have finished and
|
||||
// more than one disk returned an error.
|
||||
@@ -1362,7 +1362,7 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
|
||||
continue
|
||||
}
|
||||
if opts.partial != nil {
|
||||
opts.partial(topEntries, agree, errs)
|
||||
opts.partial(topEntries, errs)
|
||||
}
|
||||
// Skip the inputs we used.
|
||||
for i, r := range readers {
|
||||
|
||||
+113
-2
@@ -27,6 +27,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/kes"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
@@ -49,6 +50,7 @@ func init() {
|
||||
getNodeHealthMetrics(),
|
||||
getClusterStorageMetrics(),
|
||||
getClusterTierMetrics(),
|
||||
getKMSMetrics(),
|
||||
}
|
||||
|
||||
peerMetricsGroups = []*MetricsGroup{
|
||||
@@ -63,6 +65,7 @@ func init() {
|
||||
getILMNodeMetrics(),
|
||||
getScannerNodeMetrics(),
|
||||
getIAMNodeMetrics(),
|
||||
getKMSNodeMetrics(),
|
||||
}
|
||||
|
||||
allMetricsGroups := func() (allMetrics []*MetricsGroup) {
|
||||
@@ -80,7 +83,6 @@ func init() {
|
||||
getMinioVersionMetrics(),
|
||||
getS3TTFBMetric(),
|
||||
})
|
||||
|
||||
clusterCollector = newMinioClusterCollector(allMetricsGroups)
|
||||
}
|
||||
|
||||
@@ -123,6 +125,7 @@ const (
|
||||
ilmSubsystem MetricSubsystem = "ilm"
|
||||
scannerSubsystem MetricSubsystem = "scanner"
|
||||
iamSubsystem MetricSubsystem = "iam"
|
||||
kmsSubsystem MetricSubsystem = "kms"
|
||||
)
|
||||
|
||||
// MetricName are the individual names for the metric.
|
||||
@@ -188,6 +191,12 @@ const (
|
||||
transitionedBytes MetricName = "transitioned_bytes"
|
||||
transitionedObjects MetricName = "transitioned_objects"
|
||||
transitionedVersions MetricName = "transitioned_versions"
|
||||
|
||||
kmsOnline = "online"
|
||||
kmsRequestsSuccess = "request_success"
|
||||
kmsRequestsError = "request_error"
|
||||
kmsRequestsFail = "request_failure"
|
||||
kmsUptime = "uptime"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -232,6 +241,7 @@ type MetricsGroup struct {
|
||||
// to populate new values upon cache invalidation.
|
||||
func (g *MetricsGroup) RegisterRead(read func(ctx context.Context) []Metric) {
|
||||
g.metricsCache.Once.Do(func() {
|
||||
g.metricsCache.Relax = true
|
||||
g.metricsCache.TTL = g.cacheInterval
|
||||
g.metricsCache.Update = func() (interface{}, error) {
|
||||
return read(GlobalContext), nil
|
||||
@@ -986,7 +996,7 @@ func getMinioProcMetrics() *MetricsGroup {
|
||||
metrics = make([]Metric, 0, 20)
|
||||
p, err := procfs.Self()
|
||||
if err != nil {
|
||||
logger.LogOnceIf(ctx, err, nodeMetricNamespace)
|
||||
logger.LogOnceIf(ctx, err, string(nodeMetricNamespace))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1930,6 +1940,107 @@ func getClusterStorageMetrics() *MetricsGroup {
|
||||
return mg
|
||||
}
|
||||
|
||||
func getKMSNodeMetrics() *MetricsGroup {
|
||||
mg := &MetricsGroup{
|
||||
cacheInterval: 10 * time.Second,
|
||||
}
|
||||
|
||||
mg.RegisterRead(func(ctx context.Context) (metrics []Metric) {
|
||||
objLayer := newObjectLayerFn()
|
||||
// Service not initialized yet
|
||||
if objLayer == nil || globalIsGateway || GlobalKMS == nil {
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
Online = 1
|
||||
Offline = 0
|
||||
)
|
||||
desc := MetricDescription{
|
||||
Namespace: clusterMetricNamespace,
|
||||
Subsystem: kmsSubsystem,
|
||||
Name: kmsOnline,
|
||||
Help: "Reports whether the KMS is online (1) or offline (0)",
|
||||
Type: gaugeMetric,
|
||||
}
|
||||
_, err := GlobalKMS.Metrics(ctx)
|
||||
if _, ok := kes.IsConnError(err); ok {
|
||||
return []Metric{{
|
||||
Description: desc,
|
||||
Value: float64(Offline),
|
||||
}}
|
||||
}
|
||||
return []Metric{{
|
||||
Description: desc,
|
||||
Value: float64(Online),
|
||||
}}
|
||||
})
|
||||
return mg
|
||||
}
|
||||
|
||||
func getKMSMetrics() *MetricsGroup {
|
||||
mg := &MetricsGroup{
|
||||
cacheInterval: 10 * time.Second,
|
||||
}
|
||||
|
||||
mg.RegisterRead(func(ctx context.Context) []Metric {
|
||||
objLayer := newObjectLayerFn()
|
||||
// Service not initialized yet
|
||||
if objLayer == nil || globalIsGateway || GlobalKMS == nil {
|
||||
return []Metric{}
|
||||
}
|
||||
|
||||
metrics := make([]Metric, 0, 4)
|
||||
metric, err := GlobalKMS.Metrics(ctx)
|
||||
if err != nil {
|
||||
return metrics
|
||||
}
|
||||
metrics = append(metrics, Metric{
|
||||
Description: MetricDescription{
|
||||
Namespace: clusterMetricNamespace,
|
||||
Subsystem: kmsSubsystem,
|
||||
Name: kmsRequestsSuccess,
|
||||
Help: "Number of KMS requests that succeeded",
|
||||
Type: counterMetric,
|
||||
},
|
||||
Value: float64(metric.RequestOK),
|
||||
})
|
||||
metrics = append(metrics, Metric{
|
||||
Description: MetricDescription{
|
||||
Namespace: clusterMetricNamespace,
|
||||
Subsystem: kmsSubsystem,
|
||||
Name: kmsRequestsError,
|
||||
Help: "Number of KMS requests that failed due to some error. (HTTP 4xx status code)",
|
||||
Type: counterMetric,
|
||||
},
|
||||
Value: float64(metric.RequestErr),
|
||||
})
|
||||
metrics = append(metrics, Metric{
|
||||
Description: MetricDescription{
|
||||
Namespace: clusterMetricNamespace,
|
||||
Subsystem: kmsSubsystem,
|
||||
Name: kmsRequestsFail,
|
||||
Help: "Number of KMS requests that failed due to some internal failure. (HTTP 5xx status code)",
|
||||
Type: counterMetric,
|
||||
},
|
||||
Value: float64(metric.RequestFail),
|
||||
})
|
||||
metrics = append(metrics, Metric{
|
||||
Description: MetricDescription{
|
||||
Namespace: clusterMetricNamespace,
|
||||
Subsystem: kmsSubsystem,
|
||||
Name: kmsUptime,
|
||||
Help: "The time the KMS has been up and running in seconds.",
|
||||
Type: counterMetric,
|
||||
},
|
||||
Value: metric.UpTime.Seconds(),
|
||||
})
|
||||
|
||||
return metrics
|
||||
})
|
||||
return mg
|
||||
}
|
||||
|
||||
type minioClusterCollector struct {
|
||||
metricsGroups []*MetricsGroup
|
||||
desc *prometheus.Desc
|
||||
|
||||
@@ -221,11 +221,11 @@ func (d *naughtyDisk) CheckParts(ctx context.Context, volume string, path string
|
||||
return d.disk.CheckParts(ctx, volume, path, fi)
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) Delete(ctx context.Context, volume string, path string, recursive bool) (err error) {
|
||||
func (d *naughtyDisk) Delete(ctx context.Context, volume string, path string, deleteOpts DeleteOptions) (err error) {
|
||||
if err := d.calcError(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.disk.Delete(ctx, volume, path, recursive)
|
||||
return d.disk.Delete(ctx, volume, path, deleteOpts)
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) DeleteVersions(ctx context.Context, volume string, versions []FileInfoVersions) []error {
|
||||
@@ -301,3 +301,11 @@ func (d *naughtyDisk) StatInfoFile(ctx context.Context, volume, path string, glo
|
||||
}
|
||||
return d.disk.StatInfoFile(ctx, volume, path, glob)
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) ReadMultiple(ctx context.Context, req ReadMultipleReq, resp chan<- ReadMultipleResp) error {
|
||||
if err := d.calcError(); err != nil {
|
||||
close(resp)
|
||||
return err
|
||||
}
|
||||
return d.disk.ReadMultiple(ctx, req, resp)
|
||||
}
|
||||
|
||||
+33
-62
@@ -297,9 +297,7 @@ func (sys *NotificationSys) StartProfiling(profiler string) []NotificationPeerEr
|
||||
}
|
||||
|
||||
// DownloadProfilingData - download profiling data from all remote peers.
|
||||
func (sys *NotificationSys) DownloadProfilingData(ctx context.Context, writer io.Writer) bool {
|
||||
profilingDataFound := false
|
||||
|
||||
func (sys *NotificationSys) DownloadProfilingData(ctx context.Context, writer io.Writer) (profilingDataFound bool) {
|
||||
// Initialize a zip writer which will provide a zipped content
|
||||
// of profiling data of all nodes
|
||||
zipWriter := zip.NewWriter(writer)
|
||||
@@ -320,34 +318,11 @@ func (sys *NotificationSys) DownloadProfilingData(ctx context.Context, writer io
|
||||
profilingDataFound = true
|
||||
|
||||
for typ, data := range data {
|
||||
// Send profiling data to zip as file
|
||||
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
||||
name: fmt.Sprintf("profile-%s-%s", client.host.String(), typ),
|
||||
size: int64(len(data)),
|
||||
mode: 0o600,
|
||||
modTime: UTCNow(),
|
||||
isDir: false,
|
||||
sys: nil,
|
||||
})
|
||||
if zerr != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", client.host.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, zerr)
|
||||
continue
|
||||
}
|
||||
header.Method = zip.Deflate
|
||||
zwriter, zerr := zipWriter.CreateHeader(header)
|
||||
if zerr != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", client.host.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, zerr)
|
||||
continue
|
||||
}
|
||||
if _, err = io.Copy(zwriter, bytes.NewReader(data)); err != nil {
|
||||
err := embedFileInZip(zipWriter, fmt.Sprintf("profile-%s-%s", client.host.String(), typ), data)
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", client.host.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -371,34 +346,18 @@ func (sys *NotificationSys) DownloadProfilingData(ctx context.Context, writer io
|
||||
|
||||
// Send profiling data to zip as file
|
||||
for typ, data := range data {
|
||||
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
||||
name: fmt.Sprintf("profile-%s-%s", thisAddr, typ),
|
||||
size: int64(len(data)),
|
||||
mode: 0o600,
|
||||
modTime: UTCNow(),
|
||||
isDir: false,
|
||||
sys: nil,
|
||||
})
|
||||
if zerr != nil {
|
||||
return profilingDataFound
|
||||
}
|
||||
header.Method = zip.Deflate
|
||||
|
||||
zwriter, zerr := zipWriter.CreateHeader(header)
|
||||
if zerr != nil {
|
||||
return profilingDataFound
|
||||
}
|
||||
|
||||
if _, err = io.Copy(zwriter, bytes.NewReader(data)); err != nil {
|
||||
return profilingDataFound
|
||||
err := embedFileInZip(zipWriter, fmt.Sprintf("profile-%s-%s", thisAddr, typ), data)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
return profilingDataFound
|
||||
appendClusterMetaInfoToZip(ctx, zipWriter)
|
||||
return
|
||||
}
|
||||
|
||||
// ServerUpdate - updates remote peers.
|
||||
func (sys *NotificationSys) ServerUpdate(ctx context.Context, u *url.URL, sha256Sum []byte, lrTime time.Time, releaseInfo string) []NotificationPeerErr {
|
||||
// VerifyBinary - asks remote peers to verify the checksum
|
||||
func (sys *NotificationSys) VerifyBinary(ctx context.Context, u *url.URL, sha256Sum []byte, releaseInfo string, reader []byte) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
@@ -406,7 +365,22 @@ func (sys *NotificationSys) ServerUpdate(ctx context.Context, u *url.URL, sha256
|
||||
}
|
||||
client := client
|
||||
ng.Go(ctx, func() error {
|
||||
return client.ServerUpdate(ctx, u, sha256Sum, lrTime, releaseInfo)
|
||||
return client.VerifyBinary(ctx, u, sha256Sum, releaseInfo, reader)
|
||||
}, idx, *client.host)
|
||||
}
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// CommitBinary - asks remote peers to overwrite the old binary with the new one
|
||||
func (sys *NotificationSys) CommitBinary(ctx context.Context) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
client := client
|
||||
ng.Go(ctx, func() error {
|
||||
return client.CommitBinary(ctx)
|
||||
}, idx, *client.host)
|
||||
}
|
||||
return ng.Wait()
|
||||
@@ -482,7 +456,7 @@ func (sys *NotificationSys) updateBloomFilter(ctx context.Context, current uint6
|
||||
defer mu.Unlock()
|
||||
|
||||
if err != nil || !serverBF.Complete || bf == nil {
|
||||
logger.LogOnceIf(ctx, err, fmt.Sprintf("host:%s, cycle:%d", client.host, current), client.cycleServerBloomFilter)
|
||||
logger.LogOnceIf(ctx, err, client.host.String(), client.cycleServerBloomFilter)
|
||||
bf = nil
|
||||
return nil
|
||||
}
|
||||
@@ -743,7 +717,7 @@ func (sys *NotificationSys) InitBucketTargets(ctx context.Context, objAPI Object
|
||||
if res.Err != nil {
|
||||
reqInfo := &logger.ReqInfo{}
|
||||
reqInfo.AppendTags("targetID", res.ID.Name)
|
||||
logger.LogOnceIf(logger.SetReqInfo(GlobalContext, reqInfo), res.Err, res.ID)
|
||||
logger.LogOnceIf(logger.SetReqInfo(GlobalContext, reqInfo), res.Err, res.ID.String())
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -1512,17 +1486,15 @@ func (sys *NotificationSys) Netperf(ctx context.Context, duration time.Duration)
|
||||
return results
|
||||
}
|
||||
|
||||
// Speedtest run GET/PUT tests at input concurrency for requested object size,
|
||||
// SpeedTest run GET/PUT tests at input concurrency for requested object size,
|
||||
// optionally you can extend the tests longer with time.Duration.
|
||||
func (sys *NotificationSys) Speedtest(ctx context.Context, size int,
|
||||
concurrent int, duration time.Duration, storageClass string,
|
||||
) []SpeedtestResult {
|
||||
func (sys *NotificationSys) SpeedTest(ctx context.Context, sopts speedTestOpts) []SpeedTestResult {
|
||||
length := len(sys.allPeerClients)
|
||||
if length == 0 {
|
||||
// For single node erasure setup.
|
||||
length = 1
|
||||
}
|
||||
results := make([]SpeedtestResult, length)
|
||||
results := make([]SpeedTestResult, length)
|
||||
|
||||
scheme := "http"
|
||||
if globalIsTLS {
|
||||
@@ -1537,8 +1509,7 @@ func (sys *NotificationSys) Speedtest(ctx context.Context, size int,
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
r, err := sys.peerClients[index].Speedtest(ctx, size,
|
||||
concurrent, duration, storageClass)
|
||||
r, err := sys.peerClients[index].SpeedTest(ctx, sopts)
|
||||
u := &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: sys.peerClients[index].host.String(),
|
||||
@@ -1555,7 +1526,7 @@ func (sys *NotificationSys) Speedtest(ctx context.Context, size int,
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
r, err := selfSpeedtest(ctx, size, concurrent, duration, storageClass)
|
||||
r, err := selfSpeedTest(ctx, sopts)
|
||||
u := &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: globalLocalNodeName,
|
||||
|
||||
@@ -42,6 +42,9 @@ const (
|
||||
// Buckets meta prefix.
|
||||
bucketMetaPrefix = "buckets"
|
||||
|
||||
// Deleted Buckets prefix.
|
||||
deletedBucketsPrefix = ".deleted"
|
||||
|
||||
// ETag (hex encoded md5sum) of empty string.
|
||||
emptyETag = "d41d8cd98f00b204e9800998ecf8427e"
|
||||
)
|
||||
|
||||
@@ -78,6 +78,7 @@ type BucketInfo struct {
|
||||
|
||||
// Date and time when the bucket was created.
|
||||
Created time.Time
|
||||
Deleted time.Time
|
||||
}
|
||||
|
||||
// ObjectInfo - represents object metadata.
|
||||
@@ -261,13 +262,16 @@ func (o ObjectInfo) tierStats() tierStats {
|
||||
// ReplicateObjectInfo represents object info to be replicated
|
||||
type ReplicateObjectInfo struct {
|
||||
ObjectInfo
|
||||
OpType replication.Type
|
||||
RetryCount uint32
|
||||
ResetID string
|
||||
Dsc ReplicateDecision
|
||||
ExistingObjResync ResyncDecision
|
||||
TargetArn string
|
||||
TargetStatuses map[string]replication.StatusType
|
||||
OpType replication.Type
|
||||
EventType string
|
||||
RetryCount uint32
|
||||
ResetID string
|
||||
Dsc ReplicateDecision
|
||||
ExistingObjResync ResyncDecision
|
||||
TargetArn string
|
||||
TargetStatuses map[string]replication.StatusType
|
||||
TargetPurgeStatuses map[string]VersionPurgeStatusType
|
||||
ReplicationTimestamp time.Time
|
||||
}
|
||||
|
||||
// MultipartInfo captures metadata information about the uploadId
|
||||
|
||||
@@ -84,7 +84,7 @@ func testDeleteObject(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := obj.MakeBucketWithLocation(context.Background(), testCase.bucketName, BucketOptions{})
|
||||
err := obj.MakeBucketWithLocation(context.Background(), testCase.bucketName, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user