mirror of
https://github.com/pgsty/minio.git
synced 2026-11-12 18:45:42 +03:00
Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be97ae4c5d | |||
| 4d7d008741 | |||
| 2d7a3d1516 | |||
| dfab400d43 | |||
| 70078eab10 | |||
| 3415c4dd1e | |||
| e200808ab7 | |||
| fae563b85d | |||
| 3e6dc02f8f | |||
| 95e4cbbfde | |||
| 2825294b7b | |||
| bce93b5cfa | |||
| 7a4b250c8b | |||
| e5335450a4 | |||
| a6ffdf1dd4 | |||
| 69e41f87ef | |||
| ee48f9f206 | |||
| 9ba39d7fad | |||
| d2fb371f80 | |||
| 2f9018f03b | |||
| eb990f64a9 | |||
| bbb64eaade | |||
| 7bd1d899bc | |||
| c91d1ec2e3 | |||
| c50b64027d | |||
| 20960b6a2d | |||
| 3bd3470d0b | |||
| ba39ed9af7 | |||
| 62e6dc950d | |||
| 5a5046ce45 | |||
| ad04afe381 | |||
| ba9f0f2480 | |||
| d06b63d056 | |||
| 7ce28c3b1d | |||
| e3ac4035b9 | |||
| d21b6daa49 | |||
| 76ebb16688 | |||
| 55aa431578 | |||
| 614981e566 | |||
| b8b956a05d | |||
| d2eed44c78 | |||
| 789cbc6fb2 | |||
| 0662c90b5c | |||
| a2cab02554 | |||
| 6c7a21df6b | |||
| f933b0b708 | |||
| 9f305273a7 | |||
| cbd9efcb43 | |||
| 29a25a538f | |||
| 2dd8faaedc | |||
| f00187033d | |||
| c5141d65ac | |||
| 069c4015cd | |||
| 2f6e03fb60 | |||
| 0fbb945e13 | |||
| b94dd835c9 | |||
| 44fc707423 | |||
| 5aaef9790f | |||
| 7edc352d23 | |||
| 850a84b08a | |||
| 4148754ce0 | |||
| 2107722829 | |||
| d326ba52e9 | |||
| 91e1487de4 | |||
| 5ffb2a9605 | |||
| 17fe91d6d1 | |||
| 90a9f2dd70 | |||
| d5e48cfd65 | |||
| d274566463 | |||
| 39ac720826 | |||
| 21b6204692 | |||
| d98faeb26a | |||
| 0a63dc199c | |||
| 3ba857dfa1 | |||
| a8554c4022 | |||
| ba54b39c02 | |||
| 2a75225569 | |||
| e72429c79c | |||
| c5b3f5553f | |||
| d3ae0aaad3 | |||
| d67bccf861 | |||
| 1277ad69a6 | |||
| 8f93e81afb | |||
| 4af31e654b | |||
| aad50579ba | |||
| 38d059b0ae | |||
| bd4eeb4522 | |||
| 03e3493288 | |||
| 64baedf5a4 | |||
| 2f64d5f77e | |||
| f79a4ef4d0 | |||
| 2d53854b19 | |||
| e5c83535af | |||
| c904ef966e | |||
| 8f266e0772 |
@@ -22,7 +22,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Helm
|
- name: Install Helm
|
||||||
uses: azure/setup-helm@v3
|
uses: azure/setup-helm@v4
|
||||||
|
|
||||||
- name: Run helm lint
|
- name: Run helm lint
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -55,9 +55,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "erasure" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
|
${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "erasure" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
|
||||||
|
|
||||||
- name: resiliency
|
# FIXME: renable this back when we have a valid way to add deadlines for PUT()s (internode CreateFile)
|
||||||
run: |
|
# - name: resiliency
|
||||||
${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "resiliency" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
|
# run: |
|
||||||
|
# ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "resiliency" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
|
||||||
|
|
||||||
- name: The job must cleanup
|
- name: The job must cleanup
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.22.3
|
go-version: 1.22.4
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Get official govulncheck
|
- name: Get official govulncheck
|
||||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ lint-fix: getdeps ## runs golangci-lint suite of linters with automatic fixes
|
|||||||
check: test
|
check: test
|
||||||
test: verifiers build ## builds minio, runs linters, tests
|
test: verifiers build ## builds minio, runs linters, tests
|
||||||
@echo "Running unit tests"
|
@echo "Running unit tests"
|
||||||
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -v -tags kqueue ./...
|
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -v -tags kqueue,dev ./...
|
||||||
|
|
||||||
test-root-disable: install-race
|
test-root-disable: install-race
|
||||||
@echo "Running minio root lockdown tests"
|
@echo "Running minio root lockdown tests"
|
||||||
@@ -84,13 +84,13 @@ test-race: verifiers build ## builds minio, runs linters, tests (race)
|
|||||||
@echo "Running unit tests under -race"
|
@echo "Running unit tests under -race"
|
||||||
@(env bash $(PWD)/buildscripts/race.sh)
|
@(env bash $(PWD)/buildscripts/race.sh)
|
||||||
|
|
||||||
test-iam: build ## verify IAM (external IDP, etcd backends)
|
test-iam: install-race ## verify IAM (external IDP, etcd backends)
|
||||||
@echo "Running tests for IAM (external IDP, etcd backends)"
|
@echo "Running tests for IAM (external IDP, etcd backends)"
|
||||||
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -tags kqueue -v -run TestIAM* ./cmd
|
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -tags kqueue,dev -v -run TestIAM* ./cmd
|
||||||
@echo "Running tests for IAM (external IDP, etcd backends) with -race"
|
@echo "Running tests for IAM (external IDP, etcd backends) with -race"
|
||||||
@MINIO_API_REQUESTS_MAX=10000 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,dev -v -run TestIAM* ./cmd
|
||||||
|
|
||||||
test-iam-ldap-upgrade-import: build ## verify IAM (external LDAP IDP)
|
test-iam-ldap-upgrade-import: install-race ## verify IAM (external LDAP IDP)
|
||||||
@echo "Running upgrade tests for IAM (LDAP backend)"
|
@echo "Running upgrade tests for IAM (LDAP backend)"
|
||||||
@env bash $(PWD)/buildscripts/minio-iam-ldap-upgrade-import-test.sh
|
@env bash $(PWD)/buildscripts/minio-iam-ldap-upgrade-import-test.sh
|
||||||
|
|
||||||
@@ -106,7 +106,10 @@ test-replication-3site:
|
|||||||
test-delete-replication:
|
test-delete-replication:
|
||||||
@(env bash $(PWD)/docs/bucket/replication/delete-replication.sh)
|
@(env bash $(PWD)/docs/bucket/replication/delete-replication.sh)
|
||||||
|
|
||||||
test-replication: install-race test-replication-2site test-replication-3site test-delete-replication test-sio-error ## verify multi site replication
|
test-delete-marker-proxying:
|
||||||
|
@(env bash $(PWD)/docs/bucket/replication/test_del_marker_proxying.sh)
|
||||||
|
|
||||||
|
test-replication: install-race test-replication-2site test-replication-3site test-delete-replication test-sio-error test-delete-marker-proxying ## verify multi site replication
|
||||||
@echo "Running tests for replicating three sites"
|
@echo "Running tests for replicating three sites"
|
||||||
|
|
||||||
test-site-replication-ldap: install-race ## verify automatic site replication
|
test-site-replication-ldap: install-race ## verify automatic site replication
|
||||||
@@ -162,9 +165,9 @@ hotfix-vars:
|
|||||||
$(eval VERSION := $(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD))
|
$(eval VERSION := $(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD))
|
||||||
|
|
||||||
hotfix: hotfix-vars clean install ## builds minio binary with hotfix tags
|
hotfix: hotfix-vars clean install ## builds minio binary with hotfix tags
|
||||||
@wget -q -c https://github.com/minio/pkger/releases/download/v2.2.9/pkger_2.2.9_linux_amd64.deb
|
@wget -q -c https://github.com/minio/pkger/releases/download/v2.3.1/pkger_2.3.1_linux_amd64.deb
|
||||||
@wget -q -c https://raw.githubusercontent.com/minio/minio-service/v1.0.1/linux-systemd/distributed/minio.service
|
@wget -q -c https://raw.githubusercontent.com/minio/minio-service/v1.0.1/linux-systemd/distributed/minio.service
|
||||||
@sudo apt install ./pkger_2.2.9_linux_amd64.deb --yes
|
@sudo apt install ./pkger_2.3.1_linux_amd64.deb --yes
|
||||||
@mkdir -p minio-release/$(GOOS)-$(GOARCH)/archive
|
@mkdir -p minio-release/$(GOOS)-$(GOARCH)/archive
|
||||||
@cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio
|
@cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio
|
||||||
@cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio.$(VERSION)
|
@cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio.$(VERSION)
|
||||||
@@ -193,7 +196,7 @@ docker: build ## builds minio docker container
|
|||||||
|
|
||||||
install-race: checks build-debugging ## builds minio to $(PWD)
|
install-race: checks build-debugging ## builds minio to $(PWD)
|
||||||
@echo "Building minio binary with -race to './minio'"
|
@echo "Building minio binary with -race to './minio'"
|
||||||
@GORACE=history_size=7 CGO_ENABLED=1 go build -tags kqueue -race -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
@GORACE=history_size=7 CGO_ENABLED=1 go build -tags kqueue,dev -race -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||||
@echo "Installing minio binary with -race to '$(GOPATH)/bin/minio'"
|
@echo "Installing minio binary with -race to '$(GOPATH)/bin/minio'"
|
||||||
@mkdir -p $(GOPATH)/bin && cp -af $(PWD)/minio $(GOPATH)/bin/minio
|
@mkdir -p $(GOPATH)/bin && cp -af $(PWD)/minio $(GOPATH)/bin/minio
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+18
-4
@@ -4,10 +4,22 @@ trap 'cleanup $LINENO' ERR
|
|||||||
|
|
||||||
# shellcheck disable=SC2120
|
# shellcheck disable=SC2120
|
||||||
cleanup() {
|
cleanup() {
|
||||||
MINIO_VERSION=dev docker-compose \
|
MINIO_VERSION=dev /tmp/gopath/bin/docker-compose \
|
||||||
-f "buildscripts/upgrade-tests/compose.yml" \
|
-f "buildscripts/upgrade-tests/compose.yml" \
|
||||||
rm -s -f
|
down || true
|
||||||
|
|
||||||
|
MINIO_VERSION=dev /tmp/gopath/bin/docker-compose \
|
||||||
|
-f "buildscripts/upgrade-tests/compose.yml" \
|
||||||
|
rm || true
|
||||||
|
|
||||||
|
for volume in $(docker volume ls -q | grep upgrade); do
|
||||||
|
docker volume rm ${volume} || true
|
||||||
|
done
|
||||||
|
|
||||||
docker volume prune -f
|
docker volume prune -f
|
||||||
|
docker system prune -f || true
|
||||||
|
docker volume prune -f || true
|
||||||
|
docker volume rm $(docker volume ls -q -f dangling=true) || true
|
||||||
}
|
}
|
||||||
|
|
||||||
verify_checksum_after_heal() {
|
verify_checksum_after_heal() {
|
||||||
@@ -60,6 +72,8 @@ __init__() {
|
|||||||
go install github.com/docker/compose/v2/cmd@latest
|
go install github.com/docker/compose/v2/cmd@latest
|
||||||
mv -v /tmp/gopath/bin/cmd /tmp/gopath/bin/docker-compose
|
mv -v /tmp/gopath/bin/cmd /tmp/gopath/bin/docker-compose
|
||||||
|
|
||||||
|
cleanup
|
||||||
|
|
||||||
TAG=minio/minio:dev make docker
|
TAG=minio/minio:dev make docker
|
||||||
|
|
||||||
MINIO_VERSION=RELEASE.2019-12-19T22-52-26Z docker-compose \
|
MINIO_VERSION=RELEASE.2019-12-19T22-52-26Z docker-compose \
|
||||||
@@ -77,11 +91,11 @@ __init__() {
|
|||||||
|
|
||||||
curl -s http://127.0.0.1:9000/minio-test/to-read/hosts | sha256sum
|
curl -s http://127.0.0.1:9000/minio-test/to-read/hosts | sha256sum
|
||||||
|
|
||||||
MINIO_VERSION=dev docker-compose -f "buildscripts/upgrade-tests/compose.yml" stop
|
MINIO_VERSION=dev /tmp/gopath/bin/docker-compose -f "buildscripts/upgrade-tests/compose.yml" stop
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
MINIO_VERSION=dev docker-compose -f "buildscripts/upgrade-tests/compose.yml" up -d --build
|
MINIO_VERSION=dev /tmp/gopath/bin/docker-compose -f "buildscripts/upgrade-tests/compose.yml" up -d --build
|
||||||
|
|
||||||
add_alias
|
add_alias
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.7'
|
|
||||||
|
|
||||||
# Settings and configurations that are common for all containers
|
# Settings and configurations that are common for all containers
|
||||||
x-minio-common: &minio-common
|
x-minio-common: &minio-common
|
||||||
image: minio/minio:${MINIO_VERSION}
|
image: minio/minio:${MINIO_VERSION}
|
||||||
|
|||||||
@@ -38,40 +38,47 @@ function start_minio_3_node() {
|
|||||||
disown $pid3
|
disown $pid3
|
||||||
|
|
||||||
export MC_HOST_myminio="http://minio:minio123@127.0.0.1:$((start_port + 1))"
|
export MC_HOST_myminio="http://minio:minio123@127.0.0.1:$((start_port + 1))"
|
||||||
/tmp/mc ready myminio
|
|
||||||
|
timeout 15m /tmp/mc ready myminio || fail
|
||||||
|
|
||||||
|
# Wait for all drives to be online and formatted
|
||||||
|
while [ $(/tmp/mc admin info --json myminio | jq '.info.servers[].drives[].state | select(. != "ok")' | wc -l) -gt 0 ]; do sleep 1; done
|
||||||
|
# Wait for all drives to be healed
|
||||||
|
while [ $(/tmp/mc admin info --json myminio | jq '.info.servers[].drives[].healing | select(. != null) | select(. == true)' | wc -l) -gt 0 ]; do sleep 1; done
|
||||||
|
|
||||||
|
# Wait for Status: in MinIO output
|
||||||
|
while true; do
|
||||||
|
rv=$(check_online)
|
||||||
|
if [ "$rv" != "1" ]; then
|
||||||
|
# success
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we should retry
|
||||||
|
retry=$((retry + 1))
|
||||||
|
if [ $retry -le 20 ]; then
|
||||||
|
sleep 5
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Failure
|
||||||
|
fail
|
||||||
|
done
|
||||||
|
|
||||||
if ! ps -p $pid1 1>&2 >/dev/null; then
|
if ! ps -p $pid1 1>&2 >/dev/null; then
|
||||||
echo "server1 log:"
|
echo "minio-server-1 is not running." && fail
|
||||||
cat "${WORK_DIR}/dist-minio-server1.log"
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! ps -p $pid2 1>&2 >/dev/null; then
|
if ! ps -p $pid2 1>&2 >/dev/null; then
|
||||||
echo "server2 log:"
|
echo "minio-server-2 is not running." && fail
|
||||||
cat "${WORK_DIR}/dist-minio-server2.log"
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! ps -p $pid3 1>&2 >/dev/null; then
|
if ! ps -p $pid3 1>&2 >/dev/null; then
|
||||||
echo "server3 log:"
|
echo "minio-server-3 is not running." && fail
|
||||||
cat "${WORK_DIR}/dist-minio-server3.log"
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! pkill minio; then
|
if ! pkill minio; then
|
||||||
for i in $(seq 1 3); do
|
fail
|
||||||
echo "server$i log:"
|
|
||||||
cat "${WORK_DIR}/dist-minio-server$i.log"
|
|
||||||
done
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 1
|
sleep 1
|
||||||
@@ -83,6 +90,16 @@ function start_minio_3_node() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fail() {
|
||||||
|
for i in $(seq 1 3); do
|
||||||
|
echo "server$i log:"
|
||||||
|
cat "${WORK_DIR}/dist-minio-server$i.log"
|
||||||
|
done
|
||||||
|
echo "FAILED"
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
function check_online() {
|
function check_online() {
|
||||||
if ! grep -q 'Status:' ${WORK_DIR}/dist-minio-*.log; then
|
if ! grep -q 'Status:' ${WORK_DIR}/dist-minio-*.log; then
|
||||||
echo "1"
|
echo "1"
|
||||||
@@ -90,7 +107,7 @@ function check_online() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function purge() {
|
function purge() {
|
||||||
rm -rf "$1"
|
echo rm -rf "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
function __init__() {
|
function __init__() {
|
||||||
@@ -117,18 +134,6 @@ function perform_test() {
|
|||||||
|
|
||||||
set -x
|
set -x
|
||||||
start_minio_3_node $2
|
start_minio_3_node $2
|
||||||
|
|
||||||
rv=$(check_online)
|
|
||||||
if [ "$rv" == "1" ]; then
|
|
||||||
for i in $(seq 1 3); do
|
|
||||||
echo "server$i log:"
|
|
||||||
cat "${WORK_DIR}/dist-minio-server$i.log"
|
|
||||||
done
|
|
||||||
pkill -9 minio
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
|
|||||||
@@ -47,43 +47,25 @@ function start_minio_3_node() {
|
|||||||
disown $pid3
|
disown $pid3
|
||||||
|
|
||||||
export MC_HOST_myminio="http://minio:minio123@127.0.0.1:$((start_port + 1))"
|
export MC_HOST_myminio="http://minio:minio123@127.0.0.1:$((start_port + 1))"
|
||||||
/tmp/mc ready myminio
|
timeout 15m /tmp/mc ready myminio || fail
|
||||||
|
|
||||||
[ ${first_time} -eq 0 ] && upload_objects
|
[ ${first_time} -eq 0 ] && upload_objects
|
||||||
[ ${first_time} -ne 0 ] && sleep 120
|
[ ${first_time} -ne 0 ] && sleep 120
|
||||||
|
|
||||||
if ! ps -p $pid1 1>&2 >/dev/null; then
|
if ! ps -p $pid1 1>&2 >/dev/null; then
|
||||||
echo "server1 log:"
|
echo "minio server 1 is not running" && fail
|
||||||
cat "${WORK_DIR}/dist-minio-server1.log"
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! ps -p $pid2 1>&2 >/dev/null; then
|
if ! ps -p $pid2 1>&2 >/dev/null; then
|
||||||
echo "server2 log:"
|
echo "minio server 2 is not running" && fail
|
||||||
cat "${WORK_DIR}/dist-minio-server2.log"
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! ps -p $pid3 1>&2 >/dev/null; then
|
if ! ps -p $pid3 1>&2 >/dev/null; then
|
||||||
echo "server3 log:"
|
echo "minio server 3 is not running" && fail
|
||||||
cat "${WORK_DIR}/dist-minio-server3.log"
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! pkill minio; then
|
if ! pkill minio; then
|
||||||
for i in $(seq 1 3); do
|
fail
|
||||||
echo "server$i log:"
|
|
||||||
cat "${WORK_DIR}/dist-minio-server$i.log"
|
|
||||||
done
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 1
|
sleep 1
|
||||||
@@ -118,6 +100,17 @@ function purge() {
|
|||||||
rm -rf "$1"
|
rm -rf "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fail() {
|
||||||
|
for i in $(seq 1 3); do
|
||||||
|
echo "server$i log:"
|
||||||
|
cat "${WORK_DIR}/dist-minio-server$i.log"
|
||||||
|
done
|
||||||
|
pkill -9 minio
|
||||||
|
echo "FAILED"
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
function __init__() {
|
function __init__() {
|
||||||
echo "Initializing environment"
|
echo "Initializing environment"
|
||||||
mkdir -p "$WORK_DIR"
|
mkdir -p "$WORK_DIR"
|
||||||
@@ -155,14 +148,7 @@ function perform_test() {
|
|||||||
check_heal ${1}
|
check_heal ${1}
|
||||||
rv=$?
|
rv=$?
|
||||||
if [ "$rv" == "1" ]; then
|
if [ "$rv" == "1" ]; then
|
||||||
for i in $(seq 1 3); do
|
fail
|
||||||
echo "server$i log:"
|
|
||||||
cat "${WORK_DIR}/dist-minio-server$i.log"
|
|
||||||
done
|
|
||||||
pkill -9 minio
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -284,6 +285,18 @@ func (a adminAPIHandlers) AddServiceAccountLDAP(w http.ResponseWriter, r *http.R
|
|||||||
opts.claims[ldapUser] = targetUser // DN
|
opts.claims[ldapUser] = targetUser // DN
|
||||||
opts.claims[ldapActualUser] = lookupResult.ActualDN
|
opts.claims[ldapActualUser] = lookupResult.ActualDN
|
||||||
|
|
||||||
|
// Check if this user or their groups have a policy applied.
|
||||||
|
ldapPolicies, err := globalIAMSys.PolicyDBGet(targetUser, targetGroups...)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ldapPolicies) == 0 {
|
||||||
|
err = fmt.Errorf("No policy set for user `%s` or any of their groups: `%s`", opts.claims[ldapActualUser], strings.Join(targetGroups, "`,`"))
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminNoSuchUser, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Add LDAP attributes that were looked up into the claims.
|
// Add LDAP attributes that were looked up into the claims.
|
||||||
for attribKey, attribValue := range lookupResult.Attributes {
|
for attribKey, attribValue := range lookupResult.Attributes {
|
||||||
opts.claims[ldapAttribPrefix+attribKey] = attribValue
|
opts.claims[ldapAttribPrefix+attribKey] = attribValue
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/klauspost/compress/zip"
|
"github.com/klauspost/compress/zip"
|
||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
@@ -474,6 +475,11 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !utf8.ValidString(accessKey) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserValidUTF), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
checkDenyOnly := false
|
checkDenyOnly := false
|
||||||
if accessKey == cred.AccessKey {
|
if accessKey == cred.AccessKey {
|
||||||
// Check that there is no explicit deny - otherwise it's allowed
|
// Check that there is no explicit deny - otherwise it's allowed
|
||||||
@@ -824,7 +830,11 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
condValues := getConditionValues(r, "", cred)
|
condValues := getConditionValues(r, "", cred)
|
||||||
addExpirationToCondValues(updateReq.NewExpiration, condValues)
|
err = addExpirationToCondValues(updateReq.NewExpiration, condValues)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Permission checks:
|
// Permission checks:
|
||||||
//
|
//
|
||||||
@@ -2453,11 +2463,16 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addExpirationToCondValues(exp *time.Time, condValues map[string][]string) {
|
func addExpirationToCondValues(exp *time.Time, condValues map[string][]string) error {
|
||||||
if exp == nil {
|
if exp == nil || exp.IsZero() || exp.Equal(timeSentinel) {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
condValues["DurationSeconds"] = []string{strconv.FormatInt(int64(exp.Sub(time.Now()).Seconds()), 10)}
|
dur := exp.Sub(time.Now())
|
||||||
|
if dur <= 0 {
|
||||||
|
return errors.New("unsupported expiration time")
|
||||||
|
}
|
||||||
|
condValues["DurationSeconds"] = []string{strconv.FormatInt(int64(dur.Seconds()), 10)}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func commonAddServiceAccount(r *http.Request) (context.Context, auth.Credentials, newServiceAccountOpts, madmin.AddServiceAccountReq, string, APIError) {
|
func commonAddServiceAccount(r *http.Request) (context.Context, auth.Credentials, newServiceAccountOpts, madmin.AddServiceAccountReq, string, APIError) {
|
||||||
@@ -2522,7 +2537,10 @@ func commonAddServiceAccount(r *http.Request) (context.Context, auth.Credentials
|
|||||||
}
|
}
|
||||||
|
|
||||||
condValues := getConditionValues(r, "", cred)
|
condValues := getConditionValues(r, "", cred)
|
||||||
addExpirationToCondValues(createReq.Expiration, condValues)
|
err = addExpirationToCondValues(createReq.Expiration, condValues)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, auth.Credentials{}, newServiceAccountOpts{}, madmin.AddServiceAccountReq{}, "", toAdminAPIErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if action is allowed if creating access key for another user
|
// Check if action is allowed if creating access key for another user
|
||||||
// Check if action is explicitly denied if for self
|
// Check if action is explicitly denied if for self
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
|||||||
|
|
||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
// Notify all other MinIO peers signal service.
|
// Notify all other MinIO peers signal service.
|
||||||
|
startTime := time.Now().Add(restartUpdateDelay)
|
||||||
ng := WithNPeers(len(globalNotificationSys.peerClients))
|
ng := WithNPeers(len(globalNotificationSys.peerClients))
|
||||||
for idx, client := range globalNotificationSys.peerClients {
|
for idx, client := range globalNotificationSys.peerClients {
|
||||||
_, ok := failedClients[idx]
|
_, ok := failedClients[idx]
|
||||||
@@ -247,7 +248,7 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
|||||||
ng.Go(ctx, func() error {
|
ng.Go(ctx, func() error {
|
||||||
prs, ok := peerResults[client.String()]
|
prs, ok := peerResults[client.String()]
|
||||||
if ok && prs.CurrentVersion != prs.UpdatedVersion && prs.UpdatedVersion != "" {
|
if ok && prs.CurrentVersion != prs.UpdatedVersion && prs.UpdatedVersion != "" {
|
||||||
return client.SignalService(serviceRestart, "", dryRun)
|
return client.SignalService(serviceRestart, "", dryRun, &startTime)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, idx, *client.host)
|
}, idx, *client.host)
|
||||||
@@ -542,9 +543,12 @@ func (a adminAPIHandlers) ServiceV2Handler(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
var objectAPI ObjectLayer
|
var objectAPI ObjectLayer
|
||||||
|
var execAt *time.Time
|
||||||
switch serviceSig {
|
switch serviceSig {
|
||||||
case serviceRestart:
|
case serviceRestart:
|
||||||
objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceRestartAdminAction)
|
objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceRestartAdminAction)
|
||||||
|
t := time.Now().Add(restartUpdateDelay)
|
||||||
|
execAt = &t
|
||||||
case serviceStop:
|
case serviceStop:
|
||||||
objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceStopAdminAction)
|
objectAPI, _ = validateAdminReq(ctx, w, r, policy.ServiceStopAdminAction)
|
||||||
case serviceFreeze, serviceUnFreeze:
|
case serviceFreeze, serviceUnFreeze:
|
||||||
@@ -571,7 +575,7 @@ func (a adminAPIHandlers) ServiceV2Handler(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
for _, nerr := range globalNotificationSys.SignalServiceV2(serviceSig, dryRun) {
|
for _, nerr := range globalNotificationSys.SignalServiceV2(serviceSig, dryRun, execAt) {
|
||||||
if nerr.Err != nil && process {
|
if nerr.Err != nil && process {
|
||||||
waitingDrives := map[string]madmin.DiskMetrics{}
|
waitingDrives := map[string]madmin.DiskMetrics{}
|
||||||
jerr := json.Unmarshal([]byte(nerr.Err.Error()), &waitingDrives)
|
jerr := json.Unmarshal([]byte(nerr.Err.Error()), &waitingDrives)
|
||||||
@@ -2417,7 +2421,7 @@ func getServerInfo(ctx context.Context, pools, metrics bool, r *http.Request) ma
|
|||||||
Mode: string(mode),
|
Mode: string(mode),
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Region: globalSite.Region(),
|
Region: globalSite.Region(),
|
||||||
SQSARN: globalEventNotifier.GetARNList(false),
|
SQSARN: globalEventNotifier.GetARNList(),
|
||||||
DeploymentID: globalDeploymentID(),
|
DeploymentID: globalDeploymentID(),
|
||||||
Buckets: buckets,
|
Buckets: buckets,
|
||||||
Objects: objects,
|
Objects: objects,
|
||||||
|
|||||||
+24
-1
@@ -287,6 +287,7 @@ const (
|
|||||||
ErrAdminNoSuchGroup
|
ErrAdminNoSuchGroup
|
||||||
ErrAdminGroupNotEmpty
|
ErrAdminGroupNotEmpty
|
||||||
ErrAdminGroupDisabled
|
ErrAdminGroupDisabled
|
||||||
|
ErrAdminInvalidGroupName
|
||||||
ErrAdminNoSuchJob
|
ErrAdminNoSuchJob
|
||||||
ErrAdminNoSuchPolicy
|
ErrAdminNoSuchPolicy
|
||||||
ErrAdminPolicyChangeAlreadyApplied
|
ErrAdminPolicyChangeAlreadyApplied
|
||||||
@@ -425,6 +426,7 @@ const (
|
|||||||
ErrAdminProfilerNotEnabled
|
ErrAdminProfilerNotEnabled
|
||||||
ErrInvalidDecompressedSize
|
ErrInvalidDecompressedSize
|
||||||
ErrAddUserInvalidArgument
|
ErrAddUserInvalidArgument
|
||||||
|
ErrAddUserValidUTF
|
||||||
ErrAdminResourceInvalidArgument
|
ErrAdminResourceInvalidArgument
|
||||||
ErrAdminAccountNotEligible
|
ErrAdminAccountNotEligible
|
||||||
ErrAccountNotEligible
|
ErrAccountNotEligible
|
||||||
@@ -443,6 +445,8 @@ const (
|
|||||||
ErrAdminNoAccessKey
|
ErrAdminNoAccessKey
|
||||||
ErrAdminNoSecretKey
|
ErrAdminNoSecretKey
|
||||||
|
|
||||||
|
ErrIAMNotInitialized
|
||||||
|
|
||||||
apiErrCodeEnd // This is used only for the testing code
|
apiErrCodeEnd // This is used only for the testing code
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -965,7 +969,7 @@ var errorCodes = errorCodeMap{
|
|||||||
ErrReplicationRemoteConnectionError: {
|
ErrReplicationRemoteConnectionError: {
|
||||||
Code: "XMinioAdminReplicationRemoteConnectionError",
|
Code: "XMinioAdminReplicationRemoteConnectionError",
|
||||||
Description: "Remote service connection error",
|
Description: "Remote service connection error",
|
||||||
HTTPStatusCode: http.StatusNotFound,
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
},
|
},
|
||||||
ErrReplicationBandwidthLimitError: {
|
ErrReplicationBandwidthLimitError: {
|
||||||
Code: "XMinioAdminReplicationBandwidthLimitError",
|
Code: "XMinioAdminReplicationBandwidthLimitError",
|
||||||
@@ -1303,6 +1307,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Server not initialized yet, please try again.",
|
Description: "Server not initialized yet, please try again.",
|
||||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
},
|
},
|
||||||
|
ErrIAMNotInitialized: {
|
||||||
|
Code: "XMinioIAMNotInitialized",
|
||||||
|
Description: "IAM sub-system not initialized yet, please try again.",
|
||||||
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
ErrBucketMetadataNotInitialized: {
|
ErrBucketMetadataNotInitialized: {
|
||||||
Code: "XMinioBucketMetadataNotInitialized",
|
Code: "XMinioBucketMetadataNotInitialized",
|
||||||
Description: "Bucket metadata not initialized yet, please try again.",
|
Description: "Bucket metadata not initialized yet, please try again.",
|
||||||
@@ -2101,6 +2110,16 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Expected LDAP short username but was given full DN.",
|
Description: "Expected LDAP short username but was given full DN.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrAdminInvalidGroupName: {
|
||||||
|
Code: "XMinioInvalidGroupName",
|
||||||
|
Description: "The group name is invalid.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrAddUserValidUTF: {
|
||||||
|
Code: "XMinioInvalidUTF",
|
||||||
|
Description: "Invalid UTF-8 character detected.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// toAPIErrorCode - Converts embedded errors. Convenience
|
// toAPIErrorCode - Converts embedded errors. Convenience
|
||||||
@@ -2140,6 +2159,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrAdminNoSuchGroup
|
apiErr = ErrAdminNoSuchGroup
|
||||||
case errGroupNotEmpty:
|
case errGroupNotEmpty:
|
||||||
apiErr = ErrAdminGroupNotEmpty
|
apiErr = ErrAdminGroupNotEmpty
|
||||||
|
case errGroupNameContainsReservedChars:
|
||||||
|
apiErr = ErrAdminInvalidGroupName
|
||||||
case errNoSuchJob:
|
case errNoSuchJob:
|
||||||
apiErr = ErrAdminNoSuchJob
|
apiErr = ErrAdminNoSuchJob
|
||||||
case errNoPolicyToAttachOrDetach:
|
case errNoPolicyToAttachOrDetach:
|
||||||
@@ -2154,6 +2175,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrEntityTooSmall
|
apiErr = ErrEntityTooSmall
|
||||||
case errAuthentication:
|
case errAuthentication:
|
||||||
apiErr = ErrAccessDenied
|
apiErr = ErrAccessDenied
|
||||||
|
case auth.ErrContainsReservedChars:
|
||||||
|
apiErr = ErrAdminInvalidAccessKey
|
||||||
case auth.ErrInvalidAccessKeyLength:
|
case auth.ErrInvalidAccessKeyLength:
|
||||||
apiErr = ErrAdminInvalidAccessKey
|
apiErr = ErrAdminInvalidAccessKey
|
||||||
case auth.ErrInvalidSecretKeyLength:
|
case auth.ErrInvalidSecretKeyLength:
|
||||||
|
|||||||
+1
-1
@@ -136,7 +136,7 @@ func setObjectHeaders(ctx context.Context, w http.ResponseWriter, objInfo Object
|
|||||||
// Set tag count if object has tags
|
// Set tag count if object has tags
|
||||||
if len(objInfo.UserTags) > 0 {
|
if len(objInfo.UserTags) > 0 {
|
||||||
tags, _ := tags.ParseObjectTags(objInfo.UserTags)
|
tags, _ := tags.ParseObjectTags(objInfo.UserTags)
|
||||||
if tags.Count() > 0 {
|
if tags != nil && tags.Count() > 0 {
|
||||||
w.Header()[xhttp.AmzTagCount] = []string{strconv.Itoa(tags.Count())}
|
w.Header()[xhttp.AmzTagCount] = []string{strconv.Itoa(tags.Count())}
|
||||||
if opts.Tagging {
|
if opts.Tagging {
|
||||||
// This is MinIO only extension to return back tags along with the count.
|
// This is MinIO only extension to return back tags along with the count.
|
||||||
|
|||||||
+2
-2
@@ -789,8 +789,8 @@ func generateInitiateMultipartUploadResponse(bucket, key, uploadID string) Initi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generates CompleteMultipartUploadResponse for given bucket, key, location and ETag.
|
// generates CompleteMultipartUploadResponse for given bucket, key, location and ETag.
|
||||||
func generateCompleteMultpartUploadResponse(bucket, key, location string, oi ObjectInfo) CompleteMultipartUploadResponse {
|
func generateCompleteMultipartUploadResponse(bucket, key, location string, oi ObjectInfo, h http.Header) CompleteMultipartUploadResponse {
|
||||||
cs := oi.decryptChecksums(0)
|
cs := oi.decryptChecksums(0, h)
|
||||||
c := CompleteMultipartUploadResponse{
|
c := CompleteMultipartUploadResponse{
|
||||||
Location: location,
|
Location: location,
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
|
|||||||
+2
-2
@@ -436,7 +436,7 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
Queries("notification", "")
|
Queries("notification", "")
|
||||||
// ListenNotification
|
// ListenNotification
|
||||||
router.Methods(http.MethodGet).
|
router.Methods(http.MethodGet).
|
||||||
HandlerFunc(s3APIMiddleware(api.ListenNotificationHandler, noThrottleS3HFlag)).
|
HandlerFunc(s3APIMiddleware(api.ListenNotificationHandler, noThrottleS3HFlag, traceHdrsS3HFlag)).
|
||||||
Queries("events", "{events:.*}")
|
Queries("events", "{events:.*}")
|
||||||
// ResetBucketReplicationStatus - MinIO extension API
|
// ResetBucketReplicationStatus - MinIO extension API
|
||||||
router.Methods(http.MethodGet).
|
router.Methods(http.MethodGet).
|
||||||
@@ -615,7 +615,7 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
|
|
||||||
// ListenNotification
|
// ListenNotification
|
||||||
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).
|
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).
|
||||||
HandlerFunc(s3APIMiddleware(api.ListenNotificationHandler, noThrottleS3HFlag)).
|
HandlerFunc(s3APIMiddleware(api.ListenNotificationHandler, noThrottleS3HFlag, traceHdrsS3HFlag)).
|
||||||
Queries("events", "{events:.*}")
|
Queries("events", "{events:.*}")
|
||||||
|
|
||||||
// ListBuckets
|
// ListBuckets
|
||||||
|
|||||||
+145
-142
File diff suppressed because one or more lines are too long
@@ -141,14 +141,14 @@ func initHealingTracker(disk StorageAPI, healID string) *healingTracker {
|
|||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h healingTracker) getLastUpdate() time.Time {
|
func (h *healingTracker) getLastUpdate() time.Time {
|
||||||
h.mu.RLock()
|
h.mu.RLock()
|
||||||
defer h.mu.RUnlock()
|
defer h.mu.RUnlock()
|
||||||
|
|
||||||
return h.LastUpdate
|
return h.LastUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h healingTracker) getBucket() string {
|
func (h *healingTracker) getBucket() string {
|
||||||
h.mu.RLock()
|
h.mu.RLock()
|
||||||
defer h.mu.RUnlock()
|
defer h.mu.RUnlock()
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ func (h *healingTracker) setBucket(bucket string) {
|
|||||||
h.Bucket = bucket
|
h.Bucket = bucket
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h healingTracker) getObject() string {
|
func (h *healingTracker) getObject() string {
|
||||||
h.mu.RLock()
|
h.mu.RLock()
|
||||||
defer h.mu.RUnlock()
|
defer h.mu.RUnlock()
|
||||||
|
|
||||||
@@ -453,10 +453,6 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
|||||||
|
|
||||||
healingLogEvent(ctx, "Healing of drive '%s' is finished (healed: %d, skipped: %d, failed: %d).", disk, tracker.ItemsHealed, tracker.ItemsSkipped, tracker.ItemsFailed)
|
healingLogEvent(ctx, "Healing of drive '%s' is finished (healed: %d, skipped: %d, failed: %d).", disk, tracker.ItemsHealed, tracker.ItemsSkipped, tracker.ItemsFailed)
|
||||||
|
|
||||||
if len(tracker.QueuedBuckets) > 0 {
|
|
||||||
return fmt.Errorf("not all buckets were healed: %v", tracker.QueuedBuckets)
|
|
||||||
}
|
|
||||||
|
|
||||||
if serverDebugLog {
|
if serverDebugLog {
|
||||||
tracker.printTo(os.Stdout)
|
tracker.printTo(os.Stdout)
|
||||||
fmt.Printf("\n")
|
fmt.Printf("\n")
|
||||||
|
|||||||
+4
-4
@@ -432,7 +432,7 @@ func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stopFn(exp, err)
|
stopFn(exp, err)
|
||||||
batchLogIf(ctx, fmt.Errorf("Failed to expire %s/%s versionID=%s due to %v (attempts=%d)", toExpire[i].Bucket, toExpire[i].Name, toExpire[i].VersionID, err, attempts))
|
batchLogIf(ctx, fmt.Errorf("Failed to expire %s/%s due to %v (attempts=%d)", exp.Bucket, exp.Name, err, attempts))
|
||||||
} else {
|
} else {
|
||||||
stopFn(exp, err)
|
stopFn(exp, err)
|
||||||
success = true
|
success = true
|
||||||
@@ -464,13 +464,13 @@ func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo
|
|||||||
copy(toDelCopy, toDel)
|
copy(toDelCopy, toDel)
|
||||||
var failed int
|
var failed int
|
||||||
errs := r.Expire(ctx, api, vc, toDel)
|
errs := r.Expire(ctx, api, vc, toDel)
|
||||||
// reslice toDel in preparation for next retry
|
// reslice toDel in preparation for next retry attempt
|
||||||
// attempt
|
|
||||||
toDel = toDel[:0]
|
toDel = toDel[:0]
|
||||||
for i, err := range errs {
|
for i, err := range errs {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stopFn(toDelCopy[i], err)
|
stopFn(toDelCopy[i], err)
|
||||||
batchLogIf(ctx, fmt.Errorf("Failed to expire %s/%s versionID=%s due to %v (attempts=%d)", ri.Bucket, toDelCopy[i].ObjectName, toDelCopy[i].VersionID, err, attempts))
|
batchLogIf(ctx, fmt.Errorf("Failed to expire %s/%s versionID=%s due to %v (attempts=%d)", ri.Bucket, toDelCopy[i].ObjectName, toDelCopy[i].VersionID,
|
||||||
|
err, attempts))
|
||||||
failed++
|
failed++
|
||||||
if oi, ok := oiCache.Get(toDelCopy[i]); ok {
|
if oi, ok := oiCache.Get(toDelCopy[i]); ok {
|
||||||
ri.trackCurrentBucketObject(r.Bucket, *oi, false, attempts)
|
ri.trackCurrentBucketObject(r.Bucket, *oi, false, attempts)
|
||||||
|
|||||||
@@ -1476,7 +1476,7 @@ func (j *BatchJobRequest) load(ctx context.Context, api ObjectLayer, name string
|
|||||||
|
|
||||||
func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions, err error) {
|
func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions, err error) {
|
||||||
// TODO: support custom storage class for remote replication
|
// TODO: support custom storage class for remote replication
|
||||||
putOpts, err = putReplicationOpts(ctx, "", objInfo)
|
putOpts, err = putReplicationOpts(ctx, "", objInfo, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return putOpts, err
|
return putOpts, err
|
||||||
}
|
}
|
||||||
@@ -1633,7 +1633,7 @@ func (a adminAPIHandlers) StartBatchJob(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
job.ID = fmt.Sprintf("%s:%d", shortuuid.New(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
|
job.ID = fmt.Sprintf("%s%s%d", shortuuid.New(), getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
|
||||||
job.User = user
|
job.User = user
|
||||||
job.Started = time.Now()
|
job.Started = time.Now()
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,13 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -43,10 +47,15 @@ type ServerSystemConfig struct {
|
|||||||
NEndpoints int
|
NEndpoints int
|
||||||
CmdLines []string
|
CmdLines []string
|
||||||
MinioEnv map[string]string
|
MinioEnv map[string]string
|
||||||
|
Checksum string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diff - returns error on first difference found in two configs.
|
// Diff - returns error on first difference found in two configs.
|
||||||
func (s1 *ServerSystemConfig) Diff(s2 *ServerSystemConfig) error {
|
func (s1 *ServerSystemConfig) Diff(s2 *ServerSystemConfig) error {
|
||||||
|
if s1.Checksum != s2.Checksum {
|
||||||
|
return fmt.Errorf("Expected MinIO binary checksum: %s, seen: %s", s1.Checksum, s2.Checksum)
|
||||||
|
}
|
||||||
|
|
||||||
ns1 := s1.NEndpoints
|
ns1 := s1.NEndpoints
|
||||||
ns2 := s2.NEndpoints
|
ns2 := s2.NEndpoints
|
||||||
if ns1 != ns2 {
|
if ns1 != ns2 {
|
||||||
@@ -82,7 +91,7 @@ func (s1 *ServerSystemConfig) Diff(s2 *ServerSystemConfig) error {
|
|||||||
extra = append(extra, k)
|
extra = append(extra, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
msg := "Expected same MINIO_ environment variables and values across all servers: "
|
msg := "Expected MINIO_* environment name and values across all servers to be same: "
|
||||||
if len(missing) > 0 {
|
if len(missing) > 0 {
|
||||||
msg += fmt.Sprintf(`Missing environment values: %v. `, missing)
|
msg += fmt.Sprintf(`Missing environment values: %v. `, missing)
|
||||||
}
|
}
|
||||||
@@ -120,7 +129,7 @@ func getServerSystemCfg() *ServerSystemConfig {
|
|||||||
}
|
}
|
||||||
envValues[envK] = logger.HashString(env.Get(envK, ""))
|
envValues[envK] = logger.HashString(env.Get(envK, ""))
|
||||||
}
|
}
|
||||||
scfg := &ServerSystemConfig{NEndpoints: globalEndpoints.NEndpoints(), MinioEnv: envValues}
|
scfg := &ServerSystemConfig{NEndpoints: globalEndpoints.NEndpoints(), MinioEnv: envValues, Checksum: binaryChecksum}
|
||||||
var cmdLines []string
|
var cmdLines []string
|
||||||
for _, ep := range globalEndpoints {
|
for _, ep := range globalEndpoints {
|
||||||
cmdLines = append(cmdLines, ep.CmdLine)
|
cmdLines = append(cmdLines, ep.CmdLine)
|
||||||
@@ -167,6 +176,18 @@ func (client *bootstrapRESTClient) String() string {
|
|||||||
return client.gridConn.String()
|
return client.gridConn.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var binaryChecksum = getBinaryChecksum()
|
||||||
|
|
||||||
|
func getBinaryChecksum() string {
|
||||||
|
mw := md5.New()
|
||||||
|
b, err := os.Open(os.Args[0])
|
||||||
|
if err == nil {
|
||||||
|
defer b.Close()
|
||||||
|
io.Copy(mw, b)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(mw.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
func verifyServerSystemConfig(ctx context.Context, endpointServerPools EndpointServerPools, gm *grid.Manager) error {
|
func verifyServerSystemConfig(ctx context.Context, endpointServerPools EndpointServerPools, gm *grid.Manager) error {
|
||||||
srcCfg := getServerSystemCfg()
|
srcCfg := getServerSystemCfg()
|
||||||
clnts := newBootstrapRESTClients(endpointServerPools, gm)
|
clnts := newBootstrapRESTClients(endpointServerPools, gm)
|
||||||
@@ -196,7 +217,7 @@ func verifyServerSystemConfig(ctx context.Context, endpointServerPools EndpointS
|
|||||||
err := clnt.Verify(ctx, srcCfg)
|
err := clnt.Verify(ctx, srcCfg)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bootstrapTraceMsg(fmt.Sprintf("clnt.Verify: %v, endpoint: %s", err, clnt))
|
bootstrapTraceMsg(fmt.Sprintf("bootstrapVerify: %v, endpoint: %s", err, clnt))
|
||||||
if !isNetworkError(err) {
|
if !isNetworkError(err) {
|
||||||
bootLogOnceIf(context.Background(), fmt.Errorf("%s has incorrect configuration: %w", clnt, err), "incorrect_"+clnt.String())
|
bootLogOnceIf(context.Background(), fmt.Errorf("%s has incorrect configuration: %w", clnt, err), "incorrect_"+clnt.String())
|
||||||
incorrectConfigs = append(incorrectConfigs, fmt.Errorf("%s has incorrect configuration: %w", clnt, err))
|
incorrectConfigs = append(incorrectConfigs, fmt.Errorf("%s has incorrect configuration: %w", clnt, err))
|
||||||
|
|||||||
@@ -79,6 +79,12 @@ func (z *ServerSystemConfig) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
}
|
}
|
||||||
z.MinioEnv[za0002] = za0003
|
z.MinioEnv[za0002] = za0003
|
||||||
}
|
}
|
||||||
|
case "Checksum":
|
||||||
|
z.Checksum, err = dc.ReadString()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Checksum")
|
||||||
|
return
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err = dc.Skip()
|
err = dc.Skip()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,9 +98,9 @@ func (z *ServerSystemConfig) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
|
|
||||||
// EncodeMsg implements msgp.Encodable
|
// EncodeMsg implements msgp.Encodable
|
||||||
func (z *ServerSystemConfig) EncodeMsg(en *msgp.Writer) (err error) {
|
func (z *ServerSystemConfig) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
// map header, size 3
|
// map header, size 4
|
||||||
// write "NEndpoints"
|
// write "NEndpoints"
|
||||||
err = en.Append(0x83, 0xaa, 0x4e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73)
|
err = en.Append(0x84, 0xaa, 0x4e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -142,15 +148,25 @@ func (z *ServerSystemConfig) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// write "Checksum"
|
||||||
|
err = en.Append(0xa8, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteString(z.Checksum)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Checksum")
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalMsg implements msgp.Marshaler
|
// MarshalMsg implements msgp.Marshaler
|
||||||
func (z *ServerSystemConfig) MarshalMsg(b []byte) (o []byte, err error) {
|
func (z *ServerSystemConfig) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
o = msgp.Require(b, z.Msgsize())
|
o = msgp.Require(b, z.Msgsize())
|
||||||
// map header, size 3
|
// map header, size 4
|
||||||
// string "NEndpoints"
|
// string "NEndpoints"
|
||||||
o = append(o, 0x83, 0xaa, 0x4e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73)
|
o = append(o, 0x84, 0xaa, 0x4e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73)
|
||||||
o = msgp.AppendInt(o, z.NEndpoints)
|
o = msgp.AppendInt(o, z.NEndpoints)
|
||||||
// string "CmdLines"
|
// string "CmdLines"
|
||||||
o = append(o, 0xa8, 0x43, 0x6d, 0x64, 0x4c, 0x69, 0x6e, 0x65, 0x73)
|
o = append(o, 0xa8, 0x43, 0x6d, 0x64, 0x4c, 0x69, 0x6e, 0x65, 0x73)
|
||||||
@@ -165,6 +181,9 @@ func (z *ServerSystemConfig) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
o = msgp.AppendString(o, za0002)
|
o = msgp.AppendString(o, za0002)
|
||||||
o = msgp.AppendString(o, za0003)
|
o = msgp.AppendString(o, za0003)
|
||||||
}
|
}
|
||||||
|
// string "Checksum"
|
||||||
|
o = append(o, 0xa8, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d)
|
||||||
|
o = msgp.AppendString(o, z.Checksum)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +260,12 @@ func (z *ServerSystemConfig) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
}
|
}
|
||||||
z.MinioEnv[za0002] = za0003
|
z.MinioEnv[za0002] = za0003
|
||||||
}
|
}
|
||||||
|
case "Checksum":
|
||||||
|
z.Checksum, bts, err = msgp.ReadStringBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Checksum")
|
||||||
|
return
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
bts, err = msgp.Skip(bts)
|
bts, err = msgp.Skip(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -266,5 +291,6 @@ func (z *ServerSystemConfig) Msgsize() (s int) {
|
|||||||
s += msgp.StringPrefixSize + len(za0002) + msgp.StringPrefixSize + len(za0003)
|
s += msgp.StringPrefixSize + len(za0002) + msgp.StringPrefixSize + len(za0003)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
s += 9 + msgp.StringPrefixSize + len(z.Checksum)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-39
@@ -51,7 +51,6 @@ import (
|
|||||||
sse "github.com/minio/minio/internal/bucket/encryption"
|
sse "github.com/minio/minio/internal/bucket/encryption"
|
||||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||||
"github.com/minio/minio/internal/bucket/replication"
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
"github.com/minio/minio/internal/config/cache"
|
|
||||||
"github.com/minio/minio/internal/config/dns"
|
"github.com/minio/minio/internal/config/dns"
|
||||||
"github.com/minio/minio/internal/crypto"
|
"github.com/minio/minio/internal/crypto"
|
||||||
"github.com/minio/minio/internal/event"
|
"github.com/minio/minio/internal/event"
|
||||||
@@ -72,6 +71,8 @@ const (
|
|||||||
|
|
||||||
xMinIOErrCodeHeader = "x-minio-error-code"
|
xMinIOErrCodeHeader = "x-minio-error-code"
|
||||||
xMinIOErrDescHeader = "x-minio-error-desc"
|
xMinIOErrDescHeader = "x-minio-error-desc"
|
||||||
|
|
||||||
|
postPolicyBucketTagging = "tagging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if there are buckets on server without corresponding entry in etcd backend and
|
// Check if there are buckets on server without corresponding entry in etcd backend and
|
||||||
@@ -90,7 +91,7 @@ const (
|
|||||||
// -- If IP of the entry doesn't match, this means entry is
|
// -- If IP of the entry doesn't match, this means entry is
|
||||||
//
|
//
|
||||||
// for another instance. Log an error to console.
|
// for another instance. Log an error to console.
|
||||||
func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
|
func initFederatorBackend(buckets []string, objLayer ObjectLayer) {
|
||||||
if len(buckets) == 0 {
|
if len(buckets) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -111,10 +112,10 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
|
|||||||
domainMissing := err == dns.ErrDomainMissing
|
domainMissing := err == dns.ErrDomainMissing
|
||||||
if dnsBuckets != nil {
|
if dnsBuckets != nil {
|
||||||
for _, bucket := range buckets {
|
for _, bucket := range buckets {
|
||||||
bucketsSet.Add(bucket.Name)
|
bucketsSet.Add(bucket)
|
||||||
r, ok := dnsBuckets[bucket.Name]
|
r, ok := dnsBuckets[bucket]
|
||||||
if !ok {
|
if !ok {
|
||||||
bucketsToBeUpdated.Add(bucket.Name)
|
bucketsToBeUpdated.Add(bucket)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() {
|
if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() {
|
||||||
@@ -133,7 +134,7 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
|
|||||||
// but if we do see a difference with local domain IPs with
|
// but if we do see a difference with local domain IPs with
|
||||||
// hostSlice from etcd then we should update with newer
|
// hostSlice from etcd then we should update with newer
|
||||||
// domainIPs, we proceed to do that here.
|
// domainIPs, we proceed to do that here.
|
||||||
bucketsToBeUpdated.Add(bucket.Name)
|
bucketsToBeUpdated.Add(bucket)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +143,7 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
|
|||||||
// bucket names are globally unique in federation at a given
|
// bucket names are globally unique in federation at a given
|
||||||
// path prefix, name collision is not allowed. We simply log
|
// path prefix, name collision is not allowed. We simply log
|
||||||
// an error and continue.
|
// an error and continue.
|
||||||
bucketsInConflict.Add(bucket.Name)
|
bucketsInConflict.Add(bucket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1340,22 +1341,6 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
asize, err := objInfo.GetActualSize()
|
|
||||||
if err != nil {
|
|
||||||
asize = objInfo.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
globalCacheConfig.Set(&cache.ObjectInfo{
|
|
||||||
Key: objInfo.Name,
|
|
||||||
Bucket: objInfo.Bucket,
|
|
||||||
ETag: getDecryptedETag(formValues, objInfo, false),
|
|
||||||
ModTime: objInfo.ModTime,
|
|
||||||
Expires: objInfo.Expires.UTC().Format(http.TimeFormat),
|
|
||||||
CacheControl: objInfo.CacheControl,
|
|
||||||
Metadata: cleanReservedKeys(objInfo.UserDefined),
|
|
||||||
Size: asize,
|
|
||||||
})
|
|
||||||
|
|
||||||
fanOutResp = append(fanOutResp, minio.PutObjectFanOutResponse{
|
fanOutResp = append(fanOutResp, minio.PutObjectFanOutResponse{
|
||||||
Key: objInfo.Name,
|
Key: objInfo.Name,
|
||||||
ETag: getDecryptedETag(formValues, objInfo, false),
|
ETag: getDecryptedETag(formValues, objInfo, false),
|
||||||
@@ -1415,6 +1400,19 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if formValues.Get(postPolicyBucketTagging) != "" {
|
||||||
|
tags, err := tags.ParseObjectXML(strings.NewReader(formValues.Get(postPolicyBucketTagging)))
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tagsStr := tags.String()
|
||||||
|
opts.UserDefined[xhttp.AmzObjectTagging] = tagsStr
|
||||||
|
} else {
|
||||||
|
// avoid user set an invalid tag using `X-Amz-Tagging`
|
||||||
|
delete(opts.UserDefined, xhttp.AmzObjectTagging)
|
||||||
|
}
|
||||||
|
|
||||||
objInfo, err := objectAPI.PutObject(ctx, bucket, object, pReader, opts)
|
objInfo, err := objectAPI.PutObject(ctx, bucket, object, pReader, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
@@ -1437,22 +1435,6 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
w.Header().Set(xhttp.Location, obj)
|
w.Header().Set(xhttp.Location, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
asize, err := objInfo.GetActualSize()
|
|
||||||
if err != nil {
|
|
||||||
asize = objInfo.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
defer globalCacheConfig.Set(&cache.ObjectInfo{
|
|
||||||
Key: objInfo.Name,
|
|
||||||
Bucket: objInfo.Bucket,
|
|
||||||
ETag: etag,
|
|
||||||
ModTime: objInfo.ModTime,
|
|
||||||
Expires: objInfo.ExpiresStr(),
|
|
||||||
CacheControl: objInfo.CacheControl,
|
|
||||||
Metadata: cleanReservedKeys(objInfo.UserDefined),
|
|
||||||
Size: asize,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Notify object created event.
|
// Notify object created event.
|
||||||
defer sendEvent(eventArgs{
|
defer sendEvent(eventArgs{
|
||||||
EventName: event.ObjectCreatedPost,
|
EventName: event.ObjectCreatedPost,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import (
|
|||||||
|
|
||||||
// Wrapper for calling RemoveBucket HTTP handler tests for both Erasure multiple disks and single node setup.
|
// Wrapper for calling RemoveBucket HTTP handler tests for both Erasure multiple disks and single node setup.
|
||||||
func TestRemoveBucketHandler(t *testing.T) {
|
func TestRemoveBucketHandler(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testRemoveBucketHandler, []string{"RemoveBucket"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testRemoveBucketHandler, endpoints: []string{"RemoveBucket"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRemoveBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testRemoveBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
@@ -78,7 +78,7 @@ func testRemoveBucketHandler(obj ObjectLayer, instanceType, bucketName string, a
|
|||||||
|
|
||||||
// Wrapper for calling GetBucketPolicy HTTP handler tests for both Erasure multiple disks and single node setup.
|
// Wrapper for calling GetBucketPolicy HTTP handler tests for both Erasure multiple disks and single node setup.
|
||||||
func TestGetBucketLocationHandler(t *testing.T) {
|
func TestGetBucketLocationHandler(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testGetBucketLocationHandler, []string{"GetBucketLocation"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testGetBucketLocationHandler, endpoints: []string{"GetBucketLocation"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
@@ -220,7 +220,7 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
|||||||
|
|
||||||
// Wrapper for calling HeadBucket HTTP handler tests for both Erasure multiple disks and single node setup.
|
// Wrapper for calling HeadBucket HTTP handler tests for both Erasure multiple disks and single node setup.
|
||||||
func TestHeadBucketHandler(t *testing.T) {
|
func TestHeadBucketHandler(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testHeadBucketHandler, []string{"HeadBucket"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testHeadBucketHandler, endpoints: []string{"HeadBucket"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
@@ -322,7 +322,7 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
|
|||||||
|
|
||||||
// Wrapper for calling TestListMultipartUploadsHandler tests for both Erasure multiple disks and single node setup.
|
// Wrapper for calling TestListMultipartUploadsHandler tests for both Erasure multiple disks and single node setup.
|
||||||
func TestListMultipartUploadsHandler(t *testing.T) {
|
func TestListMultipartUploadsHandler(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testListMultipartUploadsHandler, []string{"ListMultipartUploads"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testListMultipartUploadsHandler, endpoints: []string{"ListMultipartUploads"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// testListMultipartUploadsHandler - Tests validate listing of multipart uploads.
|
// testListMultipartUploadsHandler - Tests validate listing of multipart uploads.
|
||||||
@@ -558,7 +558,7 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
|
|||||||
|
|
||||||
// Wrapper for calling TestListBucketsHandler tests for both Erasure multiple disks and single node setup.
|
// Wrapper for calling TestListBucketsHandler tests for both Erasure multiple disks and single node setup.
|
||||||
func TestListBucketsHandler(t *testing.T) {
|
func TestListBucketsHandler(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testListBucketsHandler, []string{"ListBuckets"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testListBucketsHandler, endpoints: []string{"ListBuckets"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// testListBucketsHandler - Tests validate listing of buckets.
|
// testListBucketsHandler - Tests validate listing of buckets.
|
||||||
@@ -649,7 +649,7 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
|
|||||||
|
|
||||||
// Wrapper for calling DeleteMultipleObjects HTTP handler tests for both Erasure multiple disks and single node setup.
|
// Wrapper for calling DeleteMultipleObjects HTTP handler tests for both Erasure multiple disks and single node setup.
|
||||||
func TestAPIDeleteMultipleObjectsHandler(t *testing.T) {
|
func TestAPIDeleteMultipleObjectsHandler(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testAPIDeleteMultipleObjectsHandler, []string{"DeleteMultipleObjects", "PutBucketPolicy"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPIDeleteMultipleObjectsHandler, endpoints: []string{"DeleteMultipleObjects", "PutBucketPolicy"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import (
|
|||||||
|
|
||||||
// Test S3 Bucket lifecycle APIs with wrong credentials
|
// Test S3 Bucket lifecycle APIs with wrong credentials
|
||||||
func TestBucketLifecycleWrongCredentials(t *testing.T) {
|
func TestBucketLifecycleWrongCredentials(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testBucketLifecycleHandlersWrongCredentials, []string{"GetBucketLifecycle", "PutBucketLifecycle", "DeleteBucketLifecycle"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testBucketLifecycleHandlersWrongCredentials, endpoints: []string{"GetBucketLifecycle", "PutBucketLifecycle", "DeleteBucketLifecycle"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test for authentication
|
// Test for authentication
|
||||||
@@ -145,7 +145,7 @@ func testBucketLifecycleHandlersWrongCredentials(obj ObjectLayer, instanceType,
|
|||||||
|
|
||||||
// Test S3 Bucket lifecycle APIs
|
// Test S3 Bucket lifecycle APIs
|
||||||
func TestBucketLifecycle(t *testing.T) {
|
func TestBucketLifecycle(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testBucketLifecycleHandlers, []string{"GetBucketLifecycle", "PutBucketLifecycle", "DeleteBucketLifecycle"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testBucketLifecycleHandlers, endpoints: []string{"GetBucketLifecycle", "PutBucketLifecycle", "DeleteBucketLifecycle"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple tests of bucket lifecycle: PUT, GET, DELETE.
|
// Simple tests of bucket lifecycle: PUT, GET, DELETE.
|
||||||
|
|||||||
+15
-1
@@ -72,6 +72,7 @@ func NewLifecycleSys() *LifecycleSys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event string) madmin.TraceInfo {
|
func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event string) madmin.TraceInfo {
|
||||||
|
sz, _ := oi.GetActualSize()
|
||||||
return madmin.TraceInfo{
|
return madmin.TraceInfo{
|
||||||
TraceType: madmin.TraceILM,
|
TraceType: madmin.TraceILM,
|
||||||
Time: startTime,
|
Time: startTime,
|
||||||
@@ -79,6 +80,7 @@ func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event
|
|||||||
FuncName: event,
|
FuncName: event,
|
||||||
Duration: duration,
|
Duration: duration,
|
||||||
Path: pathJoin(oi.Bucket, oi.Name),
|
Path: pathJoin(oi.Bucket, oi.Name),
|
||||||
|
Bytes: sz,
|
||||||
Error: "",
|
Error: "",
|
||||||
Message: getSource(4),
|
Message: getSource(4),
|
||||||
Custom: map[string]string{"version-id": oi.VersionID},
|
Custom: map[string]string{"version-id": oi.VersionID},
|
||||||
@@ -277,6 +279,10 @@ func (es *expiryState) getWorkerCh(h uint64) chan<- expiryOp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (es *expiryState) ResizeWorkers(n int) {
|
func (es *expiryState) ResizeWorkers(n int) {
|
||||||
|
if n == 0 {
|
||||||
|
n = 100
|
||||||
|
}
|
||||||
|
|
||||||
// Lock to avoid multiple resizes to happen at the same time.
|
// Lock to avoid multiple resizes to happen at the same time.
|
||||||
es.mu.Lock()
|
es.mu.Lock()
|
||||||
defer es.mu.Unlock()
|
defer es.mu.Unlock()
|
||||||
@@ -538,6 +544,10 @@ func (t *transitionState) UpdateWorkers(n int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *transitionState) updateWorkers(n int) {
|
func (t *transitionState) updateWorkers(n int) {
|
||||||
|
if n == 0 {
|
||||||
|
n = 100
|
||||||
|
}
|
||||||
|
|
||||||
for t.numWorkers < n {
|
for t.numWorkers < n {
|
||||||
go t.worker(t.objAPI)
|
go t.worker(t.objAPI)
|
||||||
t.numWorkers++
|
t.numWorkers++
|
||||||
@@ -573,6 +583,10 @@ func enqueueTransitionImmediate(obj ObjectInfo, src lcEventSrc) {
|
|||||||
if lc, err := globalLifecycleSys.Get(obj.Bucket); err == nil {
|
if lc, err := globalLifecycleSys.Get(obj.Bucket); err == nil {
|
||||||
switch event := lc.Eval(obj.ToLifecycleOpts()); event.Action {
|
switch event := lc.Eval(obj.ToLifecycleOpts()); event.Action {
|
||||||
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
|
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
|
||||||
|
if obj.DeleteMarker || obj.IsDir {
|
||||||
|
// nothing to transition
|
||||||
|
return
|
||||||
|
}
|
||||||
globalTransitionState.queueTransitionTask(obj, event, src)
|
globalTransitionState.queueTransitionTask(obj, event, src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,7 +741,7 @@ func getTransitionedObjectReader(ctx context.Context, bucket, object string, rs
|
|||||||
return nil, fmt.Errorf("transition storage class not configured: %w", err)
|
return nil, fmt.Errorf("transition storage class not configured: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn, off, length, err := NewGetObjectReader(rs, oi, opts)
|
fn, off, length, err := NewGetObjectReader(rs, oi, opts, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrorRespToObjectError(err, bucket, object)
|
return nil, ErrorRespToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ func (api objectAPIHandlers) listObjectsV2Handler(ctx context.Context, w http.Re
|
|||||||
|
|
||||||
if r.Header.Get(xMinIOExtract) == "true" && strings.Contains(prefix, archivePattern) {
|
if r.Header.Get(xMinIOExtract) == "true" && strings.Contains(prefix, archivePattern) {
|
||||||
// Initiate a list objects operation inside a zip file based in the input params
|
// Initiate a list objects operation inside a zip file based in the input params
|
||||||
listObjectsV2Info, err = listObjectsV2InArchive(ctx, objectAPI, bucket, prefix, token, delimiter, maxKeys, fetchOwner, startAfter)
|
listObjectsV2Info, err = listObjectsV2InArchive(ctx, objectAPI, bucket, prefix, token, delimiter, maxKeys, startAfter, r.Header)
|
||||||
} else {
|
} else {
|
||||||
// Initiate a list objects operation based on the input params.
|
// Initiate a list objects operation based on the input params.
|
||||||
// On success would return back ListObjectsInfo object to be
|
// On success would return back ListObjectsInfo object to be
|
||||||
@@ -231,7 +231,7 @@ func parseRequestToken(token string) (subToken string, nodeIndex int) {
|
|||||||
if token == "" {
|
if token == "" {
|
||||||
return token, -1
|
return token, -1
|
||||||
}
|
}
|
||||||
i := strings.Index(token, ":")
|
i := strings.Index(token, getKeySeparator())
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
return token, -1
|
return token, -1
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-18
@@ -392,9 +392,7 @@ func (sys *BucketMetadataSys) GetReplicationConfig(ctx context.Context, bucket s
|
|||||||
return nil, time.Time{}, BucketReplicationConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketReplicationConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
if reloaded {
|
if reloaded {
|
||||||
globalBucketTargetSys.set(BucketInfo{
|
globalBucketTargetSys.set(bucket, meta)
|
||||||
Name: bucket,
|
|
||||||
}, meta)
|
|
||||||
}
|
}
|
||||||
return meta.replicationConfig, meta.ReplicationConfigUpdatedAt, nil
|
return meta.replicationConfig, meta.ReplicationConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
@@ -413,9 +411,7 @@ func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.Buc
|
|||||||
return nil, BucketRemoteTargetNotFound{Bucket: bucket}
|
return nil, BucketRemoteTargetNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
if reloaded {
|
if reloaded {
|
||||||
globalBucketTargetSys.set(BucketInfo{
|
globalBucketTargetSys.set(bucket, meta)
|
||||||
Name: bucket,
|
|
||||||
}, meta)
|
|
||||||
}
|
}
|
||||||
return meta.bucketTargetConfig, nil
|
return meta.bucketTargetConfig, nil
|
||||||
}
|
}
|
||||||
@@ -471,7 +467,7 @@ func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (met
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init - initializes bucket metadata system for all buckets.
|
// Init - initializes bucket metadata system for all buckets.
|
||||||
func (sys *BucketMetadataSys) Init(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) error {
|
func (sys *BucketMetadataSys) Init(ctx context.Context, buckets []string, objAPI ObjectLayer) error {
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return errServerNotInitialized
|
return errServerNotInitialized
|
||||||
}
|
}
|
||||||
@@ -484,7 +480,7 @@ func (sys *BucketMetadataSys) Init(ctx context.Context, buckets []BucketInfo, ob
|
|||||||
}
|
}
|
||||||
|
|
||||||
// concurrently load bucket metadata to speed up loading bucket metadata.
|
// concurrently load bucket metadata to speed up loading bucket metadata.
|
||||||
func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []BucketInfo, failedBuckets map[string]struct{}) {
|
func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []string, failedBuckets map[string]struct{}) {
|
||||||
g := errgroup.WithNErrs(len(buckets))
|
g := errgroup.WithNErrs(len(buckets))
|
||||||
bucketMetas := make([]BucketMetadata, len(buckets))
|
bucketMetas := make([]BucketMetadata, len(buckets))
|
||||||
for index := range buckets {
|
for index := range buckets {
|
||||||
@@ -494,8 +490,8 @@ func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []Buck
|
|||||||
// herd upon start up sequence.
|
// herd upon start up sequence.
|
||||||
time.Sleep(25*time.Millisecond + time.Duration(rand.Int63n(int64(100*time.Millisecond))))
|
time.Sleep(25*time.Millisecond + time.Duration(rand.Int63n(int64(100*time.Millisecond))))
|
||||||
|
|
||||||
_, _ = sys.objAPI.HealBucket(ctx, buckets[index].Name, madmin.HealOpts{Recreate: true})
|
_, _ = sys.objAPI.HealBucket(ctx, buckets[index], madmin.HealOpts{Recreate: true})
|
||||||
meta, err := loadBucketMetadata(ctx, sys.objAPI, buckets[index].Name)
|
meta, err := loadBucketMetadata(ctx, sys.objAPI, buckets[index])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -508,7 +504,7 @@ func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []Buck
|
|||||||
for index, err := range errs {
|
for index, err := range errs {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalLogOnceIf(ctx, fmt.Errorf("Unable to load bucket metadata, will be retried: %w", err),
|
internalLogOnceIf(ctx, fmt.Errorf("Unable to load bucket metadata, will be retried: %w", err),
|
||||||
"load-bucket-metadata-"+buckets[index].Name, logger.WarningKind)
|
"load-bucket-metadata-"+buckets[index], logger.WarningKind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,7 +515,7 @@ func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []Buck
|
|||||||
if errs[i] != nil {
|
if errs[i] != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sys.metadataMap[buckets[i].Name] = meta
|
sys.metadataMap[buckets[i]] = meta
|
||||||
}
|
}
|
||||||
sys.Unlock()
|
sys.Unlock()
|
||||||
|
|
||||||
@@ -528,7 +524,7 @@ func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []Buck
|
|||||||
if failedBuckets == nil {
|
if failedBuckets == nil {
|
||||||
failedBuckets = make(map[string]struct{})
|
failedBuckets = make(map[string]struct{})
|
||||||
}
|
}
|
||||||
failedBuckets[buckets[i].Name] = struct{}{}
|
failedBuckets[buckets[i]] = struct{}{}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
globalEventNotifier.set(buckets[i], meta) // set notification targets
|
globalEventNotifier.set(buckets[i], meta) // set notification targets
|
||||||
@@ -548,7 +544,7 @@ func (sys *BucketMetadataSys) refreshBucketsMetadataLoop(ctx context.Context, fa
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
buckets, err := sys.objAPI.ListBuckets(ctx, BucketOptions{})
|
buckets, err := sys.objAPI.ListBuckets(ctx, BucketOptions{NoMetadata: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internalLogIf(ctx, err, logger.WarningKind)
|
internalLogIf(ctx, err, logger.WarningKind)
|
||||||
break
|
break
|
||||||
@@ -579,8 +575,8 @@ func (sys *BucketMetadataSys) refreshBucketsMetadataLoop(ctx context.Context, fa
|
|||||||
|
|
||||||
// Initialize the failed buckets
|
// Initialize the failed buckets
|
||||||
if _, ok := failedBuckets[buckets[i].Name]; ok {
|
if _, ok := failedBuckets[buckets[i].Name]; ok {
|
||||||
globalEventNotifier.set(buckets[i], meta)
|
globalEventNotifier.set(buckets[i].Name, meta)
|
||||||
globalBucketTargetSys.set(buckets[i], meta)
|
globalBucketTargetSys.set(buckets[i].Name, meta)
|
||||||
delete(failedBuckets, buckets[i].Name)
|
delete(failedBuckets, buckets[i].Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,8 +596,8 @@ func (sys *BucketMetadataSys) Initialized() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loads bucket metadata for all buckets into BucketMetadataSys.
|
// Loads bucket metadata for all buckets into BucketMetadataSys.
|
||||||
func (sys *BucketMetadataSys) init(ctx context.Context, buckets []BucketInfo) {
|
func (sys *BucketMetadataSys) init(ctx context.Context, buckets []string) {
|
||||||
count := 100 // load 100 bucket metadata at a time.
|
count := globalEndpoints.ESCount() * 10
|
||||||
failedBuckets := make(map[string]struct{})
|
failedBuckets := make(map[string]struct{})
|
||||||
for {
|
for {
|
||||||
if len(buckets) < count {
|
if len(buckets) < count {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func getAnonWriteOnlyObjectPolicy(bucketName, prefix string) *policy.BucketPolic
|
|||||||
|
|
||||||
// Wrapper for calling Create Bucket and ensure we get one and only one success.
|
// Wrapper for calling Create Bucket and ensure we get one and only one success.
|
||||||
func TestCreateBucket(t *testing.T) {
|
func TestCreateBucket(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testCreateBucket, []string{"MakeBucket"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testCreateBucket, endpoints: []string{"MakeBucket"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// testCreateBucket - Test for calling Create Bucket and ensure we get one and only one success.
|
// testCreateBucket - Test for calling Create Bucket and ensure we get one and only one success.
|
||||||
@@ -154,7 +154,7 @@ func testCreateBucket(obj ObjectLayer, instanceType, bucketName string, apiRoute
|
|||||||
|
|
||||||
// Wrapper for calling Put Bucket Policy HTTP handler tests for both Erasure multiple disks and single node setup.
|
// Wrapper for calling Put Bucket Policy HTTP handler tests for both Erasure multiple disks and single node setup.
|
||||||
func TestPutBucketPolicyHandler(t *testing.T) {
|
func TestPutBucketPolicyHandler(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testPutBucketPolicyHandler, []string{"PutBucketPolicy"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testPutBucketPolicyHandler, endpoints: []string{"PutBucketPolicy"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// testPutBucketPolicyHandler - Test for Bucket policy end point.
|
// testPutBucketPolicyHandler - Test for Bucket policy end point.
|
||||||
@@ -373,7 +373,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
|
|
||||||
// Wrapper for calling Get Bucket Policy HTTP handler tests for both Erasure multiple disks and single node setup.
|
// Wrapper for calling Get Bucket Policy HTTP handler tests for both Erasure multiple disks and single node setup.
|
||||||
func TestGetBucketPolicyHandler(t *testing.T) {
|
func TestGetBucketPolicyHandler(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testGetBucketPolicyHandler, []string{"PutBucketPolicy", "GetBucketPolicy"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testGetBucketPolicyHandler, endpoints: []string{"PutBucketPolicy", "GetBucketPolicy"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// testGetBucketPolicyHandler - Test for end point which fetches the access policy json of the given bucket.
|
// testGetBucketPolicyHandler - Test for end point which fetches the access policy json of the given bucket.
|
||||||
@@ -577,7 +577,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
|
|
||||||
// Wrapper for calling Delete Bucket Policy HTTP handler tests for both Erasure multiple disks and single node setup.
|
// Wrapper for calling Delete Bucket Policy HTTP handler tests for both Erasure multiple disks and single node setup.
|
||||||
func TestDeleteBucketPolicyHandler(t *testing.T) {
|
func TestDeleteBucketPolicyHandler(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testDeleteBucketPolicyHandler, []string{"PutBucketPolicy", "DeleteBucketPolicy"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testDeleteBucketPolicyHandler, endpoints: []string{"PutBucketPolicy", "DeleteBucketPolicy"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// testDeleteBucketPolicyHandler - Test for Delete bucket policy end point.
|
// testDeleteBucketPolicyHandler - Test for Delete bucket policy end point.
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ func getHealReplicateObjectInfo(oi ObjectInfo, rcfg replicationConfig) Replicate
|
|||||||
rstate.ReplicateDecisionStr = dsc.String()
|
rstate.ReplicateDecisionStr = dsc.String()
|
||||||
asz, _ := oi.GetActualSize()
|
asz, _ := oi.GetActualSize()
|
||||||
|
|
||||||
return ReplicateObjectInfo{
|
r := ReplicateObjectInfo{
|
||||||
Name: oi.Name,
|
Name: oi.Name,
|
||||||
Size: oi.Size,
|
Size: oi.Size,
|
||||||
ActualSize: asz,
|
ActualSize: asz,
|
||||||
@@ -558,6 +558,10 @@ func getHealReplicateObjectInfo(oi ObjectInfo, rcfg replicationConfig) Replicate
|
|||||||
SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
|
SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
|
||||||
UserTags: oi.UserTags,
|
UserTags: oi.UserTags,
|
||||||
}
|
}
|
||||||
|
if r.SSEC {
|
||||||
|
r.Checksum = oi.Checksum
|
||||||
|
}
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplicationState - returns replication state using other internal replication metadata in ObjectInfo
|
// ReplicationState - returns replication state using other internal replication metadata in ObjectInfo
|
||||||
|
|||||||
+96
-24
@@ -19,6 +19,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -74,8 +75,9 @@ const (
|
|||||||
ObjectLockRetentionTimestamp = "objectlock-retention-timestamp"
|
ObjectLockRetentionTimestamp = "objectlock-retention-timestamp"
|
||||||
// ObjectLockLegalHoldTimestamp - the last time a legal hold metadata modification happened on this cluster for this object version
|
// ObjectLockLegalHoldTimestamp - the last time a legal hold metadata modification happened on this cluster for this object version
|
||||||
ObjectLockLegalHoldTimestamp = "objectlock-legalhold-timestamp"
|
ObjectLockLegalHoldTimestamp = "objectlock-legalhold-timestamp"
|
||||||
// ReplicationWorkerMultiplier is suggested worker multiplier if traffic exceeds replication worker capacity
|
|
||||||
ReplicationWorkerMultiplier = 1.5
|
// ReplicationSsecChecksumHeader - the encrypted checksum of the SSE-C encrypted object.
|
||||||
|
ReplicationSsecChecksumHeader = "X-Minio-Replication-Ssec-Crc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// gets replication config associated to a given bucket name.
|
// gets replication config associated to a given bucket name.
|
||||||
@@ -763,12 +765,34 @@ func (m caseInsensitiveMap) Lookup(key string) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts minio.PutObjectOptions, err error) {
|
func getCRCMeta(oi ObjectInfo, partNum int, h http.Header) map[string]string {
|
||||||
meta := make(map[string]string)
|
meta := make(map[string]string)
|
||||||
|
cs := oi.decryptChecksums(partNum, h)
|
||||||
|
for k, v := range cs {
|
||||||
|
cksum := hash.NewChecksumString(k, v)
|
||||||
|
if cksum == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cksum.Valid() {
|
||||||
|
meta[cksum.Type.Key()] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, partNum int) (putOpts minio.PutObjectOptions, err error) {
|
||||||
|
meta := make(map[string]string)
|
||||||
|
isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined)
|
||||||
|
|
||||||
for k, v := range objInfo.UserDefined {
|
for k, v := range objInfo.UserDefined {
|
||||||
// In case of SSE-C objects copy the allowed internal headers as well
|
// In case of SSE-C objects copy the allowed internal headers as well
|
||||||
if !crypto.SSEC.IsEncrypted(objInfo.UserDefined) || !slices.Contains(maps.Keys(validSSEReplicationHeaders), k) {
|
if !isSSEC || !slices.Contains(maps.Keys(validSSEReplicationHeaders), k) {
|
||||||
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
||||||
|
if strings.EqualFold(k, ReservedMetadataPrefixLower+"crc") {
|
||||||
|
for k, v := range getCRCMeta(objInfo, partNum, nil) {
|
||||||
|
meta[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isStandardHeader(k) {
|
if isStandardHeader(k) {
|
||||||
@@ -782,6 +806,17 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (put
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(objInfo.Checksum) > 0 {
|
||||||
|
// Add encrypted CRC to metadata for SSE-C objects.
|
||||||
|
if isSSEC {
|
||||||
|
meta[ReplicationSsecChecksumHeader] = base64.StdEncoding.EncodeToString(objInfo.Checksum)
|
||||||
|
} else {
|
||||||
|
for k, v := range getCRCMeta(objInfo, 0, nil) {
|
||||||
|
meta[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if sc == "" && (objInfo.StorageClass == storageclass.STANDARD || objInfo.StorageClass == storageclass.RRS) {
|
if sc == "" && (objInfo.StorageClass == storageclass.STANDARD || objInfo.StorageClass == storageclass.RRS) {
|
||||||
sc = objInfo.StorageClass
|
sc = objInfo.StorageClass
|
||||||
}
|
}
|
||||||
@@ -1201,7 +1236,12 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
// make sure we have the latest metadata for metrics calculation
|
// make sure we have the latest metadata for metrics calculation
|
||||||
rinfo.PrevReplicationStatus = objInfo.TargetReplicationStatus(tgt.ARN)
|
rinfo.PrevReplicationStatus = objInfo.TargetReplicationStatus(tgt.ARN)
|
||||||
|
|
||||||
size, err := objInfo.GetActualSize()
|
// Set the encrypted size for SSE-C objects
|
||||||
|
var size int64
|
||||||
|
if crypto.SSEC.IsEncrypted(objInfo.UserDefined) {
|
||||||
|
size = objInfo.Size
|
||||||
|
} else {
|
||||||
|
size, err = objInfo.GetActualSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
replLogIf(ctx, err)
|
replLogIf(ctx, err)
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
@@ -1213,6 +1253,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if tgt.Bucket == "" {
|
if tgt.Bucket == "" {
|
||||||
replLogIf(ctx, fmt.Errorf("unable to replicate object %s(%s), bucket is empty for target %s", objInfo.Name, objInfo.VersionID, tgt.EndpointURL()))
|
replLogIf(ctx, fmt.Errorf("unable to replicate object %s(%s), bucket is empty for target %s", objInfo.Name, objInfo.VersionID, tgt.EndpointURL()))
|
||||||
@@ -1239,7 +1280,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
// use core client to avoid doing multipart on PUT
|
// use core client to avoid doing multipart on PUT
|
||||||
c := &minio.Core{Client: tgt.Client}
|
c := &minio.Core{Client: tgt.Client}
|
||||||
|
|
||||||
putOpts, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo)
|
putOpts, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
replLogIf(ctx, fmt.Errorf("failure setting options for replication bucket:%s err:%w", bucket, err))
|
replLogIf(ctx, fmt.Errorf("failure setting options for replication bucket:%s err:%w", bucket, err))
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
@@ -1377,7 +1418,8 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set the encrypted size for SSE-C objects
|
// Set the encrypted size for SSE-C objects
|
||||||
if crypto.SSEC.IsEncrypted(objInfo.UserDefined) {
|
isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined)
|
||||||
|
if isSSEC {
|
||||||
size = objInfo.Size
|
size = objInfo.Size
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1437,6 +1479,13 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// SSEC objects will refuse HeadObject without the decryption key.
|
||||||
|
// Ignore the error, since we know the object exists and versioning prevents overwriting existing versions.
|
||||||
|
if isSSEC && strings.Contains(cerr.Error(), errorCodes[ErrSSEEncryptedObject].Description) {
|
||||||
|
rinfo.ReplicationStatus = replication.Completed
|
||||||
|
rinfo.ReplicationAction = replicateNone
|
||||||
|
goto applyAction
|
||||||
|
}
|
||||||
// if target returns error other than NoSuchKey, defer replication attempt
|
// if target returns error other than NoSuchKey, defer replication attempt
|
||||||
if minio.IsNetworkOrHostDown(cerr, true) && !globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
|
if minio.IsNetworkOrHostDown(cerr, true) && !globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
|
||||||
globalBucketTargetSys.markOffline(tgt.EndpointURL())
|
globalBucketTargetSys.markOffline(tgt.EndpointURL())
|
||||||
@@ -1464,6 +1513,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
applyAction:
|
||||||
rinfo.ReplicationStatus = replication.Completed
|
rinfo.ReplicationStatus = replication.Completed
|
||||||
rinfo.Size = size
|
rinfo.Size = size
|
||||||
rinfo.ReplicationAction = rAction
|
rinfo.ReplicationAction = rAction
|
||||||
@@ -1506,7 +1556,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var putOpts minio.PutObjectOptions
|
var putOpts minio.PutObjectOptions
|
||||||
putOpts, err = putReplicationOpts(ctx, tgt.StorageClass, objInfo)
|
putOpts, err = putReplicationOpts(ctx, tgt.StorageClass, objInfo, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
replLogIf(ctx, fmt.Errorf("failed to set replicate options for object %s/%s(%s) (target %s) err:%w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), err))
|
replLogIf(ctx, fmt.Errorf("failed to set replicate options for object %s/%s(%s) (target %s) err:%w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), err))
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
@@ -1599,11 +1649,12 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
|||||||
var (
|
var (
|
||||||
hr *hash.Reader
|
hr *hash.Reader
|
||||||
pInfo minio.ObjectPart
|
pInfo minio.ObjectPart
|
||||||
|
isSSEC = crypto.SSEC.IsEncrypted(objInfo.UserDefined)
|
||||||
)
|
)
|
||||||
|
|
||||||
var objectSize int64
|
var objectSize int64
|
||||||
for _, partInfo := range objInfo.Parts {
|
for _, partInfo := range objInfo.Parts {
|
||||||
if crypto.SSEC.IsEncrypted(objInfo.UserDefined) {
|
if isSSEC {
|
||||||
hr, err = hash.NewReader(ctx, io.LimitReader(r, partInfo.Size), partInfo.Size, "", "", partInfo.ActualSize)
|
hr, err = hash.NewReader(ctx, io.LimitReader(r, partInfo.Size), partInfo.Size, "", "", partInfo.ActualSize)
|
||||||
} else {
|
} else {
|
||||||
hr, err = hash.NewReader(ctx, io.LimitReader(r, partInfo.ActualSize), partInfo.ActualSize, "", "", partInfo.ActualSize)
|
hr, err = hash.NewReader(ctx, io.LimitReader(r, partInfo.ActualSize), partInfo.ActualSize, "", "", partInfo.ActualSize)
|
||||||
@@ -1614,12 +1665,18 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
|||||||
|
|
||||||
cHeader := http.Header{}
|
cHeader := http.Header{}
|
||||||
cHeader.Add(xhttp.MinIOSourceReplicationRequest, "true")
|
cHeader.Add(xhttp.MinIOSourceReplicationRequest, "true")
|
||||||
|
if !isSSEC {
|
||||||
|
crc := getCRCMeta(objInfo, partInfo.Number, nil) // No SSE-C keys here.
|
||||||
|
for k, v := range crc {
|
||||||
|
cHeader.Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
popts := minio.PutObjectPartOptions{
|
popts := minio.PutObjectPartOptions{
|
||||||
SSE: opts.ServerSideEncryption,
|
SSE: opts.ServerSideEncryption,
|
||||||
CustomHeader: cHeader,
|
CustomHeader: cHeader,
|
||||||
}
|
}
|
||||||
|
|
||||||
if crypto.SSEC.IsEncrypted(objInfo.UserDefined) {
|
if isSSEC {
|
||||||
objectSize += partInfo.Size
|
objectSize += partInfo.Size
|
||||||
pInfo, err = c.PutObjectPart(ctx, bucket, object, uploadID, partInfo.Number, hr, partInfo.Size, popts)
|
pInfo, err = c.PutObjectPart(ctx, bucket, object, uploadID, partInfo.Number, hr, partInfo.Size, popts)
|
||||||
} else {
|
} else {
|
||||||
@@ -1629,20 +1686,30 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !crypto.SSEC.IsEncrypted(objInfo.UserDefined) && pInfo.Size != partInfo.ActualSize {
|
if !isSSEC && pInfo.Size != partInfo.ActualSize {
|
||||||
return fmt.Errorf("Part size mismatch: got %d, want %d", pInfo.Size, partInfo.ActualSize)
|
return fmt.Errorf("Part size mismatch: got %d, want %d", pInfo.Size, partInfo.ActualSize)
|
||||||
}
|
}
|
||||||
uploadedParts = append(uploadedParts, minio.CompletePart{
|
uploadedParts = append(uploadedParts, minio.CompletePart{
|
||||||
PartNumber: pInfo.PartNumber,
|
PartNumber: pInfo.PartNumber,
|
||||||
ETag: pInfo.ETag,
|
ETag: pInfo.ETag,
|
||||||
|
ChecksumCRC32: pInfo.ChecksumCRC32,
|
||||||
|
ChecksumCRC32C: pInfo.ChecksumCRC32C,
|
||||||
|
ChecksumSHA1: pInfo.ChecksumSHA1,
|
||||||
|
ChecksumSHA256: pInfo.ChecksumSHA256,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
userMeta := map[string]string{
|
||||||
|
validSSEReplicationHeaders[ReservedMetadataPrefix+"Actual-Object-Size"]: objInfo.UserDefined[ReservedMetadataPrefix+"actual-size"],
|
||||||
|
}
|
||||||
|
if isSSEC && objInfo.UserDefined[ReplicationSsecChecksumHeader] != "" {
|
||||||
|
userMeta[ReplicationSsecChecksumHeader] = objInfo.UserDefined[ReplicationSsecChecksumHeader]
|
||||||
|
}
|
||||||
|
|
||||||
// really big value but its okay on heavily loaded systems. This is just tail end timeout.
|
// really big value but its okay on heavily loaded systems. This is just tail end timeout.
|
||||||
cctx, ccancel := context.WithTimeout(ctx, 10*time.Minute)
|
cctx, ccancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||||
defer ccancel()
|
defer ccancel()
|
||||||
_, err = c.CompleteMultipartUpload(cctx, bucket, object, uploadID, uploadedParts, minio.PutObjectOptions{
|
_, err = c.CompleteMultipartUpload(cctx, bucket, object, uploadID, uploadedParts, minio.PutObjectOptions{
|
||||||
UserMetadata: map[string]string{validSSEReplicationHeaders[ReservedMetadataPrefix+"Actual-Object-Size"]: objInfo.UserDefined[ReservedMetadataPrefix+"actual-size"]},
|
UserMetadata: userMeta,
|
||||||
Internal: minio.AdvancedPutOptions{
|
Internal: minio.AdvancedPutOptions{
|
||||||
SourceMTime: objInfo.ModTime,
|
SourceMTime: objInfo.ModTime,
|
||||||
SourceETag: objInfo.ETag,
|
SourceETag: objInfo.ETag,
|
||||||
@@ -2179,12 +2246,12 @@ type proxyResult struct {
|
|||||||
|
|
||||||
// get Reader from replication target if active-active replication is in place and
|
// get Reader from replication target if active-active replication is in place and
|
||||||
// this node returns a 404
|
// this node returns a 404
|
||||||
func proxyGetToReplicationTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, _ http.Header, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (gr *GetObjectReader, proxy proxyResult, err error) {
|
func proxyGetToReplicationTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (gr *GetObjectReader, proxy proxyResult, err error) {
|
||||||
tgt, oi, proxy := proxyHeadToRepTarget(ctx, bucket, object, rs, opts, proxyTargets)
|
tgt, oi, proxy := proxyHeadToRepTarget(ctx, bucket, object, rs, opts, proxyTargets)
|
||||||
if !proxy.Proxy {
|
if !proxy.Proxy {
|
||||||
return nil, proxy, nil
|
return nil, proxy, nil
|
||||||
}
|
}
|
||||||
fn, _, _, err := NewGetObjectReader(nil, oi, opts)
|
fn, _, _, err := NewGetObjectReader(nil, oi, opts, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, proxy, err
|
return nil, proxy, err
|
||||||
}
|
}
|
||||||
@@ -2369,7 +2436,9 @@ func scheduleReplication(ctx context.Context, oi ObjectInfo, o ObjectLayer, dsc
|
|||||||
SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
|
SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
|
||||||
UserTags: oi.UserTags,
|
UserTags: oi.UserTags,
|
||||||
}
|
}
|
||||||
|
if ri.SSEC {
|
||||||
|
ri.Checksum = oi.Checksum
|
||||||
|
}
|
||||||
if dsc.Synchronous() {
|
if dsc.Synchronous() {
|
||||||
replicateObject(ctx, ri, o)
|
replicateObject(ctx, ri, o)
|
||||||
} else {
|
} else {
|
||||||
@@ -2849,17 +2918,19 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object
|
|||||||
ReplicationProxyRequest: "false",
|
ReplicationProxyRequest: "false",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
sz := roi.Size
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if roi.DeleteMarker && isErrMethodNotAllowed(ErrorRespToObjectError(err, opts.bucket, roi.Name)) {
|
if roi.DeleteMarker && isErrMethodNotAllowed(ErrorRespToObjectError(err, opts.bucket, roi.Name)) {
|
||||||
st.ReplicatedCount++
|
st.ReplicatedCount++
|
||||||
} else {
|
} else {
|
||||||
st.FailedCount++
|
st.FailedCount++
|
||||||
}
|
}
|
||||||
|
sz = 0
|
||||||
} else {
|
} else {
|
||||||
st.ReplicatedCount++
|
st.ReplicatedCount++
|
||||||
st.ReplicatedSize += roi.Size
|
st.ReplicatedSize += roi.Size
|
||||||
}
|
}
|
||||||
traceFn(err)
|
traceFn(sz, err)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
@@ -2974,17 +3045,17 @@ func (s *replicationResyncer) start(ctx context.Context, objAPI ObjectLayer, opt
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *replicationResyncer) trace(resyncID string, path string) func(err error) {
|
func (s *replicationResyncer) trace(resyncID string, path string) func(sz int64, err error) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
return func(err error) {
|
return func(sz int64, err error) {
|
||||||
duration := time.Since(startTime)
|
duration := time.Since(startTime)
|
||||||
if globalTrace.NumSubscribers(madmin.TraceReplicationResync) > 0 {
|
if globalTrace.NumSubscribers(madmin.TraceReplicationResync) > 0 {
|
||||||
globalTrace.Publish(replicationResyncTrace(resyncID, startTime, duration, path, err))
|
globalTrace.Publish(replicationResyncTrace(resyncID, startTime, duration, path, err, sz))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func replicationResyncTrace(resyncID string, startTime time.Time, duration time.Duration, path string, err error) madmin.TraceInfo {
|
func replicationResyncTrace(resyncID string, startTime time.Time, duration time.Duration, path string, err error, sz int64) madmin.TraceInfo {
|
||||||
var errStr string
|
var errStr string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr = err.Error()
|
errStr = err.Error()
|
||||||
@@ -2998,6 +3069,7 @@ func replicationResyncTrace(resyncID string, startTime time.Time, duration time.
|
|||||||
Duration: duration,
|
Duration: duration,
|
||||||
Path: path,
|
Path: path,
|
||||||
Error: errStr,
|
Error: errStr,
|
||||||
|
Bytes: sz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3014,7 +3086,7 @@ func (p *ReplicationPool) deleteResyncMetadata(ctx context.Context, bucket strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initResync - initializes bucket replication resync for all buckets.
|
// initResync - initializes bucket replication resync for all buckets.
|
||||||
func (p *ReplicationPool) initResync(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) error {
|
func (p *ReplicationPool) initResync(ctx context.Context, buckets []string, objAPI ObjectLayer) error {
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return errServerNotInitialized
|
return errServerNotInitialized
|
||||||
}
|
}
|
||||||
@@ -3023,7 +3095,7 @@ func (p *ReplicationPool) initResync(ctx context.Context, buckets []BucketInfo,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ReplicationPool) startResyncRoutine(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) {
|
func (p *ReplicationPool) startResyncRoutine(ctx context.Context, buckets []string, objAPI ObjectLayer) {
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
// Run the replication resync in a loop
|
// Run the replication resync in a loop
|
||||||
for {
|
for {
|
||||||
@@ -3041,13 +3113,13 @@ func (p *ReplicationPool) startResyncRoutine(ctx context.Context, buckets []Buck
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loads bucket replication resync statuses into memory.
|
// Loads bucket replication resync statuses into memory.
|
||||||
func (p *ReplicationPool) loadResync(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) error {
|
func (p *ReplicationPool) loadResync(ctx context.Context, buckets []string, objAPI ObjectLayer) error {
|
||||||
// Make sure only one node running resync on the cluster.
|
// Make sure only one node running resync on the cluster.
|
||||||
ctx, cancel := globalLeaderLock.GetLock(ctx)
|
ctx, cancel := globalLeaderLock.GetLock(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
for index := range buckets {
|
for index := range buckets {
|
||||||
bucket := buckets[index].Name
|
bucket := buckets[index]
|
||||||
|
|
||||||
meta, err := loadBucketResyncMetadata(ctx, bucket, objAPI)
|
meta, err := loadBucketResyncMetadata(ctx, bucket, objAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -612,7 +612,7 @@ func (sys *BucketTargetSys) UpdateAllTargets(bucket string, tgts *madmin.BucketT
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create minio-go clients for buckets having remote targets
|
// create minio-go clients for buckets having remote targets
|
||||||
func (sys *BucketTargetSys) set(bucket BucketInfo, meta BucketMetadata) {
|
func (sys *BucketTargetSys) set(bucket string, meta BucketMetadata) {
|
||||||
cfg := meta.bucketTargetConfig
|
cfg := meta.bucketTargetConfig
|
||||||
if cfg == nil || cfg.Empty() {
|
if cfg == nil || cfg.Empty() {
|
||||||
return
|
return
|
||||||
@@ -626,9 +626,9 @@ func (sys *BucketTargetSys) set(bucket BucketInfo, meta BucketMetadata) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sys.arnRemotesMap[tgt.Arn] = arnTarget{Client: tgtClient}
|
sys.arnRemotesMap[tgt.Arn] = arnTarget{Client: tgtClient}
|
||||||
sys.updateBandwidthLimit(bucket.Name, tgt.Arn, tgt.BandwidthLimit)
|
sys.updateBandwidthLimit(bucket, tgt.Arn, tgt.BandwidthLimit)
|
||||||
}
|
}
|
||||||
sys.targetsMap[bucket.Name] = cfg.Targets
|
sys.targetsMap[bucket] = cfg.Targets
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a minio-go Client configured to access remote host described in replication target config.
|
// Returns a minio-go Client configured to access remote host described in replication target config.
|
||||||
|
|||||||
+23
-1
@@ -402,6 +402,29 @@ func buildServerCtxt(ctx *cli.Context, ctxt *serverCtxt) (err error) {
|
|||||||
ctxt.certsDirSet = true
|
ctxt.certsDirSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memAvailable := availableMemory()
|
||||||
|
if ctx.IsSet("memlimit") || ctx.GlobalIsSet("memlimit") {
|
||||||
|
memlimit := ctx.String("memlimit")
|
||||||
|
if memlimit == "" {
|
||||||
|
memlimit = ctx.GlobalString("memlimit")
|
||||||
|
}
|
||||||
|
mlimit, err := humanize.ParseBytes(memlimit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if mlimit > memAvailable {
|
||||||
|
logger.Info("WARNING: maximum memory available (%s) smaller than specified --memlimit=%s, ignoring --memlimit value",
|
||||||
|
humanize.IBytes(memAvailable), memlimit)
|
||||||
|
}
|
||||||
|
ctxt.MemLimit = mlimit
|
||||||
|
} else {
|
||||||
|
ctxt.MemLimit = memAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
if memAvailable < ctxt.MemLimit {
|
||||||
|
ctxt.MemLimit = memAvailable
|
||||||
|
}
|
||||||
|
|
||||||
ctxt.FTP = ctx.StringSlice("ftp")
|
ctxt.FTP = ctx.StringSlice("ftp")
|
||||||
ctxt.SFTP = ctx.StringSlice("sftp")
|
ctxt.SFTP = ctx.StringSlice("sftp")
|
||||||
ctxt.Interface = ctx.String("interface")
|
ctxt.Interface = ctx.String("interface")
|
||||||
@@ -410,7 +433,6 @@ func buildServerCtxt(ctx *cli.Context, ctxt *serverCtxt) (err error) {
|
|||||||
ctxt.RecvBufSize = ctx.Int("recv-buf-size")
|
ctxt.RecvBufSize = ctx.Int("recv-buf-size")
|
||||||
ctxt.IdleTimeout = ctx.Duration("idle-timeout")
|
ctxt.IdleTimeout = ctx.Duration("idle-timeout")
|
||||||
ctxt.UserTimeout = ctx.Duration("conn-user-timeout")
|
ctxt.UserTimeout = ctx.Duration("conn-user-timeout")
|
||||||
ctxt.ShutdownTimeout = ctx.Duration("shutdown-timeout")
|
|
||||||
|
|
||||||
if conf := ctx.String("config"); len(conf) > 0 {
|
if conf := ctx.String("config"); len(conf) > 0 {
|
||||||
err = mergeServerCtxtFromConfigFile(conf, ctxt)
|
err = mergeServerCtxtFromConfigFile(conf, ctxt)
|
||||||
|
|||||||
@@ -240,6 +240,11 @@ func initHelp() {
|
|||||||
Description: "manage Browser HTTP specific features, such as Security headers, etc.",
|
Description: "manage Browser HTTP specific features, such as Security headers, etc.",
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: config.ILMSubSys,
|
||||||
|
Description: "manage ILM settings for expiration and transition workers",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalIsErasure {
|
if globalIsErasure {
|
||||||
@@ -288,6 +293,7 @@ func initHelp() {
|
|||||||
config.DriveSubSys: drive.HelpDrive,
|
config.DriveSubSys: drive.HelpDrive,
|
||||||
config.CacheSubSys: cache.Help,
|
config.CacheSubSys: cache.Help,
|
||||||
config.BrowserSubSys: browser.Help,
|
config.BrowserSubSys: browser.Help,
|
||||||
|
config.ILMSubSys: ilm.Help,
|
||||||
}
|
}
|
||||||
|
|
||||||
config.RegisterHelpSubSys(helpMap)
|
config.RegisterHelpSubSys(helpMap)
|
||||||
|
|||||||
+34
-9
@@ -31,6 +31,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
@@ -250,6 +251,7 @@ type folderScanner struct {
|
|||||||
scanMode madmin.HealScanMode
|
scanMode madmin.HealScanMode
|
||||||
|
|
||||||
weSleep func() bool
|
weSleep func() bool
|
||||||
|
shouldHeal func() bool
|
||||||
|
|
||||||
disks []StorageAPI
|
disks []StorageAPI
|
||||||
disksQuorum int
|
disksQuorum int
|
||||||
@@ -304,11 +306,12 @@ type folderScanner struct {
|
|||||||
// The returned cache will always be valid, but may not be updated from the existing.
|
// The returned cache will always be valid, but may not be updated from the existing.
|
||||||
// Before each operation sleepDuration is called which can be used to temporarily halt the scanner.
|
// Before each operation sleepDuration is called which can be used to temporarily halt the scanner.
|
||||||
// If the supplied context is canceled the function will return at the first chance.
|
// If the supplied context is canceled the function will return at the first chance.
|
||||||
func scanDataFolder(ctx context.Context, disks []StorageAPI, basePath string, cache dataUsageCache, getSize getSizeFn, scanMode madmin.HealScanMode, weSleep func() bool) (dataUsageCache, error) {
|
func scanDataFolder(ctx context.Context, disks []StorageAPI, drive *xlStorage, cache dataUsageCache, getSize getSizeFn, scanMode madmin.HealScanMode, weSleep func() bool) (dataUsageCache, error) {
|
||||||
switch cache.Info.Name {
|
switch cache.Info.Name {
|
||||||
case "", dataUsageRoot:
|
case "", dataUsageRoot:
|
||||||
return cache, errors.New("internal error: root scan attempted")
|
return cache, errors.New("internal error: root scan attempted")
|
||||||
}
|
}
|
||||||
|
basePath := drive.drivePath
|
||||||
updatePath, closeDisk := globalScannerMetrics.currentPathUpdater(basePath, cache.Info.Name)
|
updatePath, closeDisk := globalScannerMetrics.currentPathUpdater(basePath, cache.Info.Name)
|
||||||
defer closeDisk()
|
defer closeDisk()
|
||||||
|
|
||||||
@@ -328,6 +331,26 @@ func scanDataFolder(ctx context.Context, disks []StorageAPI, basePath string, ca
|
|||||||
disksQuorum: len(disks) / 2,
|
disksQuorum: len(disks) / 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var skipHeal atomic.Bool
|
||||||
|
if globalIsErasure || cache.Info.SkipHealing {
|
||||||
|
skipHeal.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should do healing at all.
|
||||||
|
s.shouldHeal = func() bool {
|
||||||
|
if skipHeal.Load() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s.healObjectSelect == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if di, _ := drive.DiskInfo(ctx, DiskInfoOptions{}); di.Healing {
|
||||||
|
skipHeal.Store(true)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Enable healing in XL mode.
|
// Enable healing in XL mode.
|
||||||
if globalIsErasure && !cache.Info.SkipHealing {
|
if globalIsErasure && !cache.Info.SkipHealing {
|
||||||
// Do a heal check on an object once every n cycles. Must divide into healFolderInclude
|
// Do a heal check on an object once every n cycles. Must divide into healFolderInclude
|
||||||
@@ -482,14 +505,9 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
replication: replicationCfg,
|
replication: replicationCfg,
|
||||||
}
|
}
|
||||||
|
|
||||||
item.heal.enabled = thisHash.modAlt(f.oldCache.Info.NextCycle/folder.objectHealProbDiv, f.healObjectSelect/folder.objectHealProbDiv) && globalIsErasure
|
item.heal.enabled = thisHash.modAlt(f.oldCache.Info.NextCycle/folder.objectHealProbDiv, f.healObjectSelect/folder.objectHealProbDiv) && f.shouldHeal()
|
||||||
item.heal.bitrot = f.scanMode == madmin.HealDeepScan
|
item.heal.bitrot = f.scanMode == madmin.HealDeepScan
|
||||||
|
|
||||||
// if the drive belongs to an erasure set
|
|
||||||
// that is already being healed, skip the
|
|
||||||
// healing attempt on this drive.
|
|
||||||
item.heal.enabled = item.heal.enabled && f.healObjectSelect > 0
|
|
||||||
|
|
||||||
sz, err := f.getSize(item)
|
sz, err := f.getSize(item)
|
||||||
if err != nil && err != errIgnoreFileContrib {
|
if err != nil && err != errIgnoreFileContrib {
|
||||||
wait() // wait to proceed to next entry.
|
wait() // wait to proceed to next entry.
|
||||||
@@ -652,7 +670,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan for healing
|
// Scan for healing
|
||||||
if f.healObjectSelect == 0 || len(abandonedChildren) == 0 {
|
if len(abandonedChildren) == 0 || !f.shouldHeal() {
|
||||||
// If we are not heal scanning, return now.
|
// If we are not heal scanning, return now.
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -687,6 +705,9 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
|
|
||||||
healObjectsPrefix := color.Green("healObjects:")
|
healObjectsPrefix := color.Green("healObjects:")
|
||||||
for k := range abandonedChildren {
|
for k := range abandonedChildren {
|
||||||
|
if !f.shouldHeal() {
|
||||||
|
break
|
||||||
|
}
|
||||||
bucket, prefix := path2BucketObject(k)
|
bucket, prefix := path2BucketObject(k)
|
||||||
stopFn := globalScannerMetrics.time(scannerMetricCheckMissing)
|
stopFn := globalScannerMetrics.time(scannerMetricCheckMissing)
|
||||||
f.updateCurrentPath(k)
|
f.updateCurrentPath(k)
|
||||||
@@ -720,6 +741,10 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
},
|
},
|
||||||
// Some disks have data for this.
|
// Some disks have data for this.
|
||||||
partial: func(entries metaCacheEntries, errs []error) {
|
partial: func(entries metaCacheEntries, errs []error) {
|
||||||
|
if !f.shouldHeal() {
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
entry, ok := entries.resolve(&resolver)
|
entry, ok := entries.resolve(&resolver)
|
||||||
if !ok {
|
if !ok {
|
||||||
// check if we can get one entry at least
|
// check if we can get one entry at least
|
||||||
@@ -1233,7 +1258,7 @@ func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr loc
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applyTransitionRule(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) bool {
|
func applyTransitionRule(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) bool {
|
||||||
if obj.DeleteMarker {
|
if obj.DeleteMarker || obj.IsDir {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
globalTransitionState.queueTransitionTask(obj, event, src)
|
globalTransitionState.queueTransitionTask(obj, event, src)
|
||||||
|
|||||||
+20
-6
@@ -26,6 +26,9 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/internal/cachevalue"
|
||||||
)
|
)
|
||||||
|
|
||||||
type usageTestFile struct {
|
type usageTestFile struct {
|
||||||
@@ -61,10 +64,13 @@ func TestDataUsageUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
xls := xlStorage{drivePath: base, diskInfoCache: cachevalue.New[DiskInfo]()}
|
||||||
|
xls.diskInfoCache.InitOnce(time.Second, cachevalue.Opts{}, func(ctx context.Context) (DiskInfo, error) {
|
||||||
|
return DiskInfo{Total: 1 << 40, Free: 1 << 40}, nil
|
||||||
|
})
|
||||||
weSleep := func() bool { return false }
|
weSleep := func() bool { return false }
|
||||||
|
|
||||||
got, err := scanDataFolder(context.Background(), nil, base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize, 0, weSleep)
|
got, err := scanDataFolder(context.Background(), nil, &xls, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize, 0, weSleep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -174,7 +180,7 @@ func TestDataUsageUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Changed dir must be picked up in this many cycles.
|
// Changed dir must be picked up in this many cycles.
|
||||||
for i := 0; i < dataUsageUpdateDirCycles; i++ {
|
for i := 0; i < dataUsageUpdateDirCycles; i++ {
|
||||||
got, err = scanDataFolder(context.Background(), nil, base, got, getSize, 0, weSleep)
|
got, err = scanDataFolder(context.Background(), nil, &xls, got, getSize, 0, weSleep)
|
||||||
got.Info.NextCycle++
|
got.Info.NextCycle++
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -283,8 +289,12 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
weSleep := func() bool { return false }
|
weSleep := func() bool { return false }
|
||||||
|
xls := xlStorage{drivePath: base, diskInfoCache: cachevalue.New[DiskInfo]()}
|
||||||
|
xls.diskInfoCache.InitOnce(time.Second, cachevalue.Opts{}, func(ctx context.Context) (DiskInfo, error) {
|
||||||
|
return DiskInfo{Total: 1 << 40, Free: 1 << 40}, nil
|
||||||
|
})
|
||||||
|
|
||||||
got, err := scanDataFolder(context.Background(), nil, base, dataUsageCache{Info: dataUsageCacheInfo{Name: "bucket"}}, getSize, 0, weSleep)
|
got, err := scanDataFolder(context.Background(), nil, &xls, dataUsageCache{Info: dataUsageCacheInfo{Name: "bucket"}}, getSize, 0, weSleep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -419,7 +429,7 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Changed dir must be picked up in this many cycles.
|
// Changed dir must be picked up in this many cycles.
|
||||||
for i := 0; i < dataUsageUpdateDirCycles; i++ {
|
for i := 0; i < dataUsageUpdateDirCycles; i++ {
|
||||||
got, err = scanDataFolder(context.Background(), nil, base, got, getSize, 0, weSleep)
|
got, err = scanDataFolder(context.Background(), nil, &xls, got, getSize, 0, weSleep)
|
||||||
got.Info.NextCycle++
|
got.Info.NextCycle++
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -567,8 +577,12 @@ func TestDataUsageCacheSerialize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
xls := xlStorage{drivePath: base, diskInfoCache: cachevalue.New[DiskInfo]()}
|
||||||
|
xls.diskInfoCache.InitOnce(time.Second, cachevalue.Opts{}, func(ctx context.Context) (DiskInfo, error) {
|
||||||
|
return DiskInfo{Total: 1 << 40, Free: 1 << 40}, nil
|
||||||
|
})
|
||||||
weSleep := func() bool { return false }
|
weSleep := func() bool { return false }
|
||||||
want, err := scanDataFolder(context.Background(), nil, base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize, 0, weSleep)
|
want, err := scanDataFolder(context.Background(), nil, &xls, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize, 0, weSleep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-7
@@ -1025,9 +1025,11 @@ func DecryptObjectInfo(info *ObjectInfo, r *http.Request) (encrypted bool, err e
|
|||||||
if encrypted {
|
if encrypted {
|
||||||
if crypto.SSEC.IsEncrypted(info.UserDefined) {
|
if crypto.SSEC.IsEncrypted(info.UserDefined) {
|
||||||
if !(crypto.SSEC.IsRequested(headers) || crypto.SSECopy.IsRequested(headers)) {
|
if !(crypto.SSEC.IsRequested(headers) || crypto.SSECopy.IsRequested(headers)) {
|
||||||
|
if r.Header.Get(xhttp.MinIOSourceReplicationRequest) != "true" {
|
||||||
return encrypted, errEncryptedObject
|
return encrypted, errEncryptedObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if crypto.S3.IsEncrypted(info.UserDefined) && r.Header.Get(xhttp.AmzCopySource) == "" {
|
if crypto.S3.IsEncrypted(info.UserDefined) && r.Header.Get(xhttp.AmzCopySource) == "" {
|
||||||
if crypto.SSEC.IsRequested(headers) || crypto.SSECopy.IsRequested(headers) {
|
if crypto.SSEC.IsRequested(headers) || crypto.SSECopy.IsRequested(headers) {
|
||||||
@@ -1077,13 +1079,16 @@ func metadataEncrypter(key crypto.ObjectKey) objectMetaEncryptFn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// metadataDecrypter reverses metadataEncrypter.
|
// metadataDecrypter reverses metadataEncrypter.
|
||||||
func (o *ObjectInfo) metadataDecrypter() objectMetaDecryptFn {
|
func (o *ObjectInfo) metadataDecrypter(h http.Header) objectMetaDecryptFn {
|
||||||
return func(baseKey string, input []byte) ([]byte, error) {
|
return func(baseKey string, input []byte) ([]byte, error) {
|
||||||
if len(input) == 0 {
|
if len(input) == 0 {
|
||||||
return input, nil
|
return input, nil
|
||||||
}
|
}
|
||||||
|
var key []byte
|
||||||
key, err := decryptObjectMeta(nil, o.Bucket, o.Name, o.UserDefined)
|
if k, err := crypto.SSEC.ParseHTTP(h); err == nil {
|
||||||
|
key = k[:]
|
||||||
|
}
|
||||||
|
key, err := decryptObjectMeta(key, o.Bucket, o.Name, o.UserDefined)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1095,13 +1100,13 @@ func (o *ObjectInfo) metadataDecrypter() objectMetaDecryptFn {
|
|||||||
|
|
||||||
// decryptChecksums will attempt to decode checksums and return it/them if set.
|
// decryptChecksums will attempt to decode checksums and return it/them if set.
|
||||||
// if part > 0, and we have the checksum for the part that will be returned.
|
// if part > 0, and we have the checksum for the part that will be returned.
|
||||||
func (o *ObjectInfo) decryptPartsChecksums() {
|
func (o *ObjectInfo) decryptPartsChecksums(h http.Header) {
|
||||||
data := o.Checksum
|
data := o.Checksum
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
|
if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
|
||||||
decrypted, err := o.metadataDecrypter()("object-checksum", data)
|
decrypted, err := o.metadataDecrypter(h)("object-checksum", data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
encLogIf(GlobalContext, err)
|
encLogIf(GlobalContext, err)
|
||||||
return
|
return
|
||||||
@@ -1157,15 +1162,17 @@ func (o *ObjectInfo) metadataEncryptFn(headers http.Header) (objectMetaEncryptFn
|
|||||||
|
|
||||||
// decryptChecksums will attempt to decode checksums and return it/them if set.
|
// decryptChecksums will attempt to decode checksums and return it/them if set.
|
||||||
// if part > 0, and we have the checksum for the part that will be returned.
|
// if part > 0, and we have the checksum for the part that will be returned.
|
||||||
func (o *ObjectInfo) decryptChecksums(part int) map[string]string {
|
func (o *ObjectInfo) decryptChecksums(part int, h http.Header) map[string]string {
|
||||||
data := o.Checksum
|
data := o.Checksum
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
|
if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
|
||||||
decrypted, err := o.metadataDecrypter()("object-checksum", data)
|
decrypted, err := o.metadataDecrypter(h)("object-checksum", data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err != crypto.ErrSecretKeyMismatch {
|
||||||
encLogIf(GlobalContext, err)
|
encLogIf(GlobalContext, err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
data = decrypted
|
data = decrypted
|
||||||
|
|||||||
@@ -253,6 +253,14 @@ type PoolEndpoints struct {
|
|||||||
// EndpointServerPools - list of list of endpoints
|
// EndpointServerPools - list of list of endpoints
|
||||||
type EndpointServerPools []PoolEndpoints
|
type EndpointServerPools []PoolEndpoints
|
||||||
|
|
||||||
|
// ESCount returns the total number of erasure sets in this cluster
|
||||||
|
func (l EndpointServerPools) ESCount() (count int) {
|
||||||
|
for _, p := range l {
|
||||||
|
count += p.SetCount
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// GetNodes returns a sorted list of nodes in this cluster
|
// GetNodes returns a sorted list of nodes in this cluster
|
||||||
func (l EndpointServerPools) GetNodes() (nodes []Node) {
|
func (l EndpointServerPools) GetNodes() (nodes []Node) {
|
||||||
nodesMap := make(map[string]Node)
|
nodesMap := make(map[string]Node)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
@@ -253,6 +254,33 @@ func listOnlineDisks(disks []StorageAPI, partsMetadata []FileInfo, errs []error,
|
|||||||
return onlineDisks, modTime, ""
|
return onlineDisks, modTime, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert verify or check parts returned error to integer representation
|
||||||
|
func convPartErrToInt(err error) int {
|
||||||
|
err = unwrapAll(err)
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
return checkPartSuccess
|
||||||
|
case errFileNotFound, errFileVersionNotFound:
|
||||||
|
return checkPartFileNotFound
|
||||||
|
case errFileCorrupt:
|
||||||
|
return checkPartFileCorrupt
|
||||||
|
case errVolumeNotFound:
|
||||||
|
return checkPartVolumeNotFound
|
||||||
|
case errDiskNotFound:
|
||||||
|
return checkPartDiskNotFound
|
||||||
|
default:
|
||||||
|
return checkPartUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func partNeedsHealing(partErrs []int) bool {
|
||||||
|
return slices.IndexFunc(partErrs, func(i int) bool { return i != checkPartSuccess && i != checkPartUnknown }) > -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasPartErr(partErrs []int) bool {
|
||||||
|
return slices.IndexFunc(partErrs, func(i int) bool { return i != checkPartSuccess }) > -1
|
||||||
|
}
|
||||||
|
|
||||||
// disksWithAllParts - This function needs to be called with
|
// disksWithAllParts - This function needs to be called with
|
||||||
// []StorageAPI returned by listOnlineDisks. Returns,
|
// []StorageAPI returned by listOnlineDisks. Returns,
|
||||||
//
|
//
|
||||||
@@ -262,10 +290,19 @@ func listOnlineDisks(disks []StorageAPI, partsMetadata []FileInfo, errs []error,
|
|||||||
// a not-found error or a hash-mismatch error.
|
// a not-found error or a hash-mismatch error.
|
||||||
func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo,
|
func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo,
|
||||||
errs []error, latestMeta FileInfo, bucket, object string,
|
errs []error, latestMeta FileInfo, bucket, object string,
|
||||||
scanMode madmin.HealScanMode) ([]StorageAPI, []error, time.Time,
|
scanMode madmin.HealScanMode,
|
||||||
) {
|
) (availableDisks []StorageAPI, dataErrsByDisk map[int][]int, dataErrsByPart map[int][]int) {
|
||||||
availableDisks := make([]StorageAPI, len(onlineDisks))
|
availableDisks = make([]StorageAPI, len(onlineDisks))
|
||||||
dataErrs := make([]error, len(onlineDisks))
|
|
||||||
|
dataErrsByDisk = make(map[int][]int, len(onlineDisks))
|
||||||
|
for i := range onlineDisks {
|
||||||
|
dataErrsByDisk[i] = make([]int, len(latestMeta.Parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
dataErrsByPart = make(map[int][]int, len(latestMeta.Parts))
|
||||||
|
for i := range latestMeta.Parts {
|
||||||
|
dataErrsByPart[i] = make([]int, len(onlineDisks))
|
||||||
|
}
|
||||||
|
|
||||||
inconsistent := 0
|
inconsistent := 0
|
||||||
for i, meta := range partsMetadata {
|
for i, meta := range partsMetadata {
|
||||||
@@ -295,19 +332,21 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
|||||||
erasureDistributionReliable = false
|
erasureDistributionReliable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metaErrs := make([]error, len(errs))
|
||||||
|
|
||||||
for i, onlineDisk := range onlineDisks {
|
for i, onlineDisk := range onlineDisks {
|
||||||
if errs[i] != nil {
|
if errs[i] != nil {
|
||||||
dataErrs[i] = errs[i]
|
metaErrs[i] = errs[i]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if onlineDisk == OfflineDisk {
|
if onlineDisk == OfflineDisk {
|
||||||
dataErrs[i] = errDiskNotFound
|
metaErrs[i] = errDiskNotFound
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := partsMetadata[i]
|
meta := partsMetadata[i]
|
||||||
if !meta.ModTime.Equal(latestMeta.ModTime) || meta.DataDir != latestMeta.DataDir {
|
if !meta.ModTime.Equal(latestMeta.ModTime) || meta.DataDir != latestMeta.DataDir {
|
||||||
dataErrs[i] = errFileCorrupt
|
metaErrs[i] = errFileCorrupt
|
||||||
partsMetadata[i] = FileInfo{}
|
partsMetadata[i] = FileInfo{}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -315,7 +354,7 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
|||||||
if erasureDistributionReliable {
|
if erasureDistributionReliable {
|
||||||
if !meta.IsValid() {
|
if !meta.IsValid() {
|
||||||
partsMetadata[i] = FileInfo{}
|
partsMetadata[i] = FileInfo{}
|
||||||
dataErrs[i] = errFileCorrupt
|
metaErrs[i] = errFileCorrupt
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,46 +364,79 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
|||||||
// attempt a fix if possible, assuming other entries
|
// attempt a fix if possible, assuming other entries
|
||||||
// might have the right erasure distribution.
|
// might have the right erasure distribution.
|
||||||
partsMetadata[i] = FileInfo{}
|
partsMetadata[i] = FileInfo{}
|
||||||
dataErrs[i] = errFileCorrupt
|
metaErrs[i] = errFileCorrupt
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy meta errors to part errors
|
||||||
|
for i, err := range metaErrs {
|
||||||
|
if err != nil {
|
||||||
|
partErr := convPartErrToInt(err)
|
||||||
|
for p := range latestMeta.Parts {
|
||||||
|
dataErrsByPart[p][i] = partErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, onlineDisk := range onlineDisks {
|
||||||
|
if metaErrs[i] != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
meta := partsMetadata[i]
|
||||||
|
|
||||||
|
if meta.Deleted || meta.IsRemote() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Always check data, if we got it.
|
// Always check data, if we got it.
|
||||||
if (len(meta.Data) > 0 || meta.Size == 0) && len(meta.Parts) > 0 {
|
if (len(meta.Data) > 0 || meta.Size == 0) && len(meta.Parts) > 0 {
|
||||||
checksumInfo := meta.Erasure.GetChecksumInfo(meta.Parts[0].Number)
|
checksumInfo := meta.Erasure.GetChecksumInfo(meta.Parts[0].Number)
|
||||||
dataErrs[i] = bitrotVerify(bytes.NewReader(meta.Data),
|
verifyErr := bitrotVerify(bytes.NewReader(meta.Data),
|
||||||
int64(len(meta.Data)),
|
int64(len(meta.Data)),
|
||||||
meta.Erasure.ShardFileSize(meta.Size),
|
meta.Erasure.ShardFileSize(meta.Size),
|
||||||
checksumInfo.Algorithm,
|
checksumInfo.Algorithm,
|
||||||
checksumInfo.Hash, meta.Erasure.ShardSize())
|
checksumInfo.Hash, meta.Erasure.ShardSize())
|
||||||
if dataErrs[i] == nil {
|
dataErrsByPart[0][i] = convPartErrToInt(verifyErr)
|
||||||
// All parts verified, mark it as all data available.
|
|
||||||
availableDisks[i] = onlineDisk
|
|
||||||
} else {
|
|
||||||
// upon errors just make that disk's fileinfo invalid
|
|
||||||
partsMetadata[i] = FileInfo{}
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
verifyErr error
|
||||||
|
verifyResp *CheckPartsResp
|
||||||
|
)
|
||||||
|
|
||||||
meta.DataDir = latestMeta.DataDir
|
meta.DataDir = latestMeta.DataDir
|
||||||
switch scanMode {
|
switch scanMode {
|
||||||
case madmin.HealDeepScan:
|
case madmin.HealDeepScan:
|
||||||
// disk has a valid xl.meta but may not have all the
|
// disk has a valid xl.meta but may not have all the
|
||||||
// parts. This is considered an outdated disk, since
|
// parts. This is considered an outdated disk, since
|
||||||
// it needs healing too.
|
// it needs healing too.
|
||||||
if !meta.Deleted && !meta.IsRemote() {
|
verifyResp, verifyErr = onlineDisk.VerifyFile(ctx, bucket, object, meta)
|
||||||
dataErrs[i] = onlineDisk.VerifyFile(ctx, bucket, object, meta)
|
default:
|
||||||
|
verifyResp, verifyErr = onlineDisk.CheckParts(ctx, bucket, object, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
for p := range latestMeta.Parts {
|
||||||
|
if verifyErr != nil {
|
||||||
|
dataErrsByPart[p][i] = convPartErrToInt(verifyErr)
|
||||||
|
} else {
|
||||||
|
dataErrsByPart[p][i] = verifyResp.Results[p]
|
||||||
}
|
}
|
||||||
case madmin.HealNormalScan:
|
|
||||||
if !meta.Deleted && !meta.IsRemote() {
|
|
||||||
dataErrs[i] = onlineDisk.CheckParts(ctx, bucket, object, meta)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataErrs[i] == nil {
|
// Build dataErrs by disk from dataErrs by part
|
||||||
|
for part, disks := range dataErrsByPart {
|
||||||
|
for disk := range disks {
|
||||||
|
dataErrsByDisk[disk][part] = dataErrsByPart[part][disk]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, onlineDisk := range onlineDisks {
|
||||||
|
if metaErrs[i] == nil && !hasPartErr(dataErrsByDisk[i]) {
|
||||||
// All parts verified, mark it as all data available.
|
// All parts verified, mark it as all data available.
|
||||||
availableDisks[i] = onlineDisk
|
availableDisks[i] = onlineDisk
|
||||||
} else {
|
} else {
|
||||||
@@ -373,5 +445,5 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return availableDisks, dataErrs, timeSentinel
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,9 +308,8 @@ func TestListOnlineDisks(t *testing.T) {
|
|||||||
t.Fatalf("Expected modTime to be equal to %v but was found to be %v",
|
t.Fatalf("Expected modTime to be equal to %v but was found to be %v",
|
||||||
test.expectedTime, modTime)
|
test.expectedTime, modTime)
|
||||||
}
|
}
|
||||||
availableDisks, newErrs, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
availableDisks, _, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||||
test.errs, fi, bucket, object, madmin.HealDeepScan)
|
test.errs, fi, bucket, object, madmin.HealDeepScan)
|
||||||
test.errs = newErrs
|
|
||||||
|
|
||||||
if test._tamperBackend != noTamper {
|
if test._tamperBackend != noTamper {
|
||||||
if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil {
|
if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil {
|
||||||
@@ -491,9 +490,8 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
|
|||||||
test.expectedTime, modTime)
|
test.expectedTime, modTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
availableDisks, newErrs, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
availableDisks, _, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||||
test.errs, fi, bucket, object, madmin.HealDeepScan)
|
test.errs, fi, bucket, object, madmin.HealDeepScan)
|
||||||
test.errs = newErrs
|
|
||||||
|
|
||||||
if test._tamperBackend != noTamper {
|
if test._tamperBackend != noTamper {
|
||||||
if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil {
|
if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil {
|
||||||
@@ -554,7 +552,7 @@ func TestDisksWithAllParts(t *testing.T) {
|
|||||||
|
|
||||||
erasureDisks, _, _ = listOnlineDisks(erasureDisks, partsMetadata, errs, readQuorum)
|
erasureDisks, _, _ = listOnlineDisks(erasureDisks, partsMetadata, errs, readQuorum)
|
||||||
|
|
||||||
filteredDisks, errs, _ := disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
filteredDisks, _, dataErrsPerDisk := disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||||
errs, fi, bucket, object, madmin.HealDeepScan)
|
errs, fi, bucket, object, madmin.HealDeepScan)
|
||||||
|
|
||||||
if len(filteredDisks) != len(erasureDisks) {
|
if len(filteredDisks) != len(erasureDisks) {
|
||||||
@@ -562,8 +560,8 @@ func TestDisksWithAllParts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for diskIndex, disk := range filteredDisks {
|
for diskIndex, disk := range filteredDisks {
|
||||||
if errs[diskIndex] != nil {
|
if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
|
||||||
t.Errorf("Unexpected error %s", errs[diskIndex])
|
t.Errorf("Unexpected error: %v", dataErrsPerDisk[diskIndex])
|
||||||
}
|
}
|
||||||
|
|
||||||
if disk == nil {
|
if disk == nil {
|
||||||
@@ -634,7 +632,7 @@ func TestDisksWithAllParts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errs = make([]error, len(erasureDisks))
|
errs = make([]error, len(erasureDisks))
|
||||||
filteredDisks, errs, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
filteredDisks, dataErrsPerDisk, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||||
errs, fi, bucket, object, madmin.HealDeepScan)
|
errs, fi, bucket, object, madmin.HealDeepScan)
|
||||||
|
|
||||||
if len(filteredDisks) != len(erasureDisks) {
|
if len(filteredDisks) != len(erasureDisks) {
|
||||||
@@ -646,15 +644,15 @@ func TestDisksWithAllParts(t *testing.T) {
|
|||||||
if disk != nil {
|
if disk != nil {
|
||||||
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
|
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
|
||||||
}
|
}
|
||||||
if errs[diskIndex] == nil {
|
if !partNeedsHealing(dataErrsPerDisk[diskIndex]) {
|
||||||
t.Errorf("Expected error not received, driveIndex: %d", diskIndex)
|
t.Errorf("Disk expected to be healed, driveIndex: %d", diskIndex)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if disk == nil {
|
if disk == nil {
|
||||||
t.Errorf("Drive erroneously filtered, driveIndex: %d", diskIndex)
|
t.Errorf("Drive erroneously filtered, driveIndex: %d", diskIndex)
|
||||||
}
|
}
|
||||||
if errs[diskIndex] != nil {
|
if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
|
||||||
t.Errorf("Unexpected error, %s, driveIndex: %d", errs[diskIndex], diskIndex)
|
t.Errorf("Disk not expected to be healed, driveIndex: %d", diskIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-45
@@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/minio/minio/internal/grid"
|
"github.com/minio/minio/internal/grid"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/v3/sync/errgroup"
|
"github.com/minio/pkg/v3/sync/errgroup"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate stringer -type=healingMetric -trimprefix=healingMetric $GOFILE
|
//go:generate stringer -type=healingMetric -trimprefix=healingMetric $GOFILE
|
||||||
@@ -144,36 +145,41 @@ func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets
|
|||||||
return reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, readQuorum)
|
return reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, readQuorum)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errLegacyXLMeta = errors.New("legacy XL meta")
|
||||||
|
|
||||||
|
var errOutdatedXLMeta = errors.New("outdated XL meta")
|
||||||
|
|
||||||
|
var errPartMissingOrCorrupt = errors.New("part missing or corrupt")
|
||||||
|
|
||||||
// Only heal on disks where we are sure that healing is needed. We can expand
|
// Only heal on disks where we are sure that healing is needed. We can expand
|
||||||
// this list as and when we figure out more errors can be added to this list safely.
|
// this list as and when we figure out more errors can be added to this list safely.
|
||||||
func shouldHealObjectOnDisk(erErr, dataErr error, meta FileInfo, latestMeta FileInfo) bool {
|
func shouldHealObjectOnDisk(erErr error, partsErrs []int, meta FileInfo, latestMeta FileInfo) (bool, error) {
|
||||||
switch {
|
if errors.Is(erErr, errFileNotFound) || errors.Is(erErr, errFileVersionNotFound) || errors.Is(erErr, errFileCorrupt) {
|
||||||
case errors.Is(erErr, errFileNotFound) || errors.Is(erErr, errFileVersionNotFound):
|
return true, erErr
|
||||||
return true
|
|
||||||
case errors.Is(erErr, errFileCorrupt):
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
if erErr == nil {
|
if erErr == nil {
|
||||||
if meta.XLV1 {
|
if meta.XLV1 {
|
||||||
// Legacy means heal always
|
// Legacy means heal always
|
||||||
// always check first.
|
// always check first.
|
||||||
return true
|
return true, errLegacyXLMeta
|
||||||
|
}
|
||||||
|
if !latestMeta.Equals(meta) {
|
||||||
|
return true, errOutdatedXLMeta
|
||||||
}
|
}
|
||||||
if !meta.Deleted && !meta.IsRemote() {
|
if !meta.Deleted && !meta.IsRemote() {
|
||||||
// If xl.meta was read fine but there may be problem with the part.N files.
|
// If xl.meta was read fine but there may be problem with the part.N files.
|
||||||
if IsErr(dataErr, []error{
|
for _, partErr := range partsErrs {
|
||||||
errFileNotFound,
|
if slices.Contains([]int{
|
||||||
errFileVersionNotFound,
|
checkPartFileNotFound,
|
||||||
errFileCorrupt,
|
checkPartFileCorrupt,
|
||||||
}...) {
|
}, partErr) {
|
||||||
return true
|
return true, errPartMissingOrCorrupt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !latestMeta.Equals(meta) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
return false
|
return false, erErr
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -332,7 +338,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
// used here for reconstruction. This is done to ensure that
|
// used here for reconstruction. This is done to ensure that
|
||||||
// we do not skip drives that have inconsistent metadata to be
|
// we do not skip drives that have inconsistent metadata to be
|
||||||
// skipped from purging when they are stale.
|
// skipped from purging when they are stale.
|
||||||
availableDisks, dataErrs, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
availableDisks, dataErrsByDisk, dataErrsByPart := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||||
errs, latestMeta, bucket, object, scanMode)
|
errs, latestMeta, bucket, object, scanMode)
|
||||||
|
|
||||||
var erasure Erasure
|
var erasure Erasure
|
||||||
@@ -355,15 +361,20 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
// to be healed.
|
// to be healed.
|
||||||
outDatedDisks := make([]StorageAPI, len(storageDisks))
|
outDatedDisks := make([]StorageAPI, len(storageDisks))
|
||||||
disksToHealCount := 0
|
disksToHealCount := 0
|
||||||
for i, v := range availableDisks {
|
for i := range availableDisks {
|
||||||
|
yes, reason := shouldHealObjectOnDisk(errs[i], dataErrsByDisk[i], partsMetadata[i], latestMeta)
|
||||||
|
if yes {
|
||||||
|
outDatedDisks[i] = storageDisks[i]
|
||||||
|
disksToHealCount++
|
||||||
|
}
|
||||||
|
|
||||||
driveState := ""
|
driveState := ""
|
||||||
switch {
|
switch {
|
||||||
case v != nil:
|
case reason == nil:
|
||||||
driveState = madmin.DriveStateOk
|
driveState = madmin.DriveStateOk
|
||||||
case errors.Is(errs[i], errDiskNotFound), errors.Is(dataErrs[i], errDiskNotFound):
|
case IsErr(reason, errDiskNotFound):
|
||||||
driveState = madmin.DriveStateOffline
|
driveState = madmin.DriveStateOffline
|
||||||
case IsErr(errs[i], errFileNotFound, errFileVersionNotFound, errVolumeNotFound),
|
case IsErr(reason, errFileNotFound, errFileVersionNotFound, errVolumeNotFound, errPartMissingOrCorrupt, errOutdatedXLMeta, errLegacyXLMeta):
|
||||||
IsErr(dataErrs[i], errFileNotFound, errFileVersionNotFound, errVolumeNotFound):
|
|
||||||
driveState = madmin.DriveStateMissing
|
driveState = madmin.DriveStateMissing
|
||||||
default:
|
default:
|
||||||
// all remaining cases imply corrupt data/metadata
|
// all remaining cases imply corrupt data/metadata
|
||||||
@@ -380,12 +391,6 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
Endpoint: storageEndpoints[i].String(),
|
Endpoint: storageEndpoints[i].String(),
|
||||||
State: driveState,
|
State: driveState,
|
||||||
})
|
})
|
||||||
|
|
||||||
if shouldHealObjectOnDisk(errs[i], dataErrs[i], partsMetadata[i], latestMeta) {
|
|
||||||
outDatedDisks[i] = storageDisks[i]
|
|
||||||
disksToHealCount++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if isAllNotFound(errs) {
|
if isAllNotFound(errs) {
|
||||||
@@ -412,7 +417,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
if !latestMeta.XLV1 && !latestMeta.Deleted && disksToHealCount > latestMeta.Erasure.ParityBlocks {
|
if !latestMeta.XLV1 && !latestMeta.Deleted && disksToHealCount > latestMeta.Erasure.ParityBlocks {
|
||||||
// Allow for dangling deletes, on versions that have DataDir missing etc.
|
// Allow for dangling deletes, on versions that have DataDir missing etc.
|
||||||
// this would end up restoring the correct readable versions.
|
// this would end up restoring the correct readable versions.
|
||||||
m, err := er.deleteIfDangling(ctx, bucket, object, partsMetadata, errs, dataErrs, ObjectOptions{
|
m, err := er.deleteIfDangling(ctx, bucket, object, partsMetadata, errs, dataErrsByPart, ObjectOptions{
|
||||||
VersionID: versionID,
|
VersionID: versionID,
|
||||||
})
|
})
|
||||||
errs = make([]error, len(errs))
|
errs = make([]error, len(errs))
|
||||||
@@ -908,18 +913,7 @@ func isObjectDirDangling(errs []error) (ok bool) {
|
|||||||
return found < notFound && found > 0
|
return found < notFound && found > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object is considered dangling/corrupted if and only
|
func danglingMetaErrsCount(cerrs []error) (notFoundCount int, nonActionableCount int) {
|
||||||
// if total disks - a combination of corrupted and missing
|
|
||||||
// files is lesser than number of data blocks.
|
|
||||||
func isObjectDangling(metaArr []FileInfo, errs []error, dataErrs []error) (validMeta FileInfo, ok bool) {
|
|
||||||
// We can consider an object data not reliable
|
|
||||||
// when xl.meta is not found in read quorum disks.
|
|
||||||
// or when xl.meta is not readable in read quorum disks.
|
|
||||||
danglingErrsCount := func(cerrs []error) (int, int) {
|
|
||||||
var (
|
|
||||||
notFoundCount int
|
|
||||||
nonActionableCount int
|
|
||||||
)
|
|
||||||
for _, readErr := range cerrs {
|
for _, readErr := range cerrs {
|
||||||
if readErr == nil {
|
if readErr == nil {
|
||||||
continue
|
continue
|
||||||
@@ -932,11 +926,39 @@ func isObjectDangling(metaArr []FileInfo, errs []error, dataErrs []error) (valid
|
|||||||
nonActionableCount++
|
nonActionableCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return notFoundCount, nonActionableCount
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
notFoundMetaErrs, nonActionableMetaErrs := danglingErrsCount(errs)
|
func danglingPartErrsCount(results []int) (notFoundCount int, nonActionableCount int) {
|
||||||
notFoundPartsErrs, nonActionablePartsErrs := danglingErrsCount(dataErrs)
|
for _, partResult := range results {
|
||||||
|
switch partResult {
|
||||||
|
case checkPartSuccess:
|
||||||
|
continue
|
||||||
|
case checkPartFileNotFound:
|
||||||
|
notFoundCount++
|
||||||
|
default:
|
||||||
|
// All other errors are non-actionable
|
||||||
|
nonActionableCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object is considered dangling/corrupted if and only
|
||||||
|
// if total disks - a combination of corrupted and missing
|
||||||
|
// files is lesser than number of data blocks.
|
||||||
|
func isObjectDangling(metaArr []FileInfo, errs []error, dataErrsByPart map[int][]int) (validMeta FileInfo, ok bool) {
|
||||||
|
// We can consider an object data not reliable
|
||||||
|
// when xl.meta is not found in read quorum disks.
|
||||||
|
// or when xl.meta is not readable in read quorum disks.
|
||||||
|
notFoundMetaErrs, nonActionableMetaErrs := danglingMetaErrsCount(errs)
|
||||||
|
|
||||||
|
notFoundPartsErrs, nonActionablePartsErrs := 0, 0
|
||||||
|
for _, dataErrs := range dataErrsByPart {
|
||||||
|
if nf, na := danglingPartErrsCount(dataErrs); nf > notFoundPartsErrs {
|
||||||
|
notFoundPartsErrs, nonActionablePartsErrs = nf, na
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, m := range metaArr {
|
for _, m := range metaArr {
|
||||||
if m.IsValid() {
|
if m.IsValid() {
|
||||||
@@ -948,7 +970,7 @@ func isObjectDangling(metaArr []FileInfo, errs []error, dataErrs []error) (valid
|
|||||||
if !validMeta.IsValid() {
|
if !validMeta.IsValid() {
|
||||||
// validMeta is invalid because all xl.meta is missing apparently
|
// validMeta is invalid because all xl.meta is missing apparently
|
||||||
// we should figure out if dataDirs are also missing > dataBlocks.
|
// we should figure out if dataDirs are also missing > dataBlocks.
|
||||||
dataBlocks := (len(dataErrs) + 1) / 2
|
dataBlocks := (len(metaArr) + 1) / 2
|
||||||
if notFoundPartsErrs > dataBlocks {
|
if notFoundPartsErrs > dataBlocks {
|
||||||
// Not using parity to ensure that we do not delete
|
// Not using parity to ensure that we do not delete
|
||||||
// any valid content, if any is recoverable. But if
|
// any valid content, if any is recoverable. But if
|
||||||
@@ -1065,6 +1087,7 @@ func healTrace(funcName healingMetric, startTime time.Time, bucket, object strin
|
|||||||
if result != nil {
|
if result != nil {
|
||||||
tr.Custom["version-id"] = result.VersionID
|
tr.Custom["version-id"] = result.VersionID
|
||||||
tr.Custom["disks"] = strconv.Itoa(result.DiskCount)
|
tr.Custom["disks"] = strconv.Itoa(result.DiskCount)
|
||||||
|
tr.Bytes = result.ObjectSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+53
-16
@@ -49,7 +49,7 @@ func TestIsObjectDangling(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
metaArr []FileInfo
|
metaArr []FileInfo
|
||||||
errs []error
|
errs []error
|
||||||
dataErrs []error
|
dataErrs map[int][]int
|
||||||
expectedMeta FileInfo
|
expectedMeta FileInfo
|
||||||
expectedDangling bool
|
expectedDangling bool
|
||||||
}{
|
}{
|
||||||
@@ -165,11 +165,8 @@ func TestIsObjectDangling(t *testing.T) {
|
|||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
dataErrs: []error{
|
dataErrs: map[int][]int{
|
||||||
errFileCorrupt,
|
0: {checkPartFileCorrupt, checkPartFileNotFound, checkPartSuccess, checkPartFileCorrupt},
|
||||||
errFileNotFound,
|
|
||||||
nil,
|
|
||||||
errFileCorrupt,
|
|
||||||
},
|
},
|
||||||
expectedMeta: fi,
|
expectedMeta: fi,
|
||||||
expectedDangling: false,
|
expectedDangling: false,
|
||||||
@@ -188,11 +185,8 @@ func TestIsObjectDangling(t *testing.T) {
|
|||||||
errFileNotFound,
|
errFileNotFound,
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
dataErrs: []error{
|
dataErrs: map[int][]int{
|
||||||
errFileNotFound,
|
0: {checkPartFileNotFound, checkPartFileCorrupt, checkPartSuccess, checkPartSuccess},
|
||||||
errFileCorrupt,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
},
|
},
|
||||||
expectedMeta: fi,
|
expectedMeta: fi,
|
||||||
expectedDangling: false,
|
expectedDangling: false,
|
||||||
@@ -247,15 +241,58 @@ func TestIsObjectDangling(t *testing.T) {
|
|||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
dataErrs: []error{
|
dataErrs: map[int][]int{
|
||||||
errFileNotFound,
|
0: {checkPartFileNotFound, checkPartFileNotFound, checkPartSuccess, checkPartFileNotFound},
|
||||||
errFileNotFound,
|
|
||||||
nil,
|
|
||||||
errFileNotFound,
|
|
||||||
},
|
},
|
||||||
expectedMeta: fi,
|
expectedMeta: fi,
|
||||||
expectedDangling: true,
|
expectedDangling: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "FileInfoDecided-case4-(missing data-dir for part 2)",
|
||||||
|
metaArr: []FileInfo{
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
fi,
|
||||||
|
},
|
||||||
|
errs: []error{
|
||||||
|
errFileNotFound,
|
||||||
|
errFileNotFound,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
dataErrs: map[int][]int{
|
||||||
|
0: {checkPartSuccess, checkPartSuccess, checkPartSuccess, checkPartSuccess},
|
||||||
|
1: {checkPartSuccess, checkPartFileNotFound, checkPartFileNotFound, checkPartFileNotFound},
|
||||||
|
},
|
||||||
|
expectedMeta: fi,
|
||||||
|
expectedDangling: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "FileInfoDecided-case4-(enough data-dir existing for each part)",
|
||||||
|
metaArr: []FileInfo{
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
fi,
|
||||||
|
},
|
||||||
|
errs: []error{
|
||||||
|
errFileNotFound,
|
||||||
|
errFileNotFound,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
dataErrs: map[int][]int{
|
||||||
|
0: {checkPartFileNotFound, checkPartSuccess, checkPartSuccess, checkPartSuccess},
|
||||||
|
1: {checkPartSuccess, checkPartFileNotFound, checkPartSuccess, checkPartSuccess},
|
||||||
|
2: {checkPartSuccess, checkPartSuccess, checkPartFileNotFound, checkPartSuccess},
|
||||||
|
3: {checkPartSuccess, checkPartSuccess, checkPartSuccess, checkPartFileNotFound},
|
||||||
|
},
|
||||||
|
expectedMeta: fi,
|
||||||
|
expectedDangling: false,
|
||||||
|
},
|
||||||
|
|
||||||
// Add new cases as seen
|
// Add new cases as seen
|
||||||
}
|
}
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
|
|||||||
@@ -483,6 +483,11 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
|||||||
}
|
}
|
||||||
fi.DataDir = mustGetUUID()
|
fi.DataDir = mustGetUUID()
|
||||||
|
|
||||||
|
if userDefined[ReplicationSsecChecksumHeader] != "" {
|
||||||
|
fi.Checksum, _ = base64.StdEncoding.DecodeString(userDefined[ReplicationSsecChecksumHeader])
|
||||||
|
delete(userDefined, ReplicationSsecChecksumHeader)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize erasure metadata.
|
// Initialize erasure metadata.
|
||||||
for index := range partsMetadata {
|
for index := range partsMetadata {
|
||||||
partsMetadata[index] = fi
|
partsMetadata[index] = fi
|
||||||
@@ -1294,9 +1299,18 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
defer lk.Unlock(lkctx)
|
defer lk.Unlock(lkctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accept encrypted checksum from incoming request.
|
||||||
|
if opts.UserDefined[ReplicationSsecChecksumHeader] != "" {
|
||||||
|
if v, err := base64.StdEncoding.DecodeString(opts.UserDefined[ReplicationSsecChecksumHeader]); err == nil {
|
||||||
|
fi.Checksum = v
|
||||||
|
}
|
||||||
|
delete(opts.UserDefined, ReplicationSsecChecksumHeader)
|
||||||
|
}
|
||||||
|
|
||||||
if checksumType.IsSet() {
|
if checksumType.IsSet() {
|
||||||
checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
|
checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
|
||||||
cs := hash.NewChecksumFromData(checksumType, checksumCombined)
|
var cs *hash.Checksum
|
||||||
|
cs = hash.NewChecksumFromData(checksumType, checksumCombined)
|
||||||
fi.Checksum = cs.AppendTo(nil, checksumCombined)
|
fi.Checksum = cs.AppendTo(nil, checksumCombined)
|
||||||
if opts.EncryptFn != nil {
|
if opts.EncryptFn != nil {
|
||||||
fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
|
fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
|
||||||
|
|||||||
+31
-35
@@ -20,6 +20,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -276,7 +277,7 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
|
|||||||
return gr.WithCleanupFuncs(nsUnlocker), nil
|
return gr.WithCleanupFuncs(nsUnlocker), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fn, off, length, err := NewGetObjectReader(rs, objInfo, opts)
|
fn, off, length, err := NewGetObjectReader(rs, objInfo, opts, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -483,8 +484,8 @@ func joinErrs(errs []error) []string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object string, metaArr []FileInfo, errs []error, dataErrs []error, opts ObjectOptions) (FileInfo, error) {
|
func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object string, metaArr []FileInfo, errs []error, dataErrsByPart map[int][]int, opts ObjectOptions) (FileInfo, error) {
|
||||||
m, ok := isObjectDangling(metaArr, errs, dataErrs)
|
m, ok := isObjectDangling(metaArr, errs, dataErrsByPart)
|
||||||
if !ok {
|
if !ok {
|
||||||
// We only come here if we cannot figure out if the object
|
// We only come here if we cannot figure out if the object
|
||||||
// can be deleted safely, in such a scenario return ReadQuorum error.
|
// can be deleted safely, in such a scenario return ReadQuorum error.
|
||||||
@@ -494,7 +495,7 @@ func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object st
|
|||||||
tags["set"] = er.setIndex
|
tags["set"] = er.setIndex
|
||||||
tags["pool"] = er.poolIndex
|
tags["pool"] = er.poolIndex
|
||||||
tags["merrs"] = joinErrs(errs)
|
tags["merrs"] = joinErrs(errs)
|
||||||
tags["derrs"] = joinErrs(dataErrs)
|
tags["derrs"] = dataErrsByPart
|
||||||
if m.IsValid() {
|
if m.IsValid() {
|
||||||
tags["size"] = m.Size
|
tags["size"] = m.Size
|
||||||
tags["mtime"] = m.ModTime.Format(http.TimeFormat)
|
tags["mtime"] = m.ModTime.Format(http.TimeFormat)
|
||||||
@@ -508,8 +509,20 @@ func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object st
|
|||||||
|
|
||||||
// count the number of offline disks
|
// count the number of offline disks
|
||||||
offline := 0
|
offline := 0
|
||||||
for i := 0; i < max(len(errs), len(dataErrs)); i++ {
|
for i := 0; i < len(errs); i++ {
|
||||||
if i < len(errs) && errors.Is(errs[i], errDiskNotFound) || i < len(dataErrs) && errors.Is(dataErrs[i], errDiskNotFound) {
|
var found bool
|
||||||
|
switch {
|
||||||
|
case errors.Is(errs[i], errDiskNotFound):
|
||||||
|
found = true
|
||||||
|
default:
|
||||||
|
for p := range dataErrsByPart {
|
||||||
|
if dataErrsByPart[p][i] == checkPartDiskNotFound {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
offline++
|
offline++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -563,35 +576,11 @@ func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object st
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fileInfoFromRaw(ri RawFileInfo, bucket, object string, readData, inclFreeVers, allParts bool) (FileInfo, error) {
|
func fileInfoFromRaw(ri RawFileInfo, bucket, object string, readData, inclFreeVers, allParts bool) (FileInfo, error) {
|
||||||
var xl xlMetaV2
|
return getFileInfo(ri.Buf, bucket, object, "", fileInfoOpts{
|
||||||
if err := xl.LoadOrConvert(ri.Buf); err != nil {
|
Data: readData,
|
||||||
return FileInfo{}, errFileCorrupt
|
InclFreeVersions: inclFreeVers,
|
||||||
}
|
AllParts: allParts,
|
||||||
|
})
|
||||||
fi, err := xl.ToFileInfo(bucket, object, "", inclFreeVers, allParts)
|
|
||||||
if err != nil {
|
|
||||||
return FileInfo{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !fi.IsValid() {
|
|
||||||
return FileInfo{}, errFileCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
versionID := fi.VersionID
|
|
||||||
if versionID == "" {
|
|
||||||
versionID = nullVersionID
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInfo, err := xl.ToFileInfo(bucket, object, versionID, inclFreeVers, allParts)
|
|
||||||
if err != nil {
|
|
||||||
return FileInfo{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if readData {
|
|
||||||
fileInfo.Data = xl.data.find(versionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileInfo, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readAllRawFileInfo(ctx context.Context, disks []StorageAPI, bucket, object string, readData bool) ([]RawFileInfo, []error) {
|
func readAllRawFileInfo(ctx context.Context, disks []StorageAPI, bucket, object string, readData bool) ([]RawFileInfo, []error) {
|
||||||
@@ -744,6 +733,7 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
|||||||
|
|
||||||
ropts := ReadOptions{
|
ropts := ReadOptions{
|
||||||
ReadData: readData,
|
ReadData: readData,
|
||||||
|
InclFreeVersions: opts.InclFreeVersions,
|
||||||
Healing: false,
|
Healing: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1355,6 +1345,12 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
|||||||
if opts.EncryptFn != nil {
|
if opts.EncryptFn != nil {
|
||||||
fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
|
fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
|
||||||
}
|
}
|
||||||
|
if userDefined[ReplicationSsecChecksumHeader] != "" {
|
||||||
|
if v, err := base64.StdEncoding.DecodeString(userDefined[ReplicationSsecChecksumHeader]); err == nil {
|
||||||
|
fi.Checksum = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(userDefined, ReplicationSsecChecksumHeader)
|
||||||
uniqueID := mustGetUUID()
|
uniqueID := mustGetUUID()
|
||||||
tempObj := uniqueID
|
tempObj := uniqueID
|
||||||
|
|
||||||
|
|||||||
@@ -836,7 +836,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
if filterLifecycle(bi.Name, version.Name, version) {
|
if filterLifecycle(bi.Name, version.Name, version) {
|
||||||
expired++
|
expired++
|
||||||
decommissioned++
|
decommissioned++
|
||||||
stopFn(errors.New("ILM expired object/version will be skipped"))
|
stopFn(version.Size, errors.New("ILM expired object/version will be skipped"))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -846,7 +846,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
remainingVersions := len(fivs.Versions) - expired
|
remainingVersions := len(fivs.Versions) - expired
|
||||||
if version.Deleted && remainingVersions == 1 {
|
if version.Deleted && remainingVersions == 1 {
|
||||||
decommissioned++
|
decommissioned++
|
||||||
stopFn(errors.New("DELETE marked object with no other non-current versions will be skipped"))
|
stopFn(version.Size, errors.New("DELETE marked object with no other non-current versions will be skipped"))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -877,7 +877,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stopFn(err)
|
stopFn(version.Size, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
decomLogIf(ctx, err)
|
decomLogIf(ctx, err)
|
||||||
failure = true
|
failure = true
|
||||||
@@ -902,12 +902,12 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
MTime: version.ModTime,
|
MTime: version.ModTime,
|
||||||
UserDefined: version.Metadata,
|
UserDefined: version.Metadata,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
stopFn(err)
|
stopFn(version.Size, err)
|
||||||
failure = true
|
failure = true
|
||||||
decomLogIf(ctx, err)
|
decomLogIf(ctx, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
stopFn(nil)
|
stopFn(version.Size, nil)
|
||||||
failure = false
|
failure = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -925,14 +925,14 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||||
// object deleted by the application, nothing to do here we move on.
|
// object deleted by the application, nothing to do here we move on.
|
||||||
ignore = true
|
ignore = true
|
||||||
stopFn(nil)
|
stopFn(version.Size, nil)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil && !ignore {
|
if err != nil && !ignore {
|
||||||
// if usage-cache.bin is not readable log and ignore it.
|
// if usage-cache.bin is not readable log and ignore it.
|
||||||
if bi.Name == minioMetaBucket && strings.Contains(version.Name, dataUsageCacheName) {
|
if bi.Name == minioMetaBucket && strings.Contains(version.Name, dataUsageCacheName) {
|
||||||
ignore = true
|
ignore = true
|
||||||
stopFn(err)
|
stopFn(version.Size, err)
|
||||||
decomLogIf(ctx, err)
|
decomLogIf(ctx, err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -940,16 +940,16 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
failure = true
|
failure = true
|
||||||
decomLogIf(ctx, err)
|
decomLogIf(ctx, err)
|
||||||
stopFn(err)
|
stopFn(version.Size, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err = z.decommissionObject(ctx, bi.Name, gr); err != nil {
|
if err = z.decommissionObject(ctx, bi.Name, gr); err != nil {
|
||||||
stopFn(err)
|
stopFn(version.Size, err)
|
||||||
failure = true
|
failure = true
|
||||||
decomLogIf(ctx, err)
|
decomLogIf(ctx, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
stopFn(nil)
|
stopFn(version.Size, nil)
|
||||||
failure = false
|
failure = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -977,7 +977,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
NoAuditLog: true,
|
NoAuditLog: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
stopFn(err)
|
stopFn(0, err)
|
||||||
auditLogDecom(ctx, "DecomDeleteObject", bi.Name, entry.name, "", err)
|
auditLogDecom(ctx, "DecomDeleteObject", bi.Name, entry.name, "", err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
decomLogIf(ctx, err)
|
decomLogIf(ctx, err)
|
||||||
@@ -1038,7 +1038,7 @@ const (
|
|||||||
decomMetricDecommissionRemoveObject
|
decomMetricDecommissionRemoveObject
|
||||||
)
|
)
|
||||||
|
|
||||||
func decomTrace(d decomMetric, poolIdx int, startTime time.Time, duration time.Duration, path string, err error) madmin.TraceInfo {
|
func decomTrace(d decomMetric, poolIdx int, startTime time.Time, duration time.Duration, path string, err error, sz int64) madmin.TraceInfo {
|
||||||
var errStr string
|
var errStr string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr = err.Error()
|
errStr = err.Error()
|
||||||
@@ -1051,15 +1051,16 @@ func decomTrace(d decomMetric, poolIdx int, startTime time.Time, duration time.D
|
|||||||
Duration: duration,
|
Duration: duration,
|
||||||
Path: path,
|
Path: path,
|
||||||
Error: errStr,
|
Error: errStr,
|
||||||
|
Bytes: sz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *decomMetrics) log(d decomMetric, poolIdx int, paths ...string) func(err error) {
|
func (m *decomMetrics) log(d decomMetric, poolIdx int, paths ...string) func(z int64, err error) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
return func(err error) {
|
return func(sz int64, err error) {
|
||||||
duration := time.Since(startTime)
|
duration := time.Since(startTime)
|
||||||
if globalTrace.NumSubscribers(madmin.TraceDecommission) > 0 {
|
if globalTrace.NumSubscribers(madmin.TraceDecommission) > 0 {
|
||||||
globalTrace.Publish(decomTrace(d, poolIdx, startTime, duration, strings.Join(paths, " "), err))
|
globalTrace.Publish(decomTrace(d, poolIdx, startTime, duration, strings.Join(paths, " "), err, sz))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1092,10 +1093,10 @@ func (z *erasureServerPools) decommissionInBackground(ctx context.Context, idx i
|
|||||||
}
|
}
|
||||||
stopFn := globalDecommissionMetrics.log(decomMetricDecommissionBucket, idx, bucket.Name)
|
stopFn := globalDecommissionMetrics.log(decomMetricDecommissionBucket, idx, bucket.Name)
|
||||||
if err := z.decommissionPool(ctx, idx, pool, bucket); err != nil {
|
if err := z.decommissionPool(ctx, idx, pool, bucket); err != nil {
|
||||||
stopFn(err)
|
stopFn(0, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stopFn(nil)
|
stopFn(0, nil)
|
||||||
|
|
||||||
z.poolMetaMutex.Lock()
|
z.poolMetaMutex.Lock()
|
||||||
if z.poolMeta.BucketDone(idx, bucket) {
|
if z.poolMeta.BucketDone(idx, bucket) {
|
||||||
@@ -1326,11 +1327,7 @@ func (z *erasureServerPools) Status(ctx context.Context, idx int) (PoolStatus, e
|
|||||||
poolInfo := z.poolMeta.Pools[idx].Clone()
|
poolInfo := z.poolMeta.Pools[idx].Clone()
|
||||||
if poolInfo.Decommission != nil {
|
if poolInfo.Decommission != nil {
|
||||||
poolInfo.Decommission.TotalSize = pi.Total
|
poolInfo.Decommission.TotalSize = pi.Total
|
||||||
if poolInfo.Decommission.Failed || poolInfo.Decommission.Canceled {
|
|
||||||
poolInfo.Decommission.CurrentSize = pi.Free
|
poolInfo.Decommission.CurrentSize = pi.Free
|
||||||
} else {
|
|
||||||
poolInfo.Decommission.CurrentSize = poolInfo.Decommission.StartSize + poolInfo.Decommission.BytesDone
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
poolInfo.Decommission = &PoolDecommissionInfo{
|
poolInfo.Decommission = &PoolDecommissionInfo{
|
||||||
TotalSize: pi.Total,
|
TotalSize: pi.Total,
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ func (z *erasureServerPools) rebalanceBuckets(ctx context.Context, poolIdx int)
|
|||||||
|
|
||||||
stopFn := globalRebalanceMetrics.log(rebalanceMetricSaveMetadata, poolIdx, traceMsg)
|
stopFn := globalRebalanceMetrics.log(rebalanceMetricSaveMetadata, poolIdx, traceMsg)
|
||||||
err := z.saveRebalanceStats(GlobalContext, poolIdx, rebalSaveStats)
|
err := z.saveRebalanceStats(GlobalContext, poolIdx, rebalSaveStats)
|
||||||
stopFn(err)
|
stopFn(0, err)
|
||||||
rebalanceLogIf(GlobalContext, err)
|
rebalanceLogIf(GlobalContext, err)
|
||||||
|
|
||||||
if quit {
|
if quit {
|
||||||
@@ -456,7 +456,7 @@ func (z *erasureServerPools) rebalanceBuckets(ctx context.Context, poolIdx int)
|
|||||||
|
|
||||||
stopFn := globalRebalanceMetrics.log(rebalanceMetricRebalanceBucket, poolIdx, bucket)
|
stopFn := globalRebalanceMetrics.log(rebalanceMetricRebalanceBucket, poolIdx, bucket)
|
||||||
if err = z.rebalanceBucket(ctx, bucket, poolIdx); err != nil {
|
if err = z.rebalanceBucket(ctx, bucket, poolIdx); err != nil {
|
||||||
stopFn(err)
|
stopFn(0, err)
|
||||||
if errors.Is(err, errServerNotInitialized) || errors.Is(err, errBucketMetadataNotInitialized) {
|
if errors.Is(err, errServerNotInitialized) || errors.Is(err, errBucketMetadataNotInitialized) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -464,7 +464,7 @@ func (z *erasureServerPools) rebalanceBuckets(ctx context.Context, poolIdx int)
|
|||||||
doneCh <- err
|
doneCh <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stopFn(nil)
|
stopFn(0, nil)
|
||||||
z.bucketRebalanceDone(bucket, poolIdx)
|
z.bucketRebalanceDone(bucket, poolIdx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,24 +692,24 @@ func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string,
|
|||||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||||
// object deleted by the application, nothing to do here we move on.
|
// object deleted by the application, nothing to do here we move on.
|
||||||
ignore = true
|
ignore = true
|
||||||
stopFn(nil)
|
stopFn(0, nil)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
failure = true
|
failure = true
|
||||||
rebalanceLogIf(ctx, err)
|
rebalanceLogIf(ctx, err)
|
||||||
stopFn(err)
|
stopFn(0, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = z.rebalanceObject(ctx, bucket, gr); err != nil {
|
if err = z.rebalanceObject(ctx, bucket, gr); err != nil {
|
||||||
failure = true
|
failure = true
|
||||||
rebalanceLogIf(ctx, err)
|
rebalanceLogIf(ctx, err)
|
||||||
stopFn(err)
|
stopFn(version.Size, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
stopFn(nil)
|
stopFn(version.Size, nil)
|
||||||
failure = false
|
failure = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -735,7 +735,7 @@ func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string,
|
|||||||
NoAuditLog: true,
|
NoAuditLog: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
stopFn(err)
|
stopFn(0, err)
|
||||||
auditLogRebalance(ctx, "Rebalance:DeleteObject", bucket, entry.name, "", err)
|
auditLogRebalance(ctx, "Rebalance:DeleteObject", bucket, entry.name, "", err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rebalanceLogIf(ctx, err)
|
rebalanceLogIf(ctx, err)
|
||||||
@@ -935,7 +935,7 @@ func (z *erasureServerPools) StartRebalance() {
|
|||||||
go func(idx int) {
|
go func(idx int) {
|
||||||
stopfn := globalRebalanceMetrics.log(rebalanceMetricRebalanceBuckets, idx)
|
stopfn := globalRebalanceMetrics.log(rebalanceMetricRebalanceBuckets, idx)
|
||||||
err := z.rebalanceBuckets(ctx, idx)
|
err := z.rebalanceBuckets(ctx, idx)
|
||||||
stopfn(err)
|
stopfn(0, err)
|
||||||
}(poolIdx)
|
}(poolIdx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -975,7 +975,7 @@ const (
|
|||||||
rebalanceMetricSaveMetadata
|
rebalanceMetricSaveMetadata
|
||||||
)
|
)
|
||||||
|
|
||||||
func rebalanceTrace(r rebalanceMetric, poolIdx int, startTime time.Time, duration time.Duration, err error, path string) madmin.TraceInfo {
|
func rebalanceTrace(r rebalanceMetric, poolIdx int, startTime time.Time, duration time.Duration, err error, path string, sz int64) madmin.TraceInfo {
|
||||||
var errStr string
|
var errStr string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr = err.Error()
|
errStr = err.Error()
|
||||||
@@ -988,15 +988,16 @@ func rebalanceTrace(r rebalanceMetric, poolIdx int, startTime time.Time, duratio
|
|||||||
Duration: duration,
|
Duration: duration,
|
||||||
Path: path,
|
Path: path,
|
||||||
Error: errStr,
|
Error: errStr,
|
||||||
|
Bytes: sz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *rebalanceMetrics) log(r rebalanceMetric, poolIdx int, paths ...string) func(err error) {
|
func (p *rebalanceMetrics) log(r rebalanceMetric, poolIdx int, paths ...string) func(sz int64, err error) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
return func(err error) {
|
return func(sz int64, err error) {
|
||||||
duration := time.Since(startTime)
|
duration := time.Since(startTime)
|
||||||
if globalTrace.NumSubscribers(madmin.TraceRebalance) > 0 {
|
if globalTrace.NumSubscribers(madmin.TraceRebalance) > 0 {
|
||||||
globalTrace.Publish(rebalanceTrace(r, poolIdx, startTime, duration, err, strings.Join(paths, " ")))
|
globalTrace.Publish(rebalanceTrace(r, poolIdx, startTime, duration, err, strings.Join(paths, " "), sz))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-41
@@ -104,7 +104,11 @@ func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServ
|
|||||||
// Initialize byte pool once for all sets, bpool size is set to
|
// Initialize byte pool once for all sets, bpool size is set to
|
||||||
// setCount * setDriveCount with each memory upto blockSizeV2.
|
// setCount * setDriveCount with each memory upto blockSizeV2.
|
||||||
buffers := bpool.NewBytePoolCap(n, blockSizeV2, blockSizeV2*2)
|
buffers := bpool.NewBytePoolCap(n, blockSizeV2, blockSizeV2*2)
|
||||||
|
if n >= 16384 {
|
||||||
|
// pre-populate buffers only n >= 16384 which is (32Gi/2Mi)
|
||||||
|
// for all setups smaller than this avoid pre-alloc.
|
||||||
buffers.Populate()
|
buffers.Populate()
|
||||||
|
}
|
||||||
globalBytePoolCap.Store(buffers)
|
globalBytePoolCap.Store(buffers)
|
||||||
|
|
||||||
var localDrives []StorageAPI
|
var localDrives []StorageAPI
|
||||||
@@ -165,6 +169,9 @@ func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServ
|
|||||||
if !globalIsDistErasure {
|
if !globalIsDistErasure {
|
||||||
globalLocalDrivesMu.Lock()
|
globalLocalDrivesMu.Lock()
|
||||||
globalLocalDrives = localDrives
|
globalLocalDrives = localDrives
|
||||||
|
for _, drive := range localDrives {
|
||||||
|
globalLocalDrivesMap[drive.Endpoint().String()] = drive
|
||||||
|
}
|
||||||
globalLocalDrivesMu.Unlock()
|
globalLocalDrivesMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1535,7 +1542,7 @@ func (z *erasureServerPools) listObjectsGeneric(ctx context.Context, bucket, pre
|
|||||||
return loi, nil
|
return loi, nil
|
||||||
}
|
}
|
||||||
ri := logger.GetReqInfo(ctx)
|
ri := logger.GetReqInfo(ctx)
|
||||||
hadoop := ri != nil && strings.Contains(ri.UserAgent, `Hadoop `) && strings.Contains(ri.UserAgent, "scala/")
|
hadoop := ri != nil && strings.Contains(ri.UserAgent, "Hadoop ") && strings.Contains(ri.UserAgent, "scala/")
|
||||||
matches := func() bool {
|
matches := func() bool {
|
||||||
if prefix == "" {
|
if prefix == "" {
|
||||||
return false
|
return false
|
||||||
@@ -1600,7 +1607,7 @@ func (z *erasureServerPools) listObjectsGeneric(ctx context.Context, bucket, pre
|
|||||||
// }
|
// }
|
||||||
if matches() {
|
if matches() {
|
||||||
objInfo, err := z.GetObjectInfo(ctx, bucket, path.Dir(prefix), ObjectOptions{NoLock: true})
|
objInfo, err := z.GetObjectInfo(ctx, bucket, path.Dir(prefix), ObjectOptions{NoLock: true})
|
||||||
if err == nil {
|
if err == nil || objInfo.IsLatest && objInfo.DeleteMarker {
|
||||||
if opts.Lifecycle != nil {
|
if opts.Lifecycle != nil {
|
||||||
evt := evalActionFromLifecycle(ctx, *opts.Lifecycle, opts.Retention, opts.Replication.Config, objInfo)
|
evt := evalActionFromLifecycle(ctx, *opts.Lifecycle, opts.Retention, opts.Replication.Config, objInfo)
|
||||||
if evt.Action.Delete() {
|
if evt.Action.Delete() {
|
||||||
@@ -1626,41 +1633,29 @@ func (z *erasureServerPools) listObjectsGeneric(ctx context.Context, bucket, pre
|
|||||||
// and throw a 404 exception. This is especially a problem for spark jobs overwriting the same partition
|
// and throw a 404 exception. This is especially a problem for spark jobs overwriting the same partition
|
||||||
// repeatedly. This workaround recursively lists the top 3 entries including delete markers to reflect the
|
// repeatedly. This workaround recursively lists the top 3 entries including delete markers to reflect the
|
||||||
// correct state of the directory in the list results.
|
// correct state of the directory in the list results.
|
||||||
opts.Recursive = true
|
if strings.HasSuffix(opts.Prefix, SlashSeparator) {
|
||||||
opts.InclDeleted = true
|
li, err := listFn(ctx, opts, maxKeys)
|
||||||
opts.Limit = maxKeys + 1
|
if err != nil {
|
||||||
li, err := listFn(ctx, opts, opts.Limit)
|
return loi, err
|
||||||
if err == nil {
|
}
|
||||||
switch {
|
if len(li.Objects) == 0 {
|
||||||
case len(li.Objects) == 0 && len(li.Prefixes) == 0:
|
prefixes := li.Prefixes[:0]
|
||||||
|
for _, prefix := range li.Prefixes {
|
||||||
|
objInfo, _ := z.GetObjectInfo(ctx, bucket, pathJoin(prefix, "_SUCCESS"), ObjectOptions{NoLock: true})
|
||||||
|
if objInfo.IsLatest && objInfo.DeleteMarker {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prefixes = append(prefixes, prefix)
|
||||||
|
}
|
||||||
|
if len(prefixes) > 0 {
|
||||||
|
objInfo, _ := z.GetObjectInfo(ctx, bucket, pathJoin(opts.Prefix, "_SUCCESS"), ObjectOptions{NoLock: true})
|
||||||
|
if objInfo.IsLatest && objInfo.DeleteMarker {
|
||||||
return loi, nil
|
return loi, nil
|
||||||
case len(li.Objects) > 0 || len(li.Prefixes) > 0:
|
|
||||||
var o ObjectInfo
|
|
||||||
var pfx string
|
|
||||||
if len(li.Objects) > 0 {
|
|
||||||
o = li.Objects[0]
|
|
||||||
p := strings.TrimPrefix(o.Name, opts.Prefix)
|
|
||||||
if p != "" {
|
|
||||||
sidx := strings.Index(p, "/")
|
|
||||||
if sidx > 0 {
|
|
||||||
pfx = p[:sidx]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
li.Prefixes = prefixes
|
||||||
}
|
}
|
||||||
switch {
|
return li, nil
|
||||||
case o.DeleteMarker:
|
|
||||||
loi.Objects = append(loi.Objects, ObjectInfo{Bucket: bucket, IsDir: true, Name: prefix})
|
|
||||||
return loi, nil
|
|
||||||
case len(li.Objects) >= 1:
|
|
||||||
loi.Objects = append(loi.Objects, o)
|
|
||||||
if pfx != "" {
|
|
||||||
loi.Prefixes = append(loi.Prefixes, path.Join(opts.Prefix, pfx))
|
|
||||||
}
|
|
||||||
case len(li.Prefixes) > 0:
|
|
||||||
loi.Prefixes = append(loi.Prefixes, li.Prefixes...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return loi, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2013,12 +2008,14 @@ func (z *erasureServerPools) ListBuckets(ctx context.Context, opts BucketOptions
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if !opts.NoMetadata {
|
||||||
for i := range buckets {
|
for i := range buckets {
|
||||||
createdAt, err := globalBucketMetadataSys.CreatedAt(buckets[i].Name)
|
createdAt, err := globalBucketMetadataSys.CreatedAt(buckets[i].Name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
buckets[i].Created = createdAt
|
buckets[i].Created = createdAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return buckets, nil
|
return buckets, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -2030,12 +2027,15 @@ func (z *erasureServerPools) ListBuckets(ctx context.Context, opts BucketOptions
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !opts.NoMetadata {
|
||||||
for i := range buckets {
|
for i := range buckets {
|
||||||
createdAt, err := globalBucketMetadataSys.CreatedAt(buckets[i].Name)
|
createdAt, err := globalBucketMetadataSys.CreatedAt(buckets[i].Name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
buckets[i].Created = createdAt
|
buckets[i].Created = createdAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return buckets, nil
|
return buckets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2087,7 +2087,6 @@ func (z *erasureServerPools) HealBucket(ctx context.Context, bucket string, opts
|
|||||||
|
|
||||||
// Walk a bucket, optionally prefix recursively, until we have returned
|
// Walk a bucket, optionally prefix recursively, until we have returned
|
||||||
// all the contents of the provided bucket+prefix.
|
// all the contents of the provided bucket+prefix.
|
||||||
// TODO: Note that most errors will result in a truncated listing.
|
|
||||||
func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, results chan<- itemOrErr[ObjectInfo], opts WalkOptions) error {
|
func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, results chan<- itemOrErr[ObjectInfo], opts WalkOptions) error {
|
||||||
if err := checkListObjsArgs(ctx, bucket, prefix, ""); err != nil {
|
if err := checkListObjsArgs(ctx, bucket, prefix, ""); err != nil {
|
||||||
xioutil.SafeClose(results)
|
xioutil.SafeClose(results)
|
||||||
@@ -2204,9 +2203,8 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
|
|||||||
// Convert and filter merged entries.
|
// Convert and filter merged entries.
|
||||||
merged := make(chan metaCacheEntry, 100)
|
merged := make(chan metaCacheEntry, 100)
|
||||||
vcfg, _ := globalBucketVersioningSys.Get(bucket)
|
vcfg, _ := globalBucketVersioningSys.Get(bucket)
|
||||||
|
errCh := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
defer cancelCause(nil)
|
|
||||||
defer xioutil.SafeClose(results)
|
|
||||||
sentErr := false
|
sentErr := false
|
||||||
sendErr := func(err error) {
|
sendErr := func(err error) {
|
||||||
if !sentErr {
|
if !sentErr {
|
||||||
@@ -2217,6 +2215,15 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
sendErr(ctx.Err())
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
xioutil.SafeClose(results)
|
||||||
|
cancelCause(nil)
|
||||||
|
}()
|
||||||
send := func(oi ObjectInfo) bool {
|
send := func(oi ObjectInfo) bool {
|
||||||
select {
|
select {
|
||||||
case results <- itemOrErr[ObjectInfo]{Item: oi}:
|
case results <- itemOrErr[ObjectInfo]{Item: oi}:
|
||||||
@@ -2271,12 +2278,16 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := <-errCh; err != nil {
|
||||||
|
sendErr(err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
|
defer close(errCh)
|
||||||
// Merge all entries from all disks.
|
// Merge all entries from all disks.
|
||||||
// We leave quorum at 1, since entries are already resolved to have the desired quorum.
|
// We leave quorum at 1, since entries are already resolved to have the desired quorum.
|
||||||
// mergeEntryChannels will close 'merged' channel upon completion or cancellation.
|
// mergeEntryChannels will close 'merged' channel upon completion or cancellation.
|
||||||
storageLogIf(ctx, mergeEntryChannels(ctx, entries, merged, 1))
|
errCh <- mergeEntryChannels(ctx, entries, merged, 1)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -2454,6 +2465,24 @@ type HealthResult struct {
|
|||||||
UsingDefaults bool
|
UsingDefaults bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hr HealthResult) String() string {
|
||||||
|
var str strings.Builder
|
||||||
|
for i, es := range hr.ESHealth {
|
||||||
|
str.WriteString("(Pool: ")
|
||||||
|
str.WriteString(strconv.Itoa(es.PoolID))
|
||||||
|
str.WriteString(" Set: ")
|
||||||
|
str.WriteString(strconv.Itoa(es.SetID))
|
||||||
|
str.WriteString(" Healthy: ")
|
||||||
|
str.WriteString(strconv.FormatBool(es.Healthy))
|
||||||
|
if i == 0 {
|
||||||
|
str.WriteString(")")
|
||||||
|
} else {
|
||||||
|
str.WriteString("), ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str.String()
|
||||||
|
}
|
||||||
|
|
||||||
// Health - returns current status of the object layer health,
|
// Health - returns current status of the object layer health,
|
||||||
// provides if write access exists across sets, additionally
|
// provides if write access exists across sets, additionally
|
||||||
// can be used to query scenarios if health may be lost
|
// can be used to query scenarios if health may be lost
|
||||||
@@ -2573,16 +2602,16 @@ func (z *erasureServerPools) Health(ctx context.Context, opts HealthOptions) Hea
|
|||||||
healthy := erasureSetUpCount[poolIdx][setIdx].online >= poolWriteQuorums[poolIdx]
|
healthy := erasureSetUpCount[poolIdx][setIdx].online >= poolWriteQuorums[poolIdx]
|
||||||
if !healthy {
|
if !healthy {
|
||||||
storageLogIf(logger.SetReqInfo(ctx, reqInfo),
|
storageLogIf(logger.SetReqInfo(ctx, reqInfo),
|
||||||
fmt.Errorf("Write quorum may be lost on pool: %d, set: %d, expected write quorum: %d",
|
fmt.Errorf("Write quorum could not be established on pool: %d, set: %d, expected write quorum: %d, drives-online: %d",
|
||||||
poolIdx, setIdx, poolWriteQuorums[poolIdx]), logger.FatalKind)
|
poolIdx, setIdx, poolWriteQuorums[poolIdx], erasureSetUpCount[poolIdx][setIdx].online), logger.FatalKind)
|
||||||
}
|
}
|
||||||
result.Healthy = result.Healthy && healthy
|
result.Healthy = result.Healthy && healthy
|
||||||
|
|
||||||
healthyRead := erasureSetUpCount[poolIdx][setIdx].online >= poolReadQuorums[poolIdx]
|
healthyRead := erasureSetUpCount[poolIdx][setIdx].online >= poolReadQuorums[poolIdx]
|
||||||
if !healthyRead {
|
if !healthyRead {
|
||||||
storageLogIf(logger.SetReqInfo(ctx, reqInfo),
|
storageLogIf(logger.SetReqInfo(ctx, reqInfo),
|
||||||
fmt.Errorf("Read quorum may be lost on pool: %d, set: %d, expected read quorum: %d",
|
fmt.Errorf("Read quorum could not be established on pool: %d, set: %d, expected read quorum: %d, drives-online: %d",
|
||||||
poolIdx, setIdx, poolReadQuorums[poolIdx]))
|
poolIdx, setIdx, poolReadQuorums[poolIdx], erasureSetUpCount[poolIdx][setIdx].online))
|
||||||
}
|
}
|
||||||
result.HealthyRead = result.HealthyRead && healthyRead
|
result.HealthyRead = result.HealthyRead && healthyRead
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import (
|
|||||||
"github.com/minio/minio-go/v7/pkg/set"
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
"github.com/minio/minio-go/v7/pkg/tags"
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
"github.com/minio/minio/internal/dsync"
|
"github.com/minio/minio/internal/dsync"
|
||||||
xioutil "github.com/minio/minio/internal/ioutil"
|
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/v3/console"
|
"github.com/minio/pkg/v3/console"
|
||||||
"github.com/minio/pkg/v3/sync/errgroup"
|
"github.com/minio/pkg/v3/sync/errgroup"
|
||||||
@@ -80,10 +79,6 @@ type erasureSets struct {
|
|||||||
|
|
||||||
poolIndex int
|
poolIndex int
|
||||||
|
|
||||||
// A channel to send the set index to the MRF when
|
|
||||||
// any disk belonging to that set is connected
|
|
||||||
setReconnectEvent chan int
|
|
||||||
|
|
||||||
// Distribution algorithm of choice.
|
// Distribution algorithm of choice.
|
||||||
distributionAlgo string
|
distributionAlgo string
|
||||||
deploymentID [16]byte
|
deploymentID [16]byte
|
||||||
@@ -380,7 +375,6 @@ func newErasureSets(ctx context.Context, endpoints PoolEndpoints, storageDisks [
|
|||||||
setDriveCount: setDriveCount,
|
setDriveCount: setDriveCount,
|
||||||
defaultParityCount: defaultParityCount,
|
defaultParityCount: defaultParityCount,
|
||||||
format: format,
|
format: format,
|
||||||
setReconnectEvent: make(chan int),
|
|
||||||
distributionAlgo: format.Erasure.DistributionAlgo,
|
distributionAlgo: format.Erasure.DistributionAlgo,
|
||||||
deploymentID: uuid.MustParse(format.ID),
|
deploymentID: uuid.MustParse(format.ID),
|
||||||
poolIndex: poolIdx,
|
poolIndex: poolIdx,
|
||||||
@@ -662,14 +656,6 @@ func (s *erasureSets) Shutdown(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
select {
|
|
||||||
case _, ok := <-s.setReconnectEvent:
|
|
||||||
if ok {
|
|
||||||
xioutil.SafeClose(s.setReconnectEvent)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
xioutil.SafeClose(s.setReconnectEvent)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,23 +49,18 @@ func NewEventNotifier(ctx context.Context) *EventNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetARNList - returns available ARNs.
|
// GetARNList - returns available ARNs.
|
||||||
func (evnot *EventNotifier) GetARNList(onlyActive bool) []string {
|
func (evnot *EventNotifier) GetARNList() []string {
|
||||||
arns := []string{}
|
arns := []string{}
|
||||||
if evnot == nil {
|
if evnot == nil {
|
||||||
return arns
|
return arns
|
||||||
}
|
}
|
||||||
region := globalSite.Region()
|
region := globalSite.Region()
|
||||||
for targetID, target := range evnot.targetList.TargetMap() {
|
for targetID := range evnot.targetList.TargetMap() {
|
||||||
// httpclient target is part of ListenNotification
|
// httpclient target is part of ListenNotification
|
||||||
// which doesn't need to be listed as part of the ARN list
|
// which doesn't need to be listed as part of the ARN list
|
||||||
// This list is only meant for external targets, filter
|
// This list is only meant for external targets, filter
|
||||||
// this out pro-actively.
|
// this out pro-actively.
|
||||||
if !strings.HasPrefix(targetID.ID, "httpclient+") {
|
if !strings.HasPrefix(targetID.ID, "httpclient+") {
|
||||||
if onlyActive {
|
|
||||||
if _, err := target.IsActive(); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
arns = append(arns, targetID.ToARN(region).String())
|
arns = append(arns, targetID.ToARN(region).String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +69,7 @@ func (evnot *EventNotifier) GetARNList(onlyActive bool) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loads notification policies for all buckets into EventNotifier.
|
// Loads notification policies for all buckets into EventNotifier.
|
||||||
func (evnot *EventNotifier) set(bucket BucketInfo, meta BucketMetadata) {
|
func (evnot *EventNotifier) set(bucket string, meta BucketMetadata) {
|
||||||
config := meta.notificationConfig
|
config := meta.notificationConfig
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return
|
return
|
||||||
@@ -86,7 +81,7 @@ func (evnot *EventNotifier) set(bucket BucketInfo, meta BucketMetadata) {
|
|||||||
internalLogIf(GlobalContext, err)
|
internalLogIf(GlobalContext, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
evnot.AddRulesMap(bucket.Name, config.ToRulesMap())
|
evnot.AddRulesMap(bucket, config.ToRulesMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Targets returns all the registered targets
|
// Targets returns all the registered targets
|
||||||
|
|||||||
+26
-17
@@ -105,7 +105,7 @@ type ftpMetrics struct{}
|
|||||||
|
|
||||||
var globalFtpMetrics ftpMetrics
|
var globalFtpMetrics ftpMetrics
|
||||||
|
|
||||||
func ftpTrace(s *ftp.Context, startTime time.Time, source, objPath string, err error) madmin.TraceInfo {
|
func ftpTrace(s *ftp.Context, startTime time.Time, source, objPath string, err error, sz int64) madmin.TraceInfo {
|
||||||
var errStr string
|
var errStr string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr = err.Error()
|
errStr = err.Error()
|
||||||
@@ -114,25 +114,33 @@ func ftpTrace(s *ftp.Context, startTime time.Time, source, objPath string, err e
|
|||||||
TraceType: madmin.TraceFTP,
|
TraceType: madmin.TraceFTP,
|
||||||
Time: startTime,
|
Time: startTime,
|
||||||
NodeName: globalLocalNodeName,
|
NodeName: globalLocalNodeName,
|
||||||
FuncName: fmt.Sprintf("ftp USER=%s COMMAND=%s PARAM=%s ISLOGIN=%t, Source=%s", s.Sess.LoginUser(), s.Cmd, s.Param, s.Sess.IsLogin(), source),
|
FuncName: fmt.Sprintf(s.Cmd),
|
||||||
Duration: time.Since(startTime),
|
Duration: time.Since(startTime),
|
||||||
Path: objPath,
|
Path: objPath,
|
||||||
Error: errStr,
|
Error: errStr,
|
||||||
|
Bytes: sz,
|
||||||
|
Custom: map[string]string{
|
||||||
|
"user": s.Sess.LoginUser(),
|
||||||
|
"cmd": s.Cmd,
|
||||||
|
"param": s.Param,
|
||||||
|
"login": fmt.Sprintf("%t", s.Sess.IsLogin()),
|
||||||
|
"source": source,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ftpMetrics) log(s *ftp.Context, paths ...string) func(err error) {
|
func (m *ftpMetrics) log(s *ftp.Context, paths ...string) func(sz int64, err error) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
source := getSource(2)
|
source := getSource(2)
|
||||||
return func(err error) {
|
return func(sz int64, err error) {
|
||||||
globalTrace.Publish(ftpTrace(s, startTime, source, strings.Join(paths, " "), err))
|
globalTrace.Publish(ftpTrace(s, startTime, source, strings.Join(paths, " "), err, sz))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat implements ftpDriver
|
// Stat implements ftpDriver
|
||||||
func (driver *ftpDriver) Stat(ctx *ftp.Context, objPath string) (fi os.FileInfo, err error) {
|
func (driver *ftpDriver) Stat(ctx *ftp.Context, objPath string) (fi os.FileInfo, err error) {
|
||||||
stopFn := globalFtpMetrics.log(ctx, objPath)
|
stopFn := globalFtpMetrics.log(ctx, objPath)
|
||||||
defer stopFn(err)
|
defer stopFn(0, err)
|
||||||
|
|
||||||
if objPath == SlashSeparator {
|
if objPath == SlashSeparator {
|
||||||
return &minioFileInfo{
|
return &minioFileInfo{
|
||||||
@@ -190,7 +198,7 @@ func (driver *ftpDriver) Stat(ctx *ftp.Context, objPath string) (fi os.FileInfo,
|
|||||||
// ListDir implements ftpDriver
|
// ListDir implements ftpDriver
|
||||||
func (driver *ftpDriver) ListDir(ctx *ftp.Context, objPath string, callback func(os.FileInfo) error) (err error) {
|
func (driver *ftpDriver) ListDir(ctx *ftp.Context, objPath string, callback func(os.FileInfo) error) (err error) {
|
||||||
stopFn := globalFtpMetrics.log(ctx, objPath)
|
stopFn := globalFtpMetrics.log(ctx, objPath)
|
||||||
defer stopFn(err)
|
defer stopFn(0, err)
|
||||||
|
|
||||||
clnt, err := driver.getMinIOClient(ctx)
|
clnt, err := driver.getMinIOClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -252,7 +260,7 @@ func (driver *ftpDriver) ListDir(ctx *ftp.Context, objPath string, callback func
|
|||||||
|
|
||||||
func (driver *ftpDriver) CheckPasswd(c *ftp.Context, username, password string) (ok bool, err error) {
|
func (driver *ftpDriver) CheckPasswd(c *ftp.Context, username, password string) (ok bool, err error) {
|
||||||
stopFn := globalFtpMetrics.log(c, username)
|
stopFn := globalFtpMetrics.log(c, username)
|
||||||
defer stopFn(err)
|
defer stopFn(0, err)
|
||||||
|
|
||||||
if globalIAMSys.LDAPConfig.Enabled() {
|
if globalIAMSys.LDAPConfig.Enabled() {
|
||||||
sa, _, err := globalIAMSys.getServiceAccount(context.Background(), username)
|
sa, _, err := globalIAMSys.getServiceAccount(context.Background(), username)
|
||||||
@@ -376,7 +384,7 @@ func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error)
|
|||||||
// DeleteDir implements ftpDriver
|
// DeleteDir implements ftpDriver
|
||||||
func (driver *ftpDriver) DeleteDir(ctx *ftp.Context, objPath string) (err error) {
|
func (driver *ftpDriver) DeleteDir(ctx *ftp.Context, objPath string) (err error) {
|
||||||
stopFn := globalFtpMetrics.log(ctx, objPath)
|
stopFn := globalFtpMetrics.log(ctx, objPath)
|
||||||
defer stopFn(err)
|
defer stopFn(0, err)
|
||||||
|
|
||||||
bucket, prefix := path2BucketObject(objPath)
|
bucket, prefix := path2BucketObject(objPath)
|
||||||
if bucket == "" {
|
if bucket == "" {
|
||||||
@@ -426,7 +434,7 @@ func (driver *ftpDriver) DeleteDir(ctx *ftp.Context, objPath string) (err error)
|
|||||||
// DeleteFile implements ftpDriver
|
// DeleteFile implements ftpDriver
|
||||||
func (driver *ftpDriver) DeleteFile(ctx *ftp.Context, objPath string) (err error) {
|
func (driver *ftpDriver) DeleteFile(ctx *ftp.Context, objPath string) (err error) {
|
||||||
stopFn := globalFtpMetrics.log(ctx, objPath)
|
stopFn := globalFtpMetrics.log(ctx, objPath)
|
||||||
defer stopFn(err)
|
defer stopFn(0, err)
|
||||||
|
|
||||||
bucket, object := path2BucketObject(objPath)
|
bucket, object := path2BucketObject(objPath)
|
||||||
if bucket == "" {
|
if bucket == "" {
|
||||||
@@ -444,7 +452,7 @@ func (driver *ftpDriver) DeleteFile(ctx *ftp.Context, objPath string) (err error
|
|||||||
// Rename implements ftpDriver
|
// Rename implements ftpDriver
|
||||||
func (driver *ftpDriver) Rename(ctx *ftp.Context, fromObjPath string, toObjPath string) (err error) {
|
func (driver *ftpDriver) Rename(ctx *ftp.Context, fromObjPath string, toObjPath string) (err error) {
|
||||||
stopFn := globalFtpMetrics.log(ctx, fromObjPath, toObjPath)
|
stopFn := globalFtpMetrics.log(ctx, fromObjPath, toObjPath)
|
||||||
defer stopFn(err)
|
defer stopFn(0, err)
|
||||||
|
|
||||||
return NotImplemented{}
|
return NotImplemented{}
|
||||||
}
|
}
|
||||||
@@ -452,7 +460,7 @@ func (driver *ftpDriver) Rename(ctx *ftp.Context, fromObjPath string, toObjPath
|
|||||||
// MakeDir implements ftpDriver
|
// MakeDir implements ftpDriver
|
||||||
func (driver *ftpDriver) MakeDir(ctx *ftp.Context, objPath string) (err error) {
|
func (driver *ftpDriver) MakeDir(ctx *ftp.Context, objPath string) (err error) {
|
||||||
stopFn := globalFtpMetrics.log(ctx, objPath)
|
stopFn := globalFtpMetrics.log(ctx, objPath)
|
||||||
defer stopFn(err)
|
defer stopFn(0, err)
|
||||||
|
|
||||||
bucket, prefix := path2BucketObject(objPath)
|
bucket, prefix := path2BucketObject(objPath)
|
||||||
if bucket == "" {
|
if bucket == "" {
|
||||||
@@ -479,7 +487,7 @@ func (driver *ftpDriver) MakeDir(ctx *ftp.Context, objPath string) (err error) {
|
|||||||
// GetFile implements ftpDriver
|
// GetFile implements ftpDriver
|
||||||
func (driver *ftpDriver) GetFile(ctx *ftp.Context, objPath string, offset int64) (n int64, rc io.ReadCloser, err error) {
|
func (driver *ftpDriver) GetFile(ctx *ftp.Context, objPath string, offset int64) (n int64, rc io.ReadCloser, err error) {
|
||||||
stopFn := globalFtpMetrics.log(ctx, objPath)
|
stopFn := globalFtpMetrics.log(ctx, objPath)
|
||||||
defer stopFn(err)
|
defer stopFn(n, err)
|
||||||
|
|
||||||
bucket, object := path2BucketObject(objPath)
|
bucket, object := path2BucketObject(objPath)
|
||||||
if bucket == "" {
|
if bucket == "" {
|
||||||
@@ -511,14 +519,14 @@ func (driver *ftpDriver) GetFile(ctx *ftp.Context, objPath string, offset int64)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
n = info.Size - offset
|
||||||
return info.Size - offset, obj, nil
|
return n, obj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutFile implements ftpDriver
|
// PutFile implements ftpDriver
|
||||||
func (driver *ftpDriver) PutFile(ctx *ftp.Context, objPath string, data io.Reader, offset int64) (n int64, err error) {
|
func (driver *ftpDriver) PutFile(ctx *ftp.Context, objPath string, data io.Reader, offset int64) (n int64, err error) {
|
||||||
stopFn := globalFtpMetrics.log(ctx, objPath)
|
stopFn := globalFtpMetrics.log(ctx, objPath)
|
||||||
defer stopFn(err)
|
defer stopFn(n, err)
|
||||||
|
|
||||||
bucket, object := path2BucketObject(objPath)
|
bucket, object := path2BucketObject(objPath)
|
||||||
if bucket == "" {
|
if bucket == "" {
|
||||||
@@ -539,5 +547,6 @@ func (driver *ftpDriver) PutFile(ctx *ftp.Context, objPath string, data io.Reade
|
|||||||
ContentType: mimedb.TypeByExtension(path.Ext(object)),
|
ContentType: mimedb.TypeByExtension(path.Ext(object)),
|
||||||
DisableContentSha256: true,
|
DisableContentSha256: true,
|
||||||
})
|
})
|
||||||
return info.Size, err
|
n = info.Size
|
||||||
|
return n, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,13 +601,6 @@ func setUploadForwardingMiddleware(h http.Handler) http.Handler {
|
|||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// forward request to peer handling this upload
|
|
||||||
if globalBucketTargetSys.isOffline(remote.EndpointURL) {
|
|
||||||
defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
|
|
||||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrReplicationRemoteConnectionError), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r.URL.Scheme = remote.EndpointURL.Scheme
|
r.URL.Scheme = remote.EndpointURL.Scheme
|
||||||
r.URL.Host = remote.EndpointURL.Host
|
r.URL.Host = remote.EndpointURL.Host
|
||||||
// Make sure we remove any existing headers before
|
// Make sure we remove any existing headers before
|
||||||
|
|||||||
+15
-2
@@ -163,7 +163,10 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, bucket := range healBuckets {
|
for _, bucket := range healBuckets {
|
||||||
_, err := objAPI.HealBucket(ctx, bucket, madmin.HealOpts{ScanMode: scanMode})
|
_, err := objAPI.HealBucket(ctx, bucket, madmin.HealOpts{
|
||||||
|
Recreate: true,
|
||||||
|
ScanMode: scanMode,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log bucket healing error if any, we shall retry again.
|
// Log bucket healing error if any, we shall retry again.
|
||||||
healingLogIf(ctx, err)
|
healingLogIf(ctx, err)
|
||||||
@@ -262,6 +265,7 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
// Heal current bucket again in case if it is failed
|
// Heal current bucket again in case if it is failed
|
||||||
// in the beginning of erasure set healing
|
// in the beginning of erasure set healing
|
||||||
if _, err := objAPI.HealBucket(ctx, bucket, madmin.HealOpts{
|
if _, err := objAPI.HealBucket(ctx, bucket, madmin.HealOpts{
|
||||||
|
Recreate: true,
|
||||||
ScanMode: scanMode,
|
ScanMode: scanMode,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
// Set this such that when we return this function
|
// Set this such that when we return this function
|
||||||
@@ -513,7 +517,7 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
// we let the caller retry this disk again for the
|
// we let the caller retry this disk again for the
|
||||||
// buckets it failed to list.
|
// buckets it failed to list.
|
||||||
retErr = err
|
retErr = err
|
||||||
healingLogIf(ctx, err)
|
healingLogIf(ctx, fmt.Errorf("listing failed with: %v on bucket: %v", err, bucket))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,9 +534,18 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
tracker.setObject("")
|
tracker.setObject("")
|
||||||
tracker.setBucket("")
|
tracker.setBucket("")
|
||||||
|
|
||||||
|
if retErr != nil {
|
||||||
return retErr
|
return retErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Last sanity check
|
||||||
|
if len(tracker.QueuedBuckets) > 0 {
|
||||||
|
return fmt.Errorf("not all buckets were healed: %v", tracker.QueuedBuckets)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func healBucket(bucket string, scan madmin.HealScanMode) error {
|
func healBucket(bucket string, scan madmin.HealScanMode) error {
|
||||||
// Get background heal sequence to send elements to heal
|
// Get background heal sequence to send elements to heal
|
||||||
bgSeq, ok := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
|
bgSeq, ok := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
|
||||||
|
|||||||
+2
-1
@@ -160,8 +160,9 @@ type serverCtxt struct {
|
|||||||
FTP []string
|
FTP []string
|
||||||
SFTP []string
|
SFTP []string
|
||||||
|
|
||||||
|
MemLimit uint64
|
||||||
|
|
||||||
UserTimeout time.Duration
|
UserTimeout time.Duration
|
||||||
ShutdownTimeout time.Duration
|
|
||||||
IdleTimeout time.Duration
|
IdleTimeout time.Duration
|
||||||
ReadHeaderTimeout time.Duration
|
ReadHeaderTimeout time.Duration
|
||||||
MaxIdleConnsPerHost int
|
MaxIdleConnsPerHost int
|
||||||
|
|||||||
+12
-8
@@ -91,11 +91,11 @@ func availableMemory() (available uint64) {
|
|||||||
available = 2048 * blockSizeV2 * 2 // Default to 4 GiB when we can't find the limits.
|
available = 2048 * blockSizeV2 * 2 // Default to 4 GiB when we can't find the limits.
|
||||||
|
|
||||||
if runtime.GOOS == "linux" {
|
if runtime.GOOS == "linux" {
|
||||||
// Useful in container mode
|
// Honor cgroup limits if set.
|
||||||
limit := cgroupMemLimit()
|
limit := cgroupMemLimit()
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
// A valid value is found, return its 75%
|
// A valid value is found, return its 90%
|
||||||
available = (limit * 3) / 4
|
available = (limit * 9) / 10
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} // for all other platforms limits are based on virtual memory.
|
} // for all other platforms limits are based on virtual memory.
|
||||||
@@ -104,8 +104,9 @@ func availableMemory() (available uint64) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// A valid value is available return its 75%
|
|
||||||
available = (memStats.Available * 3) / 4
|
// A valid value is available return its 90%
|
||||||
|
available = (memStats.Available * 9) / 10
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int, legacy bool) {
|
|||||||
maxSetDrives := slices.Max(setDriveCounts)
|
maxSetDrives := slices.Max(setDriveCounts)
|
||||||
|
|
||||||
// Returns 75% of max memory allowed
|
// Returns 75% of max memory allowed
|
||||||
maxMem := availableMemory()
|
maxMem := globalServerCtxt.MemLimit
|
||||||
|
|
||||||
// max requests per node is calculated as
|
// max requests per node is calculated as
|
||||||
// total_ram / ram_per_request
|
// total_ram / ram_per_request
|
||||||
@@ -322,10 +323,9 @@ func maxClients(f http.HandlerFunc) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
globalHTTPStats.addRequestsInQueue(1)
|
globalHTTPStats.addRequestsInQueue(1)
|
||||||
defer globalHTTPStats.addRequestsInQueue(-1)
|
|
||||||
|
|
||||||
pool, deadline := globalAPIConfig.getRequestsPool()
|
pool, deadline := globalAPIConfig.getRequestsPool()
|
||||||
if pool == nil {
|
if pool == nil {
|
||||||
|
globalHTTPStats.addRequestsInQueue(-1)
|
||||||
f.ServeHTTP(w, r)
|
f.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -333,6 +333,7 @@ func maxClients(f http.HandlerFunc) http.HandlerFunc {
|
|||||||
// No deadline to wait, there is nothing to queue
|
// No deadline to wait, there is nothing to queue
|
||||||
// perform the API call immediately.
|
// perform the API call immediately.
|
||||||
if deadline <= 0 {
|
if deadline <= 0 {
|
||||||
|
globalHTTPStats.addRequestsInQueue(-1)
|
||||||
f.ServeHTTP(w, r)
|
f.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -348,12 +349,14 @@ func maxClients(f http.HandlerFunc) http.HandlerFunc {
|
|||||||
select {
|
select {
|
||||||
case pool <- struct{}{}:
|
case pool <- struct{}{}:
|
||||||
defer func() { <-pool }()
|
defer func() { <-pool }()
|
||||||
|
globalHTTPStats.addRequestsInQueue(-1)
|
||||||
if contextCanceled(ctx) {
|
if contextCanceled(ctx) {
|
||||||
w.WriteHeader(499)
|
w.WriteHeader(499)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.ServeHTTP(w, r)
|
f.ServeHTTP(w, r)
|
||||||
case <-deadlineTimer.C:
|
case <-deadlineTimer.C:
|
||||||
|
globalHTTPStats.addRequestsInQueue(-1)
|
||||||
if contextCanceled(ctx) {
|
if contextCanceled(ctx) {
|
||||||
w.WriteHeader(499)
|
w.WriteHeader(499)
|
||||||
return
|
return
|
||||||
@@ -363,6 +366,7 @@ func maxClients(f http.HandlerFunc) http.HandlerFunc {
|
|||||||
errorCodes.ToAPIErr(ErrTooManyRequests),
|
errorCodes.ToAPIErr(ErrTooManyRequests),
|
||||||
r.URL)
|
r.URL)
|
||||||
case <-r.Context().Done():
|
case <-r.Context().Done():
|
||||||
|
globalHTTPStats.addRequestsInQueue(-1)
|
||||||
// When the client disconnects before getting the S3 handler
|
// When the client disconnects before getting the S3 handler
|
||||||
// status code response, set the status code to 499 so this request
|
// status code response, set the status code to 499 so this request
|
||||||
// will be properly audited and traced.
|
// will be properly audited and traced.
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ import (
|
|||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/minio/internal/mcontext"
|
"github.com/minio/minio/internal/mcontext"
|
||||||
xnet "github.com/minio/pkg/v3/net"
|
xnet "github.com/minio/pkg/v3/net"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -90,6 +88,7 @@ var supportedHeaders = []string{
|
|||||||
"X-Minio-Replication-Server-Side-Encryption-Iv",
|
"X-Minio-Replication-Server-Side-Encryption-Iv",
|
||||||
"X-Minio-Replication-Encrypted-Multipart",
|
"X-Minio-Replication-Encrypted-Multipart",
|
||||||
"X-Minio-Replication-Actual-Object-Size",
|
"X-Minio-Replication-Actual-Object-Size",
|
||||||
|
ReplicationSsecChecksumHeader,
|
||||||
// Add more supported headers here.
|
// Add more supported headers here.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +109,7 @@ var replicationToInternalHeaders = map[string]string{
|
|||||||
"X-Minio-Replication-Server-Side-Encryption-Iv": "X-Minio-Internal-Server-Side-Encryption-Iv",
|
"X-Minio-Replication-Server-Side-Encryption-Iv": "X-Minio-Internal-Server-Side-Encryption-Iv",
|
||||||
"X-Minio-Replication-Encrypted-Multipart": "X-Minio-Internal-Encrypted-Multipart",
|
"X-Minio-Replication-Encrypted-Multipart": "X-Minio-Internal-Encrypted-Multipart",
|
||||||
"X-Minio-Replication-Actual-Object-Size": "X-Minio-Internal-Actual-Object-Size",
|
"X-Minio-Replication-Actual-Object-Size": "X-Minio-Internal-Actual-Object-Size",
|
||||||
|
ReplicationSsecChecksumHeader: ReplicationSsecChecksumHeader,
|
||||||
// Add more supported headers here.
|
// Add more supported headers here.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,8 +206,8 @@ func extractMetadataFromMime(ctx context.Context, v textproto.MIMEHeader, m map[
|
|||||||
for _, supportedHeader := range supportedHeaders {
|
for _, supportedHeader := range supportedHeaders {
|
||||||
value, ok := nv[http.CanonicalHeaderKey(supportedHeader)]
|
value, ok := nv[http.CanonicalHeaderKey(supportedHeader)]
|
||||||
if ok {
|
if ok {
|
||||||
if slices.Contains(maps.Keys(replicationToInternalHeaders), supportedHeader) {
|
if v, ok := replicationToInternalHeaders[supportedHeader]; ok {
|
||||||
m[replicationToInternalHeaders[supportedHeader]] = strings.Join(value, ",")
|
m[v] = strings.Join(value, ",")
|
||||||
} else {
|
} else {
|
||||||
m[supportedHeader] = strings.Join(value, ",")
|
m[supportedHeader] = strings.Join(value, ",")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ func httpTracerMiddleware(h http.Handler) http.Handler {
|
|||||||
Time: reqStartTime,
|
Time: reqStartTime,
|
||||||
Duration: reqEndTime.Sub(respRecorder.StartTime),
|
Duration: reqEndTime.Sub(respRecorder.StartTime),
|
||||||
Path: reqPath,
|
Path: reqPath,
|
||||||
|
Bytes: int64(inputBytes + respRecorder.Size()),
|
||||||
HTTP: &madmin.TraceHTTPStats{
|
HTTP: &madmin.TraceHTTPStats{
|
||||||
ReqInfo: madmin.TraceRequestInfo{
|
ReqInfo: madmin.TraceRequestInfo{
|
||||||
Time: reqStartTime,
|
Time: reqStartTime,
|
||||||
|
|||||||
@@ -236,9 +236,8 @@ func (ies *IAMEtcdStore) addUser(ctx context.Context, user string, userType IAMU
|
|||||||
// for the expiring credentials.
|
// for the expiring credentials.
|
||||||
deleteKeyEtcd(ctx, ies.client, getUserIdentityPath(user, userType))
|
deleteKeyEtcd(ctx, ies.client, getUserIdentityPath(user, userType))
|
||||||
deleteKeyEtcd(ctx, ies.client, getMappedPolicyPath(user, userType, false))
|
deleteKeyEtcd(ctx, ies.client, getMappedPolicyPath(user, userType, false))
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
u.Credentials.Claims = jwtClaims.Map()
|
u.Credentials.Claims = jwtClaims.Map()
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-60
@@ -25,7 +25,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -34,6 +33,7 @@ import (
|
|||||||
"github.com/minio/minio/internal/config"
|
"github.com/minio/minio/internal/config"
|
||||||
xioutil "github.com/minio/minio/internal/ioutil"
|
xioutil "github.com/minio/minio/internal/ioutil"
|
||||||
"github.com/minio/minio/internal/kms"
|
"github.com/minio/minio/internal/kms"
|
||||||
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,8 +44,6 @@ type IAMObjectStore struct {
|
|||||||
|
|
||||||
*iamCache
|
*iamCache
|
||||||
|
|
||||||
cachedIAMListing atomic.Value
|
|
||||||
|
|
||||||
usersSysType UsersSysType
|
usersSysType UsersSysType
|
||||||
|
|
||||||
objAPI ObjectLayer
|
objAPI ObjectLayer
|
||||||
@@ -256,9 +254,8 @@ func (iamOS *IAMObjectStore) loadUser(ctx context.Context, user string, userType
|
|||||||
// for the expiring credentials.
|
// for the expiring credentials.
|
||||||
iamOS.deleteIAMConfig(ctx, getUserIdentityPath(user, userType))
|
iamOS.deleteIAMConfig(ctx, getUserIdentityPath(user, userType))
|
||||||
iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(user, userType, false))
|
iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(user, userType, false))
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return err
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
u.Credentials.Claims = jwtClaims.Map()
|
u.Credentials.Claims = jwtClaims.Map()
|
||||||
@@ -421,8 +418,8 @@ func splitPath(s string, lastIndex bool) (string, string) {
|
|||||||
return s[:i+1], s[i+1:]
|
return s[:i+1], s[i+1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (iamOS *IAMObjectStore) listAllIAMConfigItems(ctx context.Context) (map[string][]string, error) {
|
func (iamOS *IAMObjectStore) listAllIAMConfigItems(ctx context.Context) (res map[string][]string, err error) {
|
||||||
res := make(map[string][]string)
|
res = make(map[string][]string)
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPrefix+SlashSeparator) {
|
for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPrefix+SlashSeparator) {
|
||||||
@@ -438,62 +435,10 @@ func (iamOS *IAMObjectStore) listAllIAMConfigItems(ctx context.Context) (map[str
|
|||||||
|
|
||||||
res[listKey] = append(res[listKey], trimmedItem)
|
res[listKey] = append(res[listKey], trimmedItem)
|
||||||
}
|
}
|
||||||
// Store the listing for later re-use.
|
|
||||||
iamOS.cachedIAMListing.Store(res)
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PurgeExpiredSTS - purge expired STS credentials from object store.
|
|
||||||
func (iamOS *IAMObjectStore) PurgeExpiredSTS(ctx context.Context) error {
|
|
||||||
if iamOS.objAPI == nil {
|
|
||||||
return errServerNotInitialized
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrapTraceMsg("purging expired STS credentials")
|
|
||||||
|
|
||||||
iamListing, ok := iamOS.cachedIAMListing.Load().(map[string][]string)
|
|
||||||
if !ok {
|
|
||||||
// There has been no store yet. This should never happen!
|
|
||||||
iamLogIf(GlobalContext, errors.New("WARNING: no cached IAM listing found"))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan STS users on disk and purge expired ones. We do not need to hold a
|
|
||||||
// lock with store.lock() here.
|
|
||||||
stsAccountsFromStore := map[string]UserIdentity{}
|
|
||||||
stsAccPoliciesFromStore := xsync.NewMapOf[string, MappedPolicy]()
|
|
||||||
for _, item := range iamListing[stsListKey] {
|
|
||||||
userName := path.Dir(item)
|
|
||||||
// loadUser() will delete expired user during the load.
|
|
||||||
err := iamOS.loadUser(ctx, userName, stsUser, stsAccountsFromStore)
|
|
||||||
if err != nil && !errors.Is(err, errNoSuchUser) {
|
|
||||||
iamLogIf(GlobalContext,
|
|
||||||
fmt.Errorf("unable to load user during STS purge: %w (%s)", err, item))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
// Loading the STS policy mappings from disk ensures that stale entries
|
|
||||||
// (removed during loadUser() in the loop above) are removed from memory.
|
|
||||||
for _, item := range iamListing[policyDBSTSUsersListKey] {
|
|
||||||
stsName := strings.TrimSuffix(item, ".json")
|
|
||||||
err := iamOS.loadMappedPolicy(ctx, stsName, stsUser, false, stsAccPoliciesFromStore)
|
|
||||||
if err != nil && !errors.Is(err, errNoSuchPolicy) {
|
|
||||||
iamLogIf(GlobalContext,
|
|
||||||
fmt.Errorf("unable to load policies during STS purge: %w (%s)", err, item))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the newly populated map in the iam cache. This takes care of
|
|
||||||
// removing stale entries from the existing map.
|
|
||||||
iamOS.Lock()
|
|
||||||
defer iamOS.Unlock()
|
|
||||||
iamOS.iamCache.iamSTSAccountsMap = stsAccountsFromStore
|
|
||||||
iamOS.iamCache.iamSTSPolicyMap = stsAccPoliciesFromStore
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assumes cache is locked by caller.
|
// Assumes cache is locked by caller.
|
||||||
func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iamCache) error {
|
func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iamCache) error {
|
||||||
if iamOS.objAPI == nil {
|
if iamOS.objAPI == nil {
|
||||||
@@ -594,6 +539,45 @@ func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iam
|
|||||||
}
|
}
|
||||||
|
|
||||||
cache.buildUserGroupMemberships()
|
cache.buildUserGroupMemberships()
|
||||||
|
|
||||||
|
purgeStart := time.Now()
|
||||||
|
|
||||||
|
// Purge expired STS credentials.
|
||||||
|
|
||||||
|
// Scan STS users on disk and purge expired ones.
|
||||||
|
stsAccountsFromStore := map[string]UserIdentity{}
|
||||||
|
stsAccPoliciesFromStore := xsync.NewMapOf[string, MappedPolicy]()
|
||||||
|
for _, item := range listedConfigItems[stsListKey] {
|
||||||
|
userName := path.Dir(item)
|
||||||
|
// loadUser() will delete expired user during the load.
|
||||||
|
err := iamOS.loadUser(ctx, userName, stsUser, stsAccountsFromStore)
|
||||||
|
if err != nil && !errors.Is(err, errNoSuchUser) {
|
||||||
|
return fmt.Errorf("unable to load user during STS purge: %w (%s)", err, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// Loading the STS policy mappings from disk ensures that stale entries
|
||||||
|
// (removed during loadUser() in the loop above) are removed from memory.
|
||||||
|
for _, item := range listedConfigItems[policyDBSTSUsersListKey] {
|
||||||
|
stsName := strings.TrimSuffix(item, ".json")
|
||||||
|
err := iamOS.loadMappedPolicy(ctx, stsName, stsUser, false, stsAccPoliciesFromStore)
|
||||||
|
if err != nil && !errors.Is(err, errNoSuchPolicy) {
|
||||||
|
return fmt.Errorf("unable to load policies during STS purge: %w (%s)", err, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
took := time.Since(purgeStart).Seconds()
|
||||||
|
if took > maxDurationSecondsForLog {
|
||||||
|
// Log if we took a lot of time to load.
|
||||||
|
logger.Info("IAM expired STS purge took %.2fs", took)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the newly populated map in the iam cache. This takes care of
|
||||||
|
// removing stale entries from the existing map.
|
||||||
|
cache.iamSTSAccountsMap = stsAccountsFromStore
|
||||||
|
cache.iamSTSPolicyMap = stsAccPoliciesFromStore
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
-24
@@ -33,8 +33,10 @@ import (
|
|||||||
"github.com/minio/minio/internal/auth"
|
"github.com/minio/minio/internal/auth"
|
||||||
"github.com/minio/minio/internal/config/identity/openid"
|
"github.com/minio/minio/internal/config/identity/openid"
|
||||||
"github.com/minio/minio/internal/jwt"
|
"github.com/minio/minio/internal/jwt"
|
||||||
|
"github.com/minio/pkg/v3/console"
|
||||||
"github.com/minio/pkg/v3/policy"
|
"github.com/minio/pkg/v3/policy"
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -532,16 +534,6 @@ func setDefaultCannedPolicies(policies map[string]PolicyDoc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PurgeExpiredSTS - purges expired STS credentials.
|
|
||||||
func (store *IAMStoreSys) PurgeExpiredSTS(ctx context.Context) error {
|
|
||||||
iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore)
|
|
||||||
if !ok {
|
|
||||||
// No purging is done for non-object storage.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return iamOS.PurgeExpiredSTS(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadIAMCache reads all IAM items and populates a new iamCache object and
|
// LoadIAMCache reads all IAM items and populates a new iamCache object and
|
||||||
// replaces the in-memory cache object.
|
// replaces the in-memory cache object.
|
||||||
func (store *IAMStoreSys) LoadIAMCache(ctx context.Context, firstTime bool) error {
|
func (store *IAMStoreSys) LoadIAMCache(ctx context.Context, firstTime bool) error {
|
||||||
@@ -643,6 +635,8 @@ func (store *IAMStoreSys) LoadIAMCache(ctx context.Context, firstTime bool) erro
|
|||||||
// layer.
|
// layer.
|
||||||
type IAMStoreSys struct {
|
type IAMStoreSys struct {
|
||||||
IAMStorageAPI
|
IAMStorageAPI
|
||||||
|
|
||||||
|
group *singleflight.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasWatcher - returns if the storage system has a watcher.
|
// HasWatcher - returns if the storage system has a watcher.
|
||||||
@@ -1810,6 +1804,7 @@ func (store *IAMStoreSys) DeleteUser(ctx context.Context, accessKey string, user
|
|||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
delete(cache.iamUsersMap, accessKey)
|
delete(cache.iamUsersMap, accessKey)
|
||||||
|
store.group.Forget(accessKey)
|
||||||
|
|
||||||
cache.updatedAt = time.Now()
|
cache.updatedAt = time.Now()
|
||||||
|
|
||||||
@@ -1885,6 +1880,7 @@ func (store *IAMStoreSys) DeleteUsers(ctx context.Context, users []string) error
|
|||||||
err := store.deleteUserIdentity(ctx, user, userType)
|
err := store.deleteUserIdentity(ctx, user, userType)
|
||||||
iamLogIf(GlobalContext, err)
|
iamLogIf(GlobalContext, err)
|
||||||
delete(cache.iamUsersMap, user)
|
delete(cache.iamUsersMap, user)
|
||||||
|
store.group.Forget(user)
|
||||||
|
|
||||||
deleted = true
|
deleted = true
|
||||||
}
|
}
|
||||||
@@ -2371,7 +2367,7 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
|
|||||||
return updatedAt, err
|
return updatedAt, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(policyBuf) > 2048 {
|
if len(policyBuf) > maxSVCSessionPolicySize {
|
||||||
return updatedAt, errSessionPolicyTooLarge
|
return updatedAt, errSessionPolicyTooLarge
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2572,37 +2568,43 @@ func (store *IAMStoreSys) UpdateUserIdentity(ctx context.Context, cred auth.Cred
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadUser - attempts to load user info from storage and updates cache.
|
// LoadUser - attempts to load user info from storage and updates cache.
|
||||||
func (store *IAMStoreSys) LoadUser(ctx context.Context, accessKey string) {
|
func (store *IAMStoreSys) LoadUser(ctx context.Context, accessKey string) error {
|
||||||
|
// We use singleflight to de-duplicate requests when server
|
||||||
|
// is coming up and loading accessKey and its associated assets
|
||||||
|
val, err, shared := store.group.Do(accessKey, func() (val interface{}, err error) {
|
||||||
cache := store.lock()
|
cache := store.lock()
|
||||||
defer store.unlock()
|
defer func() {
|
||||||
|
|
||||||
cache.updatedAt = time.Now()
|
cache.updatedAt = time.Now()
|
||||||
|
store.unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
_, found := cache.iamUsersMap[accessKey]
|
_, found := cache.iamUsersMap[accessKey]
|
||||||
|
|
||||||
// Check for regular user access key
|
// Check for regular user access key
|
||||||
if !found {
|
if !found {
|
||||||
store.loadUser(ctx, accessKey, regUser, cache.iamUsersMap)
|
err = store.loadUser(ctx, accessKey, regUser, cache.iamUsersMap)
|
||||||
if _, found = cache.iamUsersMap[accessKey]; found {
|
if _, found = cache.iamUsersMap[accessKey]; found {
|
||||||
// load mapped policies
|
// load mapped policies
|
||||||
store.loadMappedPolicyWithRetry(ctx, accessKey, regUser, false, cache.iamUserPolicyMap, 3)
|
err = store.loadMappedPolicyWithRetry(ctx, accessKey, regUser, false, cache.iamUserPolicyMap, 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for service account
|
// Check for service account
|
||||||
if !found {
|
if !found {
|
||||||
store.loadUser(ctx, accessKey, svcUser, cache.iamUsersMap)
|
err = store.loadUser(ctx, accessKey, svcUser, cache.iamUsersMap)
|
||||||
var svc UserIdentity
|
var svc UserIdentity
|
||||||
svc, found = cache.iamUsersMap[accessKey]
|
svc, found = cache.iamUsersMap[accessKey]
|
||||||
if found {
|
if found {
|
||||||
// Load parent user and mapped policies.
|
// Load parent user and mapped policies.
|
||||||
if store.getUsersSysType() == MinIOUsersSysType {
|
if store.getUsersSysType() == MinIOUsersSysType {
|
||||||
store.loadUser(ctx, svc.Credentials.ParentUser, regUser, cache.iamUsersMap)
|
err = store.loadUser(ctx, svc.Credentials.ParentUser, regUser, cache.iamUsersMap)
|
||||||
store.loadMappedPolicyWithRetry(ctx, svc.Credentials.ParentUser, regUser, false, cache.iamUserPolicyMap, 3)
|
if err == nil {
|
||||||
|
err = store.loadMappedPolicyWithRetry(ctx, svc.Credentials.ParentUser, regUser, false, cache.iamUserPolicyMap, 3)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// In case of LDAP the parent user's policy mapping needs to be
|
// In case of LDAP the parent user's policy mapping needs to be
|
||||||
// loaded into sts map
|
// loaded into sts map
|
||||||
store.loadMappedPolicyWithRetry(ctx, svc.Credentials.ParentUser, stsUser, false, cache.iamSTSPolicyMap, 3)
|
err = store.loadMappedPolicyWithRetry(ctx, svc.Credentials.ParentUser, stsUser, false, cache.iamSTSPolicyMap, 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2611,10 +2613,10 @@ func (store *IAMStoreSys) LoadUser(ctx context.Context, accessKey string) {
|
|||||||
stsAccountFound := false
|
stsAccountFound := false
|
||||||
var stsUserCred UserIdentity
|
var stsUserCred UserIdentity
|
||||||
if !found {
|
if !found {
|
||||||
store.loadUser(ctx, accessKey, stsUser, cache.iamSTSAccountsMap)
|
err = store.loadUser(ctx, accessKey, stsUser, cache.iamSTSAccountsMap)
|
||||||
if stsUserCred, found = cache.iamSTSAccountsMap[accessKey]; found {
|
if stsUserCred, found = cache.iamSTSAccountsMap[accessKey]; found {
|
||||||
// Load mapped policy
|
// Load mapped policy
|
||||||
store.loadMappedPolicyWithRetry(ctx, stsUserCred.Credentials.ParentUser, stsUser, false, cache.iamSTSPolicyMap, 3)
|
err = store.loadMappedPolicyWithRetry(ctx, stsUserCred.Credentials.ParentUser, stsUser, false, cache.iamSTSPolicyMap, 3)
|
||||||
stsAccountFound = true
|
stsAccountFound = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2624,17 +2626,30 @@ func (store *IAMStoreSys) LoadUser(ctx context.Context, accessKey string) {
|
|||||||
pols, _ := cache.iamUserPolicyMap.Load(accessKey)
|
pols, _ := cache.iamUserPolicyMap.Load(accessKey)
|
||||||
for _, policy := range pols.toSlice() {
|
for _, policy := range pols.toSlice() {
|
||||||
if _, found = cache.iamPolicyDocsMap[policy]; !found {
|
if _, found = cache.iamPolicyDocsMap[policy]; !found {
|
||||||
store.loadPolicyDocWithRetry(ctx, policy, cache.iamPolicyDocsMap, 3)
|
err = store.loadPolicyDocWithRetry(ctx, policy, cache.iamPolicyDocsMap, 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pols, _ := cache.iamSTSPolicyMap.Load(stsUserCred.Credentials.AccessKey)
|
pols, _ := cache.iamSTSPolicyMap.Load(stsUserCred.Credentials.AccessKey)
|
||||||
for _, policy := range pols.toSlice() {
|
for _, policy := range pols.toSlice() {
|
||||||
if _, found = cache.iamPolicyDocsMap[policy]; !found {
|
if _, found = cache.iamPolicyDocsMap[policy]; !found {
|
||||||
store.loadPolicyDocWithRetry(ctx, policy, cache.iamPolicyDocsMap, 3)
|
err = store.loadPolicyDocWithRetry(ctx, policy, cache.iamPolicyDocsMap, 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "done", err
|
||||||
|
})
|
||||||
|
|
||||||
|
if serverDebugLog {
|
||||||
|
console.Debugln("loadUser: loading shared", val, err, shared)
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsErr(err, errNoSuchUser, errNoSuchPolicy, errNoSuchGroup) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractJWTClaims(u UserIdentity) (*jwt.MapClaims, error) {
|
func extractJWTClaims(u UserIdentity) (*jwt.MapClaims, error) {
|
||||||
|
|||||||
+46
-42
@@ -52,6 +52,7 @@ import (
|
|||||||
"github.com/minio/pkg/v3/ldap"
|
"github.com/minio/pkg/v3/ldap"
|
||||||
"github.com/minio/pkg/v3/policy"
|
"github.com/minio/pkg/v3/policy"
|
||||||
etcd "go.etcd.io/etcd/client/v3"
|
etcd "go.etcd.io/etcd/client/v3"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UsersSysType - defines the type of users and groups system that is
|
// UsersSysType - defines the type of users and groups system that is
|
||||||
@@ -78,6 +79,10 @@ const (
|
|||||||
inheritedPolicyType = "inherited-policy"
|
inheritedPolicyType = "inherited-policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxSVCSessionPolicySize = 4096
|
||||||
|
)
|
||||||
|
|
||||||
// IAMSys - config system.
|
// IAMSys - config system.
|
||||||
type IAMSys struct {
|
type IAMSys struct {
|
||||||
// Need to keep them here to keep alignment - ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
// Need to keep them here to keep alignment - ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
||||||
@@ -173,9 +178,9 @@ func (sys *IAMSys) initStore(objAPI ObjectLayer, etcdClient *etcd.Client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if etcdClient == nil {
|
if etcdClient == nil {
|
||||||
sys.store = &IAMStoreSys{newIAMObjectStore(objAPI, sys.usersSysType)}
|
sys.store = &IAMStoreSys{newIAMObjectStore(objAPI, sys.usersSysType), &singleflight.Group{}}
|
||||||
} else {
|
} else {
|
||||||
sys.store = &IAMStoreSys{newIAMEtcdStore(etcdClient, sys.usersSysType)}
|
sys.store = &IAMStoreSys{newIAMEtcdStore(etcdClient, sys.usersSysType), &singleflight.Group{}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,6 +353,8 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
|||||||
bootstrapTraceMsg("finishing IAM loading")
|
bootstrapTraceMsg("finishing IAM loading")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxDurationSecondsForLog = 5
|
||||||
|
|
||||||
func (sys *IAMSys) periodicRoutines(ctx context.Context, baseInterval time.Duration) {
|
func (sys *IAMSys) periodicRoutines(ctx context.Context, baseInterval time.Duration) {
|
||||||
// Watch for IAM config changes for iamStorageWatcher.
|
// Watch for IAM config changes for iamStorageWatcher.
|
||||||
watcher, isWatcher := sys.store.IAMStorageAPI.(iamStorageWatcher)
|
watcher, isWatcher := sys.store.IAMStorageAPI.(iamStorageWatcher)
|
||||||
@@ -380,7 +387,6 @@ func (sys *IAMSys) periodicRoutines(ctx context.Context, baseInterval time.Durat
|
|||||||
return baseInterval/2 + randAmt
|
return baseInterval/2 + randAmt
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxDurationSecondsForLog float64 = 5
|
|
||||||
timer := time.NewTimer(waitInterval())
|
timer := time.NewTimer(waitInterval())
|
||||||
defer timer.Stop()
|
defer timer.Stop()
|
||||||
|
|
||||||
@@ -399,18 +405,6 @@ func (sys *IAMSys) periodicRoutines(ctx context.Context, baseInterval time.Durat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Purge expired STS credentials.
|
|
||||||
purgeStart := time.Now()
|
|
||||||
if err := sys.store.PurgeExpiredSTS(ctx); err != nil {
|
|
||||||
iamLogIf(ctx, fmt.Errorf("Failure in periodic STS purge for IAM (took %.2fs): %v", time.Since(purgeStart).Seconds(), err))
|
|
||||||
} else {
|
|
||||||
took := time.Since(purgeStart).Seconds()
|
|
||||||
if took > maxDurationSecondsForLog {
|
|
||||||
// Log if we took a lot of time to load.
|
|
||||||
logger.Info("IAM expired STS purge took %.2fs", took)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following actions are performed about once in 4 times that
|
// The following actions are performed about once in 4 times that
|
||||||
// IAM is refreshed:
|
// IAM is refreshed:
|
||||||
if r.Intn(4) == 0 {
|
if r.Intn(4) == 0 {
|
||||||
@@ -977,7 +971,7 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return auth.Credentials{}, time.Time{}, err
|
return auth.Credentials{}, time.Time{}, err
|
||||||
}
|
}
|
||||||
if len(policyBuf) > 2048 {
|
if len(policyBuf) > maxSVCSessionPolicySize {
|
||||||
return auth.Credentials{}, time.Time{}, errSessionPolicyTooLarge
|
return auth.Credentials{}, time.Time{}, errSessionPolicyTooLarge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1273,6 +1267,10 @@ func (sys *IAMSys) CreateUser(ctx context.Context, accessKey string, ureq madmin
|
|||||||
return updatedAt, auth.ErrInvalidAccessKeyLength
|
return updatedAt, auth.ErrInvalidAccessKeyLength
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if auth.ContainsReservedChars(accessKey) {
|
||||||
|
return updatedAt, auth.ErrContainsReservedChars
|
||||||
|
}
|
||||||
|
|
||||||
if !auth.IsSecretKeyValid(ureq.SecretKey) {
|
if !auth.IsSecretKeyValid(ureq.SecretKey) {
|
||||||
return updatedAt, auth.ErrInvalidSecretKeyLength
|
return updatedAt, auth.ErrInvalidSecretKeyLength
|
||||||
}
|
}
|
||||||
@@ -1570,20 +1568,6 @@ func (sys *IAMSys) NormalizeLDAPAccessKeypairs(ctx context.Context, accessKeyMap
|
|||||||
|
|
||||||
func (sys *IAMSys) getStoredLDAPPolicyMappingKeys(ctx context.Context, isGroup bool) set.StringSet {
|
func (sys *IAMSys) getStoredLDAPPolicyMappingKeys(ctx context.Context, isGroup bool) set.StringSet {
|
||||||
entityKeysInStorage := set.NewStringSet()
|
entityKeysInStorage := set.NewStringSet()
|
||||||
if iamOS, ok := sys.store.IAMStorageAPI.(*IAMObjectStore); ok {
|
|
||||||
// Load existing mapping keys from the cached listing for
|
|
||||||
// `IAMObjectStore`.
|
|
||||||
iamFilesListing := iamOS.cachedIAMListing.Load().(map[string][]string)
|
|
||||||
listKey := policyDBSTSUsersListKey
|
|
||||||
if isGroup {
|
|
||||||
listKey = policyDBGroupsListKey
|
|
||||||
}
|
|
||||||
for _, item := range iamFilesListing[listKey] {
|
|
||||||
stsUserName := strings.TrimSuffix(item, ".json")
|
|
||||||
entityKeysInStorage.Add(stsUserName)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For non-iam object store, we copy the mapping keys from the cache.
|
|
||||||
cache := sys.store.rlock()
|
cache := sys.store.rlock()
|
||||||
defer sys.store.runlock()
|
defer sys.store.runlock()
|
||||||
cachedPolicyMap := cache.iamSTSPolicyMap
|
cachedPolicyMap := cache.iamSTSPolicyMap
|
||||||
@@ -1594,7 +1578,6 @@ func (sys *IAMSys) getStoredLDAPPolicyMappingKeys(ctx context.Context, isGroup b
|
|||||||
entityKeysInStorage.Add(k)
|
entityKeysInStorage.Add(k)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return entityKeysInStorage
|
return entityKeysInStorage
|
||||||
}
|
}
|
||||||
@@ -1720,31 +1703,46 @@ func (sys *IAMSys) NormalizeLDAPMappingImport(ctx context.Context, isGroup bool,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUser - get user credentials
|
// CheckKey validates the incoming accessKey
|
||||||
func (sys *IAMSys) GetUser(ctx context.Context, accessKey string) (u UserIdentity, ok bool) {
|
func (sys *IAMSys) CheckKey(ctx context.Context, accessKey string) (u UserIdentity, ok bool, err error) {
|
||||||
if !sys.Initialized() {
|
if !sys.Initialized() {
|
||||||
return u, false
|
return u, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if accessKey == globalActiveCred.AccessKey {
|
if accessKey == globalActiveCred.AccessKey {
|
||||||
return newUserIdentity(globalActiveCred), true
|
return newUserIdentity(globalActiveCred), true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
loadUserCalled := false
|
loadUserCalled := false
|
||||||
select {
|
select {
|
||||||
case <-sys.configLoaded:
|
case <-sys.configLoaded:
|
||||||
default:
|
default:
|
||||||
sys.store.LoadUser(ctx, accessKey)
|
err = sys.store.LoadUser(ctx, accessKey)
|
||||||
loadUserCalled = true
|
loadUserCalled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
u, ok = sys.store.GetUser(accessKey)
|
u, ok = sys.store.GetUser(accessKey)
|
||||||
if !ok && !loadUserCalled {
|
if !ok && !loadUserCalled {
|
||||||
sys.store.LoadUser(ctx, accessKey)
|
err = sys.store.LoadUser(ctx, accessKey)
|
||||||
|
loadUserCalled = true
|
||||||
|
|
||||||
u, ok = sys.store.GetUser(accessKey)
|
u, ok = sys.store.GetUser(accessKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return u, ok && u.Credentials.IsValid()
|
if !ok && loadUserCalled && err != nil {
|
||||||
|
iamLogOnceIf(ctx, err, accessKey)
|
||||||
|
|
||||||
|
// return 503 to application
|
||||||
|
return u, false, errIAMNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, ok && u.Credentials.IsValid(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser - get user credentials
|
||||||
|
func (sys *IAMSys) GetUser(ctx context.Context, accessKey string) (u UserIdentity, ok bool) {
|
||||||
|
u, ok, _ = sys.CheckKey(ctx, accessKey)
|
||||||
|
return u, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify all other MinIO peers to load group.
|
// Notify all other MinIO peers to load group.
|
||||||
@@ -1766,6 +1764,10 @@ func (sys *IAMSys) AddUsersToGroup(ctx context.Context, group string, members []
|
|||||||
return updatedAt, errServerNotInitialized
|
return updatedAt, errServerNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if auth.ContainsReservedChars(group) {
|
||||||
|
return updatedAt, errGroupNameContainsReservedChars
|
||||||
|
}
|
||||||
|
|
||||||
updatedAt, err = sys.store.AddUsersToGroup(ctx, group, members)
|
updatedAt, err = sys.store.AddUsersToGroup(ctx, group, members)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return updatedAt, err
|
return updatedAt, err
|
||||||
@@ -1974,20 +1976,22 @@ func (sys *IAMSys) PolicyDBUpdateLDAP(ctx context.Context, isAttach bool,
|
|||||||
}
|
}
|
||||||
isGroup = false
|
isGroup = false
|
||||||
} else {
|
} else {
|
||||||
if isAttach {
|
|
||||||
var underBaseDN bool
|
var underBaseDN bool
|
||||||
if dnResult, underBaseDN, err = sys.LDAPConfig.GetValidatedGroupDN(nil, r.Group); err != nil {
|
if dnResult, underBaseDN, err = sys.LDAPConfig.GetValidatedGroupDN(nil, r.Group); err != nil {
|
||||||
iamLogIf(ctx, err)
|
iamLogIf(ctx, err)
|
||||||
return
|
return
|
||||||
} else if dnResult == nil || !underBaseDN {
|
}
|
||||||
|
if dnResult == nil || !underBaseDN {
|
||||||
|
if !isAttach {
|
||||||
|
dn = r.Group
|
||||||
|
} else {
|
||||||
err = errNoSuchGroup
|
err = errNoSuchGroup
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// We use the group DN returned by the LDAP server (this may not
|
// We use the group DN returned by the LDAP server (this may not
|
||||||
// equal the input group name, but we assume it is canonical).
|
// equal the input group name, but we assume it is canonical).
|
||||||
dn = dnResult.NormDN
|
dn = dnResult.NormDN
|
||||||
} else {
|
|
||||||
dn = r.Group
|
|
||||||
}
|
}
|
||||||
isGroup = true
|
isGroup = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import (
|
|||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func proxyLogIf(ctx context.Context, err error, errKind ...interface{}) {
|
||||||
|
logger.LogIf(ctx, "proxy", err, errKind...)
|
||||||
|
}
|
||||||
|
|
||||||
func replLogIf(ctx context.Context, err error, errKind ...interface{}) {
|
func replLogIf(ctx context.Context, err error, errKind ...interface{}) {
|
||||||
logger.LogIf(ctx, "replication", err, errKind...)
|
logger.LogIf(ctx, "replication", err, errKind...)
|
||||||
}
|
}
|
||||||
@@ -16,6 +20,10 @@ func replLogOnceIf(ctx context.Context, err error, id string, errKind ...interfa
|
|||||||
logger.LogOnceIf(ctx, "replication", err, id, errKind...)
|
logger.LogOnceIf(ctx, "replication", err, id, errKind...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func iamLogOnceIf(ctx context.Context, err error, id string, errKind ...interface{}) {
|
||||||
|
logger.LogOnceIf(ctx, "iam", err, id, errKind...)
|
||||||
|
}
|
||||||
|
|
||||||
func iamLogIf(ctx context.Context, err error, errKind ...interface{}) {
|
func iamLogIf(ctx context.Context, err error, errKind ...interface{}) {
|
||||||
if !errors.Is(err, grid.ErrDisconnected) {
|
if !errors.Is(err, grid.ErrDisconnected) {
|
||||||
logger.LogIf(ctx, "iam", err, errKind...)
|
logger.LogIf(ctx, "iam", err, errKind...)
|
||||||
|
|||||||
+3
-1
@@ -189,12 +189,14 @@ func printMinIOVersion(c *cli.Context) {
|
|||||||
io.Copy(c.App.Writer, versionBanner(c))
|
io.Copy(c.App.Writer, versionBanner(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var debugNoExit = env.Get("_MINIO_DEBUG_NO_EXIT", "") != ""
|
||||||
|
|
||||||
// Main main for minio server.
|
// Main main for minio server.
|
||||||
func Main(args []string) {
|
func Main(args []string) {
|
||||||
// Set the minio app name.
|
// Set the minio app name.
|
||||||
appName := filepath.Base(args[0])
|
appName := filepath.Base(args[0])
|
||||||
|
|
||||||
if env.Get("_MINIO_DEBUG_NO_EXIT", "") != "" {
|
if debugNoExit {
|
||||||
freeze := func(_ int) {
|
freeze := func(_ int) {
|
||||||
// Infinite blocking op
|
// Infinite blocking op
|
||||||
<-make(chan struct{})
|
<-make(chan struct{})
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -260,7 +259,7 @@ func (e *metaCacheEntry) fileInfo(bucket string) (FileInfo, error) {
|
|||||||
}
|
}
|
||||||
return e.cached.ToFileInfo(bucket, e.name, "", false, false)
|
return e.cached.ToFileInfo(bucket, e.name, "", false, false)
|
||||||
}
|
}
|
||||||
return getFileInfo(e.metadata, bucket, e.name, "", false, false)
|
return getFileInfo(e.metadata, bucket, e.name, "", fileInfoOpts{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// xlmeta returns the decoded metadata.
|
// xlmeta returns the decoded metadata.
|
||||||
@@ -301,7 +300,7 @@ func (e *metaCacheEntry) fileInfoVersions(bucket string) (FileInfoVersions, erro
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
// Too small gains to reuse cache here.
|
// Too small gains to reuse cache here.
|
||||||
return getFileInfoVersions(e.metadata, bucket, e.name, false)
|
return getFileInfoVersions(e.metadata, bucket, e.name, false, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// metaCacheEntries is a slice of metacache entries.
|
// metaCacheEntries is a slice of metacache entries.
|
||||||
@@ -385,9 +384,6 @@ func (m metaCacheEntries) resolve(r *metadataResolutionParams) (selected *metaCa
|
|||||||
// shallow decode.
|
// shallow decode.
|
||||||
xl, err := entry.xlmeta()
|
xl, err := entry.xlmeta()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, errFileNotFound) {
|
|
||||||
internalLogIf(GlobalContext, err)
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
objsValid++
|
objsValid++
|
||||||
@@ -752,7 +748,9 @@ func mergeEntryChannels(ctx context.Context, in []chan metaCacheEntry, out chan<
|
|||||||
if !dirMatches {
|
if !dirMatches {
|
||||||
// We have an object `name` or 'name/' and a directory `name/`.
|
// We have an object `name` or 'name/' and a directory `name/`.
|
||||||
if other.isDir() {
|
if other.isDir() {
|
||||||
|
if serverDebugLog {
|
||||||
console.Debugln("mergeEntryChannels: discarding directory", other.name, "for object", best.name)
|
console.Debugln("mergeEntryChannels: discarding directory", other.name, "for object", best.name)
|
||||||
|
}
|
||||||
// Discard the directory.
|
// Discard the directory.
|
||||||
if err := selectFrom(otherIdx); err != nil {
|
if err := selectFrom(otherIdx); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -760,7 +758,9 @@ func mergeEntryChannels(ctx context.Context, in []chan metaCacheEntry, out chan<
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Replace directory with object.
|
// Replace directory with object.
|
||||||
|
if serverDebugLog {
|
||||||
console.Debugln("mergeEntryChannels: discarding directory", best.name, "for object", other.name)
|
console.Debugln("mergeEntryChannels: discarding directory", best.name, "for object", other.name)
|
||||||
|
}
|
||||||
toMerge = toMerge[:0]
|
toMerge = toMerge[:0]
|
||||||
best = other
|
best = other
|
||||||
bestIdx = otherIdx
|
bestIdx = otherIdx
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ func (r *metacacheReader) filter(o listPathOptions) (entries metaCacheEntriesSor
|
|||||||
if !o.InclDeleted && entry.isObject() && entry.isLatestDeletemarker() && !entry.isObjectDir() {
|
if !o.InclDeleted && entry.isObject() && entry.isLatestDeletemarker() && !entry.isObjectDir() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if entry.isAllFreeVersions() {
|
if !o.InclDeleted && entry.isAllFreeVersions() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
entries.o = append(entries.o, entry)
|
entries.o = append(entries.o, entry)
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ func (p *xlStorageDiskIDCheck) WalkDir(ctx context.Context, opts WalkDirOptions,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer done(&err)
|
defer done(0, &err)
|
||||||
|
|
||||||
return p.storage.WalkDir(ctx, opts, wr)
|
return p.storage.WalkDir(ctx, opts, wr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,15 @@ func collectLocalMetrics(types madmin.MetricType, opts collectMetricsOpts) (m ma
|
|||||||
m.Aggregated.CPU.LoadStat = loadStat
|
m.Aggregated.CPU.LoadStat = loadStat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if types.Contains(madmin.MetricsRPC) {
|
||||||
|
gr := globalGrid.Load()
|
||||||
|
if gr == nil {
|
||||||
|
m.Errors = append(m.Errors, fmt.Sprintf("%s: Grid not initialized", byHostName))
|
||||||
|
} else {
|
||||||
|
stats := gr.ConnStats()
|
||||||
|
m.Aggregated.RPC = &stats
|
||||||
|
}
|
||||||
|
}
|
||||||
// Add types...
|
// Add types...
|
||||||
|
|
||||||
// ByHost is a shallow reference, so careful about sharing.
|
// ByHost is a shallow reference, so careful about sharing.
|
||||||
|
|||||||
+20
-23
@@ -273,13 +273,10 @@ const (
|
|||||||
vmemory = "virtual_memory_bytes"
|
vmemory = "virtual_memory_bytes"
|
||||||
cpu = "cpu_total_seconds"
|
cpu = "cpu_total_seconds"
|
||||||
|
|
||||||
expiryPendingTasks MetricName = "expiry_pending_tasks"
|
|
||||||
expiryMissedTasks MetricName = "expiry_missed_tasks"
|
expiryMissedTasks MetricName = "expiry_missed_tasks"
|
||||||
expiryMissedFreeVersions MetricName = "expiry_missed_freeversions"
|
expiryMissedFreeVersions MetricName = "expiry_missed_freeversions"
|
||||||
expiryMissedTierJournalTasks MetricName = "expiry_missed_tierjournal_tasks"
|
expiryMissedTierJournalTasks MetricName = "expiry_missed_tierjournal_tasks"
|
||||||
expiryNumWorkers MetricName = "expiry_num_workers"
|
expiryNumWorkers MetricName = "expiry_num_workers"
|
||||||
transitionPendingTasks MetricName = "transition_pending_tasks"
|
|
||||||
transitionActiveTasks MetricName = "transition_active_tasks"
|
|
||||||
transitionMissedTasks MetricName = "transition_missed_immediate_tasks"
|
transitionMissedTasks MetricName = "transition_missed_immediate_tasks"
|
||||||
|
|
||||||
transitionedBytes MetricName = "transitioned_bytes"
|
transitionedBytes MetricName = "transitioned_bytes"
|
||||||
@@ -820,7 +817,7 @@ func getClusterObjectVersionsMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterRepLinkLatencyCurrMD() MetricDescription {
|
func getClusterRepLinkLatencyCurrMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: currLinkLatency,
|
Name: currLinkLatency,
|
||||||
Help: "Replication current link latency in milliseconds",
|
Help: "Replication current link latency in milliseconds",
|
||||||
@@ -830,7 +827,7 @@ func getClusterRepLinkLatencyCurrMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterRepLinkOnlineMD() MetricDescription {
|
func getClusterRepLinkOnlineMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: linkOnline,
|
Name: linkOnline,
|
||||||
Help: "Reports whether replication link is online (1) or offline(0)",
|
Help: "Reports whether replication link is online (1) or offline(0)",
|
||||||
@@ -840,7 +837,7 @@ func getClusterRepLinkOnlineMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterRepLinkCurrOfflineDurationMD() MetricDescription {
|
func getClusterRepLinkCurrOfflineDurationMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: linkOfflineDuration,
|
Name: linkOfflineDuration,
|
||||||
Help: "Duration of replication link being offline in seconds since last offline event",
|
Help: "Duration of replication link being offline in seconds since last offline event",
|
||||||
@@ -850,7 +847,7 @@ func getClusterRepLinkCurrOfflineDurationMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterRepLinkTotalOfflineDurationMD() MetricDescription {
|
func getClusterRepLinkTotalOfflineDurationMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: linkDowntimeTotalDuration,
|
Name: linkDowntimeTotalDuration,
|
||||||
Help: "Total downtime of replication link in seconds since server uptime",
|
Help: "Total downtime of replication link in seconds since server uptime",
|
||||||
@@ -978,7 +975,7 @@ func getRepReceivedOperationsMD(namespace MetricNamespace) MetricDescription {
|
|||||||
|
|
||||||
func getClusterReplMRFFailedOperationsMD() MetricDescription {
|
func getClusterReplMRFFailedOperationsMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: recentBacklogCount,
|
Name: recentBacklogCount,
|
||||||
Help: "Total number of objects seen in replication backlog in the last 5 minutes",
|
Help: "Total number of objects seen in replication backlog in the last 5 minutes",
|
||||||
@@ -998,7 +995,7 @@ func getClusterRepCredentialErrorsMD(namespace MetricNamespace) MetricDescriptio
|
|||||||
|
|
||||||
func getClusterReplCurrQueuedOperationsMD() MetricDescription {
|
func getClusterReplCurrQueuedOperationsMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: currInQueueCount,
|
Name: currInQueueCount,
|
||||||
Help: "Total number of objects queued for replication in the last full minute",
|
Help: "Total number of objects queued for replication in the last full minute",
|
||||||
@@ -1008,7 +1005,7 @@ func getClusterReplCurrQueuedOperationsMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterReplCurrQueuedBytesMD() MetricDescription {
|
func getClusterReplCurrQueuedBytesMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: currInQueueBytes,
|
Name: currInQueueBytes,
|
||||||
Help: "Total number of bytes queued for replication in the last full minute",
|
Help: "Total number of bytes queued for replication in the last full minute",
|
||||||
@@ -1018,7 +1015,7 @@ func getClusterReplCurrQueuedBytesMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterReplActiveWorkersCountMD() MetricDescription {
|
func getClusterReplActiveWorkersCountMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: currActiveWorkers,
|
Name: currActiveWorkers,
|
||||||
Help: "Total number of active replication workers",
|
Help: "Total number of active replication workers",
|
||||||
@@ -1028,7 +1025,7 @@ func getClusterReplActiveWorkersCountMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterReplAvgActiveWorkersCountMD() MetricDescription {
|
func getClusterReplAvgActiveWorkersCountMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: avgActiveWorkers,
|
Name: avgActiveWorkers,
|
||||||
Help: "Average number of active replication workers",
|
Help: "Average number of active replication workers",
|
||||||
@@ -1038,7 +1035,7 @@ func getClusterReplAvgActiveWorkersCountMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterReplMaxActiveWorkersCountMD() MetricDescription {
|
func getClusterReplMaxActiveWorkersCountMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: maxActiveWorkers,
|
Name: maxActiveWorkers,
|
||||||
Help: "Maximum number of active replication workers seen since server uptime",
|
Help: "Maximum number of active replication workers seen since server uptime",
|
||||||
@@ -1048,7 +1045,7 @@ func getClusterReplMaxActiveWorkersCountMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterReplCurrentTransferRateMD() MetricDescription {
|
func getClusterReplCurrentTransferRateMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: currTransferRate,
|
Name: currTransferRate,
|
||||||
Help: "Current replication transfer rate in bytes/sec",
|
Help: "Current replication transfer rate in bytes/sec",
|
||||||
@@ -1058,7 +1055,7 @@ func getClusterReplCurrentTransferRateMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterRepLinkLatencyMaxMD() MetricDescription {
|
func getClusterRepLinkLatencyMaxMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: maxLinkLatency,
|
Name: maxLinkLatency,
|
||||||
Help: "Maximum replication link latency in milliseconds seen since server uptime",
|
Help: "Maximum replication link latency in milliseconds seen since server uptime",
|
||||||
@@ -1068,7 +1065,7 @@ func getClusterRepLinkLatencyMaxMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterRepLinkLatencyAvgMD() MetricDescription {
|
func getClusterRepLinkLatencyAvgMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: avgLinkLatency,
|
Name: avgLinkLatency,
|
||||||
Help: "Average replication link latency in milliseconds",
|
Help: "Average replication link latency in milliseconds",
|
||||||
@@ -1078,7 +1075,7 @@ func getClusterRepLinkLatencyAvgMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterReplAvgQueuedOperationsMD() MetricDescription {
|
func getClusterReplAvgQueuedOperationsMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: avgInQueueCount,
|
Name: avgInQueueCount,
|
||||||
Help: "Average number of objects queued for replication since server uptime",
|
Help: "Average number of objects queued for replication since server uptime",
|
||||||
@@ -1088,7 +1085,7 @@ func getClusterReplAvgQueuedOperationsMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterReplAvgQueuedBytesMD() MetricDescription {
|
func getClusterReplAvgQueuedBytesMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: avgInQueueBytes,
|
Name: avgInQueueBytes,
|
||||||
Help: "Average number of bytes queued for replication since server uptime",
|
Help: "Average number of bytes queued for replication since server uptime",
|
||||||
@@ -1098,7 +1095,7 @@ func getClusterReplAvgQueuedBytesMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterReplMaxQueuedOperationsMD() MetricDescription {
|
func getClusterReplMaxQueuedOperationsMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: maxInQueueCount,
|
Name: maxInQueueCount,
|
||||||
Help: "Maximum number of objects queued for replication since server uptime",
|
Help: "Maximum number of objects queued for replication since server uptime",
|
||||||
@@ -1108,7 +1105,7 @@ func getClusterReplMaxQueuedOperationsMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterReplMaxQueuedBytesMD() MetricDescription {
|
func getClusterReplMaxQueuedBytesMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: maxInQueueBytes,
|
Name: maxInQueueBytes,
|
||||||
Help: "Maximum number of bytes queued for replication since server uptime",
|
Help: "Maximum number of bytes queued for replication since server uptime",
|
||||||
@@ -1118,7 +1115,7 @@ func getClusterReplMaxQueuedBytesMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterReplAvgTransferRateMD() MetricDescription {
|
func getClusterReplAvgTransferRateMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: avgTransferRate,
|
Name: avgTransferRate,
|
||||||
Help: "Average replication transfer rate in bytes/sec",
|
Help: "Average replication transfer rate in bytes/sec",
|
||||||
@@ -1128,7 +1125,7 @@ func getClusterReplAvgTransferRateMD() MetricDescription {
|
|||||||
|
|
||||||
func getClusterReplMaxTransferRateMD() MetricDescription {
|
func getClusterReplMaxTransferRateMD() MetricDescription {
|
||||||
return MetricDescription{
|
return MetricDescription{
|
||||||
Namespace: clusterMetricNamespace,
|
Namespace: nodeMetricNamespace,
|
||||||
Subsystem: replicationSubsystem,
|
Subsystem: replicationSubsystem,
|
||||||
Name: maxTransferRate,
|
Name: maxTransferRate,
|
||||||
Help: "Maximum replication transfer rate in bytes/sec seen since server uptime",
|
Help: "Maximum replication transfer rate in bytes/sec seen since server uptime",
|
||||||
@@ -1691,7 +1688,7 @@ func getMinioProcMetrics() *MetricsGroupV2 {
|
|||||||
cacheInterval: 10 * time.Second,
|
cacheInterval: 10 * time.Second,
|
||||||
}
|
}
|
||||||
mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
|
mg.RegisterRead(func(ctx context.Context) (metrics []MetricV2) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == globalWindowsOSName || runtime.GOOS == globalMacOSName {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (c) 2024 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// # This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
expiryPendingTasks = "expiry_pending_tasks"
|
||||||
|
transitionActiveTasks = "transition_active_tasks"
|
||||||
|
transitionPendingTasks = "transition_pending_tasks"
|
||||||
|
transitionMissedImmediateTasks = "transition_missed_immediate_tasks"
|
||||||
|
versionsScanned = "versions_scanned"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ilmExpiryPendingTasksMD = NewGaugeMD(expiryPendingTasks, "Number of pending ILM expiry tasks in the queue")
|
||||||
|
ilmTransitionActiveTasksMD = NewGaugeMD(transitionActiveTasks, "Number of active ILM transition tasks")
|
||||||
|
ilmTransitionPendingTasksMD = NewGaugeMD(transitionPendingTasks, "Number of pending ILM transition tasks in the queue")
|
||||||
|
ilmTransitionMissedImmediateTasksMD = NewCounterMD(transitionMissedImmediateTasks, "Number of missed immediate ILM transition tasks")
|
||||||
|
ilmVersionsScannedMD = NewCounterMD(versionsScanned, "Total number of object versions checked for ILM actions since server start")
|
||||||
|
)
|
||||||
|
|
||||||
|
// loadILMMetrics - `MetricsLoaderFn` for ILM metrics.
|
||||||
|
func loadILMMetrics(_ context.Context, m MetricValues, _ *metricsCache) error {
|
||||||
|
if globalExpiryState != nil {
|
||||||
|
m.Set(expiryPendingTasks, float64(globalExpiryState.PendingTasks()))
|
||||||
|
}
|
||||||
|
if globalTransitionState != nil {
|
||||||
|
m.Set(transitionActiveTasks, float64(globalTransitionState.ActiveTasks()))
|
||||||
|
m.Set(transitionPendingTasks, float64(globalTransitionState.PendingTasks()))
|
||||||
|
m.Set(transitionMissedImmediateTasks, float64(globalTransitionState.MissedImmediateTasks()))
|
||||||
|
}
|
||||||
|
m.Set(versionsScanned, float64(globalScannerMetrics.lifetime(scannerMetricILM)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ func loadCPUMetrics(ctx context.Context, m MetricValues, c *metricsCache) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
ts := cpuMetrics.TimesStat
|
ts := cpuMetrics.TimesStat
|
||||||
|
if ts != nil {
|
||||||
tot := ts.User + ts.System + ts.Idle + ts.Iowait + ts.Nice + ts.Steal
|
tot := ts.User + ts.System + ts.Idle + ts.Iowait + ts.Nice + ts.Steal
|
||||||
cpuUserVal := math.Round(ts.User/tot*100*100) / 100
|
cpuUserVal := math.Round(ts.User/tot*100*100) / 100
|
||||||
m.Set(sysCPUUser, cpuUserVal)
|
m.Set(sysCPUUser, cpuUserVal)
|
||||||
@@ -64,6 +65,7 @@ func loadCPUMetrics(ctx context.Context, m MetricValues, c *metricsCache) error
|
|||||||
m.Set(sysCPUNice, cpuNiceVal)
|
m.Set(sysCPUNice, cpuNiceVal)
|
||||||
cpuStealVal := math.Round(ts.Steal/tot*100*100) / 100
|
cpuStealVal := math.Round(ts.Steal/tot*100*100) / 100
|
||||||
m.Set(sysCPUSteal, cpuStealVal)
|
m.Set(sysCPUSteal, cpuStealVal)
|
||||||
|
}
|
||||||
|
|
||||||
// metrics-resource.go runs a job to collect resource metrics including their Avg values and
|
// metrics-resource.go runs a job to collect resource metrics including their Avg values and
|
||||||
// stores them in resourceMetricsMap. We can use it to get the Avg values of CPU idle and IOWait.
|
// stores them in resourceMetricsMap. We can use it to get the Avg values of CPU idle and IOWait.
|
||||||
|
|||||||
@@ -156,12 +156,14 @@ func loadProcessMetrics(ctx context.Context, m MetricValues, c *metricsCache) er
|
|||||||
m.Set(processUptimeSeconds, time.Since(globalBootTime).Seconds())
|
m.Set(processUptimeSeconds, time.Since(globalBootTime).Seconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS != globalWindowsOSName && runtime.GOOS != globalMacOSName {
|
||||||
p, err := procfs.Self()
|
p, err := procfs.Self()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metricsLogIf(ctx, err)
|
metricsLogIf(ctx, err)
|
||||||
} else {
|
} else {
|
||||||
loadProcFSMetrics(ctx, p, m)
|
loadProcFSMetrics(ctx, p, m)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if globalIsDistErasure && globalLockServer != nil {
|
if globalIsDistErasure && globalLockServer != nil {
|
||||||
st := globalLockServer.stats()
|
st := globalLockServer.stats()
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const (
|
|||||||
clusterIAMCollectorPath collectorPath = "/cluster/iam"
|
clusterIAMCollectorPath collectorPath = "/cluster/iam"
|
||||||
clusterConfigCollectorPath collectorPath = "/cluster/config"
|
clusterConfigCollectorPath collectorPath = "/cluster/config"
|
||||||
|
|
||||||
|
ilmCollectorPath collectorPath = "/ilm"
|
||||||
auditCollectorPath collectorPath = "/audit"
|
auditCollectorPath collectorPath = "/audit"
|
||||||
loggerWebhookCollectorPath collectorPath = "/logger/webhook"
|
loggerWebhookCollectorPath collectorPath = "/logger/webhook"
|
||||||
replicationCollectorPath collectorPath = "/replication"
|
replicationCollectorPath collectorPath = "/replication"
|
||||||
@@ -382,6 +383,17 @@ func newMetricGroups(r *prometheus.Registry) *metricsV3Collection {
|
|||||||
loadAuditMetrics,
|
loadAuditMetrics,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ilmMG := NewMetricsGroup(ilmCollectorPath,
|
||||||
|
[]MetricDescriptor{
|
||||||
|
ilmExpiryPendingTasksMD,
|
||||||
|
ilmTransitionActiveTasksMD,
|
||||||
|
ilmTransitionPendingTasksMD,
|
||||||
|
ilmTransitionMissedImmediateTasksMD,
|
||||||
|
ilmVersionsScannedMD,
|
||||||
|
},
|
||||||
|
loadILMMetrics,
|
||||||
|
)
|
||||||
|
|
||||||
allMetricGroups := []*MetricsGroup{
|
allMetricGroups := []*MetricsGroup{
|
||||||
apiRequestsMG,
|
apiRequestsMG,
|
||||||
bucketAPIMG,
|
bucketAPIMG,
|
||||||
@@ -402,6 +414,7 @@ func newMetricGroups(r *prometheus.Registry) *metricsV3Collection {
|
|||||||
clusterReplicationMG,
|
clusterReplicationMG,
|
||||||
clusterConfigMG,
|
clusterConfigMG,
|
||||||
|
|
||||||
|
ilmMG,
|
||||||
scannerMG,
|
scannerMG,
|
||||||
auditMG,
|
auditMG,
|
||||||
loggerWebhookMG,
|
loggerWebhookMG,
|
||||||
|
|||||||
@@ -215,9 +215,9 @@ func (d *naughtyDisk) RenameFile(ctx context.Context, srcVolume, srcPath, dstVol
|
|||||||
return d.disk.RenameFile(ctx, srcVolume, srcPath, dstVolume, dstPath)
|
return d.disk.RenameFile(ctx, srcVolume, srcPath, dstVolume, dstPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *naughtyDisk) CheckParts(ctx context.Context, volume string, path string, fi FileInfo) (err error) {
|
func (d *naughtyDisk) CheckParts(ctx context.Context, volume string, path string, fi FileInfo) (*CheckPartsResp, error) {
|
||||||
if err := d.calcError(); err != nil {
|
if err := d.calcError(); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
return d.disk.CheckParts(ctx, volume, path, fi)
|
return d.disk.CheckParts(ctx, volume, path, fi)
|
||||||
}
|
}
|
||||||
@@ -289,9 +289,9 @@ func (d *naughtyDisk) ReadXL(ctx context.Context, volume string, path string, re
|
|||||||
return d.disk.ReadXL(ctx, volume, path, readData)
|
return d.disk.ReadXL(ctx, volume, path, readData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *naughtyDisk) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) error {
|
func (d *naughtyDisk) VerifyFile(ctx context.Context, volume, path string, fi FileInfo) (*CheckPartsResp, error) {
|
||||||
if err := d.calcError(); err != nil {
|
if err := d.calcError(); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
return d.disk.VerifyFile(ctx, volume, path, fi)
|
return d.disk.VerifyFile(ctx, volume, path, fi)
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-10
@@ -19,7 +19,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -32,8 +31,16 @@ import (
|
|||||||
xnet "github.com/minio/pkg/v3/net"
|
xnet "github.com/minio/pkg/v3/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
// IPv4 addresses of localhost.
|
// IPv4 addresses of localhost.
|
||||||
var localIP4 = mustGetLocalIP4()
|
localIP4 = mustGetLocalIP4()
|
||||||
|
|
||||||
|
// IPv6 addresses of localhost.
|
||||||
|
localIP6 = mustGetLocalIP6()
|
||||||
|
|
||||||
|
// List of all local loopback addresses.
|
||||||
|
localLoopbacks = mustGetLocalLoopbacks()
|
||||||
|
)
|
||||||
|
|
||||||
// mustSplitHostPort is a wrapper to net.SplitHostPort() where error is assumed to be a fatal.
|
// mustSplitHostPort is a wrapper to net.SplitHostPort() where error is assumed to be a fatal.
|
||||||
func mustSplitHostPort(hostPort string) (host, port string) {
|
func mustSplitHostPort(hostPort string) (host, port string) {
|
||||||
@@ -74,6 +81,16 @@ func mustGetLocalIPs() (ipList []net.IP) {
|
|||||||
return ipList
|
return ipList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustGetLocalLoopbacks() (ipList set.StringSet) {
|
||||||
|
ipList = set.NewStringSet()
|
||||||
|
for _, ip := range mustGetLocalIPs() {
|
||||||
|
if ip != nil && ip.IsLoopback() {
|
||||||
|
ipList.Add(ip.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// mustGetLocalIP4 returns IPv4 addresses of localhost. It panics on error.
|
// mustGetLocalIP4 returns IPv4 addresses of localhost. It panics on error.
|
||||||
func mustGetLocalIP4() (ipList set.StringSet) {
|
func mustGetLocalIP4() (ipList set.StringSet) {
|
||||||
ipList = set.NewStringSet()
|
ipList = set.NewStringSet()
|
||||||
@@ -160,15 +177,15 @@ func getConsoleEndpoints() (consoleEndpoints []string) {
|
|||||||
}
|
}
|
||||||
var ipList []string
|
var ipList []string
|
||||||
if globalMinioConsoleHost == "" {
|
if globalMinioConsoleHost == "" {
|
||||||
ipList = sortIPs(mustGetLocalIP4().ToSlice())
|
ipList = sortIPs(localIP4.ToSlice())
|
||||||
ipList = append(ipList, mustGetLocalIP6().ToSlice()...)
|
ipList = append(ipList, localIP6.ToSlice()...)
|
||||||
} else {
|
} else {
|
||||||
ipList = []string{globalMinioConsoleHost}
|
ipList = []string{globalMinioConsoleHost}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
consoleEndpoints = make([]string, 0, len(ipList))
|
||||||
for _, ip := range ipList {
|
for _, ip := range ipList {
|
||||||
endpoint := fmt.Sprintf("%s://%s", getURLScheme(globalIsTLS), net.JoinHostPort(ip, globalMinioConsolePort))
|
consoleEndpoints = append(consoleEndpoints, getURLScheme(globalIsTLS)+"://"+net.JoinHostPort(ip, globalMinioConsolePort))
|
||||||
consoleEndpoints = append(consoleEndpoints, endpoint)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return consoleEndpoints
|
return consoleEndpoints
|
||||||
@@ -180,15 +197,15 @@ func getAPIEndpoints() (apiEndpoints []string) {
|
|||||||
}
|
}
|
||||||
var ipList []string
|
var ipList []string
|
||||||
if globalMinioHost == "" {
|
if globalMinioHost == "" {
|
||||||
ipList = sortIPs(mustGetLocalIP4().ToSlice())
|
ipList = sortIPs(localIP4.ToSlice())
|
||||||
ipList = append(ipList, mustGetLocalIP6().ToSlice()...)
|
ipList = append(ipList, localIP6.ToSlice()...)
|
||||||
} else {
|
} else {
|
||||||
ipList = []string{globalMinioHost}
|
ipList = []string{globalMinioHost}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apiEndpoints = make([]string, 0, len(ipList))
|
||||||
for _, ip := range ipList {
|
for _, ip := range ipList {
|
||||||
endpoint := fmt.Sprintf("%s://%s", getURLScheme(globalIsTLS), net.JoinHostPort(ip, globalMinioPort))
|
apiEndpoints = append(apiEndpoints, getURLScheme(globalIsTLS)+"://"+net.JoinHostPort(ip, globalMinioPort))
|
||||||
apiEndpoints = append(apiEndpoints, endpoint)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiEndpoints
|
return apiEndpoints
|
||||||
|
|||||||
+5
-5
@@ -424,7 +424,7 @@ func (sys *NotificationSys) SignalConfigReload(subSys string) []NotificationPeer
|
|||||||
}
|
}
|
||||||
client := client
|
client := client
|
||||||
ng.Go(GlobalContext, func() error {
|
ng.Go(GlobalContext, func() error {
|
||||||
return client.SignalService(serviceReloadDynamic, subSys, false)
|
return client.SignalService(serviceReloadDynamic, subSys, false, nil)
|
||||||
}, idx, *client.host)
|
}, idx, *client.host)
|
||||||
}
|
}
|
||||||
return ng.Wait()
|
return ng.Wait()
|
||||||
@@ -440,14 +440,14 @@ func (sys *NotificationSys) SignalService(sig serviceSignal) []NotificationPeerE
|
|||||||
client := client
|
client := client
|
||||||
ng.Go(GlobalContext, func() error {
|
ng.Go(GlobalContext, func() error {
|
||||||
// force == true preserves the current behavior
|
// force == true preserves the current behavior
|
||||||
return client.SignalService(sig, "", false)
|
return client.SignalService(sig, "", false, nil)
|
||||||
}, idx, *client.host)
|
}, idx, *client.host)
|
||||||
}
|
}
|
||||||
return ng.Wait()
|
return ng.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignalServiceV2 - calls signal service RPC call on all peers with v2 API
|
// SignalServiceV2 - calls signal service RPC call on all peers with v2 API
|
||||||
func (sys *NotificationSys) SignalServiceV2(sig serviceSignal, dryRun bool) []NotificationPeerErr {
|
func (sys *NotificationSys) SignalServiceV2(sig serviceSignal, dryRun bool, execAt *time.Time) []NotificationPeerErr {
|
||||||
ng := WithNPeers(len(sys.peerClients))
|
ng := WithNPeers(len(sys.peerClients))
|
||||||
for idx, client := range sys.peerClients {
|
for idx, client := range sys.peerClients {
|
||||||
if client == nil {
|
if client == nil {
|
||||||
@@ -455,7 +455,7 @@ func (sys *NotificationSys) SignalServiceV2(sig serviceSignal, dryRun bool) []No
|
|||||||
}
|
}
|
||||||
client := client
|
client := client
|
||||||
ng.Go(GlobalContext, func() error {
|
ng.Go(GlobalContext, func() error {
|
||||||
return client.SignalService(sig, "", dryRun)
|
return client.SignalService(sig, "", dryRun, execAt)
|
||||||
}, idx, *client.host)
|
}, idx, *client.host)
|
||||||
}
|
}
|
||||||
return ng.Wait()
|
return ng.Wait()
|
||||||
@@ -1314,7 +1314,7 @@ func (sys *NotificationSys) ServiceFreeze(ctx context.Context, freeze bool) []No
|
|||||||
}
|
}
|
||||||
client := client
|
client := client
|
||||||
ng.Go(GlobalContext, func() error {
|
ng.Go(GlobalContext, func() error {
|
||||||
return client.SignalService(serviceSig, "", false)
|
return client.SignalService(serviceSig, "", false, nil)
|
||||||
}, idx, *client.host)
|
}, idx, *client.host)
|
||||||
}
|
}
|
||||||
nerrs := ng.Wait()
|
nerrs := ng.Wait()
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ func (o ObjectInfo) ExpiresStr() string {
|
|||||||
|
|
||||||
// ArchiveInfo returns any saved zip archive meta information.
|
// ArchiveInfo returns any saved zip archive meta information.
|
||||||
// It will be decrypted if needed.
|
// It will be decrypted if needed.
|
||||||
func (o *ObjectInfo) ArchiveInfo() []byte {
|
func (o *ObjectInfo) ArchiveInfo(h http.Header) []byte {
|
||||||
if len(o.UserDefined) == 0 {
|
if len(o.UserDefined) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -243,7 +243,7 @@ func (o *ObjectInfo) ArchiveInfo() []byte {
|
|||||||
}
|
}
|
||||||
data := []byte(z)
|
data := []byte(z)
|
||||||
if v, ok := o.UserDefined[archiveTypeMetadataKey]; ok && v == archiveTypeEnc {
|
if v, ok := o.UserDefined[archiveTypeMetadataKey]; ok && v == archiveTypeEnc {
|
||||||
decrypted, err := o.metadataDecrypter()(archiveTypeEnc, data)
|
decrypted, err := o.metadataDecrypter(h)(archiveTypeEnc, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
encLogIf(GlobalContext, err)
|
encLogIf(GlobalContext, err)
|
||||||
return nil
|
return nil
|
||||||
@@ -326,6 +326,7 @@ func (ri ReplicateObjectInfo) ToObjectInfo() ObjectInfo {
|
|||||||
VersionPurgeStatusInternal: ri.VersionPurgeStatusInternal,
|
VersionPurgeStatusInternal: ri.VersionPurgeStatusInternal,
|
||||||
DeleteMarker: true,
|
DeleteMarker: true,
|
||||||
UserDefined: map[string]string{},
|
UserDefined: map[string]string{},
|
||||||
|
Checksum: ri.Checksum,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,6 +358,7 @@ type ReplicateObjectInfo struct {
|
|||||||
TargetStatuses map[string]replication.StatusType
|
TargetStatuses map[string]replication.StatusType
|
||||||
TargetPurgeStatuses map[string]VersionPurgeStatusType
|
TargetPurgeStatuses map[string]VersionPurgeStatusType
|
||||||
ReplicationTimestamp time.Time
|
ReplicationTimestamp time.Time
|
||||||
|
Checksum []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// MultipartInfo captures metadata information about the uploadId
|
// MultipartInfo captures metadata information about the uploadId
|
||||||
|
|||||||
@@ -1896,9 +1896,9 @@ func (z *PartInfo) Msgsize() (s int) {
|
|||||||
// MarshalMsg implements msgp.Marshaler
|
// MarshalMsg implements msgp.Marshaler
|
||||||
func (z *ReplicateObjectInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
func (z *ReplicateObjectInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
o = msgp.Require(b, z.Msgsize())
|
o = msgp.Require(b, z.Msgsize())
|
||||||
// map header, size 25
|
// map header, size 26
|
||||||
// string "Name"
|
// string "Name"
|
||||||
o = append(o, 0xde, 0x0, 0x19, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
o = append(o, 0xde, 0x0, 0x1a, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||||
o = msgp.AppendString(o, z.Name)
|
o = msgp.AppendString(o, z.Name)
|
||||||
// string "Bucket"
|
// string "Bucket"
|
||||||
o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
|
o = append(o, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
|
||||||
@@ -2012,6 +2012,9 @@ func (z *ReplicateObjectInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
// string "ReplicationTimestamp"
|
// string "ReplicationTimestamp"
|
||||||
o = append(o, 0xb4, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70)
|
o = append(o, 0xb4, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70)
|
||||||
o = msgp.AppendTime(o, z.ReplicationTimestamp)
|
o = msgp.AppendTime(o, z.ReplicationTimestamp)
|
||||||
|
// string "Checksum"
|
||||||
|
o = append(o, 0xa8, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d)
|
||||||
|
o = msgp.AppendBytes(o, z.Checksum)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2231,6 +2234,12 @@ func (z *ReplicateObjectInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
err = msgp.WrapError(err, "ReplicationTimestamp")
|
err = msgp.WrapError(err, "ReplicationTimestamp")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "Checksum":
|
||||||
|
z.Checksum, bts, err = msgp.ReadBytesBytes(bts, z.Checksum)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Checksum")
|
||||||
|
return
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
bts, err = msgp.Skip(bts)
|
bts, err = msgp.Skip(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2259,7 +2268,7 @@ func (z *ReplicateObjectInfo) Msgsize() (s int) {
|
|||||||
s += msgp.StringPrefixSize + len(za0003) + za0004.Msgsize()
|
s += msgp.StringPrefixSize + len(za0003) + za0004.Msgsize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s += 21 + msgp.TimeSize
|
s += 21 + msgp.TimeSize + 9 + msgp.BytesPrefixSize + len(z.Checksum)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ type DeleteBucketOptions struct {
|
|||||||
type BucketOptions struct {
|
type BucketOptions struct {
|
||||||
Deleted bool // true only when site replication is enabled
|
Deleted bool // true only when site replication is enabled
|
||||||
Cached bool // true only when we are requesting a cached response instead of hitting the disk for example ListBuckets() call.
|
Cached bool // true only when we are requesting a cached response instead of hitting the disk for example ListBuckets() call.
|
||||||
|
NoMetadata bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetReplicaStatus sets replica status and timestamp for delete operations in ObjectOptions
|
// SetReplicaStatus sets replica status and timestamp for delete operations in ObjectOptions
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ import (
|
|||||||
// MarshalMsg implements msgp.Marshaler
|
// MarshalMsg implements msgp.Marshaler
|
||||||
func (z BucketOptions) MarshalMsg(b []byte) (o []byte, err error) {
|
func (z BucketOptions) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
o = msgp.Require(b, z.Msgsize())
|
o = msgp.Require(b, z.Msgsize())
|
||||||
// map header, size 2
|
// map header, size 3
|
||||||
// string "Deleted"
|
// string "Deleted"
|
||||||
o = append(o, 0x82, 0xa7, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64)
|
o = append(o, 0x83, 0xa7, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64)
|
||||||
o = msgp.AppendBool(o, z.Deleted)
|
o = msgp.AppendBool(o, z.Deleted)
|
||||||
// string "Cached"
|
// string "Cached"
|
||||||
o = append(o, 0xa6, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64)
|
o = append(o, 0xa6, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64)
|
||||||
o = msgp.AppendBool(o, z.Cached)
|
o = msgp.AppendBool(o, z.Cached)
|
||||||
|
// string "NoMetadata"
|
||||||
|
o = append(o, 0xaa, 0x4e, 0x6f, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61)
|
||||||
|
o = msgp.AppendBool(o, z.NoMetadata)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +52,12 @@ func (z *BucketOptions) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
err = msgp.WrapError(err, "Cached")
|
err = msgp.WrapError(err, "Cached")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "NoMetadata":
|
||||||
|
z.NoMetadata, bts, err = msgp.ReadBoolBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "NoMetadata")
|
||||||
|
return
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
bts, err = msgp.Skip(bts)
|
bts, err = msgp.Skip(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -63,7 +72,7 @@ func (z *BucketOptions) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
|
|
||||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||||
func (z BucketOptions) Msgsize() (s int) {
|
func (z BucketOptions) Msgsize() (s int) {
|
||||||
s = 1 + 8 + msgp.BoolSize + 7 + msgp.BoolSize
|
s = 1 + 8 + msgp.BoolSize + 7 + msgp.BoolSize + 11 + msgp.BoolSize
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,15 @@ func getDefaultOpts(header http.Header, copySource bool, metadata map[string]str
|
|||||||
var sse encrypt.ServerSide
|
var sse encrypt.ServerSide
|
||||||
|
|
||||||
opts = ObjectOptions{UserDefined: metadata}
|
opts = ObjectOptions{UserDefined: metadata}
|
||||||
|
if v, ok := header[xhttp.MinIOSourceProxyRequest]; ok {
|
||||||
|
opts.ProxyHeaderSet = true
|
||||||
|
opts.ProxyRequest = strings.Join(v, "") == "true"
|
||||||
|
}
|
||||||
|
if _, ok := header[xhttp.MinIOSourceReplicationRequest]; ok {
|
||||||
|
opts.ReplicationRequest = true
|
||||||
|
}
|
||||||
|
opts.Speedtest = header.Get(globalObjectPerfUserMetadata) != ""
|
||||||
|
|
||||||
if copySource {
|
if copySource {
|
||||||
if crypto.SSECopy.IsRequested(header) {
|
if crypto.SSECopy.IsRequested(header) {
|
||||||
clientKey, err = crypto.SSECopy.ParseHTTP(header)
|
clientKey, err = crypto.SSECopy.ParseHTTP(header)
|
||||||
@@ -66,14 +75,7 @@ func getDefaultOpts(header http.Header, copySource bool, metadata map[string]str
|
|||||||
if crypto.S3.IsRequested(header) || (metadata != nil && crypto.S3.IsEncrypted(metadata)) {
|
if crypto.S3.IsRequested(header) || (metadata != nil && crypto.S3.IsEncrypted(metadata)) {
|
||||||
opts.ServerSideEncryption = encrypt.NewSSE()
|
opts.ServerSideEncryption = encrypt.NewSSE()
|
||||||
}
|
}
|
||||||
if v, ok := header[xhttp.MinIOSourceProxyRequest]; ok {
|
|
||||||
opts.ProxyHeaderSet = true
|
|
||||||
opts.ProxyRequest = strings.Join(v, "") == "true"
|
|
||||||
}
|
|
||||||
if _, ok := header[xhttp.MinIOSourceReplicationRequest]; ok {
|
|
||||||
opts.ReplicationRequest = true
|
|
||||||
}
|
|
||||||
opts.Speedtest = header.Get(globalObjectPerfUserMetadata) != ""
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,5 +496,8 @@ func completeMultipartOpts(ctx context.Context, r *http.Request, bucket, object
|
|||||||
if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok {
|
if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok {
|
||||||
opts.ReplicationRequest = true
|
opts.ReplicationRequest = true
|
||||||
}
|
}
|
||||||
|
if r.Header.Get(ReplicationSsecChecksumHeader) != "" {
|
||||||
|
opts.UserDefined[ReplicationSsecChecksumHeader] = r.Header.Get(ReplicationSsecChecksumHeader)
|
||||||
|
}
|
||||||
return opts, nil
|
return opts, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-5
@@ -80,6 +80,18 @@ const (
|
|||||||
compMinIndexSize = 8 << 20
|
compMinIndexSize = 8 << 20
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// getkeyeparator - returns the separator to be used for
|
||||||
|
// persisting on drive.
|
||||||
|
//
|
||||||
|
// - ":" is used on non-windows platforms
|
||||||
|
// - "_" is used on windows platforms
|
||||||
|
func getKeySeparator() string {
|
||||||
|
if runtime.GOOS == globalWindowsOSName {
|
||||||
|
return "_"
|
||||||
|
}
|
||||||
|
return ":"
|
||||||
|
}
|
||||||
|
|
||||||
// isMinioBucket returns true if given bucket is a MinIO internal
|
// isMinioBucket returns true if given bucket is a MinIO internal
|
||||||
// bucket and false otherwise.
|
// bucket and false otherwise.
|
||||||
func isMinioMetaBucketName(bucket string) bool {
|
func isMinioMetaBucketName(bucket string) bool {
|
||||||
@@ -755,7 +767,7 @@ type ObjReaderFn func(inputReader io.Reader, h http.Header, cleanupFns ...func()
|
|||||||
// are called on Close() in FIFO order as passed in ObjReadFn(). NOTE: It is
|
// are called on Close() in FIFO order as passed in ObjReadFn(). NOTE: It is
|
||||||
// assumed that clean up functions do not panic (otherwise, they may
|
// assumed that clean up functions do not panic (otherwise, they may
|
||||||
// not all run!).
|
// not all run!).
|
||||||
func NewGetObjectReader(rs *HTTPRangeSpec, oi ObjectInfo, opts ObjectOptions) (
|
func NewGetObjectReader(rs *HTTPRangeSpec, oi ObjectInfo, opts ObjectOptions, h http.Header) (
|
||||||
fn ObjReaderFn, off, length int64, err error,
|
fn ObjReaderFn, off, length int64, err error,
|
||||||
) {
|
) {
|
||||||
if opts.CheckPrecondFn != nil && opts.CheckPrecondFn(oi) {
|
if opts.CheckPrecondFn != nil && opts.CheckPrecondFn(oi) {
|
||||||
@@ -810,7 +822,9 @@ func NewGetObjectReader(rs *HTTPRangeSpec, oi ObjectInfo, opts ObjectOptions) (
|
|||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
if isEncrypted {
|
if isEncrypted {
|
||||||
decrypt = oi.compressionIndexDecrypt
|
decrypt = func(b []byte) ([]byte, error) {
|
||||||
|
return oi.compressionIndexDecrypt(b, h)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// In case of range based queries on multiparts, the offset and length are reduced.
|
// In case of range based queries on multiparts, the offset and length are reduced.
|
||||||
off, decOff, firstPart, decryptSkip, seqNum = getCompressedOffsets(oi, off, decrypt)
|
off, decOff, firstPart, decryptSkip, seqNum = getCompressedOffsets(oi, off, decrypt)
|
||||||
@@ -982,8 +996,8 @@ func compressionIndexEncrypter(key crypto.ObjectKey, input func() []byte) func()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// compressionIndexDecrypt reverses compressionIndexEncrypter.
|
// compressionIndexDecrypt reverses compressionIndexEncrypter.
|
||||||
func (o *ObjectInfo) compressionIndexDecrypt(input []byte) ([]byte, error) {
|
func (o *ObjectInfo) compressionIndexDecrypt(input []byte, h http.Header) ([]byte, error) {
|
||||||
return o.metadataDecrypter()("compression-index", input)
|
return o.metadataDecrypter(h)("compression-index", input)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SealMD5CurrFn seals md5sum with object encryption key and returns sealed
|
// SealMD5CurrFn seals md5sum with object encryption key and returns sealed
|
||||||
@@ -1206,7 +1220,20 @@ func hasSpaceFor(di []*DiskInfo, size int64) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if nDisks < len(di)/2 || nDisks <= 0 {
|
if nDisks < len(di)/2 || nDisks <= 0 {
|
||||||
return false, fmt.Errorf("not enough online disks to calculate the available space, expected (%d)/(%d)", (len(di)/2)+1, nDisks)
|
var errs []error
|
||||||
|
for index, disk := range di {
|
||||||
|
switch {
|
||||||
|
case disk == nil:
|
||||||
|
errs = append(errs, fmt.Errorf("disk[%d]: offline", index))
|
||||||
|
case disk.Error != "":
|
||||||
|
errs = append(errs, fmt.Errorf("disk %s: %s", disk.Endpoint, disk.Error))
|
||||||
|
case disk.Total == 0:
|
||||||
|
errs = append(errs, fmt.Errorf("disk %s: total is zero", disk.Endpoint))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Log disk errors.
|
||||||
|
peersLogIf(context.Background(), errors.Join(errs...))
|
||||||
|
return false, fmt.Errorf("not enough online disks to calculate the available space, need %d, found %d", (len(di)/2)+1, nDisks)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check we have enough on each disk, ignoring diskFillFraction.
|
// Check we have enough on each disk, ignoring diskFillFraction.
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ func isETagEqual(left, right string) bool {
|
|||||||
// setPutObjHeaders sets all the necessary headers returned back
|
// setPutObjHeaders sets all the necessary headers returned back
|
||||||
// upon a success Put/Copy/CompleteMultipart/Delete requests
|
// upon a success Put/Copy/CompleteMultipart/Delete requests
|
||||||
// to activate delete only headers set delete as true
|
// to activate delete only headers set delete as true
|
||||||
func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, delete bool) {
|
func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, delete bool, h http.Header) {
|
||||||
// We must not use the http.Header().Set method here because some (broken)
|
// We must not use the http.Header().Set method here because some (broken)
|
||||||
// clients expect the ETag header key to be literally "ETag" - not "Etag" (case-sensitive).
|
// clients expect the ETag header key to be literally "ETag" - not "Etag" (case-sensitive).
|
||||||
// Therefore, we have to set the ETag directly as map entry.
|
// Therefore, we have to set the ETag directly as map entry.
|
||||||
@@ -357,7 +357,7 @@ func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, delete bool) {
|
|||||||
lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts())
|
lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hash.AddChecksumHeader(w, objInfo.decryptChecksums(0))
|
hash.AddChecksumHeader(w, objInfo.decryptChecksums(0, h))
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteObjectVersions(ctx context.Context, o ObjectLayer, bucket string, toDel []ObjectToDelete, lcEvent lifecycle.Event) {
|
func deleteObjectVersions(ctx context.Context, o ObjectLayer, bucket string, toDel []ObjectToDelete, lcEvent lifecycle.Event) {
|
||||||
|
|||||||
+55
-108
@@ -384,6 +384,10 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj
|
|||||||
}
|
}
|
||||||
|
|
||||||
cachedResult := globalCacheConfig.Enabled() && opts.VersionID == ""
|
cachedResult := globalCacheConfig.Enabled() && opts.VersionID == ""
|
||||||
|
if _, ok := crypto.IsRequested(r.Header); ok {
|
||||||
|
// No need to check cache for encrypted objects.
|
||||||
|
cachedResult = false
|
||||||
|
}
|
||||||
|
|
||||||
var update bool
|
var update bool
|
||||||
if cachedResult {
|
if cachedResult {
|
||||||
@@ -611,40 +615,7 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj
|
|||||||
|
|
||||||
if r.Header.Get(xhttp.AmzChecksumMode) == "ENABLED" && rs == nil {
|
if r.Header.Get(xhttp.AmzChecksumMode) == "ENABLED" && rs == nil {
|
||||||
// AWS S3 silently drops checksums on range requests.
|
// AWS S3 silently drops checksums on range requests.
|
||||||
hash.AddChecksumHeader(w, objInfo.decryptChecksums(opts.PartNumber))
|
hash.AddChecksumHeader(w, objInfo.decryptChecksums(opts.PartNumber, r.Header))
|
||||||
}
|
|
||||||
|
|
||||||
var buf *bytebufferpool.ByteBuffer
|
|
||||||
if update {
|
|
||||||
if globalCacheConfig.MatchesSize(objInfo.Size) {
|
|
||||||
buf = bytebufferpool.Get()
|
|
||||||
defer bytebufferpool.Put(buf)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
var data []byte
|
|
||||||
if buf != nil {
|
|
||||||
data = buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
asize, err := objInfo.GetActualSize()
|
|
||||||
if err != nil {
|
|
||||||
asize = objInfo.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
globalCacheConfig.Set(&cache.ObjectInfo{
|
|
||||||
Key: objInfo.Name,
|
|
||||||
Bucket: objInfo.Bucket,
|
|
||||||
ETag: objInfo.ETag,
|
|
||||||
ModTime: objInfo.ModTime,
|
|
||||||
Expires: objInfo.ExpiresStr(),
|
|
||||||
CacheControl: objInfo.CacheControl,
|
|
||||||
Metadata: cleanReservedKeys(objInfo.UserDefined),
|
|
||||||
Range: rangeHeader,
|
|
||||||
PartNumber: opts.PartNumber,
|
|
||||||
Size: asize,
|
|
||||||
Data: data,
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = setObjectHeaders(ctx, w, objInfo, rs, opts); err != nil {
|
if err = setObjectHeaders(ctx, w, objInfo, rs, opts); err != nil {
|
||||||
@@ -659,6 +630,12 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj
|
|||||||
|
|
||||||
setHeadGetRespHeaders(w, r.Form)
|
setHeadGetRespHeaders(w, r.Form)
|
||||||
|
|
||||||
|
var buf *bytebufferpool.ByteBuffer
|
||||||
|
if update && globalCacheConfig.MatchesSize(objInfo.Size) {
|
||||||
|
buf = bytebufferpool.Get()
|
||||||
|
defer bytebufferpool.Put(buf)
|
||||||
|
}
|
||||||
|
|
||||||
var iw io.Writer
|
var iw io.Writer
|
||||||
iw = w
|
iw = w
|
||||||
if buf != nil {
|
if buf != nil {
|
||||||
@@ -690,6 +667,31 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
var data []byte
|
||||||
|
if buf != nil {
|
||||||
|
data = buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
globalCacheConfig.Set(&cache.ObjectInfo{
|
||||||
|
Key: objInfo.Name,
|
||||||
|
Bucket: objInfo.Bucket,
|
||||||
|
ETag: objInfo.ETag,
|
||||||
|
ModTime: objInfo.ModTime,
|
||||||
|
Expires: objInfo.ExpiresStr(),
|
||||||
|
CacheControl: objInfo.CacheControl,
|
||||||
|
Metadata: cleanReservedKeys(objInfo.UserDefined),
|
||||||
|
Range: rangeHeader,
|
||||||
|
PartNumber: opts.PartNumber,
|
||||||
|
Size: int64(len(data)),
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
// Notify object accessed via a GET request.
|
// Notify object accessed via a GET request.
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
EventName: event.ObjectAccessedGet,
|
EventName: event.ObjectAccessedGet,
|
||||||
@@ -758,7 +760,7 @@ func (api objectAPIHandlers) getObjectAttributesHandler(ctx context.Context, obj
|
|||||||
w.Header().Del(xhttp.ContentType)
|
w.Header().Del(xhttp.ContentType)
|
||||||
|
|
||||||
if _, ok := opts.ObjectAttributes[xhttp.Checksum]; ok {
|
if _, ok := opts.ObjectAttributes[xhttp.Checksum]; ok {
|
||||||
chkSums := objInfo.decryptChecksums(0)
|
chkSums := objInfo.decryptChecksums(0, r.Header)
|
||||||
// AWS does not appear to append part number on this API call.
|
// AWS does not appear to append part number on this API call.
|
||||||
switch {
|
switch {
|
||||||
case chkSums["CRC32"] != "":
|
case chkSums["CRC32"] != "":
|
||||||
@@ -789,7 +791,7 @@ func (api objectAPIHandlers) getObjectAttributesHandler(ctx context.Context, obj
|
|||||||
OA.StorageClass = filterStorageClass(ctx, objInfo.StorageClass)
|
OA.StorageClass = filterStorageClass(ctx, objInfo.StorageClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
objInfo.decryptPartsChecksums()
|
objInfo.decryptPartsChecksums(r.Header)
|
||||||
|
|
||||||
if _, ok := opts.ObjectAttributes[xhttp.ObjectParts]; ok {
|
if _, ok := opts.ObjectAttributes[xhttp.ObjectParts]; ok {
|
||||||
OA.ObjectParts = new(objectAttributesParts)
|
OA.ObjectParts = new(objectAttributesParts)
|
||||||
@@ -949,8 +951,10 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob
|
|||||||
}
|
}
|
||||||
|
|
||||||
cachedResult := globalCacheConfig.Enabled() && opts.VersionID == ""
|
cachedResult := globalCacheConfig.Enabled() && opts.VersionID == ""
|
||||||
|
if _, ok := crypto.IsRequested(r.Header); ok {
|
||||||
var update bool
|
// No need to check cache for encrypted objects.
|
||||||
|
cachedResult = false
|
||||||
|
}
|
||||||
if cachedResult {
|
if cachedResult {
|
||||||
rc := &cache.CondCheck{}
|
rc := &cache.CondCheck{}
|
||||||
h := r.Header.Clone()
|
h := r.Header.Clone()
|
||||||
@@ -959,7 +963,7 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob
|
|||||||
}
|
}
|
||||||
rc.Init(bucket, object, h)
|
rc.Init(bucket, object, h)
|
||||||
|
|
||||||
ci, err := globalCacheConfig.Get(rc)
|
ci, _ := globalCacheConfig.Get(rc)
|
||||||
if ci != nil {
|
if ci != nil {
|
||||||
tgs, ok := ci.Metadata[xhttp.AmzObjectTagging]
|
tgs, ok := ci.Metadata[xhttp.AmzObjectTagging]
|
||||||
if ok {
|
if ok {
|
||||||
@@ -1018,9 +1022,6 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errors.Is(err, cache.ErrKeyMissing) {
|
|
||||||
update = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.FastGetObjInfo = true
|
opts.FastGetObjInfo = true
|
||||||
@@ -1124,24 +1125,6 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if update {
|
|
||||||
asize, err := objInfo.GetActualSize()
|
|
||||||
if err != nil {
|
|
||||||
asize = objInfo.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
defer globalCacheConfig.Set(&cache.ObjectInfo{
|
|
||||||
Key: objInfo.Name,
|
|
||||||
Bucket: objInfo.Bucket,
|
|
||||||
ETag: objInfo.ETag,
|
|
||||||
ModTime: objInfo.ModTime,
|
|
||||||
Expires: objInfo.ExpiresStr(),
|
|
||||||
CacheControl: objInfo.CacheControl,
|
|
||||||
Size: asize,
|
|
||||||
Metadata: cleanReservedKeys(objInfo.UserDefined),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate pre-conditions if any.
|
// Validate pre-conditions if any.
|
||||||
if checkPreconditions(ctx, w, r, objInfo, opts) {
|
if checkPreconditions(ctx, w, r, objInfo, opts) {
|
||||||
return
|
return
|
||||||
@@ -1169,7 +1152,7 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob
|
|||||||
|
|
||||||
if r.Header.Get(xhttp.AmzChecksumMode) == "ENABLED" && rs == nil {
|
if r.Header.Get(xhttp.AmzChecksumMode) == "ENABLED" && rs == nil {
|
||||||
// AWS S3 silently drops checksums on range requests.
|
// AWS S3 silently drops checksums on range requests.
|
||||||
hash.AddChecksumHeader(w, objInfo.decryptChecksums(opts.PartNumber))
|
hash.AddChecksumHeader(w, objInfo.decryptChecksums(opts.PartNumber, r.Header))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set standard object headers.
|
// Set standard object headers.
|
||||||
@@ -1472,6 +1455,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
VersionID: srcOpts.VersionID,
|
VersionID: srcOpts.VersionID,
|
||||||
Versioned: srcOpts.Versioned,
|
Versioned: srcOpts.Versioned,
|
||||||
VersionSuspended: srcOpts.VersionSuspended,
|
VersionSuspended: srcOpts.VersionSuspended,
|
||||||
|
ReplicationRequest: r.Header.Get(xhttp.MinIOSourceReplicationRequest) == "true",
|
||||||
}
|
}
|
||||||
getSSE := encrypt.SSE(srcOpts.ServerSideEncryption)
|
getSSE := encrypt.SSE(srcOpts.ServerSideEncryption)
|
||||||
if getSSE != srcOpts.ServerSideEncryption {
|
if getSSE != srcOpts.ServerSideEncryption {
|
||||||
@@ -1567,7 +1551,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
// Pass the decompressed stream to such calls.
|
// Pass the decompressed stream to such calls.
|
||||||
isDstCompressed := isCompressible(r.Header, dstObject) &&
|
isDstCompressed := isCompressible(r.Header, dstObject) &&
|
||||||
length > minCompressibleSize &&
|
length > minCompressibleSize &&
|
||||||
!isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI) && !cpSrcDstSame && !objectEncryption
|
!isRemoteCopyRequired(ctx, srcBucket, dstBucket, objectAPI) && !objectEncryption
|
||||||
if isDstCompressed {
|
if isDstCompressed {
|
||||||
compressMetadata = make(map[string]string, 2)
|
compressMetadata = make(map[string]string, 2)
|
||||||
// Preserving the compression metadata.
|
// Preserving the compression metadata.
|
||||||
@@ -1603,7 +1587,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Encryption parameters not present for this object.
|
// Encryption parameters not present for this object.
|
||||||
if crypto.SSEC.IsEncrypted(srcInfo.UserDefined) && !crypto.SSECopy.IsRequested(r.Header) {
|
if crypto.SSEC.IsEncrypted(srcInfo.UserDefined) && !crypto.SSECopy.IsRequested(r.Header) && r.Header.Get(xhttp.MinIOSourceReplicationRequest) != "true" {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidSSECustomerAlgorithm), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidSSECustomerAlgorithm), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1929,7 +1913,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
|
scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
|
||||||
}
|
}
|
||||||
|
|
||||||
setPutObjHeaders(w, objInfo, false)
|
setPutObjHeaders(w, objInfo, false, r.Header)
|
||||||
// We must not use the http.Header().Set method here because some (broken)
|
// We must not use the http.Header().Set method here because some (broken)
|
||||||
// clients expect the x-amz-copy-source-version-id header key to be literally
|
// clients expect the x-amz-copy-source-version-id header key to be literally
|
||||||
// "x-amz-copy-source-version-id"- not in canonicalized form, preserve it.
|
// "x-amz-copy-source-version-id"- not in canonicalized form, preserve it.
|
||||||
@@ -1951,22 +1935,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
Host: handlers.GetSourceIP(r),
|
Host: handlers.GetSourceIP(r),
|
||||||
})
|
})
|
||||||
|
|
||||||
asize, err := objInfo.GetActualSize()
|
|
||||||
if err != nil {
|
|
||||||
asize = objInfo.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
defer globalCacheConfig.Set(&cache.ObjectInfo{
|
|
||||||
Key: objInfo.Name,
|
|
||||||
Bucket: objInfo.Bucket,
|
|
||||||
ETag: objInfo.ETag,
|
|
||||||
ModTime: objInfo.ModTime,
|
|
||||||
Expires: objInfo.ExpiresStr(),
|
|
||||||
CacheControl: objInfo.CacheControl,
|
|
||||||
Size: asize,
|
|
||||||
Metadata: cleanReservedKeys(objInfo.UserDefined),
|
|
||||||
})
|
|
||||||
|
|
||||||
if !remoteCallRequired && !globalTierConfigMgr.Empty() {
|
if !remoteCallRequired && !globalTierConfigMgr.Empty() {
|
||||||
// Schedule object for immediate transition if eligible.
|
// Schedule object for immediate transition if eligible.
|
||||||
objInfo.ETag = origETag
|
objInfo.ETag = origETag
|
||||||
@@ -2139,7 +2107,8 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
|||||||
})
|
})
|
||||||
|
|
||||||
var buf *bytebufferpool.ByteBuffer
|
var buf *bytebufferpool.ByteBuffer
|
||||||
if globalCacheConfig.MatchesSize(size) {
|
// Disable cache for encrypted objects - headers are applied with sseConfig.Apply if auto encrypted.
|
||||||
|
if globalCacheConfig.MatchesSize(size) && !crypto.Requested(r.Header) {
|
||||||
buf = bytebufferpool.Get()
|
buf = bytebufferpool.Get()
|
||||||
defer bytebufferpool.Put(buf)
|
defer bytebufferpool.Put(buf)
|
||||||
}
|
}
|
||||||
@@ -2262,11 +2231,6 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if crypto.SSEC.IsRequested(r.Header) && isCompressible(r.Header, object) {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, crypto.ErrIncompatibleEncryptionWithCompression), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, objectEncryptionKey, err = EncryptRequest(hashReader, r, bucket, object, metadata)
|
reader, objectEncryptionKey, err = EncryptRequest(hashReader, r, bucket, object, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
@@ -2351,7 +2315,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
|||||||
scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
|
scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
|
||||||
}
|
}
|
||||||
|
|
||||||
setPutObjHeaders(w, objInfo, false)
|
setPutObjHeaders(w, objInfo, false, r.Header)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
var data []byte
|
var data []byte
|
||||||
@@ -2359,9 +2323,8 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
|||||||
data = buf.Bytes()
|
data = buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
asize, err := objInfo.GetActualSize()
|
if len(data) == 0 {
|
||||||
if err != nil {
|
return
|
||||||
asize = objInfo.Size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
globalCacheConfig.Set(&cache.ObjectInfo{
|
globalCacheConfig.Set(&cache.ObjectInfo{
|
||||||
@@ -2371,7 +2334,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
|||||||
ModTime: objInfo.ModTime,
|
ModTime: objInfo.ModTime,
|
||||||
Expires: objInfo.ExpiresStr(),
|
Expires: objInfo.ExpiresStr(),
|
||||||
CacheControl: objInfo.CacheControl,
|
CacheControl: objInfo.CacheControl,
|
||||||
Size: asize,
|
Size: int64(len(data)),
|
||||||
Metadata: cleanReservedKeys(objInfo.UserDefined),
|
Metadata: cleanReservedKeys(objInfo.UserDefined),
|
||||||
Data: data,
|
Data: data,
|
||||||
})
|
})
|
||||||
@@ -2728,22 +2691,6 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
|
|||||||
scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
|
scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
|
||||||
}
|
}
|
||||||
|
|
||||||
asize, err := objInfo.GetActualSize()
|
|
||||||
if err != nil {
|
|
||||||
asize = objInfo.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
defer globalCacheConfig.Set(&cache.ObjectInfo{
|
|
||||||
Key: objInfo.Name,
|
|
||||||
Bucket: objInfo.Bucket,
|
|
||||||
ETag: objInfo.ETag,
|
|
||||||
ModTime: objInfo.ModTime,
|
|
||||||
Expires: objInfo.ExpiresStr(),
|
|
||||||
CacheControl: objInfo.CacheControl,
|
|
||||||
Size: asize,
|
|
||||||
Metadata: cleanReservedKeys(objInfo.UserDefined),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Notify object created event.
|
// Notify object created event.
|
||||||
evt := eventArgs{
|
evt := eventArgs{
|
||||||
EventName: event.ObjectCreatedPut,
|
EventName: event.ObjectCreatedPut,
|
||||||
@@ -2943,7 +2890,7 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
|
|||||||
|
|
||||||
defer globalCacheConfig.Delete(bucket, object)
|
defer globalCacheConfig.Delete(bucket, object)
|
||||||
|
|
||||||
setPutObjHeaders(w, objInfo, true)
|
setPutObjHeaders(w, objInfo, true, r.Header)
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
|
|
||||||
eventName := event.ObjectRemovedDelete
|
eventName := event.ObjectRemovedDelete
|
||||||
|
|||||||
+68
-24
@@ -61,7 +61,7 @@ const (
|
|||||||
|
|
||||||
// Wrapper for calling HeadObject API handler tests for both Erasure multiple disks and FS single drive setup.
|
// Wrapper for calling HeadObject API handler tests for both Erasure multiple disks and FS single drive setup.
|
||||||
func TestAPIHeadObjectHandler(t *testing.T) {
|
func TestAPIHeadObjectHandler(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testAPIHeadObjectHandler, []string{"HeadObject"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPIHeadObjectHandler, endpoints: []string{"HeadObject"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPIHeadObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPIHeadObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
@@ -207,7 +207,7 @@ func TestAPIHeadObjectHandlerWithEncryption(t *testing.T) {
|
|||||||
defer func() { globalPolicySys = nil }()
|
defer func() { globalPolicySys = nil }()
|
||||||
|
|
||||||
defer DetectTestLeak(t)()
|
defer DetectTestLeak(t)()
|
||||||
ExecObjectLayerAPITest(t, testAPIHeadObjectHandlerWithEncryption, []string{"NewMultipart", "PutObjectPart", "CompleteMultipart", "GetObject", "PutObject", "HeadObject"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPIHeadObjectHandlerWithEncryption, endpoints: []string{"NewMultipart", "PutObjectPart", "CompleteMultipart", "GetObject", "PutObject", "HeadObject"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPIHeadObjectHandlerWithEncryption(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPIHeadObjectHandlerWithEncryption(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
@@ -707,7 +707,13 @@ func testAPIGetObjectWithMPHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
{"enc-mp-2", []int64{5487701, 5487799, 3}, mapCopy(metaWithSSEC)},
|
{"enc-mp-2", []int64{5487701, 5487799, 3}, mapCopy(metaWithSSEC)},
|
||||||
{"enc-mp-3", []int64{10499807, 10499963, 7}, mapCopy(metaWithSSEC)},
|
{"enc-mp-3", []int64{10499807, 10499963, 7}, mapCopy(metaWithSSEC)},
|
||||||
}
|
}
|
||||||
|
// SSEC can't be used with compression
|
||||||
|
globalCompressConfigMu.Lock()
|
||||||
|
globalCompressEnabled := globalCompressConfig.Enabled
|
||||||
|
globalCompressConfigMu.Unlock()
|
||||||
|
if globalCompressEnabled {
|
||||||
|
objectInputs = objectInputs[0:8]
|
||||||
|
}
|
||||||
// iterate through the above set of inputs and upload the object.
|
// iterate through the above set of inputs and upload the object.
|
||||||
for _, input := range objectInputs {
|
for _, input := range objectInputs {
|
||||||
uploadTestObject(t, apiRouter, credentials, bucketName, input.objectName, input.partLengths, input.metaData, false)
|
uploadTestObject(t, apiRouter, credentials, bucketName, input.objectName, input.partLengths, input.metaData, false)
|
||||||
@@ -898,6 +904,14 @@ func testAPIGetObjectWithPartNumberHandler(obj ObjectLayer, instanceType, bucket
|
|||||||
{"enc-mp-3", []int64{10499807, 10499963, 7}, mapCopy(metaWithSSEC)},
|
{"enc-mp-3", []int64{10499807, 10499963, 7}, mapCopy(metaWithSSEC)},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSEC can't be used with compression
|
||||||
|
globalCompressConfigMu.Lock()
|
||||||
|
globalCompressEnabled := globalCompressConfig.Enabled
|
||||||
|
globalCompressConfigMu.Unlock()
|
||||||
|
if globalCompressEnabled {
|
||||||
|
objectInputs = objectInputs[0:9]
|
||||||
|
}
|
||||||
|
|
||||||
// iterate through the above set of inputs and upload the object.
|
// iterate through the above set of inputs and upload the object.
|
||||||
for _, input := range objectInputs {
|
for _, input := range objectInputs {
|
||||||
uploadTestObject(t, apiRouter, credentials, bucketName, input.objectName, input.partLengths, input.metaData, false)
|
uploadTestObject(t, apiRouter, credentials, bucketName, input.objectName, input.partLengths, input.metaData, false)
|
||||||
@@ -2119,7 +2133,7 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
|||||||
// Wrapper for calling Copy Object API handler tests for both Erasure multiple disks and single node setup.
|
// Wrapper for calling Copy Object API handler tests for both Erasure multiple disks and single node setup.
|
||||||
func TestAPICopyObjectHandler(t *testing.T) {
|
func TestAPICopyObjectHandler(t *testing.T) {
|
||||||
defer DetectTestLeak(t)()
|
defer DetectTestLeak(t)()
|
||||||
ExecExtendedObjectLayerAPITest(t, testAPICopyObjectHandler, []string{"CopyObject"})
|
ExecExtendedObjectLayerAPITest(t, testAPICopyObjectHandler, []string{"CopyObject", "PutObject"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
@@ -2150,7 +2164,10 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
|||||||
new(bytes.Buffer),
|
new(bytes.Buffer),
|
||||||
new(bytes.Buffer),
|
new(bytes.Buffer),
|
||||||
}
|
}
|
||||||
|
bucketInfo, err := obj.GetBucketInfo(context.Background(), bucketName, BucketOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test -1: %s: Failed to get bucket info: <ERROR> %s", instanceType, err)
|
||||||
|
}
|
||||||
// set of inputs for uploading the objects before tests for downloading is done.
|
// set of inputs for uploading the objects before tests for downloading is done.
|
||||||
putObjectInputs := []struct {
|
putObjectInputs := []struct {
|
||||||
bucketName string
|
bucketName string
|
||||||
@@ -2167,21 +2184,29 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
|||||||
// used for anonymous HTTP request test.
|
// used for anonymous HTTP request test.
|
||||||
{bucketName, anonObject, int64(len(bytesData[0].byteData)), bytesData[0].byteData, bytesData[0].md5sum, make(map[string]string)},
|
{bucketName, anonObject, int64(len(bytesData[0].byteData)), bytesData[0].byteData, bytesData[0].md5sum, make(map[string]string)},
|
||||||
}
|
}
|
||||||
|
|
||||||
// iterate through the above set of inputs and upload the object.
|
// iterate through the above set of inputs and upload the object.
|
||||||
for i, input := range putObjectInputs {
|
for i, input := range putObjectInputs {
|
||||||
// uploading the object.
|
rec := httptest.NewRecorder()
|
||||||
var objInfo ObjectInfo
|
req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", input.bucketName, input.objectName),
|
||||||
objInfo, err = obj.PutObject(context.Background(), input.bucketName, input.objectName, mustGetPutObjReader(t, bytes.NewReader(input.textData), input.contentLength, input.md5sum, ""), ObjectOptions{UserDefined: input.metaData})
|
input.contentLength, bytes.NewReader(input.textData), credentials.AccessKey, credentials.SecretKey, nil)
|
||||||
// if object upload fails stop the test.
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Put Object case %d: Error uploading object: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for Put Object: <ERROR> %v", i, err)
|
||||||
}
|
}
|
||||||
if objInfo.ETag != input.md5sum {
|
apiRouter.ServeHTTP(rec, req)
|
||||||
t.Fatalf("Put Object case %d: Checksum mismatched: <ERROR> got %s, expected %s", i+1, input.md5sum, objInfo.ETag)
|
if rec.Code != http.StatusOK {
|
||||||
|
b, err := io.ReadAll(rec.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var apiErr APIErrorResponse
|
||||||
|
err = xml.Unmarshal(b, &apiErr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
gotErr := apiErr.Code
|
||||||
|
t.Errorf("test %d: want api got %q", i, gotErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// test cases with inputs and expected result for Copy Object.
|
// test cases with inputs and expected result for Copy Object.
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
bucketName string
|
bucketName string
|
||||||
@@ -2434,6 +2459,22 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
|
if bucketInfo.Versioning {
|
||||||
|
if strings.Contains(testCase.copySourceHeader, "versionId=null") {
|
||||||
|
testCase.expectedRespStatus = http.StatusNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values := url.Values{}
|
||||||
|
if testCase.expectedRespStatus == http.StatusOK {
|
||||||
|
r, err := obj.GetObjectNInfo(context.Background(), testCase.bucketName, objectName, nil, nil, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %d: %s reading completed file failed: <ERROR> %v", i, instanceType, err)
|
||||||
|
}
|
||||||
|
r.Close()
|
||||||
|
if r.ObjInfo.VersionID != "" {
|
||||||
|
values.Set(xhttp.VersionID, r.ObjInfo.VersionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
var reqV2 *http.Request
|
var reqV2 *http.Request
|
||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||||
@@ -2446,8 +2487,12 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
|||||||
}
|
}
|
||||||
// "X-Amz-Copy-Source" header contains the information about the source bucket and the object to copied.
|
// "X-Amz-Copy-Source" header contains the information about the source bucket and the object to copied.
|
||||||
if testCase.copySourceHeader != "" {
|
if testCase.copySourceHeader != "" {
|
||||||
|
if values.Encode() != "" && !strings.Contains(testCase.copySourceHeader, "?") {
|
||||||
|
req.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader+"?"+values.Encode())
|
||||||
|
} else {
|
||||||
req.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
|
req.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if testCase.copyModifiedHeader != "" {
|
if testCase.copyModifiedHeader != "" {
|
||||||
req.Header.Set("X-Amz-Copy-Source-If-Modified-Since", testCase.copyModifiedHeader)
|
req.Header.Set("X-Amz-Copy-Source-If-Modified-Since", testCase.copyModifiedHeader)
|
||||||
}
|
}
|
||||||
@@ -2552,10 +2597,10 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
|||||||
if testCase.copySourceSame {
|
if testCase.copySourceSame {
|
||||||
// encryption will rotate creds, so fail only for non-encryption scenario.
|
// encryption will rotate creds, so fail only for non-encryption scenario.
|
||||||
if GlobalKMS == nil {
|
if GlobalKMS == nil {
|
||||||
t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i, instanceType, testCase.expectedRespStatus, rec.Code)
|
t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i, instanceType, testCase.expectedRespStatus, recV2.Code)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
|
t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, recV2.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2588,7 +2633,7 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
|||||||
// The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it.
|
// The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it.
|
||||||
func TestAPINewMultipartHandler(t *testing.T) {
|
func TestAPINewMultipartHandler(t *testing.T) {
|
||||||
defer DetectTestLeak(t)()
|
defer DetectTestLeak(t)()
|
||||||
ExecObjectLayerAPITest(t, testAPINewMultipartHandler, []string{"NewMultipart"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPINewMultipartHandler, endpoints: []string{"NewMultipart"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPINewMultipartHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPINewMultipartHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
@@ -2727,7 +2772,7 @@ func testAPINewMultipartHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
// The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it.
|
// The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it.
|
||||||
func TestAPINewMultipartHandlerParallel(t *testing.T) {
|
func TestAPINewMultipartHandlerParallel(t *testing.T) {
|
||||||
defer DetectTestLeak(t)()
|
defer DetectTestLeak(t)()
|
||||||
ExecObjectLayerAPITest(t, testAPINewMultipartHandlerParallel, []string{"NewMultipart"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPINewMultipartHandlerParallel, endpoints: []string{"NewMultipart"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPINewMultipartHandlerParallel(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPINewMultipartHandlerParallel(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
@@ -2790,7 +2835,7 @@ func testAPINewMultipartHandlerParallel(obj ObjectLayer, instanceType, bucketNam
|
|||||||
// The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it.
|
// The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it.
|
||||||
func TestAPICompleteMultipartHandler(t *testing.T) {
|
func TestAPICompleteMultipartHandler(t *testing.T) {
|
||||||
defer DetectTestLeak(t)()
|
defer DetectTestLeak(t)()
|
||||||
ExecObjectLayerAPITest(t, testAPICompleteMultipartHandler, []string{"CompleteMultipart"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPICompleteMultipartHandler, endpoints: []string{"CompleteMultipart"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
@@ -2914,7 +2959,7 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
|
|||||||
s3MD5 := getCompleteMultipartMD5(inputParts[3].parts)
|
s3MD5 := getCompleteMultipartMD5(inputParts[3].parts)
|
||||||
|
|
||||||
// generating the response body content for the success case.
|
// generating the response body content for the success case.
|
||||||
successResponse := generateCompleteMultpartUploadResponse(bucketName, objectName, getGetObjectURL("", bucketName, objectName), ObjectInfo{ETag: s3MD5})
|
successResponse := generateCompleteMultipartUploadResponse(bucketName, objectName, getGetObjectURL("", bucketName, objectName), ObjectInfo{ETag: s3MD5}, nil)
|
||||||
encodedSuccessResponse := encodeResponse(successResponse)
|
encodedSuccessResponse := encodeResponse(successResponse)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -3159,7 +3204,7 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
|
|||||||
// The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it.
|
// The UploadID from the response body is parsed and its existence is asserted with an attempt to ListParts using it.
|
||||||
func TestAPIAbortMultipartHandler(t *testing.T) {
|
func TestAPIAbortMultipartHandler(t *testing.T) {
|
||||||
defer DetectTestLeak(t)()
|
defer DetectTestLeak(t)()
|
||||||
ExecObjectLayerAPITest(t, testAPIAbortMultipartHandler, []string{"AbortMultipart"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPIAbortMultipartHandler, endpoints: []string{"AbortMultipart"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPIAbortMultipartHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPIAbortMultipartHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
@@ -3319,7 +3364,7 @@ func testAPIAbortMultipartHandler(obj ObjectLayer, instanceType, bucketName stri
|
|||||||
// Wrapper for calling Delete Object API handler tests for both Erasure multiple disks and FS single drive setup.
|
// Wrapper for calling Delete Object API handler tests for both Erasure multiple disks and FS single drive setup.
|
||||||
func TestAPIDeleteObjectHandler(t *testing.T) {
|
func TestAPIDeleteObjectHandler(t *testing.T) {
|
||||||
defer DetectTestLeak(t)()
|
defer DetectTestLeak(t)()
|
||||||
ExecObjectLayerAPITest(t, testAPIDeleteObjectHandler, []string{"DeleteObject"})
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPIDeleteObjectHandler, endpoints: []string{"DeleteObject"}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPIDeleteObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPIDeleteObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
@@ -3881,8 +3926,7 @@ func testAPIPutObjectPartHandler(obj ObjectLayer, instanceType, bucketName strin
|
|||||||
// when signature type of the HTTP request is `Presigned`.
|
// when signature type of the HTTP request is `Presigned`.
|
||||||
func TestAPIListObjectPartsHandlerPreSign(t *testing.T) {
|
func TestAPIListObjectPartsHandlerPreSign(t *testing.T) {
|
||||||
defer DetectTestLeak(t)()
|
defer DetectTestLeak(t)()
|
||||||
ExecObjectLayerAPITest(t, testAPIListObjectPartsHandlerPreSign,
|
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: testAPIListObjectPartsHandlerPreSign, endpoints: []string{"PutObjectPart", "NewMultipart", "ListObjectParts"}})
|
||||||
[]string{"PutObjectPart", "NewMultipart", "ListObjectParts"})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPIListObjectPartsHandlerPreSign(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPIListObjectPartsHandlerPreSign(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import (
|
|||||||
sse "github.com/minio/minio/internal/bucket/encryption"
|
sse "github.com/minio/minio/internal/bucket/encryption"
|
||||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||||
"github.com/minio/minio/internal/bucket/replication"
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
"github.com/minio/minio/internal/config/cache"
|
|
||||||
"github.com/minio/minio/internal/config/dns"
|
"github.com/minio/minio/internal/config/dns"
|
||||||
"github.com/minio/minio/internal/config/storageclass"
|
"github.com/minio/minio/internal/config/storageclass"
|
||||||
"github.com/minio/minio/internal/crypto"
|
"github.com/minio/minio/internal/crypto"
|
||||||
@@ -116,11 +115,6 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if crypto.SSEC.IsRequested(r.Header) && isCompressible(r.Header, object) {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, crypto.ErrIncompatibleEncryptionWithCompression), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, sourceReplReq := r.Header[xhttp.MinIOSourceReplicationRequest]
|
_, sourceReplReq := r.Header[xhttp.MinIOSourceReplicationRequest]
|
||||||
ssecRepHeaders := []string{
|
ssecRepHeaders := []string{
|
||||||
"X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm",
|
"X-Minio-Replication-Server-Side-Encryption-Seal-Algorithm",
|
||||||
@@ -773,6 +767,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
|||||||
|
|
||||||
_, isEncrypted := crypto.IsEncrypted(mi.UserDefined)
|
_, isEncrypted := crypto.IsEncrypted(mi.UserDefined)
|
||||||
_, replicationStatus := mi.UserDefined[xhttp.AmzBucketReplicationStatus]
|
_, replicationStatus := mi.UserDefined[xhttp.AmzBucketReplicationStatus]
|
||||||
|
_, sourceReplReq := r.Header[xhttp.MinIOSourceReplicationRequest]
|
||||||
var objectEncryptionKey crypto.ObjectKey
|
var objectEncryptionKey crypto.ObjectKey
|
||||||
if isEncrypted {
|
if isEncrypted {
|
||||||
if !crypto.SSEC.IsRequested(r.Header) && crypto.SSEC.IsEncrypted(mi.UserDefined) && !replicationStatus {
|
if !crypto.SSEC.IsRequested(r.Header) && crypto.SSEC.IsEncrypted(mi.UserDefined) && !replicationStatus {
|
||||||
@@ -795,7 +790,6 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, sourceReplReq := r.Header[xhttp.MinIOSourceReplicationRequest]
|
|
||||||
if !(sourceReplReq && crypto.SSEC.IsEncrypted(mi.UserDefined)) {
|
if !(sourceReplReq && crypto.SSEC.IsEncrypted(mi.UserDefined)) {
|
||||||
// Calculating object encryption key
|
// Calculating object encryption key
|
||||||
key, err = decryptObjectMeta(key, bucket, object, mi.UserDefined)
|
key, err = decryptObjectMeta(key, bucket, object, mi.UserDefined)
|
||||||
@@ -847,6 +841,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
|||||||
}
|
}
|
||||||
opts.IndexCB = idxCb
|
opts.IndexCB = idxCb
|
||||||
|
|
||||||
|
opts.ReplicationRequest = sourceReplReq
|
||||||
putObjectPart := objectAPI.PutObjectPart
|
putObjectPart := objectAPI.PutObjectPart
|
||||||
|
|
||||||
partInfo, err := putObjectPart(ctx, bucket, object, uploadID, partID, pReader, opts)
|
partInfo, err := putObjectPart(ctx, bucket, object, uploadID, partID, pReader, opts)
|
||||||
@@ -1028,7 +1023,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setPutObjHeaders(w, objInfo, false)
|
setPutObjHeaders(w, objInfo, false, r.Header)
|
||||||
if dsc := mustReplicate(ctx, bucket, object, objInfo.getMustReplicateOptions(replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
|
if dsc := mustReplicate(ctx, bucket, object, objInfo.getMustReplicateOptions(replication.ObjectReplicationType, opts)); dsc.ReplicateAny() {
|
||||||
scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
|
scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
|
||||||
}
|
}
|
||||||
@@ -1040,7 +1035,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
|||||||
// Get object location.
|
// Get object location.
|
||||||
location := getObjectLocation(r, globalDomainNames, bucket, object)
|
location := getObjectLocation(r, globalDomainNames, bucket, object)
|
||||||
// Generate complete multipart response.
|
// Generate complete multipart response.
|
||||||
response := generateCompleteMultpartUploadResponse(bucket, object, location, objInfo)
|
response := generateCompleteMultipartUploadResponse(bucket, object, location, objInfo, r.Header)
|
||||||
encodedSuccessResponse := encodeResponse(response)
|
encodedSuccessResponse := encodeResponse(response)
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
@@ -1058,22 +1053,6 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
|||||||
}
|
}
|
||||||
sendEvent(evt)
|
sendEvent(evt)
|
||||||
|
|
||||||
asize, err := objInfo.GetActualSize()
|
|
||||||
if err != nil {
|
|
||||||
asize = objInfo.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
defer globalCacheConfig.Set(&cache.ObjectInfo{
|
|
||||||
Key: objInfo.Name,
|
|
||||||
Bucket: objInfo.Bucket,
|
|
||||||
ETag: objInfo.ETag,
|
|
||||||
ModTime: objInfo.ModTime,
|
|
||||||
Expires: objInfo.ExpiresStr(),
|
|
||||||
CacheControl: objInfo.CacheControl,
|
|
||||||
Size: asize,
|
|
||||||
Metadata: cleanReservedKeys(objInfo.UserDefined),
|
|
||||||
})
|
|
||||||
|
|
||||||
if objInfo.NumVersions > int(scannerExcessObjectVersions.Load()) {
|
if objInfo.NumVersions > int(scannerExcessObjectVersions.Load()) {
|
||||||
evt.EventName = event.ObjectManyVersions
|
evt.EventName = event.ObjectManyVersions
|
||||||
sendEvent(evt)
|
sendEvent(evt)
|
||||||
|
|||||||
@@ -511,12 +511,12 @@ func testBucketRecreateFails(obj ObjectLayer, instanceType string, t TestErrHand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableCompression(t *testing.T, encrypt bool) {
|
func enableCompression(t *testing.T, encrypt bool, mimeTypes []string, extensions []string) {
|
||||||
// Enable compression and exec...
|
// Enable compression and exec...
|
||||||
globalCompressConfigMu.Lock()
|
globalCompressConfigMu.Lock()
|
||||||
globalCompressConfig.Enabled = true
|
globalCompressConfig.Enabled = true
|
||||||
globalCompressConfig.MimeTypes = nil
|
globalCompressConfig.MimeTypes = mimeTypes
|
||||||
globalCompressConfig.Extensions = nil
|
globalCompressConfig.Extensions = extensions
|
||||||
globalCompressConfig.AllowEncrypted = encrypt
|
globalCompressConfig.AllowEncrypted = encrypt
|
||||||
globalCompressConfigMu.Unlock()
|
globalCompressConfigMu.Unlock()
|
||||||
if encrypt {
|
if encrypt {
|
||||||
@@ -553,11 +553,14 @@ func resetCompressEncryption() {
|
|||||||
GlobalKMS = nil
|
GlobalKMS = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func execExtended(t *testing.T, fn func(t *testing.T)) {
|
func execExtended(t *testing.T, fn func(t *testing.T, init func(), bucketOptions MakeBucketOptions)) {
|
||||||
// Exec with default settings...
|
// Exec with default settings...
|
||||||
resetCompressEncryption()
|
resetCompressEncryption()
|
||||||
t.Run("default", func(t *testing.T) {
|
t.Run("default", func(t *testing.T) {
|
||||||
fn(t)
|
fn(t, nil, MakeBucketOptions{})
|
||||||
|
})
|
||||||
|
t.Run("defaultVerioned", func(t *testing.T) {
|
||||||
|
fn(t, nil, MakeBucketOptions{VersioningEnabled: true})
|
||||||
})
|
})
|
||||||
|
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
@@ -565,28 +568,55 @@ func execExtended(t *testing.T, fn func(t *testing.T)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("compressed", func(t *testing.T) {
|
t.Run("compressed", func(t *testing.T) {
|
||||||
|
fn(t, func() {
|
||||||
resetCompressEncryption()
|
resetCompressEncryption()
|
||||||
enableCompression(t, false)
|
enableCompression(t, false, []string{"*"}, []string{"*"})
|
||||||
fn(t)
|
}, MakeBucketOptions{})
|
||||||
|
})
|
||||||
|
t.Run("compressedVerioned", func(t *testing.T) {
|
||||||
|
fn(t, func() {
|
||||||
|
resetCompressEncryption()
|
||||||
|
enableCompression(t, false, []string{"*"}, []string{"*"})
|
||||||
|
}, MakeBucketOptions{
|
||||||
|
VersioningEnabled: true,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("encrypted", func(t *testing.T) {
|
t.Run("encrypted", func(t *testing.T) {
|
||||||
|
fn(t, func() {
|
||||||
resetCompressEncryption()
|
resetCompressEncryption()
|
||||||
enableEncryption(t)
|
enableEncryption(t)
|
||||||
fn(t)
|
}, MakeBucketOptions{})
|
||||||
|
})
|
||||||
|
t.Run("encryptedVerioned", func(t *testing.T) {
|
||||||
|
fn(t, func() {
|
||||||
|
resetCompressEncryption()
|
||||||
|
enableEncryption(t)
|
||||||
|
}, MakeBucketOptions{
|
||||||
|
VersioningEnabled: true,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("compressed+encrypted", func(t *testing.T) {
|
t.Run("compressed+encrypted", func(t *testing.T) {
|
||||||
|
fn(t, func() {
|
||||||
resetCompressEncryption()
|
resetCompressEncryption()
|
||||||
enableCompression(t, true)
|
enableCompression(t, true, []string{"*"}, []string{"*"})
|
||||||
fn(t)
|
}, MakeBucketOptions{})
|
||||||
|
})
|
||||||
|
t.Run("compressed+encryptedVerioned", func(t *testing.T) {
|
||||||
|
fn(t, func() {
|
||||||
|
resetCompressEncryption()
|
||||||
|
enableCompression(t, true, []string{"*"}, []string{"*"})
|
||||||
|
}, MakeBucketOptions{
|
||||||
|
VersioningEnabled: true,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecExtendedObjectLayerTest will execute the tests with combinations of encrypted & compressed.
|
// ExecExtendedObjectLayerTest will execute the tests with combinations of encrypted & compressed.
|
||||||
// This can be used to test functionality when reading and writing data.
|
// This can be used to test functionality when reading and writing data.
|
||||||
func ExecExtendedObjectLayerTest(t *testing.T, objTest objTestType) {
|
func ExecExtendedObjectLayerTest(t *testing.T, objTest objTestType) {
|
||||||
execExtended(t, func(t *testing.T) {
|
execExtended(t, func(t *testing.T, init func(), bucketOptions MakeBucketOptions) {
|
||||||
ExecObjectLayerTest(t, objTest)
|
ExecObjectLayerTest(t, objTest)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -427,11 +427,14 @@ func (client *peerRESTClient) CommitBinary(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SignalService - sends signal to peer nodes.
|
// SignalService - sends signal to peer nodes.
|
||||||
func (client *peerRESTClient) SignalService(sig serviceSignal, subSys string, dryRun bool) error {
|
func (client *peerRESTClient) SignalService(sig serviceSignal, subSys string, dryRun bool, execAt *time.Time) error {
|
||||||
values := grid.NewMSS()
|
values := grid.NewMSS()
|
||||||
values.Set(peerRESTSignal, strconv.Itoa(int(sig)))
|
values.Set(peerRESTSignal, strconv.Itoa(int(sig)))
|
||||||
values.Set(peerRESTDryRun, strconv.FormatBool(dryRun))
|
values.Set(peerRESTDryRun, strconv.FormatBool(dryRun))
|
||||||
values.Set(peerRESTSubSys, subSys)
|
values.Set(peerRESTSubSys, subSys)
|
||||||
|
if execAt != nil {
|
||||||
|
values.Set(peerRESTExecAt, execAt.Format(time.RFC3339Nano))
|
||||||
|
}
|
||||||
_, err := signalServiceRPC.Call(context.Background(), client.gridConn(), values)
|
_, err := signalServiceRPC.Call(context.Background(), client.gridConn(), values)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
peerRESTVersion = "v39" // add more flags to speedtest API
|
peerRESTVersion = "v39" // add more flags to speedtest API
|
||||||
peerRESTVersionPrefix = SlashSeparator + peerRESTVersion
|
peerRESTVersionPrefix = SlashSeparator + peerRESTVersion
|
||||||
@@ -69,6 +71,7 @@ const (
|
|||||||
peerRESTURL = "url"
|
peerRESTURL = "url"
|
||||||
peerRESTSha256Sum = "sha256sum"
|
peerRESTSha256Sum = "sha256sum"
|
||||||
peerRESTReleaseInfo = "releaseinfo"
|
peerRESTReleaseInfo = "releaseinfo"
|
||||||
|
peerRESTExecAt = "exec-at"
|
||||||
|
|
||||||
peerRESTListenBucket = "bucket"
|
peerRESTListenBucket = "bucket"
|
||||||
peerRESTListenPrefix = "prefix"
|
peerRESTListenPrefix = "prefix"
|
||||||
@@ -76,3 +79,5 @@ const (
|
|||||||
peerRESTListenEvents = "events"
|
peerRESTListenEvents = "events"
|
||||||
peerRESTLogMask = "log-mask"
|
peerRESTLogMask = "log-mask"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const restartUpdateDelay = 250 * time.Millisecond
|
||||||
|
|||||||
@@ -693,6 +693,18 @@ func (s *peerRESTServer) SignalServiceHandler(vars *grid.MSS) (np grid.NoPayload
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return np, grid.NewRemoteErr(err)
|
return np, grid.NewRemoteErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait until the specified time before executing the signal.
|
||||||
|
if t := vars.Get(peerRESTExecAt); t != "" {
|
||||||
|
execAt, err := time.Parse(time.RFC3339Nano, vars.Get(peerRESTExecAt))
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(GlobalContext, "signalservice", err)
|
||||||
|
execAt = time.Now().Add(restartUpdateDelay)
|
||||||
|
}
|
||||||
|
if d := time.Until(execAt); d > 0 {
|
||||||
|
time.Sleep(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
signal := serviceSignal(si)
|
signal := serviceSignal(si)
|
||||||
switch signal {
|
switch signal {
|
||||||
case serviceRestart, serviceStop:
|
case serviceRestart, serviceStop:
|
||||||
|
|||||||
@@ -138,8 +138,12 @@ func (sys *S3PeerSys) HealBucket(ctx context.Context, bucket string, opts madmin
|
|||||||
poolErrs = append(poolErrs, reduceWriteQuorumErrs(ctx, perPoolErrs, bucketOpIgnoredErrs, quorum))
|
poolErrs = append(poolErrs, reduceWriteQuorumErrs(ctx, perPoolErrs, bucketOpIgnoredErrs, quorum))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !opts.Recreate {
|
||||||
|
// when there is no force recreate look for pool
|
||||||
|
// errors to recreate the bucket on all pools.
|
||||||
opts.Remove = isAllBucketsNotFound(poolErrs)
|
opts.Remove = isAllBucketsNotFound(poolErrs)
|
||||||
opts.Recreate = !opts.Remove
|
opts.Recreate = !opts.Remove
|
||||||
|
}
|
||||||
|
|
||||||
g = errgroup.WithNErrs(len(sys.peerClients))
|
g = errgroup.WithNErrs(len(sys.peerClients))
|
||||||
healBucketResults := make([]madmin.HealResultItem, len(sys.peerClients))
|
healBucketResults := make([]madmin.HealResultItem, len(sys.peerClients))
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ func healBucketLocal(ctx context.Context, bucket string, opts madmin.HealOpts) (
|
|||||||
g.Wait()
|
g.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the quorum lost volume only if its nor makred for delete
|
// Create the lost volume only if its not marked for delete
|
||||||
if !opts.Remove {
|
if !opts.Remove {
|
||||||
// Initialize sync waitgroup.
|
// Initialize sync waitgroup.
|
||||||
g = errgroup.WithNErrs(len(localDrives))
|
g = errgroup.WithNErrs(len(localDrives))
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func (api objectAPIHandlers) getObjectInArchiveFileHandler(ctx context.Context,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
zipInfo := zipObjInfo.ArchiveInfo()
|
zipInfo := zipObjInfo.ArchiveInfo(r.Header)
|
||||||
if len(zipInfo) == 0 {
|
if len(zipInfo) == 0 {
|
||||||
opts.EncryptFn, err = zipObjInfo.metadataEncryptFn(r.Header)
|
opts.EncryptFn, err = zipObjInfo.metadataEncryptFn(r.Header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -233,7 +233,7 @@ func (api objectAPIHandlers) getObjectInArchiveFileHandler(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listObjectsV2InArchive generates S3 listing result ListObjectsV2Info from zip file, all parameters are already validated by the caller.
|
// listObjectsV2InArchive generates S3 listing result ListObjectsV2Info from zip file, all parameters are already validated by the caller.
|
||||||
func listObjectsV2InArchive(ctx context.Context, objectAPI ObjectLayer, bucket, prefix, token, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (ListObjectsV2Info, error) {
|
func listObjectsV2InArchive(ctx context.Context, objectAPI ObjectLayer, bucket, prefix, token, delimiter string, maxKeys int, startAfter string, h http.Header) (ListObjectsV2Info, error) {
|
||||||
zipPath, _, err := splitZipExtensionPath(prefix)
|
zipPath, _, err := splitZipExtensionPath(prefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Return empty listing
|
// Return empty listing
|
||||||
@@ -246,7 +246,7 @@ func listObjectsV2InArchive(ctx context.Context, objectAPI ObjectLayer, bucket,
|
|||||||
return ListObjectsV2Info{}, nil
|
return ListObjectsV2Info{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
zipInfo := zipObjInfo.ArchiveInfo()
|
zipInfo := zipObjInfo.ArchiveInfo(h)
|
||||||
if len(zipInfo) == 0 {
|
if len(zipInfo) == 0 {
|
||||||
// Always update the latest version
|
// Always update the latest version
|
||||||
zipInfo, err = updateObjectMetadataWithZipInfo(ctx, objectAPI, bucket, zipPath, ObjectOptions{})
|
zipInfo, err = updateObjectMetadataWithZipInfo(ctx, objectAPI, bucket, zipPath, ObjectOptions{})
|
||||||
@@ -438,7 +438,7 @@ func (api objectAPIHandlers) headObjectInArchiveFileHandler(ctx context.Context,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
zipInfo := zipObjInfo.ArchiveInfo()
|
zipInfo := zipObjInfo.ArchiveInfo(r.Header)
|
||||||
if len(zipInfo) == 0 {
|
if len(zipInfo) == 0 {
|
||||||
opts.EncryptFn, err = zipObjInfo.metadataEncryptFn(r.Header)
|
opts.EncryptFn, err = zipObjInfo.metadataEncryptFn(r.Header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+31
-14
@@ -84,11 +84,12 @@ var ServerFlags = []cli.Flag{
|
|||||||
},
|
},
|
||||||
cli.DurationFlag{
|
cli.DurationFlag{
|
||||||
Name: "shutdown-timeout",
|
Name: "shutdown-timeout",
|
||||||
Value: xhttp.DefaultShutdownTimeout,
|
Value: time.Second * 30,
|
||||||
Usage: "shutdown timeout to gracefully shutdown server",
|
Usage: "shutdown timeout to gracefully shutdown server (DEPRECATED)",
|
||||||
EnvVar: "MINIO_SHUTDOWN_TIMEOUT",
|
EnvVar: "MINIO_SHUTDOWN_TIMEOUT",
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
cli.DurationFlag{
|
cli.DurationFlag{
|
||||||
Name: "idle-timeout",
|
Name: "idle-timeout",
|
||||||
Value: xhttp.DefaultIdleTimeout,
|
Value: xhttp.DefaultIdleTimeout,
|
||||||
@@ -415,7 +416,8 @@ func serverHandleCmdArgs(ctxt serverCtxt) {
|
|||||||
|
|
||||||
globalTCPOptions = xhttp.TCPOptions{
|
globalTCPOptions = xhttp.TCPOptions{
|
||||||
UserTimeout: int(ctxt.UserTimeout.Milliseconds()),
|
UserTimeout: int(ctxt.UserTimeout.Milliseconds()),
|
||||||
DriveOPTimeout: globalDriveConfig.GetOPTimeout,
|
// FIXME: Bring this back when we have valid way to handle deadlines
|
||||||
|
// DriveOPTimeout: globalDriveConfig.GetOPTimeout,
|
||||||
Interface: ctxt.Interface,
|
Interface: ctxt.Interface,
|
||||||
SendBufSize: ctxt.SendBufSize,
|
SendBufSize: ctxt.SendBufSize,
|
||||||
RecvBufSize: ctxt.RecvBufSize,
|
RecvBufSize: ctxt.RecvBufSize,
|
||||||
@@ -430,7 +432,7 @@ func serverHandleCmdArgs(ctxt serverCtxt) {
|
|||||||
RoundTripper: globalRemoteTargetTransport,
|
RoundTripper: globalRemoteTargetTransport,
|
||||||
Logger: func(err error) {
|
Logger: func(err error) {
|
||||||
if err != nil && !errors.Is(err, context.Canceled) {
|
if err != nil && !errors.Is(err, context.Canceled) {
|
||||||
replLogIf(GlobalContext, err)
|
proxyLogIf(GlobalContext, err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -679,10 +681,8 @@ func getServerListenAddrs() []string {
|
|||||||
// Use a string set to avoid duplication
|
// Use a string set to avoid duplication
|
||||||
addrs := set.NewStringSet()
|
addrs := set.NewStringSet()
|
||||||
// Listen on local interface to receive requests from Console
|
// Listen on local interface to receive requests from Console
|
||||||
for _, ip := range mustGetLocalIPs() {
|
for _, ip := range localLoopbacks.ToSlice() {
|
||||||
if ip != nil && ip.IsLoopback() {
|
addrs.Add(net.JoinHostPort(ip, globalMinioPort))
|
||||||
addrs.Add(net.JoinHostPort(ip.String(), globalMinioPort))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
host, _ := mustSplitHostPort(globalMinioAddr)
|
host, _ := mustSplitHostPort(globalMinioAddr)
|
||||||
if host != "" {
|
if host != "" {
|
||||||
@@ -739,6 +739,8 @@ func initializeLogRotate(ctx *cli.Context) (io.WriteCloser, error) {
|
|||||||
|
|
||||||
// serverMain handler called for 'minio server' command.
|
// serverMain handler called for 'minio server' command.
|
||||||
func serverMain(ctx *cli.Context) {
|
func serverMain(ctx *cli.Context) {
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
var warnings []string
|
var warnings []string
|
||||||
|
|
||||||
signal.Notify(globalOSSignalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
|
signal.Notify(globalOSSignalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
@@ -834,7 +836,7 @@ func serverMain(ctx *cli.Context) {
|
|||||||
|
|
||||||
// Set system resources to maximum.
|
// Set system resources to maximum.
|
||||||
bootstrapTrace("setMaxResources", func() {
|
bootstrapTrace("setMaxResources", func() {
|
||||||
_ = setMaxResources(ctx)
|
_ = setMaxResources(globalServerCtxt)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Verify kernel release and version.
|
// Verify kernel release and version.
|
||||||
@@ -865,7 +867,6 @@ func serverMain(ctx *cli.Context) {
|
|||||||
httpServer := xhttp.NewServer(getServerListenAddrs()).
|
httpServer := xhttp.NewServer(getServerListenAddrs()).
|
||||||
UseHandler(setCriticalErrorHandler(corsHandler(handler))).
|
UseHandler(setCriticalErrorHandler(corsHandler(handler))).
|
||||||
UseTLSConfig(newTLSConfig(getCert)).
|
UseTLSConfig(newTLSConfig(getCert)).
|
||||||
UseShutdownTimeout(globalServerCtxt.ShutdownTimeout).
|
|
||||||
UseIdleTimeout(globalServerCtxt.IdleTimeout).
|
UseIdleTimeout(globalServerCtxt.IdleTimeout).
|
||||||
UseReadHeaderTimeout(globalServerCtxt.ReadHeaderTimeout).
|
UseReadHeaderTimeout(globalServerCtxt.ReadHeaderTimeout).
|
||||||
UseBaseContext(GlobalContext).
|
UseBaseContext(GlobalContext).
|
||||||
@@ -919,6 +920,20 @@ func serverMain(ctx *cli.Context) {
|
|||||||
globalNodeNamesHex[hex.EncodeToString(nodeNameSum[:])] = struct{}{}
|
globalNodeNamesHex[hex.EncodeToString(nodeNameSum[:])] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bootstrapTrace("waitForQuorum", func() {
|
||||||
|
result := newObject.Health(context.Background(), HealthOptions{})
|
||||||
|
for !result.HealthyRead {
|
||||||
|
if debugNoExit {
|
||||||
|
logger.Info("Not waiting for quorum since we are debugging.. possible cause unhealthy sets (%s)", result)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
d := time.Duration(r.Float64() * float64(time.Second))
|
||||||
|
logger.Info("Waiting for quorum READ healthcheck to succeed.. possible cause unhealthy sets (%s), retrying in %s", result, d)
|
||||||
|
time.Sleep(d)
|
||||||
|
result = newObject.Health(context.Background(), HealthOptions{})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
bootstrapTrace("initServerConfig", func() {
|
bootstrapTrace("initServerConfig", func() {
|
||||||
if err = initServerConfig(GlobalContext, newObject); err != nil {
|
if err = initServerConfig(GlobalContext, newObject); err != nil {
|
||||||
@@ -985,8 +1000,6 @@ func serverMain(ctx *cli.Context) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
|
|
||||||
if !globalDisableFreezeOnBoot {
|
if !globalDisableFreezeOnBoot {
|
||||||
defer bootstrapTrace("unfreezeServices", unfreezeServices)
|
defer bootstrapTrace("unfreezeServices", unfreezeServices)
|
||||||
t := time.AfterFunc(5*time.Minute, func() {
|
t := time.AfterFunc(5*time.Minute, func() {
|
||||||
@@ -1040,11 +1053,11 @@ func serverMain(ctx *cli.Context) {
|
|||||||
bootLogIf(GlobalContext, globalEventNotifier.InitBucketTargets(GlobalContext, newObject))
|
bootLogIf(GlobalContext, globalEventNotifier.InitBucketTargets(GlobalContext, newObject))
|
||||||
})
|
})
|
||||||
|
|
||||||
var buckets []BucketInfo
|
var buckets []string
|
||||||
// List buckets to initialize bucket metadata sub-sys.
|
// List buckets to initialize bucket metadata sub-sys.
|
||||||
bootstrapTrace("listBuckets", func() {
|
bootstrapTrace("listBuckets", func() {
|
||||||
for {
|
for {
|
||||||
buckets, err = newObject.ListBuckets(GlobalContext, BucketOptions{})
|
bucketsList, err := newObject.ListBuckets(GlobalContext, BucketOptions{NoMetadata: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if configRetriableErrors(err) {
|
if configRetriableErrors(err) {
|
||||||
logger.Info("Waiting for list buckets to succeed to initialize buckets.. possible cause (%v)", err)
|
logger.Info("Waiting for list buckets to succeed to initialize buckets.. possible cause (%v)", err)
|
||||||
@@ -1054,6 +1067,10 @@ func serverMain(ctx *cli.Context) {
|
|||||||
bootLogIf(GlobalContext, fmt.Errorf("Unable to list buckets to initialize bucket metadata sub-system: %w", err))
|
bootLogIf(GlobalContext, fmt.Errorf("Unable to list buckets to initialize bucket metadata sub-system: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buckets = make([]string, len(bucketsList))
|
||||||
|
for i := range bucketsList {
|
||||||
|
buckets[i] = bucketsList[i].Name
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+20
-25
@@ -22,7 +22,6 @@ import (
|
|||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/minio/cli"
|
|
||||||
"github.com/minio/madmin-go/v3/kernel"
|
"github.com/minio/madmin-go/v3/kernel"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/v3/sys"
|
"github.com/minio/pkg/v3/sys"
|
||||||
@@ -45,7 +44,7 @@ func oldLinux() bool {
|
|||||||
return currentKernel < kernel.Version(4, 0, 0)
|
return currentKernel < kernel.Version(4, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMaxResources(ctx *cli.Context) (err error) {
|
func setMaxResources(ctx serverCtxt) (err error) {
|
||||||
// Set the Go runtime max threads threshold to 90% of kernel setting.
|
// Set the Go runtime max threads threshold to 90% of kernel setting.
|
||||||
sysMaxThreads, err := sys.GetMaxThreads()
|
sysMaxThreads, err := sys.GetMaxThreads()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -72,32 +71,28 @@ func setMaxResources(ctx *cli.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set max memory limit as current memory limit.
|
_, vssLimit, err := sys.GetMaxMemoryLimit()
|
||||||
if _, maxLimit, err = sys.GetMaxMemoryLimit(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// set debug memory limit instead of GOMEMLIMIT env
|
|
||||||
_ = setDebugMemoryLimit(ctx)
|
|
||||||
|
|
||||||
err = sys.SetMaxMemoryLimit(maxLimit, maxLimit)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDebugMemoryLimit(ctx *cli.Context) error {
|
|
||||||
if ctx == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if ctx.IsSet("memlimit") {
|
|
||||||
memlimit := ctx.String("memlimit")
|
|
||||||
if memlimit == "" {
|
|
||||||
memlimit = ctx.GlobalString("memlimit")
|
|
||||||
}
|
|
||||||
mlimit, err := humanize.ParseBytes(memlimit)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
debug.SetMemoryLimit(int64(mlimit))
|
|
||||||
|
if vssLimit > 0 && vssLimit < humanize.GiByte {
|
||||||
|
logger.Info("WARNING: maximum virtual memory limit (%s) is too small for 'go runtime', please consider setting `ulimit -v` to unlimited",
|
||||||
|
humanize.IBytes(vssLimit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ctx.MemLimit > 0 {
|
||||||
|
maxLimit = ctx.MemLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxLimit > 0 {
|
||||||
|
debug.SetMemoryLimit(int64(maxLimit))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not use RLIMIT_AS as that is not useful and at times on systems < 4Gi
|
||||||
|
// this can crash the Go runtime if the value is smaller refer
|
||||||
|
// - https://github.com/golang/go/issues/38010
|
||||||
|
// - https://github.com/golang/go/issues/43699
|
||||||
|
// So do not add `sys.SetMaxMemoryLimit()` this is not useful for any practical purposes.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ func printEventNotifiers() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
arns := globalEventNotifier.GetARNList(true)
|
arns := globalEventNotifier.GetARNList()
|
||||||
if len(arns) == 0 {
|
if len(arns) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-101
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
// Copyright (c) 2015-2024 MinIO, Inc.
|
||||||
//
|
//
|
||||||
// This file is part of MinIO Object Storage stack
|
// This file is part of MinIO Object Storage stack
|
||||||
//
|
//
|
||||||
@@ -32,13 +32,16 @@ import (
|
|||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
"github.com/minio/minio/internal/auth"
|
|
||||||
xioutil "github.com/minio/minio/internal/ioutil"
|
xioutil "github.com/minio/minio/internal/ioutil"
|
||||||
"github.com/minio/pkg/v3/mimedb"
|
"github.com/minio/pkg/v3/mimedb"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Maximum write offset for incoming SFTP blocks.
|
||||||
|
// Set to 100MiB to prevent hostile DOS attacks.
|
||||||
|
const ftpMaxWriteOffset = 100 << 20
|
||||||
|
|
||||||
type sftpDriver struct {
|
type sftpDriver struct {
|
||||||
permissions *ssh.Permissions
|
permissions *ssh.Permissions
|
||||||
endpoint string
|
endpoint string
|
||||||
@@ -49,7 +52,7 @@ type sftpMetrics struct{}
|
|||||||
|
|
||||||
var globalSftpMetrics sftpMetrics
|
var globalSftpMetrics sftpMetrics
|
||||||
|
|
||||||
func sftpTrace(s *sftp.Request, startTime time.Time, source string, user string, err error) madmin.TraceInfo {
|
func sftpTrace(s *sftp.Request, startTime time.Time, source string, user string, err error, sz int64) madmin.TraceInfo {
|
||||||
var errStr string
|
var errStr string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr = err.Error()
|
errStr = err.Error()
|
||||||
@@ -58,18 +61,25 @@ func sftpTrace(s *sftp.Request, startTime time.Time, source string, user string,
|
|||||||
TraceType: madmin.TraceFTP,
|
TraceType: madmin.TraceFTP,
|
||||||
Time: startTime,
|
Time: startTime,
|
||||||
NodeName: globalLocalNodeName,
|
NodeName: globalLocalNodeName,
|
||||||
FuncName: fmt.Sprintf("sftp USER=%s COMMAND=%s PARAM=%s, Source=%s", user, s.Method, s.Filepath, source),
|
FuncName: s.Method,
|
||||||
Duration: time.Since(startTime),
|
Duration: time.Since(startTime),
|
||||||
Path: s.Filepath,
|
Path: s.Filepath,
|
||||||
Error: errStr,
|
Error: errStr,
|
||||||
|
Bytes: sz,
|
||||||
|
Custom: map[string]string{
|
||||||
|
"user": user,
|
||||||
|
"cmd": s.Method,
|
||||||
|
"param": s.Filepath,
|
||||||
|
"source": source,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *sftpMetrics) log(s *sftp.Request, user string) func(err error) {
|
func (m *sftpMetrics) log(s *sftp.Request, user string) func(sz int64, err error) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
source := getSource(2)
|
source := getSource(2)
|
||||||
return func(err error) {
|
return func(sz int64, err error) {
|
||||||
globalTrace.Publish(sftpTrace(s, startTime, source, user, err))
|
globalTrace.Publish(sftpTrace(s, startTime, source, user, err, sz))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,77 +100,11 @@ func NewSFTPDriver(perms *ssh.Permissions) sftp.Handlers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *sftpDriver) getMinIOClient() (*minio.Client, error) {
|
func (f *sftpDriver) getMinIOClient() (*minio.Client, error) {
|
||||||
ui, ok := globalIAMSys.GetUser(context.Background(), f.AccessKey())
|
mcreds := credentials.NewStaticV4(
|
||||||
if !ok && !globalIAMSys.LDAPConfig.Enabled() {
|
f.permissions.CriticalOptions["AccessKey"],
|
||||||
return nil, errNoSuchUser
|
f.permissions.CriticalOptions["SecretKey"],
|
||||||
}
|
f.permissions.CriticalOptions["SessionToken"],
|
||||||
if !ok && globalIAMSys.LDAPConfig.Enabled() {
|
)
|
||||||
sa, _, err := globalIAMSys.getServiceAccount(context.Background(), f.AccessKey())
|
|
||||||
if err != nil && !errors.Is(err, errNoSuchServiceAccount) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var mcreds *credentials.Credentials
|
|
||||||
if errors.Is(err, errNoSuchServiceAccount) {
|
|
||||||
lookupResult, targetGroups, err := globalIAMSys.LDAPConfig.LookupUserDN(f.AccessKey())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
expiryDur, err := globalIAMSys.LDAPConfig.GetExpiryDuration("")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
claims := make(map[string]interface{})
|
|
||||||
claims[expClaim] = UTCNow().Add(expiryDur).Unix()
|
|
||||||
for k, v := range f.permissions.CriticalOptions {
|
|
||||||
claims[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set LDAP claims.
|
|
||||||
claims[ldapUserN] = f.AccessKey()
|
|
||||||
claims[ldapUser] = lookupResult.NormDN
|
|
||||||
// Add LDAP attributes that were looked up into the claims.
|
|
||||||
for attribKey, attribValue := range lookupResult.Attributes {
|
|
||||||
claims[ldapAttribPrefix+attribKey] = attribValue
|
|
||||||
}
|
|
||||||
|
|
||||||
cred, err := auth.GetNewCredentialsWithMetadata(claims, globalActiveCred.SecretKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the parent of the temporary access key, this is useful
|
|
||||||
// in obtaining service accounts by this cred.
|
|
||||||
cred.ParentUser = lookupResult.NormDN
|
|
||||||
|
|
||||||
// Set this value to LDAP groups, LDAP user can be part
|
|
||||||
// of large number of groups
|
|
||||||
cred.Groups = targetGroups
|
|
||||||
|
|
||||||
// Set the newly generated credentials, policyName is empty on purpose
|
|
||||||
// LDAP policies are applied automatically using their ldapUser, ldapGroups
|
|
||||||
// mapping.
|
|
||||||
updatedAt, err := globalIAMSys.SetTempUser(context.Background(), cred.AccessKey, cred, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call hook for site replication.
|
|
||||||
replLogIf(context.Background(), globalSiteReplicationSys.IAMChangeHook(context.Background(), madmin.SRIAMItem{
|
|
||||||
Type: madmin.SRIAMItemSTSAcc,
|
|
||||||
STSCredential: &madmin.SRSTSCredential{
|
|
||||||
AccessKey: cred.AccessKey,
|
|
||||||
SecretKey: cred.SecretKey,
|
|
||||||
SessionToken: cred.SessionToken,
|
|
||||||
ParentUser: cred.ParentUser,
|
|
||||||
},
|
|
||||||
UpdatedAt: updatedAt,
|
|
||||||
}))
|
|
||||||
|
|
||||||
mcreds = credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken)
|
|
||||||
} else {
|
|
||||||
mcreds = credentials.NewStaticV4(sa.Credentials.AccessKey, sa.Credentials.SecretKey, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
return minio.New(f.endpoint, &minio.Options{
|
return minio.New(f.endpoint, &minio.Options{
|
||||||
Creds: mcreds,
|
Creds: mcreds,
|
||||||
Secure: globalIsTLS,
|
Secure: globalIsTLS,
|
||||||
@@ -168,30 +112,14 @@ func (f *sftpDriver) getMinIOClient() (*minio.Client, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ok == true - at this point
|
|
||||||
|
|
||||||
if ui.Credentials.IsTemp() {
|
|
||||||
// Temporary credentials are not allowed.
|
|
||||||
return nil, errAuthentication
|
|
||||||
}
|
|
||||||
|
|
||||||
return minio.New(f.endpoint, &minio.Options{
|
|
||||||
Creds: credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
|
|
||||||
Secure: globalIsTLS,
|
|
||||||
Transport: globalRemoteFTPClientTransport,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *sftpDriver) AccessKey() string {
|
func (f *sftpDriver) AccessKey() string {
|
||||||
if _, ok := f.permissions.CriticalOptions["accessKey"]; !ok {
|
return f.permissions.CriticalOptions["AccessKey"]
|
||||||
return f.permissions.CriticalOptions[ldapUserN]
|
|
||||||
}
|
|
||||||
return f.permissions.CriticalOptions["accessKey"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *sftpDriver) Fileread(r *sftp.Request) (ra io.ReaderAt, err error) {
|
func (f *sftpDriver) Fileread(r *sftp.Request) (ra io.ReaderAt, err error) {
|
||||||
|
// This is not timing the actual read operation, but the time it takes to prepare the reader.
|
||||||
stopFn := globalSftpMetrics.log(r, f.AccessKey())
|
stopFn := globalSftpMetrics.log(r, f.AccessKey())
|
||||||
defer stopFn(err)
|
defer stopFn(0, err)
|
||||||
|
|
||||||
flags := r.Pflags()
|
flags := r.Pflags()
|
||||||
if !flags.Read {
|
if !flags.Read {
|
||||||
@@ -269,6 +197,9 @@ func (w *writerAt) WriteAt(b []byte, offset int64) (n int, err error) {
|
|||||||
n, err = w.w.Write(b)
|
n, err = w.w.Write(b)
|
||||||
w.nextOffset += int64(n)
|
w.nextOffset += int64(n)
|
||||||
} else {
|
} else {
|
||||||
|
if offset > w.nextOffset+ftpMaxWriteOffset {
|
||||||
|
return 0, fmt.Errorf("write offset %d is too far ahead of next offset %d", offset, w.nextOffset)
|
||||||
|
}
|
||||||
w.buffer[offset] = make([]byte, len(b))
|
w.buffer[offset] = make([]byte, len(b))
|
||||||
copy(w.buffer[offset], b)
|
copy(w.buffer[offset], b)
|
||||||
n = len(b)
|
n = len(b)
|
||||||
@@ -294,7 +225,12 @@ again:
|
|||||||
|
|
||||||
func (f *sftpDriver) Filewrite(r *sftp.Request) (w io.WriterAt, err error) {
|
func (f *sftpDriver) Filewrite(r *sftp.Request) (w io.WriterAt, err error) {
|
||||||
stopFn := globalSftpMetrics.log(r, f.AccessKey())
|
stopFn := globalSftpMetrics.log(r, f.AccessKey())
|
||||||
defer stopFn(err)
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
// If there is an error, we never started the goroutine.
|
||||||
|
stopFn(0, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
flags := r.Pflags()
|
flags := r.Pflags()
|
||||||
if !flags.Write {
|
if !flags.Write {
|
||||||
@@ -329,10 +265,11 @@ func (f *sftpDriver) Filewrite(r *sftp.Request) (w io.WriterAt, err error) {
|
|||||||
}
|
}
|
||||||
wa.wg.Add(1)
|
wa.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
_, err := clnt.PutObject(r.Context(), bucket, object, pr, -1, minio.PutObjectOptions{
|
oi, err := clnt.PutObject(r.Context(), bucket, object, pr, -1, minio.PutObjectOptions{
|
||||||
ContentType: mimedb.TypeByExtension(path.Ext(object)),
|
ContentType: mimedb.TypeByExtension(path.Ext(object)),
|
||||||
DisableContentSha256: true,
|
DisableContentSha256: true,
|
||||||
})
|
})
|
||||||
|
stopFn(oi.Size, err)
|
||||||
pr.CloseWithError(err)
|
pr.CloseWithError(err)
|
||||||
wa.wg.Done()
|
wa.wg.Done()
|
||||||
}()
|
}()
|
||||||
@@ -341,7 +278,7 @@ func (f *sftpDriver) Filewrite(r *sftp.Request) (w io.WriterAt, err error) {
|
|||||||
|
|
||||||
func (f *sftpDriver) Filecmd(r *sftp.Request) (err error) {
|
func (f *sftpDriver) Filecmd(r *sftp.Request) (err error) {
|
||||||
stopFn := globalSftpMetrics.log(r, f.AccessKey())
|
stopFn := globalSftpMetrics.log(r, f.AccessKey())
|
||||||
defer stopFn(err)
|
defer stopFn(0, err)
|
||||||
|
|
||||||
clnt, err := f.getMinIOClient()
|
clnt, err := f.getMinIOClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -437,7 +374,7 @@ func (f listerAt) ListAt(ls []os.FileInfo, offset int64) (int, error) {
|
|||||||
|
|
||||||
func (f *sftpDriver) Filelist(r *sftp.Request) (la sftp.ListerAt, err error) {
|
func (f *sftpDriver) Filelist(r *sftp.Request) (la sftp.ListerAt, err error) {
|
||||||
stopFn := globalSftpMetrics.log(r, f.AccessKey())
|
stopFn := globalSftpMetrics.log(r, f.AccessKey())
|
||||||
defer stopFn(err)
|
defer stopFn(0, err)
|
||||||
|
|
||||||
clnt, err := f.getMinIOClient()
|
clnt, err := f.getMinIOClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+256
-116
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
// Copyright (c) 2015-2024 MinIO, Inc.
|
||||||
//
|
//
|
||||||
// This file is part of MinIO Object Storage stack
|
// This file is part of MinIO Object Storage stack
|
||||||
//
|
//
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -29,18 +28,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/madmin-go/v3"
|
||||||
|
"github.com/minio/minio/internal/auth"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
|
xldap "github.com/minio/pkg/v3/ldap"
|
||||||
xsftp "github.com/minio/pkg/v3/sftp"
|
xsftp "github.com/minio/pkg/v3/sftp"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sftpLogger struct{}
|
|
||||||
|
|
||||||
func (s *sftpLogger) Info(tag xsftp.LogType, msg string) {
|
|
||||||
logger.Info(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
kexAlgoDH1SHA1 = "diffie-hellman-group1-sha1"
|
kexAlgoDH1SHA1 = "diffie-hellman-group1-sha1"
|
||||||
kexAlgoDH14SHA1 = "diffie-hellman-group14-sha1"
|
kexAlgoDH14SHA1 = "diffie-hellman-group14-sha1"
|
||||||
@@ -58,6 +54,16 @@ const (
|
|||||||
tripledescbcID = "3des-cbc"
|
tripledescbcID = "3des-cbc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errSFTPPublicKeyBadFormat = errors.New("the public key provided could not be parsed")
|
||||||
|
errSFTPUserHasNoPolicies = errors.New("no policies present on this account")
|
||||||
|
errSFTPLDAPNotEnabled = errors.New("ldap authentication is not enabled")
|
||||||
|
)
|
||||||
|
|
||||||
|
// if the sftp parameter --trusted-user-ca-key is set, then
|
||||||
|
// the final form of the key file will be set as this variable.
|
||||||
|
var caPublicKey ssh.PublicKey
|
||||||
|
|
||||||
// https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.22.0:ssh/common.go;l=46
|
// https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.22.0:ssh/common.go;l=46
|
||||||
// preferredKexAlgos specifies the default preference for key-exchange
|
// preferredKexAlgos specifies the default preference for key-exchange
|
||||||
// algorithms in preference order. The diffie-hellman-group16-sha512 algorithm
|
// algorithms in preference order. The diffie-hellman-group16-sha512 algorithm
|
||||||
@@ -120,6 +126,226 @@ var supportedMACs = []string{
|
|||||||
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com", "hmac-sha2-256", "hmac-sha2-512", "hmac-sha1", "hmac-sha1-96",
|
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-512-etm@openssh.com", "hmac-sha2-256", "hmac-sha2-512", "hmac-sha1", "hmac-sha1-96",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sshPubKeyAuth(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
|
return authenticateSSHConnection(c, key, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sshPasswordAuth(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
||||||
|
return authenticateSSHConnection(c, nil, pass)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticateSSHConnection(c ssh.ConnMetadata, key ssh.PublicKey, pass []byte) (*ssh.Permissions, error) {
|
||||||
|
user, found := strings.CutSuffix(c.User(), "=ldap")
|
||||||
|
if found {
|
||||||
|
if !globalIAMSys.LDAPConfig.Enabled() {
|
||||||
|
return nil, errSFTPLDAPNotEnabled
|
||||||
|
}
|
||||||
|
return processLDAPAuthentication(key, pass, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, found = strings.CutSuffix(c.User(), "=svc")
|
||||||
|
if found {
|
||||||
|
goto internalAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIAMSys.LDAPConfig.Enabled() {
|
||||||
|
perms, _ := processLDAPAuthentication(key, pass, user)
|
||||||
|
if perms != nil {
|
||||||
|
return perms, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internalAuth:
|
||||||
|
ui, ok := globalIAMSys.GetUser(context.Background(), user)
|
||||||
|
if !ok {
|
||||||
|
return nil, errNoSuchUser
|
||||||
|
}
|
||||||
|
|
||||||
|
if caPublicKey != nil {
|
||||||
|
err := validateKey(c, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Temporary credentials are not allowed.
|
||||||
|
if ui.Credentials.IsTemp() {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare([]byte(ui.Credentials.SecretKey), pass) != 1 {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ssh.Permissions{
|
||||||
|
CriticalOptions: map[string]string{
|
||||||
|
"AccessKey": ui.Credentials.AccessKey,
|
||||||
|
"SecretKey": ui.Credentials.SecretKey,
|
||||||
|
"SessionToken": ui.Credentials.SessionToken,
|
||||||
|
},
|
||||||
|
Extensions: make(map[string]string),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processLDAPAuthentication(key ssh.PublicKey, pass []byte, user string) (perms *ssh.Permissions, err error) {
|
||||||
|
var lookupResult *xldap.DNSearchResult
|
||||||
|
var targetGroups []string
|
||||||
|
|
||||||
|
if pass == nil && key == nil {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
|
||||||
|
if pass != nil {
|
||||||
|
sa, _, err := globalIAMSys.getServiceAccount(context.Background(), user)
|
||||||
|
if err == nil {
|
||||||
|
if subtle.ConstantTimeCompare([]byte(sa.Credentials.SecretKey), pass) != 1 {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ssh.Permissions{
|
||||||
|
CriticalOptions: map[string]string{
|
||||||
|
"AccessKey": sa.Credentials.AccessKey,
|
||||||
|
"SecretKey": sa.Credentials.SecretKey,
|
||||||
|
"SessionToken": sa.Credentials.SessionToken,
|
||||||
|
},
|
||||||
|
Extensions: make(map[string]string),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, errNoSuchServiceAccount) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lookupResult, targetGroups, err = globalIAMSys.LDAPConfig.Bind(user, string(pass))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if key != nil {
|
||||||
|
|
||||||
|
lookupResult, targetGroups, err = globalIAMSys.LDAPConfig.LookupUserDN(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if lookupResult == nil {
|
||||||
|
return nil, errNoSuchUser
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapPolicies, _ := globalIAMSys.PolicyDBGet(lookupResult.NormDN, targetGroups...)
|
||||||
|
if len(ldapPolicies) == 0 {
|
||||||
|
return nil, errSFTPUserHasNoPolicies
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := make(map[string]interface{})
|
||||||
|
for attribKey, attribValue := range lookupResult.Attributes {
|
||||||
|
// we skip multi-value attributes here, as they cannot
|
||||||
|
// be stored in the critical options.
|
||||||
|
if len(attribValue) != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if attribKey == "sshPublicKey" && key != nil {
|
||||||
|
key2, _, _, _, err := ssh.ParseAuthorizedKey([]byte(attribValue[0]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errSFTPPublicKeyBadFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare(key2.Marshal(), key.Marshal()) != 1 {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
}
|
||||||
|
claims[ldapAttribPrefix+attribKey] = attribValue[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
expiryDur, err := globalIAMSys.LDAPConfig.GetExpiryDuration("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims[expClaim] = UTCNow().Add(expiryDur).Unix()
|
||||||
|
claims[ldapUserN] = user
|
||||||
|
claims[ldapUser] = lookupResult.NormDN
|
||||||
|
|
||||||
|
cred, err := auth.GetNewCredentialsWithMetadata(claims, globalActiveCred.SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the parent of the temporary access key, this is useful
|
||||||
|
// in obtaining service accounts by this cred.
|
||||||
|
cred.ParentUser = lookupResult.NormDN
|
||||||
|
|
||||||
|
// Set this value to LDAP groups, LDAP user can be part
|
||||||
|
// of large number of groups
|
||||||
|
cred.Groups = targetGroups
|
||||||
|
|
||||||
|
// Set the newly generated credentials, policyName is empty on purpose
|
||||||
|
// LDAP policies are applied automatically using their ldapUser, ldapGroups
|
||||||
|
// mapping.
|
||||||
|
updatedAt, err := globalIAMSys.SetTempUser(context.Background(), cred.AccessKey, cred, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
replLogIf(context.Background(), globalSiteReplicationSys.IAMChangeHook(context.Background(), madmin.SRIAMItem{
|
||||||
|
Type: madmin.SRIAMItemSTSAcc,
|
||||||
|
STSCredential: &madmin.SRSTSCredential{
|
||||||
|
AccessKey: cred.AccessKey,
|
||||||
|
SecretKey: cred.SecretKey,
|
||||||
|
SessionToken: cred.SessionToken,
|
||||||
|
ParentUser: cred.ParentUser,
|
||||||
|
},
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return &ssh.Permissions{
|
||||||
|
CriticalOptions: map[string]string{
|
||||||
|
"AccessKey": cred.AccessKey,
|
||||||
|
"SecretKey": cred.SecretKey,
|
||||||
|
"SessionToken": cred.SessionToken,
|
||||||
|
},
|
||||||
|
Extensions: make(map[string]string),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateKey(c ssh.ConnMetadata, clientKey ssh.PublicKey) (err error) {
|
||||||
|
if caPublicKey == nil {
|
||||||
|
return errors.New("public key authority validation requested but no ca public key specified.")
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, ok := clientKey.(*ssh.Certificate)
|
||||||
|
if !ok {
|
||||||
|
return errSftpPublicKeyWithoutCert
|
||||||
|
}
|
||||||
|
|
||||||
|
// ssh.CheckCert called by ssh.Authenticate accepts certificates
|
||||||
|
// with empty principles list so we block those in here.
|
||||||
|
if len(cert.ValidPrincipals) == 0 {
|
||||||
|
return errSftpCertWithoutPrincipals
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that certificate provided by user is issued by trusted CA,
|
||||||
|
// username in authentication request matches to identities in certificate
|
||||||
|
// and that certificate type is correct.
|
||||||
|
checker := ssh.CertChecker{}
|
||||||
|
checker.IsUserAuthority = func(k ssh.PublicKey) bool {
|
||||||
|
return subtle.ConstantTimeCompare(caPublicKey.Marshal(), k.Marshal()) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = checker.Authenticate(c, clientKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type sftpLogger struct{}
|
||||||
|
|
||||||
|
func (s *sftpLogger) Info(tag xsftp.LogType, msg string) {
|
||||||
|
logger.Info(msg)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *sftpLogger) Error(tag xsftp.LogType, err error) {
|
func (s *sftpLogger) Error(tag xsftp.LogType, err error) {
|
||||||
switch tag {
|
switch tag {
|
||||||
case xsftp.AcceptNetworkError:
|
case xsftp.AcceptNetworkError:
|
||||||
@@ -164,12 +390,15 @@ func startSFTPServer(args []string) {
|
|||||||
publicIP string
|
publicIP string
|
||||||
sshPrivateKey string
|
sshPrivateKey string
|
||||||
userCaKeyFile string
|
userCaKeyFile string
|
||||||
|
disablePassAuth bool
|
||||||
)
|
)
|
||||||
|
|
||||||
allowPubKeys := supportedPubKeyAuthAlgos
|
allowPubKeys := supportedPubKeyAuthAlgos
|
||||||
allowKexAlgos := preferredKexAlgos
|
allowKexAlgos := preferredKexAlgos
|
||||||
allowCiphers := preferredCiphers
|
allowCiphers := preferredCiphers
|
||||||
allowMACs := supportedMACs
|
allowMACs := supportedMACs
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
tokens := strings.SplitN(arg, "=", 2)
|
tokens := strings.SplitN(arg, "=", 2)
|
||||||
if len(tokens) != 2 {
|
if len(tokens) != 2 {
|
||||||
@@ -201,6 +430,8 @@ func startSFTPServer(args []string) {
|
|||||||
allowMACs = filterAlgos(arg, strings.Split(tokens[1], ","), supportedMACs)
|
allowMACs = filterAlgos(arg, strings.Split(tokens[1], ","), supportedMACs)
|
||||||
case "trusted-user-ca-key":
|
case "trusted-user-ca-key":
|
||||||
userCaKeyFile = tokens[1]
|
userCaKeyFile = tokens[1]
|
||||||
|
case "password-auth":
|
||||||
|
disablePassAuth, _ = strconv.ParseBool(tokens[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +453,18 @@ func startSFTPServer(args []string) {
|
|||||||
logger.Fatal(fmt.Errorf("invalid arguments passed, private key file is not parseable: %v", err), "unable to start SFTP server")
|
logger.Fatal(fmt.Errorf("invalid arguments passed, private key file is not parseable: %v", err), "unable to start SFTP server")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if userCaKeyFile != "" {
|
||||||
|
keyBytes, err := os.ReadFile(userCaKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(fmt.Errorf("invalid arguments passed, trusted user certificate authority public key file is not accessible: %v", err), "unable to start SFTP server")
|
||||||
|
}
|
||||||
|
|
||||||
|
caPublicKey, _, _, _, err = ssh.ParseAuthorizedKey(keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(fmt.Errorf("invalid arguments passed, trusted user certificate authority public key file is not parseable: %v", err), "unable to start SFTP server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// An SSH server is represented by a ServerConfig, which holds
|
// An SSH server is represented by a ServerConfig, which holds
|
||||||
// certificate details and handles authentication of ServerConns.
|
// certificate details and handles authentication of ServerConns.
|
||||||
sshConfig := &ssh.ServerConfig{
|
sshConfig := &ssh.ServerConfig{
|
||||||
@@ -231,116 +474,13 @@ func startSFTPServer(args []string) {
|
|||||||
MACs: allowMACs,
|
MACs: allowMACs,
|
||||||
},
|
},
|
||||||
PublicKeyAuthAlgorithms: allowPubKeys,
|
PublicKeyAuthAlgorithms: allowPubKeys,
|
||||||
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
PublicKeyCallback: sshPubKeyAuth,
|
||||||
if globalIAMSys.LDAPConfig.Enabled() {
|
|
||||||
sa, _, err := globalIAMSys.getServiceAccount(context.Background(), c.User())
|
|
||||||
if err != nil && !errors.Is(err, errNoSuchServiceAccount) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if errors.Is(err, errNoSuchServiceAccount) {
|
|
||||||
lookupResult, targetGroups, err := globalIAMSys.LDAPConfig.Bind(c.User(), string(pass))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
targetUser := lookupResult.NormDN
|
|
||||||
ldapPolicies, _ := globalIAMSys.PolicyDBGet(targetUser, targetGroups...)
|
|
||||||
if len(ldapPolicies) == 0 {
|
|
||||||
return nil, errAuthentication
|
|
||||||
}
|
|
||||||
criticalOptions := map[string]string{
|
|
||||||
ldapUser: targetUser,
|
|
||||||
ldapActualUser: lookupResult.ActualDN,
|
|
||||||
ldapUserN: c.User(),
|
|
||||||
}
|
|
||||||
for attribKey, attribValue := range lookupResult.Attributes {
|
|
||||||
// we skip multi-value attributes here, as they cannot
|
|
||||||
// be stored in the critical options.
|
|
||||||
if len(attribValue) == 1 {
|
|
||||||
criticalOptions[ldapAttribPrefix+attribKey] = attribValue[0]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ssh.Permissions{
|
if !disablePassAuth {
|
||||||
CriticalOptions: criticalOptions,
|
sshConfig.PasswordCallback = sshPasswordAuth
|
||||||
Extensions: make(map[string]string),
|
} else {
|
||||||
}, nil
|
sshConfig.PasswordCallback = nil
|
||||||
}
|
|
||||||
if subtle.ConstantTimeCompare([]byte(sa.Credentials.SecretKey), pass) == 1 {
|
|
||||||
return &ssh.Permissions{
|
|
||||||
CriticalOptions: map[string]string{
|
|
||||||
"accessKey": c.User(),
|
|
||||||
},
|
|
||||||
Extensions: make(map[string]string),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return nil, errAuthentication
|
|
||||||
}
|
|
||||||
|
|
||||||
ui, ok := globalIAMSys.GetUser(context.Background(), c.User())
|
|
||||||
if !ok {
|
|
||||||
return nil, errNoSuchUser
|
|
||||||
}
|
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare([]byte(ui.Credentials.SecretKey), pass) == 1 {
|
|
||||||
return &ssh.Permissions{
|
|
||||||
CriticalOptions: map[string]string{
|
|
||||||
"accessKey": c.User(),
|
|
||||||
},
|
|
||||||
Extensions: make(map[string]string),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return nil, errAuthentication
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if userCaKeyFile != "" {
|
|
||||||
keyBytes, err := os.ReadFile(userCaKeyFile)
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(fmt.Errorf("invalid arguments passed, trusted user certificate authority public key file is not accessible: %v", err), "unable to start SFTP server")
|
|
||||||
}
|
|
||||||
|
|
||||||
caPublicKey, _, _, _, err := ssh.ParseAuthorizedKey(keyBytes)
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(fmt.Errorf("invalid arguments passed, trusted user certificate authority public key file is not parseable: %v", err), "unable to start SFTP server")
|
|
||||||
}
|
|
||||||
|
|
||||||
sshConfig.PublicKeyCallback = func(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
|
||||||
_, ok := globalIAMSys.GetUser(context.Background(), c.User())
|
|
||||||
if !ok {
|
|
||||||
return nil, errNoSuchUser
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that client provided certificate, not only public key.
|
|
||||||
cert, ok := key.(*ssh.Certificate)
|
|
||||||
if !ok {
|
|
||||||
return nil, errSftpPublicKeyWithoutCert
|
|
||||||
}
|
|
||||||
|
|
||||||
// ssh.CheckCert called by ssh.Authenticate accepts certificates
|
|
||||||
// with empty principles list so we block those in here.
|
|
||||||
if len(cert.ValidPrincipals) == 0 {
|
|
||||||
return nil, errSftpCertWithoutPrincipals
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that certificate provided by user is issued by trusted CA,
|
|
||||||
// username in authentication request matches to identities in certificate
|
|
||||||
// and that certificate type is correct.
|
|
||||||
checker := ssh.CertChecker{}
|
|
||||||
checker.IsUserAuthority = func(k ssh.PublicKey) bool {
|
|
||||||
return bytes.Equal(k.Marshal(), caPublicKey.Marshal())
|
|
||||||
}
|
|
||||||
_, err = checker.Authenticate(c, key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ssh.Permissions{
|
|
||||||
CriticalOptions: map[string]string{
|
|
||||||
"accessKey": c.User(),
|
|
||||||
},
|
|
||||||
Extensions: make(map[string]string),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sshConfig.AddHostKey(private)
|
sshConfig.AddHostKey(private)
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
// Copyright (c) 2015-2024 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/madmin-go/v3"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockConnMeta struct {
|
||||||
|
username string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockConnMeta) User() string {
|
||||||
|
return m.username
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockConnMeta) SessionID() []byte {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockConnMeta) ClientVersion() []byte {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockConnMeta) ServerVersion() []byte {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockConnMeta) RemoteAddr() net.Addr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockConnMeta) LocalAddr() net.Addr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSSHConnMock(username string) ssh.ConnMetadata {
|
||||||
|
return &MockConnMeta{username: username}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSFTPAuthentication(t *testing.T) {
|
||||||
|
for i, testCase := range iamTestSuites {
|
||||||
|
t.Run(
|
||||||
|
fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription),
|
||||||
|
func(t *testing.T) {
|
||||||
|
c := &check{t, testCase.serverType}
|
||||||
|
suite := testCase
|
||||||
|
|
||||||
|
suite.SetUpSuite(c)
|
||||||
|
|
||||||
|
suite.SFTPServiceAccountLogin(c)
|
||||||
|
suite.SFTPInvalidServiceAccountPassword(c)
|
||||||
|
|
||||||
|
// LDAP tests
|
||||||
|
ldapServer := os.Getenv(EnvTestLDAPServer)
|
||||||
|
if ldapServer == "" {
|
||||||
|
c.Skipf("Skipping LDAP test as no LDAP server is provided via %s", EnvTestLDAPServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.SetUpLDAP(c, ldapServer)
|
||||||
|
|
||||||
|
suite.SFTPFailedAuthDueToMissingPolicy(c)
|
||||||
|
suite.SFTPFailedAuthDueToInvalidUser(c)
|
||||||
|
suite.SFTPFailedForcedServiceAccountAuthOnLDAPUser(c)
|
||||||
|
suite.SFTPFailedAuthDueToInvalidPassword(c)
|
||||||
|
|
||||||
|
suite.SFTPValidLDAPLoginWithPassword(c)
|
||||||
|
|
||||||
|
suite.SFTPPublicKeyAuthentication(c)
|
||||||
|
suite.SFTPFailedPublicKeyAuthenticationInvalidKey(c)
|
||||||
|
|
||||||
|
suite.TearDownSuite(c)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestSuiteIAM) SFTPFailedPublicKeyAuthenticationInvalidKey(c *check) {
|
||||||
|
keyBytes, err := os.ReadFile("./testdata/invalid_test_key.pub")
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("could not read test key file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testKey, _, _, _, err := ssh.ParseAuthorizedKey(keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("could not parse test key file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSSHCon := newSSHConnMock("dillon=ldap")
|
||||||
|
_, err = sshPubKeyAuth(newSSHCon, testKey)
|
||||||
|
if err == nil || !errors.Is(err, errAuthentication) {
|
||||||
|
c.Fatalf("expected err(%s) but got (%s)", errAuthentication, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSSHCon = newSSHConnMock("dillon")
|
||||||
|
_, err = sshPubKeyAuth(newSSHCon, testKey)
|
||||||
|
if err == nil || !errors.Is(err, errNoSuchUser) {
|
||||||
|
c.Fatalf("expected err(%s) but got (%s)", errNoSuchUser, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestSuiteIAM) SFTPPublicKeyAuthentication(c *check) {
|
||||||
|
keyBytes, err := os.ReadFile("./testdata/dillon_test_key.pub")
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("could not read test key file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testKey, _, _, _, err := ssh.ParseAuthorizedKey(keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("could not parse test key file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSSHCon := newSSHConnMock("dillon=ldap")
|
||||||
|
_, err = sshPubKeyAuth(newSSHCon, testKey)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("expected no error but got(%s)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSSHCon = newSSHConnMock("dillon")
|
||||||
|
_, err = sshPubKeyAuth(newSSHCon, testKey)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("expected no error but got(%s)", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestSuiteIAM) SFTPFailedAuthDueToMissingPolicy(c *check) {
|
||||||
|
newSSHCon := newSSHConnMock("dillon=ldap")
|
||||||
|
_, err := sshPasswordAuth(newSSHCon, []byte("dillon"))
|
||||||
|
if err == nil || !errors.Is(err, errSFTPUserHasNoPolicies) {
|
||||||
|
c.Fatalf("expected err(%s) but got (%s)", errSFTPUserHasNoPolicies, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSSHCon = newSSHConnMock("dillon")
|
||||||
|
_, err = sshPasswordAuth(newSSHCon, []byte("dillon"))
|
||||||
|
if err == nil || !errors.Is(err, errNoSuchUser) {
|
||||||
|
c.Fatalf("expected err(%s) but got (%s)", errNoSuchUser, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestSuiteIAM) SFTPFailedAuthDueToInvalidUser(c *check) {
|
||||||
|
newSSHCon := newSSHConnMock("dillon_error")
|
||||||
|
_, err := sshPasswordAuth(newSSHCon, []byte("dillon_error"))
|
||||||
|
if err == nil || !errors.Is(err, errNoSuchUser) {
|
||||||
|
c.Fatalf("expected err(%s) but got (%s)", errNoSuchUser, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestSuiteIAM) SFTPFailedForcedServiceAccountAuthOnLDAPUser(c *check) {
|
||||||
|
newSSHCon := newSSHConnMock("dillon=svc")
|
||||||
|
_, err := sshPasswordAuth(newSSHCon, []byte("dillon"))
|
||||||
|
if err == nil || !errors.Is(err, errNoSuchUser) {
|
||||||
|
c.Fatalf("expected err(%s) but got (%s)", errNoSuchUser, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestSuiteIAM) SFTPFailedAuthDueToInvalidPassword(c *check) {
|
||||||
|
newSSHCon := newSSHConnMock("dillon")
|
||||||
|
_, err := sshPasswordAuth(newSSHCon, []byte("dillon_error"))
|
||||||
|
if err == nil || !errors.Is(err, errNoSuchUser) {
|
||||||
|
c.Fatalf("expected err(%s) but got (%s)", errNoSuchUser, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestSuiteIAM) SFTPInvalidServiceAccountPassword(c *check) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
accessKey, secretKey := mustGenerateCredentials(c)
|
||||||
|
err := s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("Unable to set user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.adm.SetPolicy(ctx, "readwrite", accessKey, false)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("unable to set policy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSSHCon := newSSHConnMock(accessKey + "=svc")
|
||||||
|
_, err = sshPasswordAuth(newSSHCon, []byte("invalid"))
|
||||||
|
if err == nil || !errors.Is(err, errAuthentication) {
|
||||||
|
c.Fatalf("expected err(%s) but got (%s)", errAuthentication, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSSHCon = newSSHConnMock(accessKey)
|
||||||
|
_, err = sshPasswordAuth(newSSHCon, []byte("invalid"))
|
||||||
|
if err == nil || !errors.Is(err, errAuthentication) {
|
||||||
|
c.Fatalf("expected err(%s) but got (%s)", errAuthentication, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestSuiteIAM) SFTPServiceAccountLogin(c *check) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
accessKey, secretKey := mustGenerateCredentials(c)
|
||||||
|
err := s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("Unable to set user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.adm.SetPolicy(ctx, "readwrite", accessKey, false)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("unable to set policy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSSHCon := newSSHConnMock(accessKey + "=svc")
|
||||||
|
_, err = sshPasswordAuth(newSSHCon, []byte(secretKey))
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("expected no error but got (%s)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSSHCon = newSSHConnMock(accessKey)
|
||||||
|
_, err = sshPasswordAuth(newSSHCon, []byte(secretKey))
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("expected no error but got (%s)", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestSuiteIAM) SFTPValidLDAPLoginWithPassword(c *check) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// we need to do this so that the user has a policy before authentication.
|
||||||
|
// ldap user accounts without policies are denied access in sftp.
|
||||||
|
policy := "mypolicy"
|
||||||
|
policyBytes := []byte(`{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"s3:PutObject",
|
||||||
|
"s3:GetObject",
|
||||||
|
"s3:ListBucket"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::BUCKET/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
err := s.adm.AddCannedPolicy(ctx, policy, policyBytes)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("policy add error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userDN := "uid=dillon,ou=people,ou=swengg,dc=min,dc=io"
|
||||||
|
err = s.adm.SetPolicy(ctx, policy, userDN, false)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("Unable to set policy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSSHCon := newSSHConnMock("dillon=ldap")
|
||||||
|
_, err = sshPasswordAuth(newSSHCon, []byte("dillon"))
|
||||||
|
if err != nil {
|
||||||
|
c.Fatal("Password authentication failed for user (dillon):", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSSHCon = newSSHConnMock("dillon")
|
||||||
|
_, err = sshPasswordAuth(newSSHCon, []byte("dillon"))
|
||||||
|
if err != nil {
|
||||||
|
c.Fatal("Password authentication failed for user (dillon):", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -152,11 +152,14 @@ func checkKeyValid(r *http.Request, accessKey string) (auth.Credentials, bool, A
|
|||||||
// Check if server has initialized, then only proceed
|
// Check if server has initialized, then only proceed
|
||||||
// to check for IAM users otherwise its okay for clients
|
// to check for IAM users otherwise its okay for clients
|
||||||
// to retry with 503 errors when server is coming up.
|
// to retry with 503 errors when server is coming up.
|
||||||
return auth.Credentials{}, false, ErrServerNotInitialized
|
return auth.Credentials{}, false, ErrIAMNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the access key is part of users credentials.
|
// Check if the access key is part of users credentials.
|
||||||
u, ok := globalIAMSys.GetUser(r.Context(), accessKey)
|
u, ok, err := globalIAMSys.CheckKey(r.Context(), accessKey)
|
||||||
|
if err != nil {
|
||||||
|
return auth.Credentials{}, false, ErrIAMNotInitialized
|
||||||
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
// Credentials could be valid but disabled - return a different
|
// Credentials could be valid but disabled - return a different
|
||||||
// error in such a scenario.
|
// error in such a scenario.
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ func niceError(code APIErrorCode) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDoesPolicySignatureMatch(t *testing.T) {
|
func TestDoesPolicySignatureMatch(t *testing.T) {
|
||||||
|
_, fsDir, err := prepareFS(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer removeRoots([]string{fsDir})
|
||||||
|
|
||||||
credentialTemplate := "%s/%s/%s/s3/aws4_request"
|
credentialTemplate := "%s/%s/%s/s3/aws4_request"
|
||||||
now := UTCNow()
|
now := UTCNow()
|
||||||
accessKey := globalActiveCred.AccessKey
|
accessKey := globalActiveCred.AccessKey
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user