mirror of
https://github.com/pgsty/minio.git
synced 2026-11-12 10:35:33 +03:00
Compare commits
163 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a9dc061d84 | |||
| 01a8c09920 | |||
| 4c8562bcec | |||
| f13c04629b | |||
| 80ff907d08 | |||
| 673df6d517 | |||
| 2d40433bc1 | |||
| 3bc39db34e | |||
| a17f14f73a | |||
| 6651c655cb | |||
| 3ae104edae | |||
| c87a489514 | |||
| a60267501d | |||
| 641a56da0d | |||
| 59788e25c7 | |||
| a16193bb50 | |||
| 132e7413ba | |||
| 1966668066 | |||
| 064f36ca5a | |||
| 15b609ecea | |||
| 4a1edfd9aa | |||
| b7f319b62a | |||
| 33c101544d | |||
| 21cf29330e | |||
| 3b21bb5be8 | |||
| 6fe2b3f901 | |||
| b368d4cc13 | |||
| 0680af7414 | |||
| 91805bcab6 | |||
| c0e2886e37 | |||
| 4f5dded4d4 | |||
| b3a94c4e85 | |||
| 8e618d45fc | |||
| 3ef59d2821 | |||
| 23db4958f5 | |||
| d9ee668b6d | |||
| 2e5d792f0c | |||
| b276651eaa | |||
| 3535197f99 | |||
| 95f076340a | |||
| 698bb93a46 | |||
| 2584430141 | |||
| ded373e600 | |||
| e8c54c3d6c | |||
| f944a42886 | |||
| eff0ea43aa | |||
| 3b602bb532 | |||
| 459985f0fa | |||
| d0080046c2 | |||
| 7fcb428622 | |||
| 4ea6f94ed8 | |||
| 83adc2eebf | |||
| 989c318a28 | |||
| ef802f2b2c | |||
| f5d2fbc84c | |||
| e139673969 | |||
| a8c6465f22 | |||
| 27538e2d22 | |||
| 6c6f0987dc | |||
| 5f64658faa | |||
| ce183cb2b4 | |||
| b3bac73c0f | |||
| e726d8ff0f | |||
| f4230777b3 | |||
| 380233d646 | |||
| d592bc0c1c | |||
| 0d0b0aa599 | |||
| b433bf14ba | |||
| cf371da346 | |||
| 107d951893 | |||
| 22c53b1c70 | |||
| 88926ad8e9 | |||
| 32d04091a2 | |||
| b6d4a77b94 | |||
| be84a4fd68 | |||
| 2ec1f404ac | |||
| 2040559f71 | |||
| ca0ce4c6ef | |||
| 757cf413cb | |||
| b35acb3dbc | |||
| e404abf103 | |||
| f7ff19cb18 | |||
| f736702da8 | |||
| 91faaa1387 | |||
| 68a9f521d5 | |||
| f365a98029 | |||
| 47bbc272df | |||
| aebac90013 | |||
| 7ca4ba77c4 | |||
| 13512170b5 | |||
| 154fcaeb56 | |||
| 722118386d | |||
| 709612cb37 | |||
| b35d083872 | |||
| 5e7b243bde | |||
| f8f9fc77ac | |||
| 499531f0b5 | |||
| 3c2141513f | |||
| 602f6a9ad0 | |||
| 22c5a5b91b | |||
| 41f508765d | |||
| 7dccd1f589 | |||
| 55ff598b23 | |||
| a22ce4550c | |||
| 168ae81b1f | |||
| 5f6a25cdd0 | |||
| 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 |
@@ -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: |
|
||||||
|
|||||||
@@ -21,8 +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.4
|
go-version: 1.22.5
|
||||||
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
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
extend-exclude = [
|
extend-exclude = [
|
||||||
".git/",
|
".git/",
|
||||||
"docs/",
|
"docs/",
|
||||||
|
"CREDITS",
|
||||||
|
"go.mod",
|
||||||
|
"go.sum",
|
||||||
]
|
]
|
||||||
ignore-hidden = false
|
ignore-hidden = false
|
||||||
|
|
||||||
@@ -16,6 +19,7 @@ extend-ignore-re = [
|
|||||||
"MIIDBTCCAe2gAwIBAgIQWHw7h.*",
|
"MIIDBTCCAe2gAwIBAgIQWHw7h.*",
|
||||||
'http\.Header\{"X-Amz-Server-Side-Encryptio":',
|
'http\.Header\{"X-Amz-Server-Side-Encryptio":',
|
||||||
"ZoEoZdLlzVbOlT9rbhD7ZN7TLyiYXSAlB79uGEge",
|
"ZoEoZdLlzVbOlT9rbhD7ZN7TLyiYXSAlB79uGEge",
|
||||||
|
"ERRO:",
|
||||||
]
|
]
|
||||||
|
|
||||||
[default.extend-words]
|
[default.extend-words]
|
||||||
|
|||||||
@@ -34,11 +34,14 @@ verifiers: lint check-gen
|
|||||||
|
|
||||||
check-gen: ## check for updated autogenerated files
|
check-gen: ## check for updated autogenerated files
|
||||||
@go generate ./... >/dev/null
|
@go generate ./... >/dev/null
|
||||||
|
@go mod tidy -compat=1.21
|
||||||
@(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false)
|
@(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false)
|
||||||
|
@(! git diff --name-only | grep 'go.sum') || (echo "Non-committed changes in auto-generated go.sum is detected, please commit them to proceed." && false)
|
||||||
|
|
||||||
lint: getdeps ## runs golangci-lint suite of linters
|
lint: getdeps ## runs golangci-lint suite of linters
|
||||||
@echo "Running $@ check"
|
@echo "Running $@ check"
|
||||||
@$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml
|
@$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml
|
||||||
|
@command typos && typos ./ || echo "typos binary is not found.. skipping.."
|
||||||
|
|
||||||
lint-fix: getdeps ## runs golangci-lint suite of linters with automatic fixes
|
lint-fix: getdeps ## runs golangci-lint suite of linters with automatic fixes
|
||||||
@echo "Running $@ check"
|
@echo "Running $@ check"
|
||||||
@@ -47,7 +50,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 +87,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 -timeout 15m -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 -timeout 15m -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
|
||||||
|
|
||||||
@@ -165,9 +168,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)
|
||||||
@@ -196,7 +199,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}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function start_minio_3_node() {
|
|||||||
|
|
||||||
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
|
# 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
|
while [ $(/tmp/mc admin info --json myminio | jq '.info.servers[].drives[].state | select(. != "ok")' | wc -l) -gt 0 ]; do sleep 1; done
|
||||||
@@ -62,48 +62,23 @@ function start_minio_3_node() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Failure
|
# Failure
|
||||||
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
|
|
||||||
done
|
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
|
||||||
@@ -115,8 +90,18 @@ 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 'API:' ${WORK_DIR}/dist-minio-*.log; then
|
||||||
echo "1"
|
echo "1"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -96,7 +78,7 @@ function start_minio_3_node() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function check_heal() {
|
function check_heal() {
|
||||||
if ! grep -q 'Status:' ${WORK_DIR}/dist-minio-*.log; then
|
if ! grep -q 'API:' ${WORK_DIR}/dist-minio-*.log; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,12 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
|||||||
Description: err.Error(),
|
Description: err.Error(),
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
}
|
}
|
||||||
|
case errors.Is(err, errTierInvalidConfig):
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "XMinioAdminTierInvalidConfig",
|
||||||
|
Description: err.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
apiErr = errorCodes.ToAPIErrWithErr(toAdminAPIErrCode(ctx, err), err)
|
apiErr = errorCodes.ToAPIErrWithErr(toAdminAPIErrCode(ctx, err), err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -479,3 +479,180 @@ func (a adminAPIHandlers) ListAccessKeysLDAP(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
writeSuccessResponseJSON(w, encryptedData)
|
writeSuccessResponseJSON(w, encryptedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListAccessKeysLDAPBulk - GET /minio/admin/v3/idp/ldap/list-access-keys-bulk
|
||||||
|
func (a adminAPIHandlers) ListAccessKeysLDAPBulk(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil || globalNotificationSys == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dnList := r.Form["userDNs"]
|
||||||
|
isAll := r.Form.Get("all") == "true"
|
||||||
|
onlySelf := !isAll && len(dnList) == 0
|
||||||
|
|
||||||
|
if isAll && len(dnList) > 0 {
|
||||||
|
// This should be checked on client side, so return generic error
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty DN list and not self, list access keys for all users
|
||||||
|
if isAll {
|
||||||
|
if !globalIAMSys.IsAllowed(policy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
|
Action: policy.ListUsersAdminAction,
|
||||||
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: cred.Claims,
|
||||||
|
}) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if len(dnList) == 1 {
|
||||||
|
var dn string
|
||||||
|
foundResult, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(dnList[0])
|
||||||
|
if err == nil {
|
||||||
|
dn = foundResult.NormDN
|
||||||
|
}
|
||||||
|
if dn == cred.ParentUser || dnList[0] == cred.ParentUser {
|
||||||
|
onlySelf = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !globalIAMSys.IsAllowed(policy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
|
Action: policy.ListServiceAccountsAdminAction,
|
||||||
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: cred.Claims,
|
||||||
|
DenyOnly: onlySelf,
|
||||||
|
}) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if onlySelf && len(dnList) == 0 {
|
||||||
|
selfDN := cred.AccessKey
|
||||||
|
if cred.ParentUser != "" {
|
||||||
|
selfDN = cred.ParentUser
|
||||||
|
}
|
||||||
|
dnList = append(dnList, selfDN)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ldapUserList []string
|
||||||
|
if isAll {
|
||||||
|
ldapUsers, err := globalIAMSys.ListLDAPUsers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for user := range ldapUsers {
|
||||||
|
ldapUserList = append(ldapUserList, user)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, userDN := range dnList {
|
||||||
|
// Validate the userDN
|
||||||
|
foundResult, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(userDN)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if foundResult == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ldapUserList = append(ldapUserList, foundResult.NormDN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listType := r.Form.Get("listType")
|
||||||
|
var listSTSKeys, listServiceAccounts bool
|
||||||
|
switch listType {
|
||||||
|
case madmin.AccessKeyListUsersOnly:
|
||||||
|
listSTSKeys = false
|
||||||
|
listServiceAccounts = false
|
||||||
|
case madmin.AccessKeyListSTSOnly:
|
||||||
|
listSTSKeys = true
|
||||||
|
listServiceAccounts = false
|
||||||
|
case madmin.AccessKeyListSvcaccOnly:
|
||||||
|
listSTSKeys = false
|
||||||
|
listServiceAccounts = true
|
||||||
|
case madmin.AccessKeyListAll:
|
||||||
|
listSTSKeys = true
|
||||||
|
listServiceAccounts = true
|
||||||
|
default:
|
||||||
|
err := errors.New("invalid list type")
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accessKeyMap := make(map[string]madmin.ListAccessKeysLDAPResp)
|
||||||
|
for _, internalDN := range ldapUserList {
|
||||||
|
externalDN := globalIAMSys.LDAPConfig.DecodeDN(internalDN)
|
||||||
|
accessKeys := madmin.ListAccessKeysLDAPResp{}
|
||||||
|
if listSTSKeys {
|
||||||
|
stsKeys, err := globalIAMSys.ListSTSAccounts(ctx, internalDN)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, sts := range stsKeys {
|
||||||
|
expiryTime := sts.Expiration
|
||||||
|
accessKeys.STSKeys = append(accessKeys.STSKeys, madmin.ServiceAccountInfo{
|
||||||
|
AccessKey: sts.AccessKey,
|
||||||
|
Expiration: &expiryTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// if only STS keys, skip if user has no STS keys
|
||||||
|
if !listServiceAccounts && len(stsKeys) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if listServiceAccounts {
|
||||||
|
serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, internalDN)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, svc := range serviceAccounts {
|
||||||
|
expiryTime := svc.Expiration
|
||||||
|
accessKeys.ServiceAccounts = append(accessKeys.ServiceAccounts, madmin.ServiceAccountInfo{
|
||||||
|
AccessKey: svc.AccessKey,
|
||||||
|
Expiration: &expiryTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// if only service accounts, skip if user has no service accounts
|
||||||
|
if !listSTSKeys && len(serviceAccounts) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accessKeyMap[externalDN] = accessKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(accessKeyMap)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, encryptedData)
|
||||||
|
}
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ func (a adminAPIHandlers) RebalanceStop(w http.ResponseWriter, r *http.Request)
|
|||||||
globalNotificationSys.StopRebalance(r.Context())
|
globalNotificationSys.StopRebalance(r.Context())
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
adminLogIf(ctx, pools.saveRebalanceStats(GlobalContext, 0, rebalSaveStoppedAt))
|
adminLogIf(ctx, pools.saveRebalanceStats(GlobalContext, 0, rebalSaveStoppedAt))
|
||||||
|
globalNotificationSys.LoadRebalanceMeta(ctx, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func proxyDecommissionRequest(ctx context.Context, defaultEndPoint Endpoint, w http.ResponseWriter, r *http.Request) (proxy bool) {
|
func proxyDecommissionRequest(ctx context.Context, defaultEndPoint Endpoint, w http.ResponseWriter, r *http.Request) (proxy bool) {
|
||||||
|
|||||||
@@ -120,9 +120,12 @@ func (s *TestSuiteIAM) TestDeleteUserRace(c *check) {
|
|||||||
c.Fatalf("Unable to set user: %v", err)
|
c.Fatalf("Unable to set user: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.adm.SetPolicy(ctx, policy, accessKey, false)
|
userReq := madmin.PolicyAssociationReq{
|
||||||
if err != nil {
|
Policies: []string{policy},
|
||||||
c.Fatalf("Unable to set policy: %v", err)
|
User: accessKey,
|
||||||
|
}
|
||||||
|
if _, err := s.adm.AttachPolicy(ctx, userReq); err != nil {
|
||||||
|
c.Fatalf("Unable to attach policy: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
accessKeys[i] = accessKey
|
accessKeys[i] = accessKey
|
||||||
|
|||||||
@@ -239,9 +239,12 @@ func (s *TestSuiteIAM) TestUserCreate(c *check) {
|
|||||||
c.Assert(v.Status, madmin.AccountEnabled)
|
c.Assert(v.Status, madmin.AccountEnabled)
|
||||||
|
|
||||||
// 3. Associate policy and check that user can access
|
// 3. Associate policy and check that user can access
|
||||||
err = s.adm.SetPolicy(ctx, "readwrite", accessKey, false)
|
_, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
|
||||||
|
Policies: []string{"readwrite"},
|
||||||
|
User: accessKey,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("unable to set policy: %v", err)
|
c.Fatalf("unable to attach policy: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := s.getUserClient(c, accessKey, secretKey, "")
|
client := s.getUserClient(c, accessKey, secretKey, "")
|
||||||
@@ -348,9 +351,12 @@ func (s *TestSuiteIAM) TestUserPolicyEscalationBug(c *check) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("policy add error: %v", err)
|
c.Fatalf("policy add error: %v", err)
|
||||||
}
|
}
|
||||||
err = s.adm.SetPolicy(ctx, policy, accessKey, false)
|
_, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
|
||||||
|
Policies: []string{policy},
|
||||||
|
User: accessKey,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("Unable to set policy: %v", err)
|
c.Fatalf("unable to attach policy: %v", err)
|
||||||
}
|
}
|
||||||
// 2.3 check user has access to bucket
|
// 2.3 check user has access to bucket
|
||||||
c.mustListObjects(ctx, uClient, bucket)
|
c.mustListObjects(ctx, uClient, bucket)
|
||||||
@@ -470,9 +476,12 @@ func (s *TestSuiteIAM) TestAddServiceAccountPerms(c *check) {
|
|||||||
c.mustNotListObjects(ctx, uClient, "testbucket")
|
c.mustNotListObjects(ctx, uClient, "testbucket")
|
||||||
|
|
||||||
// 3.2 associate policy to user
|
// 3.2 associate policy to user
|
||||||
err = s.adm.SetPolicy(ctx, policy1, accessKey, false)
|
_, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
|
||||||
|
Policies: []string{policy1},
|
||||||
|
User: accessKey,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("Unable to set policy: %v", err)
|
c.Fatalf("unable to attach policy: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
admClnt := s.getAdminClient(c, accessKey, secretKey, "")
|
admClnt := s.getAdminClient(c, accessKey, secretKey, "")
|
||||||
@@ -490,10 +499,22 @@ func (s *TestSuiteIAM) TestAddServiceAccountPerms(c *check) {
|
|||||||
c.Fatalf("policy was missing!")
|
c.Fatalf("policy was missing!")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.2 associate policy to user
|
// Detach policy1 to set up for policy2
|
||||||
err = s.adm.SetPolicy(ctx, policy2, accessKey, false)
|
_, err = s.adm.DetachPolicy(ctx, madmin.PolicyAssociationReq{
|
||||||
|
Policies: []string{policy1},
|
||||||
|
User: accessKey,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("Unable to set policy: %v", err)
|
c.Fatalf("unable to detach policy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.2 associate policy to user
|
||||||
|
_, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
|
||||||
|
Policies: []string{policy2},
|
||||||
|
User: accessKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("unable to attach policy: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.3 check user can create service account implicitly.
|
// 3.3 check user can create service account implicitly.
|
||||||
@@ -571,9 +592,12 @@ func (s *TestSuiteIAM) TestPolicyCreate(c *check) {
|
|||||||
c.mustNotListObjects(ctx, uClient, bucket)
|
c.mustNotListObjects(ctx, uClient, bucket)
|
||||||
|
|
||||||
// 3.2 associate policy to user
|
// 3.2 associate policy to user
|
||||||
err = s.adm.SetPolicy(ctx, policy, accessKey, false)
|
_, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
|
||||||
|
Policies: []string{policy},
|
||||||
|
User: accessKey,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("Unable to set policy: %v", err)
|
c.Fatalf("unable to attach policy: %v", err)
|
||||||
}
|
}
|
||||||
// 3.3 check user has access to bucket
|
// 3.3 check user has access to bucket
|
||||||
c.mustListObjects(ctx, uClient, bucket)
|
c.mustListObjects(ctx, uClient, bucket)
|
||||||
@@ -726,9 +750,12 @@ func (s *TestSuiteIAM) TestGroupAddRemove(c *check) {
|
|||||||
c.mustNotListObjects(ctx, uClient, bucket)
|
c.mustNotListObjects(ctx, uClient, bucket)
|
||||||
|
|
||||||
// 3. Associate policy to group and check user got access.
|
// 3. Associate policy to group and check user got access.
|
||||||
err = s.adm.SetPolicy(ctx, policy, group, true)
|
_, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
|
||||||
|
Policies: []string{policy},
|
||||||
|
Group: group,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("Unable to set policy: %v", err)
|
c.Fatalf("unable to attach policy: %v", err)
|
||||||
}
|
}
|
||||||
// 3.1 check user has access to bucket
|
// 3.1 check user has access to bucket
|
||||||
c.mustListObjects(ctx, uClient, bucket)
|
c.mustListObjects(ctx, uClient, bucket)
|
||||||
@@ -871,9 +898,12 @@ func (s *TestSuiteIAM) TestServiceAccountOpsByUser(c *check) {
|
|||||||
c.Fatalf("Unable to set user: %v", err)
|
c.Fatalf("Unable to set user: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.adm.SetPolicy(ctx, policy, accessKey, false)
|
_, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
|
||||||
|
Policies: []string{policy},
|
||||||
|
User: accessKey,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("Unable to set policy: %v", err)
|
c.Fatalf("unable to attach policy: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an madmin client with user creds
|
// Create an madmin client with user creds
|
||||||
@@ -952,9 +982,12 @@ func (s *TestSuiteIAM) TestServiceAccountDurationSecondsCondition(c *check) {
|
|||||||
c.Fatalf("Unable to set user: %v", err)
|
c.Fatalf("Unable to set user: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.adm.SetPolicy(ctx, policy, accessKey, false)
|
_, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
|
||||||
|
Policies: []string{policy},
|
||||||
|
User: accessKey,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("Unable to set policy: %v", err)
|
c.Fatalf("unable to attach policy: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an madmin client with user creds
|
// Create an madmin client with user creds
|
||||||
@@ -1031,9 +1064,12 @@ func (s *TestSuiteIAM) TestServiceAccountOpsByAdmin(c *check) {
|
|||||||
c.Fatalf("Unable to set user: %v", err)
|
c.Fatalf("Unable to set user: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.adm.SetPolicy(ctx, policy, accessKey, false)
|
_, err = s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
|
||||||
|
Policies: []string{policy},
|
||||||
|
User: accessKey,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("Unable to set policy: %v", err)
|
c.Fatalf("unable to attach policy: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Create a service account for the user
|
// 1. Create a service account for the user
|
||||||
|
|||||||
+11
-6
@@ -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)
|
||||||
@@ -844,9 +848,10 @@ func (a adminAPIHandlers) DataUsageInfoHandler(w http.ResponseWriter, r *http.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
func lriToLockEntry(l lockRequesterInfo, now time.Time, resource, server string) *madmin.LockEntry {
|
func lriToLockEntry(l lockRequesterInfo, now time.Time, resource, server string) *madmin.LockEntry {
|
||||||
|
t := time.Unix(0, l.Timestamp)
|
||||||
entry := &madmin.LockEntry{
|
entry := &madmin.LockEntry{
|
||||||
Timestamp: l.Timestamp,
|
Timestamp: t,
|
||||||
Elapsed: now.Sub(l.Timestamp),
|
Elapsed: now.Sub(t),
|
||||||
Resource: resource,
|
Resource: resource,
|
||||||
ServerList: []string{server},
|
ServerList: []string{server},
|
||||||
Source: l.Source,
|
Source: l.Source,
|
||||||
@@ -2182,7 +2187,7 @@ func (a adminAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Req
|
|||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMSKeyStatusHandler - GET /minio/admin/v3/kms/status
|
// KMSStatusHandler - GET /minio/admin/v3/kms/status
|
||||||
func (a adminAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
@@ -2417,7 +2422,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,
|
||||||
|
|||||||
@@ -463,6 +463,7 @@ func TestTopLockEntries(t *testing.T) {
|
|||||||
Owner: lri.Owner,
|
Owner: lri.Owner,
|
||||||
ID: lri.UID,
|
ID: lri.UID,
|
||||||
Quorum: lri.Quorum,
|
Quorum: lri.Quorum,
|
||||||
|
Timestamp: time.Unix(0, lri.Timestamp),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -761,6 +761,15 @@ func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItem
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countOKDrives := func(drives []madmin.HealDriveInfo) (count int) {
|
||||||
|
for _, drive := range drives {
|
||||||
|
if drive.State == madmin.DriveStateOk {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
// task queued, now wait for the response.
|
// task queued, now wait for the response.
|
||||||
select {
|
select {
|
||||||
case res := <-task.respCh:
|
case res := <-task.respCh:
|
||||||
@@ -781,6 +790,11 @@ func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItem
|
|||||||
if res.err != nil {
|
if res.err != nil {
|
||||||
res.result.Detail = res.err.Error()
|
res.result.Detail = res.err.Error()
|
||||||
}
|
}
|
||||||
|
if res.result.ParityBlocks > 0 && res.result.DataBlocks > 0 && res.result.DataBlocks > res.result.ParityBlocks {
|
||||||
|
if got := countOKDrives(res.result.After.Drives); got < res.result.ParityBlocks {
|
||||||
|
res.result.Detail = fmt.Sprintf("quorum loss - expected %d minimum, got drive states in OK %d", res.result.ParityBlocks, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
return h.pushHealResultItem(res.result)
|
return h.pushHealResultItem(res.result)
|
||||||
case <-h.ctx.Done():
|
case <-h.ctx.Done():
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+6
-2
@@ -301,8 +301,9 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
// LDAP specific service accounts ops
|
// LDAP specific service accounts ops
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/idp/ldap/add-service-account").HandlerFunc(adminMiddleware(adminAPI.AddServiceAccountLDAP))
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/idp/ldap/add-service-account").HandlerFunc(adminMiddleware(adminAPI.AddServiceAccountLDAP))
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/ldap/list-access-keys").
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/ldap/list-access-keys").
|
||||||
HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysLDAP)).
|
HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysLDAP)).Queries("userDN", "{userDN:.*}", "listType", "{listType:.*}")
|
||||||
Queries("userDN", "{userDN:.*}", "listType", "{listType:.*}")
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/ldap/list-access-keys-bulk").
|
||||||
|
HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysLDAPBulk)).Queries("listType", "{listType:.*}")
|
||||||
|
|
||||||
// LDAP IAM operations
|
// LDAP IAM operations
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/ldap/policy-entities").HandlerFunc(adminMiddleware(adminAPI.ListLDAPPolicyMappingEntities))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/ldap/policy-entities").HandlerFunc(adminMiddleware(adminAPI.ListLDAPPolicyMappingEntities))
|
||||||
@@ -340,6 +341,9 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-jobs").HandlerFunc(
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-jobs").HandlerFunc(
|
||||||
adminMiddleware(adminAPI.ListBatchJobs))
|
adminMiddleware(adminAPI.ListBatchJobs))
|
||||||
|
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/status-job").HandlerFunc(
|
||||||
|
adminMiddleware(adminAPI.BatchJobStatus))
|
||||||
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/describe-job").HandlerFunc(
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/describe-job").HandlerFunc(
|
||||||
adminMiddleware(adminAPI.DescribeBatchJob))
|
adminMiddleware(adminAPI.DescribeBatchJob))
|
||||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/cancel-job").HandlerFunc(
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/cancel-job").HandlerFunc(
|
||||||
|
|||||||
@@ -445,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
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1305,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.",
|
||||||
|
|||||||
+15
-5
@@ -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 generateCompleteMultipartUploadResponse(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,
|
||||||
@@ -946,10 +946,20 @@ func writeSuccessResponseHeadersOnly(w http.ResponseWriter) {
|
|||||||
|
|
||||||
// writeErrorResponse writes error headers
|
// writeErrorResponse writes error headers
|
||||||
func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError, reqURL *url.URL) {
|
func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError, reqURL *url.URL) {
|
||||||
if err.HTTPStatusCode == http.StatusServiceUnavailable {
|
switch err.HTTPStatusCode {
|
||||||
// Set retry-after header to indicate user-agents to retry request after 120secs.
|
case http.StatusServiceUnavailable:
|
||||||
|
// Set retry-after header to indicate user-agents to retry request after 60 seconds.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
||||||
w.Header().Set(xhttp.RetryAfter, "120")
|
w.Header().Set(xhttp.RetryAfter, "60")
|
||||||
|
case http.StatusTooManyRequests:
|
||||||
|
_, deadline := globalAPIConfig.getRequestsPool()
|
||||||
|
if deadline <= 0 {
|
||||||
|
// Set retry-after header to indicate user-agents to retry request after 10 seconds.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
||||||
|
w.Header().Set(xhttp.RetryAfter, "10")
|
||||||
|
} else {
|
||||||
|
w.Header().Set(xhttp.RetryAfter, strconv.Itoa(int(deadline.Seconds())))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch err.Code {
|
switch err.Code {
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -88,6 +88,8 @@ type healingTracker struct {
|
|||||||
|
|
||||||
ItemsSkipped uint64
|
ItemsSkipped uint64
|
||||||
BytesSkipped uint64
|
BytesSkipped uint64
|
||||||
|
|
||||||
|
RetryAttempts uint64
|
||||||
// Add future tracking capabilities
|
// Add future tracking capabilities
|
||||||
// Be sure that they are included in toHealingDisk
|
// Be sure that they are included in toHealingDisk
|
||||||
}
|
}
|
||||||
@@ -360,10 +362,10 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
|
|
||||||
func getLocalDisksToHeal() (disksToHeal Endpoints) {
|
func getLocalDisksToHeal() (disksToHeal Endpoints) {
|
||||||
globalLocalDrivesMu.RLock()
|
globalLocalDrivesMu.RLock()
|
||||||
localDrives := cloneDrives(globalLocalDrives)
|
localDrives := cloneDrives(globalLocalDrivesMap)
|
||||||
globalLocalDrivesMu.RUnlock()
|
globalLocalDrivesMu.RUnlock()
|
||||||
for _, disk := range localDrives {
|
for _, disk := range localDrives {
|
||||||
_, err := disk.GetDiskID()
|
_, err := disk.DiskInfo(context.Background(), DiskInfoOptions{})
|
||||||
if errors.Is(err, errUnformattedDisk) {
|
if errors.Is(err, errUnformattedDisk) {
|
||||||
disksToHeal = append(disksToHeal, disk.Endpoint())
|
disksToHeal = append(disksToHeal, disk.Endpoint())
|
||||||
continue
|
continue
|
||||||
@@ -382,6 +384,8 @@ func getLocalDisksToHeal() (disksToHeal Endpoints) {
|
|||||||
|
|
||||||
var newDiskHealingTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
var newDiskHealingTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
||||||
|
|
||||||
|
var errRetryHealing = errors.New("some items failed to heal, we will retry healing this drive again")
|
||||||
|
|
||||||
func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint) error {
|
func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint) error {
|
||||||
poolIdx, setIdx := endpoint.PoolIdx, endpoint.SetIdx
|
poolIdx, setIdx := endpoint.PoolIdx, endpoint.SetIdx
|
||||||
disk := getStorageViaEndpoint(endpoint)
|
disk := getStorageViaEndpoint(endpoint)
|
||||||
@@ -389,6 +393,17 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
|||||||
return fmt.Errorf("Unexpected error disk must be initialized by now after formatting: %s", endpoint)
|
return fmt.Errorf("Unexpected error disk must be initialized by now after formatting: %s", endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err := disk.DiskInfo(ctx, DiskInfoOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errDriveIsRoot) {
|
||||||
|
// This is a root drive, ignore and move on
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, errUnformattedDisk) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent parallel erasure set healing
|
// Prevent parallel erasure set healing
|
||||||
locker := z.NewNSLock(minioMetaBucket, fmt.Sprintf("new-drive-healing/%d/%d", poolIdx, setIdx))
|
locker := z.NewNSLock(minioMetaBucket, fmt.Sprintf("new-drive-healing/%d/%d", poolIdx, setIdx))
|
||||||
lkctx, err := locker.GetLock(ctx, newDiskHealingTimeout)
|
lkctx, err := locker.GetLock(ctx, newDiskHealingTimeout)
|
||||||
@@ -451,8 +466,27 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
healingLogEvent(ctx, "Healing of drive '%s' is finished (healed: %d, skipped: %d, failed: %d).", disk, tracker.ItemsHealed, tracker.ItemsSkipped, tracker.ItemsFailed)
|
// if objects have failed healing, we attempt a retry to heal the drive upto 3 times before giving up.
|
||||||
|
if tracker.ItemsFailed > 0 && tracker.RetryAttempts < 4 {
|
||||||
|
tracker.RetryAttempts++
|
||||||
|
bugLogIf(ctx, tracker.update(ctx))
|
||||||
|
|
||||||
|
healingLogEvent(ctx, "Healing of drive '%s' is incomplete, retrying %s time (healed: %d, skipped: %d, failed: %d).", disk,
|
||||||
|
humanize.Ordinal(int(tracker.RetryAttempts)), tracker.ItemsHealed, tracker.ItemsSkipped, tracker.ItemsFailed)
|
||||||
|
return errRetryHealing
|
||||||
|
}
|
||||||
|
|
||||||
|
if tracker.ItemsFailed > 0 {
|
||||||
|
healingLogEvent(ctx, "Healing of drive '%s' is incomplete, retried %d times (healed: %d, skipped: %d, failed: %d).", disk,
|
||||||
|
tracker.RetryAttempts-1, tracker.ItemsHealed, tracker.ItemsSkipped, tracker.ItemsFailed)
|
||||||
|
} else {
|
||||||
|
if tracker.RetryAttempts > 0 {
|
||||||
|
healingLogEvent(ctx, "Healing of drive '%s' is complete, retried %d times (healed: %d, skipped: %d).", disk,
|
||||||
|
tracker.RetryAttempts-1, tracker.ItemsHealed, tracker.ItemsSkipped)
|
||||||
|
} else {
|
||||||
|
healingLogEvent(ctx, "Healing of drive '%s' is finished (healed: %d, skipped: %d).", disk, tracker.ItemsHealed, tracker.ItemsSkipped)
|
||||||
|
}
|
||||||
|
}
|
||||||
if serverDebugLog {
|
if serverDebugLog {
|
||||||
tracker.printTo(os.Stdout)
|
tracker.printTo(os.Stdout)
|
||||||
fmt.Printf("\n")
|
fmt.Printf("\n")
|
||||||
@@ -524,7 +558,7 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools) {
|
|||||||
if err := healFreshDisk(ctx, z, disk); err != nil {
|
if err := healFreshDisk(ctx, z, disk); err != nil {
|
||||||
globalBackgroundHealState.setDiskHealingStatus(disk, false)
|
globalBackgroundHealState.setDiskHealingStatus(disk, false)
|
||||||
timedout := OperationTimedOut{}
|
timedout := OperationTimedOut{}
|
||||||
if !errors.Is(err, context.Canceled) && !errors.As(err, &timedout) {
|
if !errors.Is(err, context.Canceled) && !errors.As(err, &timedout) && !errors.Is(err, errRetryHealing) {
|
||||||
printEndpointError(disk, err, false)
|
printEndpointError(disk, err, false)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -200,6 +200,12 @@ func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
err = msgp.WrapError(err, "BytesSkipped")
|
err = msgp.WrapError(err, "BytesSkipped")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "RetryAttempts":
|
||||||
|
z.RetryAttempts, err = dc.ReadUint64()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "RetryAttempts")
|
||||||
|
return
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err = dc.Skip()
|
err = dc.Skip()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -213,9 +219,9 @@ func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
|
|
||||||
// EncodeMsg implements msgp.Encodable
|
// EncodeMsg implements msgp.Encodable
|
||||||
func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
|
func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
// map header, size 25
|
// map header, size 26
|
||||||
// write "ID"
|
// write "ID"
|
||||||
err = en.Append(0xde, 0x0, 0x19, 0xa2, 0x49, 0x44)
|
err = en.Append(0xde, 0x0, 0x1a, 0xa2, 0x49, 0x44)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -478,15 +484,25 @@ func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
err = msgp.WrapError(err, "BytesSkipped")
|
err = msgp.WrapError(err, "BytesSkipped")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// write "RetryAttempts"
|
||||||
|
err = en.Append(0xad, 0x52, 0x65, 0x74, 0x72, 0x79, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteUint64(z.RetryAttempts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "RetryAttempts")
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalMsg implements msgp.Marshaler
|
// MarshalMsg implements msgp.Marshaler
|
||||||
func (z *healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
|
func (z *healingTracker) 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 "ID"
|
// string "ID"
|
||||||
o = append(o, 0xde, 0x0, 0x19, 0xa2, 0x49, 0x44)
|
o = append(o, 0xde, 0x0, 0x1a, 0xa2, 0x49, 0x44)
|
||||||
o = msgp.AppendString(o, z.ID)
|
o = msgp.AppendString(o, z.ID)
|
||||||
// string "PoolIndex"
|
// string "PoolIndex"
|
||||||
o = append(o, 0xa9, 0x50, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x78)
|
o = append(o, 0xa9, 0x50, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x78)
|
||||||
@@ -566,6 +582,9 @@ func (z *healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
// string "BytesSkipped"
|
// string "BytesSkipped"
|
||||||
o = append(o, 0xac, 0x42, 0x79, 0x74, 0x65, 0x73, 0x53, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64)
|
o = append(o, 0xac, 0x42, 0x79, 0x74, 0x65, 0x73, 0x53, 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64)
|
||||||
o = msgp.AppendUint64(o, z.BytesSkipped)
|
o = msgp.AppendUint64(o, z.BytesSkipped)
|
||||||
|
// string "RetryAttempts"
|
||||||
|
o = append(o, 0xad, 0x52, 0x65, 0x74, 0x72, 0x79, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73)
|
||||||
|
o = msgp.AppendUint64(o, z.RetryAttempts)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -763,6 +782,12 @@ func (z *healingTracker) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
err = msgp.WrapError(err, "BytesSkipped")
|
err = msgp.WrapError(err, "BytesSkipped")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "RetryAttempts":
|
||||||
|
z.RetryAttempts, bts, err = msgp.ReadUint64Bytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "RetryAttempts")
|
||||||
|
return
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
bts, err = msgp.Skip(bts)
|
bts, err = msgp.Skip(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -785,6 +810,6 @@ func (z *healingTracker) Msgsize() (s int) {
|
|||||||
for za0002 := range z.HealedBuckets {
|
for za0002 := range z.HealedBuckets {
|
||||||
s += msgp.StringPrefixSize + len(z.HealedBuckets[za0002])
|
s += msgp.StringPrefixSize + len(z.HealedBuckets[za0002])
|
||||||
}
|
}
|
||||||
s += 7 + msgp.StringPrefixSize + len(z.HealID) + 13 + msgp.Uint64Size + 13 + msgp.Uint64Size
|
s += 7 + msgp.StringPrefixSize + len(z.HealID) + 13 + msgp.Uint64Size + 13 + msgp.Uint64Size + 14 + msgp.Uint64Size
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-19
@@ -36,6 +36,7 @@ import (
|
|||||||
"github.com/minio/pkg/v3/env"
|
"github.com/minio/pkg/v3/env"
|
||||||
"github.com/minio/pkg/v3/wildcard"
|
"github.com/minio/pkg/v3/wildcard"
|
||||||
"github.com/minio/pkg/v3/workers"
|
"github.com/minio/pkg/v3/workers"
|
||||||
|
"github.com/minio/pkg/v3/xtime"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ func (p BatchJobExpirePurge) Validate() error {
|
|||||||
// BatchJobExpireFilter holds all the filters currently supported for batch replication
|
// BatchJobExpireFilter holds all the filters currently supported for batch replication
|
||||||
type BatchJobExpireFilter struct {
|
type BatchJobExpireFilter struct {
|
||||||
line, col int
|
line, col int
|
||||||
OlderThan time.Duration `yaml:"olderThan,omitempty" json:"olderThan"`
|
OlderThan xtime.Duration `yaml:"olderThan,omitempty" json:"olderThan"`
|
||||||
CreatedBefore *time.Time `yaml:"createdBefore,omitempty" json:"createdBefore"`
|
CreatedBefore *time.Time `yaml:"createdBefore,omitempty" json:"createdBefore"`
|
||||||
Tags []BatchJobKV `yaml:"tags,omitempty" json:"tags"`
|
Tags []BatchJobKV `yaml:"tags,omitempty" json:"tags"`
|
||||||
Metadata []BatchJobKV `yaml:"metadata,omitempty" json:"metadata"`
|
Metadata []BatchJobKV `yaml:"metadata,omitempty" json:"metadata"`
|
||||||
@@ -162,7 +163,7 @@ func (ef BatchJobExpireFilter) Matches(obj ObjectInfo, now time.Time) bool {
|
|||||||
if len(ef.Name) > 0 && !wildcard.Match(ef.Name, obj.Name) {
|
if len(ef.Name) > 0 && !wildcard.Match(ef.Name, obj.Name) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if ef.OlderThan > 0 && now.Sub(obj.ModTime) <= ef.OlderThan {
|
if ef.OlderThan > 0 && now.Sub(obj.ModTime) <= ef.OlderThan.D() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,7 +433,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 +465,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)
|
||||||
@@ -514,7 +515,7 @@ func (r *BatchJobExpire) Start(ctx context.Context, api ObjectLayer, job BatchJo
|
|||||||
JobType: string(job.Type()),
|
JobType: string(job.Type()),
|
||||||
StartTime: job.Started,
|
StartTime: job.Started,
|
||||||
}
|
}
|
||||||
if err := ri.load(ctx, api, job); err != nil {
|
if err := ri.loadOrInit(ctx, api, job); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,22 +553,25 @@ func (r *BatchJobExpire) Start(ctx context.Context, api ObjectLayer, job BatchJo
|
|||||||
go func() {
|
go func() {
|
||||||
saveTicker := time.NewTicker(10 * time.Second)
|
saveTicker := time.NewTicker(10 * time.Second)
|
||||||
defer saveTicker.Stop()
|
defer saveTicker.Stop()
|
||||||
for {
|
quit := false
|
||||||
|
after := time.Minute
|
||||||
|
for !quit {
|
||||||
select {
|
select {
|
||||||
case <-saveTicker.C:
|
case <-saveTicker.C:
|
||||||
// persist in-memory state to disk after every 10secs.
|
|
||||||
batchLogIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job))
|
|
||||||
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// persist in-memory state immediately before exiting due to context cancellation.
|
quit = true
|
||||||
batchLogIf(ctx, ri.updateAfter(ctx, api, 0, job))
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-saverQuitCh:
|
case <-saverQuitCh:
|
||||||
// persist in-memory state immediately to disk.
|
quit = true
|
||||||
batchLogIf(ctx, ri.updateAfter(ctx, api, 0, job))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if quit {
|
||||||
|
// save immediately if we are quitting
|
||||||
|
after = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(GlobalContext, 30*time.Second) // independent context
|
||||||
|
batchLogIf(ctx, ri.updateAfter(ctx, api, after, job))
|
||||||
|
cancel()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -584,7 +588,7 @@ func (r *BatchJobExpire) Start(ctx context.Context, api ObjectLayer, job BatchJo
|
|||||||
versionsCount int
|
versionsCount int
|
||||||
toDel []expireObjInfo
|
toDel []expireObjInfo
|
||||||
)
|
)
|
||||||
failed := true
|
failed := false
|
||||||
for result := range results {
|
for result := range results {
|
||||||
if result.Err != nil {
|
if result.Err != nil {
|
||||||
failed = true
|
failed = true
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ func (z *BatchJobExpireFilter) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
}
|
}
|
||||||
switch msgp.UnsafeString(field) {
|
switch msgp.UnsafeString(field) {
|
||||||
case "OlderThan":
|
case "OlderThan":
|
||||||
z.OlderThan, err = dc.ReadDuration()
|
err = z.OlderThan.DecodeMsg(dc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "OlderThan")
|
err = msgp.WrapError(err, "OlderThan")
|
||||||
return
|
return
|
||||||
@@ -433,7 +433,7 @@ func (z *BatchJobExpireFilter) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = en.WriteDuration(z.OlderThan)
|
err = z.OlderThan.EncodeMsg(en)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "OlderThan")
|
err = msgp.WrapError(err, "OlderThan")
|
||||||
return
|
return
|
||||||
@@ -544,7 +544,11 @@ func (z *BatchJobExpireFilter) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
// map header, size 8
|
// map header, size 8
|
||||||
// string "OlderThan"
|
// string "OlderThan"
|
||||||
o = append(o, 0x88, 0xa9, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
|
o = append(o, 0x88, 0xa9, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
|
||||||
o = msgp.AppendDuration(o, z.OlderThan)
|
o, err = z.OlderThan.MarshalMsg(o)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "OlderThan")
|
||||||
|
return
|
||||||
|
}
|
||||||
// string "CreatedBefore"
|
// string "CreatedBefore"
|
||||||
o = append(o, 0xad, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65)
|
o = append(o, 0xad, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65)
|
||||||
if z.CreatedBefore == nil {
|
if z.CreatedBefore == nil {
|
||||||
@@ -613,7 +617,7 @@ func (z *BatchJobExpireFilter) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
}
|
}
|
||||||
switch msgp.UnsafeString(field) {
|
switch msgp.UnsafeString(field) {
|
||||||
case "OlderThan":
|
case "OlderThan":
|
||||||
z.OlderThan, bts, err = msgp.ReadDurationBytes(bts)
|
bts, err = z.OlderThan.UnmarshalMsg(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "OlderThan")
|
err = msgp.WrapError(err, "OlderThan")
|
||||||
return
|
return
|
||||||
@@ -734,7 +738,7 @@ func (z *BatchJobExpireFilter) 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 *BatchJobExpireFilter) Msgsize() (s int) {
|
func (z *BatchJobExpireFilter) Msgsize() (s int) {
|
||||||
s = 1 + 10 + msgp.DurationSize + 14
|
s = 1 + 10 + z.OlderThan.Msgsize() + 14
|
||||||
if z.CreatedBefore == nil {
|
if z.CreatedBefore == nil {
|
||||||
s += msgp.NilSize
|
s += msgp.NilSize
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseBatchJobExpire(t *testing.T) {
|
func TestParseBatchJobExpire(t *testing.T) {
|
||||||
@@ -32,7 +32,7 @@ expire: # Expire objects that match a condition
|
|||||||
rules:
|
rules:
|
||||||
- type: object # regular objects with zero or more older versions
|
- type: object # regular objects with zero or more older versions
|
||||||
name: NAME # match object names that satisfy the wildcard expression.
|
name: NAME # match object names that satisfy the wildcard expression.
|
||||||
olderThan: 70h # match objects older than this value
|
olderThan: 7d10h # match objects older than this value
|
||||||
createdBefore: "2006-01-02T15:04:05.00Z" # match objects created before "date"
|
createdBefore: "2006-01-02T15:04:05.00Z" # match objects created before "date"
|
||||||
tags:
|
tags:
|
||||||
- key: name
|
- key: name
|
||||||
@@ -64,7 +64,7 @@ expire: # Expire objects that match a condition
|
|||||||
delay: 500ms # least amount of delay between each retry
|
delay: 500ms # least amount of delay between each retry
|
||||||
`
|
`
|
||||||
var job BatchJobRequest
|
var job BatchJobRequest
|
||||||
err := yaml.UnmarshalStrict([]byte(expireYaml), &job)
|
err := yaml.Unmarshal([]byte(expireYaml), &job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Failed to parse batch-job-expire yaml", err)
|
t.Fatal("Failed to parse batch-job-expire yaml", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+216
-61
@@ -28,6 +28,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -57,6 +58,11 @@ import (
|
|||||||
|
|
||||||
var globalBatchConfig batch.Config
|
var globalBatchConfig batch.Config
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Keep the completed/failed job stats 3 days before removing it
|
||||||
|
oldJobsExpiration = 3 * 24 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
// BatchJobRequest this is an internal data structure not for external consumption.
|
// BatchJobRequest this is an internal data structure not for external consumption.
|
||||||
type BatchJobRequest struct {
|
type BatchJobRequest struct {
|
||||||
ID string `yaml:"-" json:"name"`
|
ID string `yaml:"-" json:"name"`
|
||||||
@@ -262,7 +268,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
|||||||
JobType: string(job.Type()),
|
JobType: string(job.Type()),
|
||||||
StartTime: job.Started,
|
StartTime: job.Started,
|
||||||
}
|
}
|
||||||
if err := ri.load(ctx, api, job); err != nil {
|
if err := ri.loadOrInit(ctx, api, job); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if ri.Complete {
|
if ri.Complete {
|
||||||
@@ -281,12 +287,12 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
|||||||
isStorageClassOnly := len(r.Flags.Filter.Metadata) == 1 && strings.EqualFold(r.Flags.Filter.Metadata[0].Key, xhttp.AmzStorageClass)
|
isStorageClassOnly := len(r.Flags.Filter.Metadata) == 1 && strings.EqualFold(r.Flags.Filter.Metadata[0].Key, xhttp.AmzStorageClass)
|
||||||
|
|
||||||
skip := func(oi ObjectInfo) (ok bool) {
|
skip := func(oi ObjectInfo) (ok bool) {
|
||||||
if r.Flags.Filter.OlderThan > 0 && time.Since(oi.ModTime) < r.Flags.Filter.OlderThan {
|
if r.Flags.Filter.OlderThan > 0 && time.Since(oi.ModTime) < r.Flags.Filter.OlderThan.D() {
|
||||||
// skip all objects that are newer than specified older duration
|
// skip all objects that are newer than specified older duration
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Flags.Filter.NewerThan > 0 && time.Since(oi.ModTime) >= r.Flags.Filter.NewerThan {
|
if r.Flags.Filter.NewerThan > 0 && time.Since(oi.ModTime) >= r.Flags.Filter.NewerThan.D() {
|
||||||
// skip all objects that are older than specified newer duration
|
// skip all objects that are older than specified newer duration
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -534,7 +540,7 @@ func toObjectInfo(bucket, object string, objInfo miniogo.ObjectInfo) ObjectInfo
|
|||||||
return oi
|
return oi
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r BatchJobReplicateV1) writeAsArchive(ctx context.Context, objAPI ObjectLayer, remoteClnt *minio.Client, entries []ObjectInfo) error {
|
func (r BatchJobReplicateV1) writeAsArchive(ctx context.Context, objAPI ObjectLayer, remoteClnt *minio.Client, entries []ObjectInfo, prefix string) error {
|
||||||
input := make(chan minio.SnowballObject, 1)
|
input := make(chan minio.SnowballObject, 1)
|
||||||
opts := minio.SnowballOptions{
|
opts := minio.SnowballOptions{
|
||||||
Opts: minio.PutObjectOptions{},
|
Opts: minio.PutObjectOptions{},
|
||||||
@@ -556,6 +562,10 @@ func (r BatchJobReplicateV1) writeAsArchive(ctx context.Context, objAPI ObjectLa
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if prefix != "" {
|
||||||
|
entry.Name = pathJoin(prefix, entry.Name)
|
||||||
|
}
|
||||||
|
|
||||||
snowballObj := minio.SnowballObject{
|
snowballObj := minio.SnowballObject{
|
||||||
// Create path to store objects within the bucket.
|
// Create path to store objects within the bucket.
|
||||||
Key: entry.Name,
|
Key: entry.Name,
|
||||||
@@ -719,57 +729,46 @@ const (
|
|||||||
|
|
||||||
batchReplJobAPIVersion = "v1"
|
batchReplJobAPIVersion = "v1"
|
||||||
batchReplJobDefaultRetries = 3
|
batchReplJobDefaultRetries = 3
|
||||||
batchReplJobDefaultRetryDelay = 250 * time.Millisecond
|
batchReplJobDefaultRetryDelay = time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func getJobReportPath(job BatchJobRequest) string {
|
|
||||||
var fileName string
|
|
||||||
switch {
|
|
||||||
case job.Replicate != nil:
|
|
||||||
fileName = batchReplName
|
|
||||||
case job.KeyRotate != nil:
|
|
||||||
fileName = batchKeyRotationName
|
|
||||||
case job.Expire != nil:
|
|
||||||
fileName = batchExpireName
|
|
||||||
}
|
|
||||||
return pathJoin(batchJobReportsPrefix, job.ID, fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getJobPath(job BatchJobRequest) string {
|
func getJobPath(job BatchJobRequest) string {
|
||||||
return pathJoin(batchJobPrefix, job.ID)
|
return pathJoin(batchJobPrefix, job.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ri *batchJobInfo) load(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
|
func (ri *batchJobInfo) getJobReportPath() (string, error) {
|
||||||
var format, version uint16
|
var fileName string
|
||||||
switch {
|
switch madmin.BatchJobType(ri.JobType) {
|
||||||
case job.Replicate != nil:
|
case madmin.BatchJobReplicate:
|
||||||
version = batchReplVersionV1
|
fileName = batchReplName
|
||||||
format = batchReplFormat
|
case madmin.BatchJobKeyRotate:
|
||||||
case job.KeyRotate != nil:
|
fileName = batchKeyRotationName
|
||||||
version = batchKeyRotateVersionV1
|
case madmin.BatchJobExpire:
|
||||||
format = batchKeyRotationFormat
|
fileName = batchExpireName
|
||||||
case job.Expire != nil:
|
|
||||||
version = batchExpireVersionV1
|
|
||||||
format = batchExpireFormat
|
|
||||||
default:
|
default:
|
||||||
return errors.New("no supported batch job request specified")
|
return "", fmt.Errorf("unknown job type: %v", ri.JobType)
|
||||||
}
|
}
|
||||||
data, err := readConfig(ctx, api, getJobReportPath(job))
|
return pathJoin(batchJobReportsPrefix, ri.JobID, fileName), nil
|
||||||
if err != nil {
|
}
|
||||||
if errors.Is(err, errConfigNotFound) || isErrObjectNotFound(err) {
|
|
||||||
ri.Version = int(version)
|
func (ri *batchJobInfo) loadOrInit(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
|
||||||
|
err := ri.load(ctx, api, job)
|
||||||
|
if errors.Is(err, errNoSuchJob) {
|
||||||
switch {
|
switch {
|
||||||
case job.Replicate != nil:
|
case job.Replicate != nil:
|
||||||
|
ri.Version = batchReplVersionV1
|
||||||
ri.RetryAttempts = batchReplJobDefaultRetries
|
ri.RetryAttempts = batchReplJobDefaultRetries
|
||||||
if job.Replicate.Flags.Retry.Attempts > 0 {
|
if job.Replicate.Flags.Retry.Attempts > 0 {
|
||||||
ri.RetryAttempts = job.Replicate.Flags.Retry.Attempts
|
ri.RetryAttempts = job.Replicate.Flags.Retry.Attempts
|
||||||
}
|
}
|
||||||
case job.KeyRotate != nil:
|
case job.KeyRotate != nil:
|
||||||
|
ri.Version = batchKeyRotateVersionV1
|
||||||
ri.RetryAttempts = batchKeyRotateJobDefaultRetries
|
ri.RetryAttempts = batchKeyRotateJobDefaultRetries
|
||||||
if job.KeyRotate.Flags.Retry.Attempts > 0 {
|
if job.KeyRotate.Flags.Retry.Attempts > 0 {
|
||||||
ri.RetryAttempts = job.KeyRotate.Flags.Retry.Attempts
|
ri.RetryAttempts = job.KeyRotate.Flags.Retry.Attempts
|
||||||
}
|
}
|
||||||
case job.Expire != nil:
|
case job.Expire != nil:
|
||||||
|
ri.Version = batchExpireVersionV1
|
||||||
ri.RetryAttempts = batchExpireJobDefaultRetries
|
ri.RetryAttempts = batchExpireJobDefaultRetries
|
||||||
if job.Expire.Retry.Attempts > 0 {
|
if job.Expire.Retry.Attempts > 0 {
|
||||||
ri.RetryAttempts = job.Expire.Retry.Attempts
|
ri.RetryAttempts = job.Expire.Retry.Attempts
|
||||||
@@ -779,6 +778,39 @@ func (ri *batchJobInfo) load(ctx context.Context, api ObjectLayer, job BatchJobR
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ri *batchJobInfo) load(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
|
||||||
|
path, err := job.getJobReportPath()
|
||||||
|
if err != nil {
|
||||||
|
batchLogIf(ctx, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ri.loadByPath(ctx, api, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ri *batchJobInfo) loadByPath(ctx context.Context, api ObjectLayer, path string) error {
|
||||||
|
var format, version uint16
|
||||||
|
switch filepath.Base(path) {
|
||||||
|
case batchReplName:
|
||||||
|
version = batchReplVersionV1
|
||||||
|
format = batchReplFormat
|
||||||
|
case batchKeyRotationName:
|
||||||
|
version = batchKeyRotateVersionV1
|
||||||
|
format = batchKeyRotationFormat
|
||||||
|
case batchExpireName:
|
||||||
|
version = batchExpireVersionV1
|
||||||
|
format = batchExpireFormat
|
||||||
|
default:
|
||||||
|
return errors.New("no supported batch job request specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := readConfig(ctx, api, path)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errConfigNotFound) || isErrObjectNotFound(err) {
|
||||||
|
return errNoSuchJob
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
// Seems to be empty create a new batchRepl object.
|
// Seems to be empty create a new batchRepl object.
|
||||||
return nil
|
return nil
|
||||||
@@ -919,7 +951,12 @@ func (ri *batchJobInfo) updateAfter(ctx context.Context, api ObjectLayer, durati
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return saveConfig(ctx, api, getJobReportPath(job), buf)
|
path, err := ri.getJobReportPath()
|
||||||
|
if err != nil {
|
||||||
|
batchLogIf(ctx, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return saveConfig(ctx, api, path, buf)
|
||||||
}
|
}
|
||||||
ri.mu.Unlock()
|
ri.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
@@ -944,8 +981,10 @@ func (ri *batchJobInfo) trackCurrentBucketObject(bucket string, info ObjectInfo,
|
|||||||
ri.mu.Lock()
|
ri.mu.Lock()
|
||||||
defer ri.mu.Unlock()
|
defer ri.mu.Unlock()
|
||||||
|
|
||||||
|
if success {
|
||||||
ri.Bucket = bucket
|
ri.Bucket = bucket
|
||||||
ri.Object = info.Name
|
ri.Object = info.Name
|
||||||
|
}
|
||||||
ri.countItem(info.Size, info.DeleteMarker, success, attempt)
|
ri.countItem(info.Size, info.DeleteMarker, success, attempt)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -971,7 +1010,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
JobType: string(job.Type()),
|
JobType: string(job.Type()),
|
||||||
StartTime: job.Started,
|
StartTime: job.Started,
|
||||||
}
|
}
|
||||||
if err := ri.load(ctx, api, job); err != nil {
|
if err := ri.loadOrInit(ctx, api, job); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if ri.Complete {
|
if ri.Complete {
|
||||||
@@ -981,18 +1020,18 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
lastObject := ri.Object
|
lastObject := ri.Object
|
||||||
|
|
||||||
delay := job.Replicate.Flags.Retry.Delay
|
delay := job.Replicate.Flags.Retry.Delay
|
||||||
if delay == 0 {
|
if delay < time.Second {
|
||||||
delay = batchReplJobDefaultRetryDelay
|
delay = batchReplJobDefaultRetryDelay
|
||||||
}
|
}
|
||||||
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
|
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
selectObj := func(info FileInfo) (ok bool) {
|
selectObj := func(info FileInfo) (ok bool) {
|
||||||
if r.Flags.Filter.OlderThan > 0 && time.Since(info.ModTime) < r.Flags.Filter.OlderThan {
|
if r.Flags.Filter.OlderThan > 0 && time.Since(info.ModTime) < r.Flags.Filter.OlderThan.D() {
|
||||||
// skip all objects that are newer than specified older duration
|
// skip all objects that are newer than specified older duration
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Flags.Filter.NewerThan > 0 && time.Since(info.ModTime) >= r.Flags.Filter.NewerThan {
|
if r.Flags.Filter.NewerThan > 0 && time.Since(info.ModTime) >= r.Flags.Filter.NewerThan.D() {
|
||||||
// skip all objects that are older than specified newer duration
|
// skip all objects that are older than specified newer duration
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -1071,12 +1110,16 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
|
|
||||||
c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
|
c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
|
||||||
|
|
||||||
|
retryAttempts := ri.RetryAttempts
|
||||||
|
retry := false
|
||||||
|
for attempts := 1; attempts <= retryAttempts; attempts++ {
|
||||||
|
attempts := attempts
|
||||||
var (
|
var (
|
||||||
walkCh = make(chan itemOrErr[ObjectInfo], 100)
|
walkCh = make(chan itemOrErr[ObjectInfo], 100)
|
||||||
slowCh = make(chan itemOrErr[ObjectInfo], 100)
|
slowCh = make(chan itemOrErr[ObjectInfo], 100)
|
||||||
)
|
)
|
||||||
|
|
||||||
if !*r.Source.Snowball.Disable && r.Source.Type.isMinio() && r.Target.Type.isMinio() {
|
if r.Source.Snowball.Disable != nil && !*r.Source.Snowball.Disable && r.Source.Type.isMinio() && r.Target.Type.isMinio() {
|
||||||
go func() {
|
go func() {
|
||||||
// Snowball currently needs the high level minio-go Client, not the Core one
|
// Snowball currently needs the high level minio-go Client, not the Core one
|
||||||
cl, err := miniogo.New(u.Host, &miniogo.Options{
|
cl, err := miniogo.New(u.Host, &miniogo.Options{
|
||||||
@@ -1086,7 +1129,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
BucketLookup: lookupStyle(r.Target.Path),
|
BucketLookup: lookupStyle(r.Target.Path),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
batchLogIf(ctx, err)
|
batchLogOnceIf(ctx, err, job.ID+"miniogo.New")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1096,8 +1139,8 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
batch := make([]ObjectInfo, 0, *r.Source.Snowball.Batch)
|
batch := make([]ObjectInfo, 0, *r.Source.Snowball.Batch)
|
||||||
writeFn := func(batch []ObjectInfo) {
|
writeFn := func(batch []ObjectInfo) {
|
||||||
if len(batch) > 0 {
|
if len(batch) > 0 {
|
||||||
if err := r.writeAsArchive(ctx, api, cl, batch); err != nil {
|
if err := r.writeAsArchive(ctx, api, cl, batch, r.Target.Prefix); err != nil {
|
||||||
batchLogIf(ctx, err)
|
batchLogOnceIf(ctx, err, job.ID+"writeAsArchive")
|
||||||
for _, b := range batch {
|
for _, b := range batch {
|
||||||
slowCh <- itemOrErr[ObjectInfo]{Item: b}
|
slowCh <- itemOrErr[ObjectInfo]{Item: b}
|
||||||
}
|
}
|
||||||
@@ -1105,7 +1148,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
ri.trackCurrentBucketBatch(r.Source.Bucket, batch)
|
ri.trackCurrentBucketBatch(r.Source.Bucket, batch)
|
||||||
globalBatchJobsMetrics.save(job.ID, ri)
|
globalBatchJobsMetrics.save(job.ID, ri)
|
||||||
// persist in-memory state to disk after every 10secs.
|
// persist in-memory state to disk after every 10secs.
|
||||||
batchLogIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job))
|
batchLogOnceIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job), job.ID+"updateAfter")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1145,12 +1188,6 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
if walkQuorum == "" {
|
if walkQuorum == "" {
|
||||||
walkQuorum = "strict"
|
walkQuorum = "strict"
|
||||||
}
|
}
|
||||||
|
|
||||||
retryAttempts := ri.RetryAttempts
|
|
||||||
retry := false
|
|
||||||
for attempts := 1; attempts <= retryAttempts; attempts++ {
|
|
||||||
attempts := attempts
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
// one of source/target is s3, skip delete marker and all versions under the same object name.
|
// one of source/target is s3, skip delete marker and all versions under the same object name.
|
||||||
s3Type := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
|
s3Type := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
|
||||||
@@ -1171,7 +1208,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
for res := range slowCh {
|
for res := range slowCh {
|
||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
ri.Failed = true
|
ri.Failed = true
|
||||||
batchLogIf(ctx, res.Err)
|
batchLogOnceIf(ctx, res.Err, job.ID+"res.Err")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
result := res.Item
|
result := res.Item
|
||||||
@@ -1198,7 +1235,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
stopFn(result, err)
|
stopFn(result, err)
|
||||||
batchLogIf(ctx, err)
|
batchLogOnceIf(ctx, err, job.ID+"ReplicateToTarget")
|
||||||
success = false
|
success = false
|
||||||
} else {
|
} else {
|
||||||
stopFn(result, nil)
|
stopFn(result, nil)
|
||||||
@@ -1206,7 +1243,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
ri.trackCurrentBucketObject(r.Source.Bucket, result, success, attempts)
|
ri.trackCurrentBucketObject(r.Source.Bucket, result, success, attempts)
|
||||||
globalBatchJobsMetrics.save(job.ID, ri)
|
globalBatchJobsMetrics.save(job.ID, ri)
|
||||||
// persist in-memory state to disk after every 10secs.
|
// persist in-memory state to disk after every 10secs.
|
||||||
batchLogIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job))
|
batchLogOnceIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job), job.ID+"updateAfter2")
|
||||||
|
|
||||||
if wait := globalBatchConfig.ReplicationWait(); wait > 0 {
|
if wait := globalBatchConfig.ReplicationWait(); wait > 0 {
|
||||||
time.Sleep(wait)
|
time.Sleep(wait)
|
||||||
@@ -1221,10 +1258,10 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
|
|
||||||
globalBatchJobsMetrics.save(job.ID, ri)
|
globalBatchJobsMetrics.save(job.ID, ri)
|
||||||
// persist in-memory state to disk.
|
// persist in-memory state to disk.
|
||||||
batchLogIf(ctx, ri.updateAfter(ctx, api, 0, job))
|
batchLogOnceIf(ctx, ri.updateAfter(ctx, api, 0, job), job.ID+"updateAfter3")
|
||||||
|
|
||||||
if err := r.Notify(ctx, ri); err != nil {
|
if err := r.Notify(ctx, ri); err != nil {
|
||||||
batchLogIf(ctx, fmt.Errorf("unable to notify %v", err))
|
batchLogOnceIf(ctx, fmt.Errorf("unable to notify %v", err), job.ID+"notify")
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
@@ -1436,10 +1473,24 @@ func (j BatchJobRequest) Validate(ctx context.Context, o ObjectLayer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (j BatchJobRequest) delete(ctx context.Context, api ObjectLayer) {
|
func (j BatchJobRequest) delete(ctx context.Context, api ObjectLayer) {
|
||||||
deleteConfig(ctx, api, getJobReportPath(j))
|
|
||||||
deleteConfig(ctx, api, getJobPath(j))
|
deleteConfig(ctx, api, getJobPath(j))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j BatchJobRequest) getJobReportPath() (string, error) {
|
||||||
|
var fileName string
|
||||||
|
switch {
|
||||||
|
case j.Replicate != nil:
|
||||||
|
fileName = batchReplName
|
||||||
|
case j.KeyRotate != nil:
|
||||||
|
fileName = batchKeyRotationName
|
||||||
|
case j.Expire != nil:
|
||||||
|
fileName = batchExpireName
|
||||||
|
default:
|
||||||
|
return "", errors.New("unknown job type")
|
||||||
|
}
|
||||||
|
return pathJoin(batchJobReportsPrefix, j.ID, fileName), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (j *BatchJobRequest) save(ctx context.Context, api ObjectLayer) error {
|
func (j *BatchJobRequest) save(ctx context.Context, api ObjectLayer) error {
|
||||||
if j.Replicate == nil && j.KeyRotate == nil && j.Expire == nil {
|
if j.Replicate == nil && j.KeyRotate == nil && j.Expire == nil {
|
||||||
return errInvalidArgument
|
return errInvalidArgument
|
||||||
@@ -1520,6 +1571,9 @@ func (a adminAPIHandlers) ListBatchJobs(w http.ResponseWriter, r *http.Request)
|
|||||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, result.Err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAPIError(ctx, result.Err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(result.Item.Name, batchJobReportsPrefix+slashSeparator) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
req := &BatchJobRequest{}
|
req := &BatchJobRequest{}
|
||||||
if err := req.load(ctx, objectAPI, result.Item.Name); err != nil {
|
if err := req.load(ctx, objectAPI, result.Item.Name); err != nil {
|
||||||
if !errors.Is(err, errNoSuchJob) {
|
if !errors.Is(err, errNoSuchJob) {
|
||||||
@@ -1542,6 +1596,55 @@ func (a adminAPIHandlers) ListBatchJobs(w http.ResponseWriter, r *http.Request)
|
|||||||
batchLogIf(ctx, json.NewEncoder(w).Encode(&listResult))
|
batchLogIf(ctx, json.NewEncoder(w).Encode(&listResult))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchJobStatus - returns the status of a batch job saved in the disk
|
||||||
|
func (a adminAPIHandlers) BatchJobStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ListBatchJobsAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobID := r.Form.Get("jobId")
|
||||||
|
if jobID == "" {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := BatchJobRequest{ID: jobID}
|
||||||
|
if i := strings.Index(jobID, "-"); i > 0 {
|
||||||
|
switch madmin.BatchJobType(jobID[:i]) {
|
||||||
|
case madmin.BatchJobReplicate:
|
||||||
|
req.Replicate = &BatchJobReplicateV1{}
|
||||||
|
case madmin.BatchJobKeyRotate:
|
||||||
|
req.KeyRotate = &BatchJobKeyRotateV1{}
|
||||||
|
case madmin.BatchJobExpire:
|
||||||
|
req.Expire = &BatchJobExpire{}
|
||||||
|
default:
|
||||||
|
writeErrorResponseJSON(ctx, w, toAPIError(ctx, errors.New("job ID format unrecognized")), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ri := &batchJobInfo{}
|
||||||
|
if err := ri.load(ctx, objectAPI, req); err != nil {
|
||||||
|
if !errors.Is(err, errNoSuchJob) {
|
||||||
|
batchLogIf(ctx, err)
|
||||||
|
}
|
||||||
|
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := json.Marshal(madmin.BatchJobStatus{LastMetric: ri.metric()})
|
||||||
|
if err != nil {
|
||||||
|
batchLogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
var errNoSuchJob = errors.New("no such job")
|
var errNoSuchJob = errors.New("no such job")
|
||||||
|
|
||||||
// DescribeBatchJob returns the currently active batch job definition
|
// DescribeBatchJob returns the currently active batch job definition
|
||||||
@@ -1633,7 +1736,7 @@ func (a adminAPIHandlers) StartBatchJob(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
job.ID = fmt.Sprintf("%s%s%d", shortuuid.New(), getBatchJobIDSeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
|
job.ID = fmt.Sprintf("%s-%s%s%d", job.Type(), shortuuid.New(), getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
|
||||||
job.User = user
|
job.User = user
|
||||||
job.Started = time.Now()
|
job.Started = time.Now()
|
||||||
|
|
||||||
@@ -1721,11 +1824,60 @@ func newBatchJobPool(ctx context.Context, o ObjectLayer, workers int) *BatchJobP
|
|||||||
jobCancelers: make(map[string]context.CancelFunc),
|
jobCancelers: make(map[string]context.CancelFunc),
|
||||||
}
|
}
|
||||||
jpool.ResizeWorkers(workers)
|
jpool.ResizeWorkers(workers)
|
||||||
jpool.resume()
|
|
||||||
|
randomWait := func() time.Duration {
|
||||||
|
// randomWait depends on the number of nodes to avoid triggering resume and cleanups at the same time.
|
||||||
|
return time.Duration(rand.Float64() * float64(time.Duration(globalEndpoints.NEndpoints())*time.Hour))
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
jpool.resume(randomWait)
|
||||||
|
jpool.cleanupReports(randomWait)
|
||||||
|
}()
|
||||||
|
|
||||||
return jpool
|
return jpool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *BatchJobPool) resume() {
|
func (j *BatchJobPool) cleanupReports(randomWait func() time.Duration) {
|
||||||
|
t := time.NewTimer(randomWait())
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-GlobalContext.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
results := make(chan itemOrErr[ObjectInfo], 100)
|
||||||
|
ctx, cancel := context.WithCancel(j.ctx)
|
||||||
|
defer cancel()
|
||||||
|
if err := j.objLayer.Walk(ctx, minioMetaBucket, batchJobReportsPrefix, results, WalkOptions{}); err != nil {
|
||||||
|
batchLogIf(j.ctx, err)
|
||||||
|
t.Reset(randomWait())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for result := range results {
|
||||||
|
if result.Err != nil {
|
||||||
|
batchLogIf(j.ctx, result.Err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ri := &batchJobInfo{}
|
||||||
|
if err := ri.loadByPath(ctx, j.objLayer, result.Item.Name); err != nil {
|
||||||
|
batchLogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ri.Complete || ri.Failed) && time.Since(ri.LastUpdate) > oldJobsExpiration {
|
||||||
|
deleteConfig(ctx, j.objLayer, result.Item.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Reset(randomWait())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *BatchJobPool) resume(randomWait func() time.Duration) {
|
||||||
|
time.Sleep(randomWait())
|
||||||
|
|
||||||
results := make(chan itemOrErr[ObjectInfo], 100)
|
results := make(chan itemOrErr[ObjectInfo], 100)
|
||||||
ctx, cancel := context.WithCancel(j.ctx)
|
ctx, cancel := context.WithCancel(j.ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -1738,6 +1890,9 @@ func (j *BatchJobPool) resume() {
|
|||||||
batchLogIf(j.ctx, result.Err)
|
batchLogIf(j.ctx, result.Err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(result.Item.Name, batchJobReportsPrefix+slashSeparator) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
// ignore batch-replicate.bin and batch-rotate.bin entries
|
// ignore batch-replicate.bin and batch-rotate.bin entries
|
||||||
if strings.HasSuffix(result.Item.Name, slashSeparator) {
|
if strings.HasSuffix(result.Item.Name, slashSeparator) {
|
||||||
continue
|
continue
|
||||||
@@ -1988,7 +2143,7 @@ func (m *batchJobMetrics) purgeJobMetrics() {
|
|||||||
var toDeleteJobMetrics []string
|
var toDeleteJobMetrics []string
|
||||||
m.RLock()
|
m.RLock()
|
||||||
for id, metrics := range m.metrics {
|
for id, metrics := range m.metrics {
|
||||||
if time.Since(metrics.LastUpdate) > 24*time.Hour && (metrics.Complete || metrics.Failed) {
|
if time.Since(metrics.LastUpdate) > oldJobsExpiration && (metrics.Complete || metrics.Failed) {
|
||||||
toDeleteJobMetrics = append(toDeleteJobMetrics, id)
|
toDeleteJobMetrics = append(toDeleteJobMetrics, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -289,12 +288,3 @@ func (s *BatchJobSize) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||||||
*s = BatchJobSize(sz)
|
*s = BatchJobSize(sz)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getBatchJobIDSeparator - returns the separator to be used in the batch job ID
|
|
||||||
// windows requires `_` as the separator `:` will be an invalid one
|
|
||||||
func getBatchJobIDSeparator() string {
|
|
||||||
if runtime.GOOS == globalWindowsOSName {
|
|
||||||
return "_"
|
|
||||||
}
|
|
||||||
return ":"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
miniogo "github.com/minio/minio-go/v7"
|
miniogo "github.com/minio/minio-go/v7"
|
||||||
|
|
||||||
"github.com/minio/minio/internal/auth"
|
"github.com/minio/minio/internal/auth"
|
||||||
|
"github.com/minio/pkg/v3/xtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate msgp -file $GOFILE
|
//go:generate msgp -file $GOFILE
|
||||||
@@ -65,8 +65,8 @@ import (
|
|||||||
|
|
||||||
// BatchReplicateFilter holds all the filters currently supported for batch replication
|
// BatchReplicateFilter holds all the filters currently supported for batch replication
|
||||||
type BatchReplicateFilter struct {
|
type BatchReplicateFilter struct {
|
||||||
NewerThan time.Duration `yaml:"newerThan,omitempty" json:"newerThan"`
|
NewerThan xtime.Duration `yaml:"newerThan,omitempty" json:"newerThan"`
|
||||||
OlderThan time.Duration `yaml:"olderThan,omitempty" json:"olderThan"`
|
OlderThan xtime.Duration `yaml:"olderThan,omitempty" json:"olderThan"`
|
||||||
CreatedAfter time.Time `yaml:"createdAfter,omitempty" json:"createdAfter"`
|
CreatedAfter time.Time `yaml:"createdAfter,omitempty" json:"createdAfter"`
|
||||||
CreatedBefore time.Time `yaml:"createdBefore,omitempty" json:"createdBefore"`
|
CreatedBefore time.Time `yaml:"createdBefore,omitempty" json:"createdBefore"`
|
||||||
Tags []BatchJobKV `yaml:"tags,omitempty" json:"tags"`
|
Tags []BatchJobKV `yaml:"tags,omitempty" json:"tags"`
|
||||||
|
|||||||
@@ -1409,13 +1409,13 @@ func (z *BatchReplicateFilter) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
}
|
}
|
||||||
switch msgp.UnsafeString(field) {
|
switch msgp.UnsafeString(field) {
|
||||||
case "NewerThan":
|
case "NewerThan":
|
||||||
z.NewerThan, err = dc.ReadDuration()
|
err = z.NewerThan.DecodeMsg(dc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "NewerThan")
|
err = msgp.WrapError(err, "NewerThan")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "OlderThan":
|
case "OlderThan":
|
||||||
z.OlderThan, err = dc.ReadDuration()
|
err = z.OlderThan.DecodeMsg(dc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "OlderThan")
|
err = msgp.WrapError(err, "OlderThan")
|
||||||
return
|
return
|
||||||
@@ -1489,7 +1489,7 @@ func (z *BatchReplicateFilter) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = en.WriteDuration(z.NewerThan)
|
err = z.NewerThan.EncodeMsg(en)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "NewerThan")
|
err = msgp.WrapError(err, "NewerThan")
|
||||||
return
|
return
|
||||||
@@ -1499,7 +1499,7 @@ func (z *BatchReplicateFilter) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = en.WriteDuration(z.OlderThan)
|
err = z.OlderThan.EncodeMsg(en)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "OlderThan")
|
err = msgp.WrapError(err, "OlderThan")
|
||||||
return
|
return
|
||||||
@@ -1567,10 +1567,18 @@ func (z *BatchReplicateFilter) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
// map header, size 6
|
// map header, size 6
|
||||||
// string "NewerThan"
|
// string "NewerThan"
|
||||||
o = append(o, 0x86, 0xa9, 0x4e, 0x65, 0x77, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
|
o = append(o, 0x86, 0xa9, 0x4e, 0x65, 0x77, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
|
||||||
o = msgp.AppendDuration(o, z.NewerThan)
|
o, err = z.NewerThan.MarshalMsg(o)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "NewerThan")
|
||||||
|
return
|
||||||
|
}
|
||||||
// string "OlderThan"
|
// string "OlderThan"
|
||||||
o = append(o, 0xa9, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
|
o = append(o, 0xa9, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e)
|
||||||
o = msgp.AppendDuration(o, z.OlderThan)
|
o, err = z.OlderThan.MarshalMsg(o)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "OlderThan")
|
||||||
|
return
|
||||||
|
}
|
||||||
// string "CreatedAfter"
|
// string "CreatedAfter"
|
||||||
o = append(o, 0xac, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72)
|
o = append(o, 0xac, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72)
|
||||||
o = msgp.AppendTime(o, z.CreatedAfter)
|
o = msgp.AppendTime(o, z.CreatedAfter)
|
||||||
@@ -1619,13 +1627,13 @@ func (z *BatchReplicateFilter) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
}
|
}
|
||||||
switch msgp.UnsafeString(field) {
|
switch msgp.UnsafeString(field) {
|
||||||
case "NewerThan":
|
case "NewerThan":
|
||||||
z.NewerThan, bts, err = msgp.ReadDurationBytes(bts)
|
bts, err = z.NewerThan.UnmarshalMsg(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "NewerThan")
|
err = msgp.WrapError(err, "NewerThan")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "OlderThan":
|
case "OlderThan":
|
||||||
z.OlderThan, bts, err = msgp.ReadDurationBytes(bts)
|
bts, err = z.OlderThan.UnmarshalMsg(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "OlderThan")
|
err = msgp.WrapError(err, "OlderThan")
|
||||||
return
|
return
|
||||||
@@ -1694,7 +1702,7 @@ func (z *BatchReplicateFilter) 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 *BatchReplicateFilter) Msgsize() (s int) {
|
func (z *BatchReplicateFilter) Msgsize() (s int) {
|
||||||
s = 1 + 10 + msgp.DurationSize + 10 + msgp.DurationSize + 13 + msgp.TimeSize + 14 + msgp.TimeSize + 5 + msgp.ArrayHeaderSize
|
s = 1 + 10 + z.NewerThan.Msgsize() + 10 + z.OlderThan.Msgsize() + 13 + msgp.TimeSize + 14 + msgp.TimeSize + 5 + msgp.ArrayHeaderSize
|
||||||
for za0001 := range z.Tags {
|
for za0001 := range z.Tags {
|
||||||
s += z.Tags[za0001].Msgsize()
|
s += z.Tags[za0001].Msgsize()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// 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 (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseBatchJobReplicate(t *testing.T) {
|
||||||
|
replicateYaml := `
|
||||||
|
replicate:
|
||||||
|
apiVersion: v1
|
||||||
|
# source of the objects to be replicated
|
||||||
|
source:
|
||||||
|
type: minio # valid values are "s3" or "minio"
|
||||||
|
bucket: mytest
|
||||||
|
prefix: object-prefix1 # 'PREFIX' is optional
|
||||||
|
# If your source is the 'local' alias specified to 'mc batch start', then the 'endpoint' and 'credentials' fields are optional and can be omitted
|
||||||
|
# Either the 'source' or 'remote' *must* be the "local" deployment
|
||||||
|
# endpoint: "http://127.0.0.1:9000"
|
||||||
|
# # path: "on|off|auto" # "on" enables path-style bucket lookup. "off" enables virtual host (DNS)-style bucket lookup. Defaults to "auto"
|
||||||
|
# credentials:
|
||||||
|
# accessKey: minioadmin # Required
|
||||||
|
# secretKey: minioadmin # Required
|
||||||
|
# # sessionToken: SESSION-TOKEN # Optional only available when rotating credentials are used
|
||||||
|
snowball: # automatically activated if the source is local
|
||||||
|
disable: true # optionally turn-off snowball archive transfer
|
||||||
|
# batch: 100 # upto this many objects per archive
|
||||||
|
# inmemory: true # indicates if the archive must be staged locally or in-memory
|
||||||
|
# compress: false # S2/Snappy compressed archive
|
||||||
|
# smallerThan: 5MiB # create archive for all objects smaller than 5MiB
|
||||||
|
# skipErrs: false # skips any source side read() errors
|
||||||
|
|
||||||
|
# target where the objects must be replicated
|
||||||
|
target:
|
||||||
|
type: minio # valid values are "s3" or "minio"
|
||||||
|
bucket: mytest
|
||||||
|
prefix: stage # 'PREFIX' is optional
|
||||||
|
# If your source is the 'local' alias specified to 'mc batch start', then the 'endpoint' and 'credentials' fields are optional and can be omitted
|
||||||
|
|
||||||
|
# Either the 'source' or 'remote' *must* be the "local" deployment
|
||||||
|
endpoint: "http://127.0.0.1:9001"
|
||||||
|
# path: "on|off|auto" # "on" enables path-style bucket lookup. "off" enables virtual host (DNS)-style bucket lookup. Defaults to "auto"
|
||||||
|
credentials:
|
||||||
|
accessKey: minioadmin
|
||||||
|
secretKey: minioadmin
|
||||||
|
# sessionToken: SESSION-TOKEN # Optional only available when rotating credentials are used
|
||||||
|
|
||||||
|
# NOTE: All flags are optional
|
||||||
|
# - filtering criteria only applies for all source objects match the criteria
|
||||||
|
# - configurable notification endpoints
|
||||||
|
# - configurable retries for the job (each retry skips successfully previously replaced objects)
|
||||||
|
flags:
|
||||||
|
filter:
|
||||||
|
newerThan: "7d10h31s" # match objects newer than this value (e.g. 7d10h31s)
|
||||||
|
olderThan: "7d" # match objects older than this value (e.g. 7d10h31s)
|
||||||
|
# createdAfter: "date" # match objects created after "date"
|
||||||
|
# createdBefore: "date" # match objects created before "date"
|
||||||
|
|
||||||
|
## NOTE: tags are not supported when "source" is remote.
|
||||||
|
tags:
|
||||||
|
- key: "name"
|
||||||
|
value: "pick*" # match objects with tag 'name', with all values starting with 'pick'
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
- key: "content-type"
|
||||||
|
value: "image/*" # match objects with 'content-type', with all values starting with 'image/'
|
||||||
|
|
||||||
|
# notify:
|
||||||
|
# endpoint: "https://notify.endpoint" # notification endpoint to receive job status events
|
||||||
|
# token: "Bearer xxxxx" # optional authentication token for the notification endpoint
|
||||||
|
#
|
||||||
|
# retry:
|
||||||
|
# attempts: 10 # number of retries for the job before giving up
|
||||||
|
# delay: "500ms" # least amount of delay between each retry
|
||||||
|
|
||||||
|
`
|
||||||
|
var job BatchJobRequest
|
||||||
|
err := yaml.Unmarshal([]byte(replicateYaml), &job)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to parse batch-job-replicate yaml", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-1
@@ -257,7 +257,7 @@ func (r *BatchJobKeyRotateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
JobType: string(job.Type()),
|
JobType: string(job.Type()),
|
||||||
StartTime: job.Started,
|
StartTime: job.Started,
|
||||||
}
|
}
|
||||||
if err := ri.load(ctx, api, job); err != nil {
|
if err := ri.loadOrInit(ctx, api, job); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if ri.Complete {
|
if ri.Complete {
|
||||||
@@ -389,6 +389,17 @@ func (r *BatchJobKeyRotateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
stopFn(result, err)
|
stopFn(result, err)
|
||||||
batchLogIf(ctx, err)
|
batchLogIf(ctx, err)
|
||||||
success = false
|
success = false
|
||||||
|
if attempts >= retryAttempts {
|
||||||
|
auditOptions := AuditLogOptions{
|
||||||
|
Event: "KeyRotate",
|
||||||
|
APIName: "StartBatchJob",
|
||||||
|
Bucket: result.Bucket,
|
||||||
|
Object: result.Name,
|
||||||
|
VersionID: result.VersionID,
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
auditLogInternal(ctx, auditOptions)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
stopFn(result, nil)
|
stopFn(result, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -105,6 +114,9 @@ var skipEnvs = map[string]struct{}{
|
|||||||
"MINIO_ROOT_PASSWORD": {},
|
"MINIO_ROOT_PASSWORD": {},
|
||||||
"MINIO_ACCESS_KEY": {},
|
"MINIO_ACCESS_KEY": {},
|
||||||
"MINIO_SECRET_KEY": {},
|
"MINIO_SECRET_KEY": {},
|
||||||
|
"MINIO_OPERATOR_VERSION": {},
|
||||||
|
"MINIO_VSPHERE_PLUGIN_VERSION": {},
|
||||||
|
"MINIO_CI_CD": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServerSystemCfg() *ServerSystemConfig {
|
func getServerSystemCfg() *ServerSystemConfig {
|
||||||
@@ -120,7 +132,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 +179,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 +220,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
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-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"
|
||||||
@@ -92,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
|
||||||
}
|
}
|
||||||
@@ -113,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() {
|
||||||
@@ -135,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1342,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),
|
||||||
@@ -1452,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.
|
||||||
|
|||||||
@@ -583,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -737,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, getBatchJobIDSeparator())
|
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.
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWr
|
|||||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig, true)
|
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig, &validateReplicationDestinationOptions{CheckRemoteBucket: true})
|
||||||
if apiErr != noError {
|
if apiErr != noError {
|
||||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
return
|
return
|
||||||
@@ -559,7 +559,7 @@ func (api objectAPIHandlers) ValidateBucketReplicationCredsHandler(w http.Respon
|
|||||||
lockEnabled = lcfg.Enabled()
|
lockEnabled = lcfg.Enabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig, true)
|
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig, &validateReplicationDestinationOptions{CheckRemoteBucket: true})
|
||||||
if apiErr != noError {
|
if apiErr != noError {
|
||||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+158
-74
@@ -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.
|
||||||
@@ -89,7 +91,10 @@ func getReplicationConfig(ctx context.Context, bucketName string) (rc *replicati
|
|||||||
|
|
||||||
// validateReplicationDestination returns error if replication destination bucket missing or not configured
|
// validateReplicationDestination returns error if replication destination bucket missing or not configured
|
||||||
// It also returns true if replication destination is same as this server.
|
// It also returns true if replication destination is same as this server.
|
||||||
func validateReplicationDestination(ctx context.Context, bucket string, rCfg *replication.Config, checkRemote bool) (bool, APIError) {
|
func validateReplicationDestination(ctx context.Context, bucket string, rCfg *replication.Config, opts *validateReplicationDestinationOptions) (bool, APIError) {
|
||||||
|
if opts == nil {
|
||||||
|
opts = &validateReplicationDestinationOptions{}
|
||||||
|
}
|
||||||
var arns []string
|
var arns []string
|
||||||
if rCfg.RoleArn != "" {
|
if rCfg.RoleArn != "" {
|
||||||
arns = append(arns, rCfg.RoleArn)
|
arns = append(arns, rCfg.RoleArn)
|
||||||
@@ -111,7 +116,7 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
|
|||||||
if clnt == nil {
|
if clnt == nil {
|
||||||
return sameTarget, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
|
return sameTarget, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
|
||||||
}
|
}
|
||||||
if checkRemote { // validate remote bucket
|
if opts.CheckRemoteBucket { // validate remote bucket
|
||||||
found, err := clnt.BucketExists(ctx, arn.Bucket)
|
found, err := clnt.BucketExists(ctx, arn.Bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sameTarget, errorCodes.ToAPIErrWithErr(ErrRemoteDestinationNotFoundError, err)
|
return sameTarget, errorCodes.ToAPIErrWithErr(ErrRemoteDestinationNotFoundError, err)
|
||||||
@@ -131,24 +136,30 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// validate replication ARN against target endpoint
|
// if checked bucket, then check the ready is unnecessary
|
||||||
c := globalBucketTargetSys.GetRemoteTargetClient(bucket, arnStr)
|
if !opts.CheckRemoteBucket && opts.CheckReady {
|
||||||
if c != nil {
|
endpoint := clnt.EndpointURL().String()
|
||||||
if err := checkRemoteEndpoint(ctx, c.EndpointURL()); err != nil {
|
if errInt, ok := opts.checkReadyErr.Load(endpoint); !ok {
|
||||||
|
err = checkRemoteEndpoint(ctx, clnt.EndpointURL())
|
||||||
|
opts.checkReadyErr.Store(endpoint, err)
|
||||||
|
} else {
|
||||||
|
if errInt == nil {
|
||||||
|
err = nil
|
||||||
|
} else {
|
||||||
|
err = errInt.(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case BucketRemoteIdenticalToSource:
|
case BucketRemoteIdenticalToSource:
|
||||||
return true, errorCodes.ToAPIErrWithErr(ErrBucketRemoteIdenticalToSource, fmt.Errorf("remote target endpoint %s is self referential", c.EndpointURL().String()))
|
return true, errorCodes.ToAPIErrWithErr(ErrBucketRemoteIdenticalToSource, fmt.Errorf("remote target endpoint %s is self referential", clnt.EndpointURL().String()))
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.EndpointURL().String() == clnt.EndpointURL().String() {
|
// validate replication ARN against target endpoint
|
||||||
selfTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort)
|
selfTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort)
|
||||||
if !sameTarget {
|
if !sameTarget {
|
||||||
sameTarget = selfTarget
|
sameTarget = selfTarget
|
||||||
}
|
}
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(arns) == 0 {
|
if len(arns) == 0 {
|
||||||
@@ -763,32 +774,14 @@ func (m caseInsensitiveMap) Lookup(key string) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCRCMeta(oi ObjectInfo, partNum int) map[string]string {
|
|
||||||
meta := make(map[string]string)
|
|
||||||
cs := oi.decryptChecksums(partNum)
|
|
||||||
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) {
|
func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, partNum int) (putOpts minio.PutObjectOptions, err error) {
|
||||||
meta := make(map[string]string)
|
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) {
|
|
||||||
meta[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isStandardHeader(k) {
|
if isStandardHeader(k) {
|
||||||
@@ -803,10 +796,19 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(objInfo.Checksum) > 0 {
|
if len(objInfo.Checksum) > 0 {
|
||||||
for k, v := range getCRCMeta(objInfo, 0) {
|
// Add encrypted CRC to metadata for SSE-C objects.
|
||||||
|
if isSSEC {
|
||||||
|
meta[ReplicationSsecChecksumHeader] = base64.StdEncoding.EncodeToString(objInfo.Checksum)
|
||||||
|
} else {
|
||||||
|
for _, pi := range objInfo.Parts {
|
||||||
|
if pi.Number == partNum {
|
||||||
|
for k, v := range pi.Checksums {
|
||||||
meta[k] = v
|
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
|
||||||
@@ -1227,7 +1229,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{
|
||||||
@@ -1239,6 +1246,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()))
|
||||||
@@ -1291,7 +1299,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
HeaderSize: headerSize,
|
HeaderSize: headerSize,
|
||||||
}
|
}
|
||||||
newCtx := ctx
|
newCtx := ctx
|
||||||
if globalBucketMonitor.IsThrottled(bucket, tgt.ARN) {
|
if globalBucketMonitor.IsThrottled(bucket, tgt.ARN) && objInfo.Size < minLargeObjSize {
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
newCtx, cancel = context.WithTimeout(ctx, throttleDeadline)
|
newCtx, cancel = context.WithTimeout(ctx, throttleDeadline)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -1403,7 +1411,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1463,6 +1472,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())
|
||||||
@@ -1490,6 +1506,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
|
||||||
@@ -1557,7 +1574,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
HeaderSize: headerSize,
|
HeaderSize: headerSize,
|
||||||
}
|
}
|
||||||
newCtx := ctx
|
newCtx := ctx
|
||||||
if globalBucketMonitor.IsThrottled(bucket, tgt.ARN) {
|
if globalBucketMonitor.IsThrottled(bucket, tgt.ARN) && objInfo.Size < minLargeObjSize {
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
newCtx, cancel = context.WithTimeout(ctx, throttleDeadline)
|
newCtx, cancel = context.WithTimeout(ctx, throttleDeadline)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -1625,11 +1642,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)
|
||||||
@@ -1640,16 +1658,17 @@ 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")
|
||||||
crc := getCRCMeta(objInfo, partInfo.Number)
|
if !isSSEC {
|
||||||
for k, v := range crc {
|
for k, v := range partInfo.Checksums {
|
||||||
cHeader.Add(k, v)
|
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 {
|
||||||
@@ -1659,7 +1678,7 @@ 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{
|
||||||
@@ -1671,12 +1690,18 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
|||||||
ChecksumSHA256: pInfo.ChecksumSHA256,
|
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,
|
||||||
@@ -1770,12 +1795,15 @@ var (
|
|||||||
type ReplicationPool struct {
|
type ReplicationPool struct {
|
||||||
// atomic ops:
|
// atomic ops:
|
||||||
activeWorkers int32
|
activeWorkers int32
|
||||||
|
activeLrgWorkers int32
|
||||||
activeMRFWorkers int32
|
activeMRFWorkers int32
|
||||||
|
|
||||||
objLayer ObjectLayer
|
objLayer ObjectLayer
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
priority string
|
priority string
|
||||||
maxWorkers int
|
maxWorkers int
|
||||||
|
maxLWorkers int
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
mrfMU sync.Mutex
|
mrfMU sync.Mutex
|
||||||
resyncer *replicationResyncer
|
resyncer *replicationResyncer
|
||||||
@@ -1849,9 +1877,13 @@ func NewReplicationPool(ctx context.Context, o ObjectLayer, opts replicationPool
|
|||||||
if maxWorkers > 0 && failedWorkers > maxWorkers {
|
if maxWorkers > 0 && failedWorkers > maxWorkers {
|
||||||
failedWorkers = maxWorkers
|
failedWorkers = maxWorkers
|
||||||
}
|
}
|
||||||
|
maxLWorkers := LargeWorkerCount
|
||||||
|
if opts.MaxLWorkers > 0 {
|
||||||
|
maxLWorkers = opts.MaxLWorkers
|
||||||
|
}
|
||||||
pool := &ReplicationPool{
|
pool := &ReplicationPool{
|
||||||
workers: make([]chan ReplicationWorkerOperation, 0, workers),
|
workers: make([]chan ReplicationWorkerOperation, 0, workers),
|
||||||
lrgworkers: make([]chan ReplicationWorkerOperation, 0, LargeWorkerCount),
|
lrgworkers: make([]chan ReplicationWorkerOperation, 0, maxLWorkers),
|
||||||
mrfReplicaCh: make(chan ReplicationWorkerOperation, 100000),
|
mrfReplicaCh: make(chan ReplicationWorkerOperation, 100000),
|
||||||
mrfWorkerKillCh: make(chan struct{}, failedWorkers),
|
mrfWorkerKillCh: make(chan struct{}, failedWorkers),
|
||||||
resyncer: newresyncer(),
|
resyncer: newresyncer(),
|
||||||
@@ -1861,9 +1893,10 @@ func NewReplicationPool(ctx context.Context, o ObjectLayer, opts replicationPool
|
|||||||
objLayer: o,
|
objLayer: o,
|
||||||
priority: priority,
|
priority: priority,
|
||||||
maxWorkers: maxWorkers,
|
maxWorkers: maxWorkers,
|
||||||
|
maxLWorkers: maxLWorkers,
|
||||||
}
|
}
|
||||||
|
|
||||||
pool.AddLargeWorkers()
|
pool.ResizeLrgWorkers(maxLWorkers, 0)
|
||||||
pool.ResizeWorkers(workers, 0)
|
pool.ResizeWorkers(workers, 0)
|
||||||
pool.ResizeFailedWorkers(failedWorkers)
|
pool.ResizeFailedWorkers(failedWorkers)
|
||||||
go pool.resyncer.PersistToDisk(ctx, o)
|
go pool.resyncer.PersistToDisk(ctx, o)
|
||||||
@@ -1942,23 +1975,8 @@ func (p *ReplicationPool) AddWorker(input <-chan ReplicationWorkerOperation, opT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddLargeWorkers adds a static number of workers to handle large uploads
|
|
||||||
func (p *ReplicationPool) AddLargeWorkers() {
|
|
||||||
for i := 0; i < LargeWorkerCount; i++ {
|
|
||||||
p.lrgworkers = append(p.lrgworkers, make(chan ReplicationWorkerOperation, 100000))
|
|
||||||
i := i
|
|
||||||
go p.AddLargeWorker(p.lrgworkers[i])
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
<-p.ctx.Done()
|
|
||||||
for i := 0; i < LargeWorkerCount; i++ {
|
|
||||||
xioutil.SafeClose(p.lrgworkers[i])
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddLargeWorker adds a replication worker to the static pool for large uploads.
|
// AddLargeWorker adds a replication worker to the static pool for large uploads.
|
||||||
func (p *ReplicationPool) AddLargeWorker(input <-chan ReplicationWorkerOperation) {
|
func (p *ReplicationPool) AddLargeWorker(input <-chan ReplicationWorkerOperation, opTracker *int32) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-p.ctx.Done():
|
case <-p.ctx.Done():
|
||||||
@@ -1969,11 +1987,23 @@ func (p *ReplicationPool) AddLargeWorker(input <-chan ReplicationWorkerOperation
|
|||||||
}
|
}
|
||||||
switch v := oi.(type) {
|
switch v := oi.(type) {
|
||||||
case ReplicateObjectInfo:
|
case ReplicateObjectInfo:
|
||||||
|
if opTracker != nil {
|
||||||
|
atomic.AddInt32(opTracker, 1)
|
||||||
|
}
|
||||||
globalReplicationStats.incQ(v.Bucket, v.Size, v.DeleteMarker, v.OpType)
|
globalReplicationStats.incQ(v.Bucket, v.Size, v.DeleteMarker, v.OpType)
|
||||||
replicateObject(p.ctx, v, p.objLayer)
|
replicateObject(p.ctx, v, p.objLayer)
|
||||||
globalReplicationStats.decQ(v.Bucket, v.Size, v.DeleteMarker, v.OpType)
|
globalReplicationStats.decQ(v.Bucket, v.Size, v.DeleteMarker, v.OpType)
|
||||||
|
if opTracker != nil {
|
||||||
|
atomic.AddInt32(opTracker, -1)
|
||||||
|
}
|
||||||
case DeletedObjectReplicationInfo:
|
case DeletedObjectReplicationInfo:
|
||||||
|
if opTracker != nil {
|
||||||
|
atomic.AddInt32(opTracker, 1)
|
||||||
|
}
|
||||||
replicateDelete(p.ctx, v, p.objLayer)
|
replicateDelete(p.ctx, v, p.objLayer)
|
||||||
|
if opTracker != nil {
|
||||||
|
atomic.AddInt32(opTracker, -1)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
bugLogIf(p.ctx, fmt.Errorf("unknown replication type: %T", oi), "unknown-replicate-type")
|
bugLogIf(p.ctx, fmt.Errorf("unknown replication type: %T", oi), "unknown-replicate-type")
|
||||||
}
|
}
|
||||||
@@ -1981,6 +2011,30 @@ func (p *ReplicationPool) AddLargeWorker(input <-chan ReplicationWorkerOperation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResizeLrgWorkers sets replication workers pool for large transfers(>=128MiB) to new size.
|
||||||
|
// checkOld can be set to an expected value.
|
||||||
|
// If the worker count changed
|
||||||
|
func (p *ReplicationPool) ResizeLrgWorkers(n, checkOld int) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if (checkOld > 0 && len(p.lrgworkers) != checkOld) || n == len(p.lrgworkers) || n < 1 {
|
||||||
|
// Either already satisfied or worker count changed while we waited for the lock.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for len(p.lrgworkers) < n {
|
||||||
|
input := make(chan ReplicationWorkerOperation, 100000)
|
||||||
|
p.lrgworkers = append(p.lrgworkers, input)
|
||||||
|
|
||||||
|
go p.AddLargeWorker(input, &p.activeLrgWorkers)
|
||||||
|
}
|
||||||
|
for len(p.lrgworkers) > n {
|
||||||
|
worker := p.lrgworkers[len(p.lrgworkers)-1]
|
||||||
|
p.lrgworkers = p.lrgworkers[:len(p.lrgworkers)-1]
|
||||||
|
xioutil.SafeClose(worker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ActiveWorkers returns the number of active workers handling replication traffic.
|
// ActiveWorkers returns the number of active workers handling replication traffic.
|
||||||
func (p *ReplicationPool) ActiveWorkers() int {
|
func (p *ReplicationPool) ActiveWorkers() int {
|
||||||
return int(atomic.LoadInt32(&p.activeWorkers))
|
return int(atomic.LoadInt32(&p.activeWorkers))
|
||||||
@@ -1991,6 +2045,11 @@ func (p *ReplicationPool) ActiveMRFWorkers() int {
|
|||||||
return int(atomic.LoadInt32(&p.activeMRFWorkers))
|
return int(atomic.LoadInt32(&p.activeMRFWorkers))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActiveLrgWorkers returns the number of active workers handling traffic > 128MiB object size.
|
||||||
|
func (p *ReplicationPool) ActiveLrgWorkers() int {
|
||||||
|
return int(atomic.LoadInt32(&p.activeLrgWorkers))
|
||||||
|
}
|
||||||
|
|
||||||
// ResizeWorkers sets replication workers pool to new size.
|
// ResizeWorkers sets replication workers pool to new size.
|
||||||
// checkOld can be set to an expected value.
|
// checkOld can be set to an expected value.
|
||||||
// If the worker count changed
|
// If the worker count changed
|
||||||
@@ -2016,7 +2075,7 @@ func (p *ReplicationPool) ResizeWorkers(n, checkOld int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ResizeWorkerPriority sets replication failed workers pool size
|
// ResizeWorkerPriority sets replication failed workers pool size
|
||||||
func (p *ReplicationPool) ResizeWorkerPriority(pri string, maxWorkers int) {
|
func (p *ReplicationPool) ResizeWorkerPriority(pri string, maxWorkers, maxLWorkers int) {
|
||||||
var workers, mrfWorkers int
|
var workers, mrfWorkers int
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
switch pri {
|
switch pri {
|
||||||
@@ -2043,11 +2102,15 @@ func (p *ReplicationPool) ResizeWorkerPriority(pri string, maxWorkers int) {
|
|||||||
if maxWorkers > 0 && mrfWorkers > maxWorkers {
|
if maxWorkers > 0 && mrfWorkers > maxWorkers {
|
||||||
mrfWorkers = maxWorkers
|
mrfWorkers = maxWorkers
|
||||||
}
|
}
|
||||||
|
if maxLWorkers <= 0 {
|
||||||
|
maxLWorkers = LargeWorkerCount
|
||||||
|
}
|
||||||
p.priority = pri
|
p.priority = pri
|
||||||
p.maxWorkers = maxWorkers
|
p.maxWorkers = maxWorkers
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
p.ResizeWorkers(workers, 0)
|
p.ResizeWorkers(workers, 0)
|
||||||
p.ResizeFailedWorkers(mrfWorkers)
|
p.ResizeFailedWorkers(mrfWorkers)
|
||||||
|
p.ResizeLrgWorkers(maxLWorkers, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResizeFailedWorkers sets replication failed workers pool size
|
// ResizeFailedWorkers sets replication failed workers pool size
|
||||||
@@ -2091,9 +2154,18 @@ func (p *ReplicationPool) queueReplicaTask(ri ReplicateObjectInfo) {
|
|||||||
h := xxh3.HashString(ri.Bucket + ri.Name)
|
h := xxh3.HashString(ri.Bucket + ri.Name)
|
||||||
select {
|
select {
|
||||||
case <-p.ctx.Done():
|
case <-p.ctx.Done():
|
||||||
case p.lrgworkers[h%LargeWorkerCount] <- ri:
|
case p.lrgworkers[h%uint64(len(p.lrgworkers))] <- ri:
|
||||||
default:
|
default:
|
||||||
globalReplicationPool.queueMRFSave(ri.ToMRFEntry())
|
globalReplicationPool.queueMRFSave(ri.ToMRFEntry())
|
||||||
|
p.mu.RLock()
|
||||||
|
maxLWorkers := p.maxLWorkers
|
||||||
|
existing := len(p.lrgworkers)
|
||||||
|
p.mu.RUnlock()
|
||||||
|
maxLWorkers = min(maxLWorkers, LargeWorkerCount)
|
||||||
|
if p.ActiveLrgWorkers() < maxLWorkers {
|
||||||
|
workers := min(existing+1, maxLWorkers)
|
||||||
|
p.ResizeLrgWorkers(workers, existing)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2198,6 +2270,7 @@ func (p *ReplicationPool) queueReplicaDeleteTask(doi DeletedObjectReplicationInf
|
|||||||
type replicationPoolOpts struct {
|
type replicationPoolOpts struct {
|
||||||
Priority string
|
Priority string
|
||||||
MaxWorkers int
|
MaxWorkers int
|
||||||
|
MaxLWorkers int
|
||||||
}
|
}
|
||||||
|
|
||||||
func initBackgroundReplication(ctx context.Context, objectAPI ObjectLayer) {
|
func initBackgroundReplication(ctx context.Context, objectAPI ObjectLayer) {
|
||||||
@@ -2213,12 +2286,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
|
||||||
}
|
}
|
||||||
@@ -2403,7 +2476,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 {
|
||||||
@@ -3051,7 +3126,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
|
||||||
}
|
}
|
||||||
@@ -3060,7 +3135,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 {
|
||||||
@@ -3078,13 +3153,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 {
|
||||||
@@ -3470,7 +3545,7 @@ func (p *ReplicationPool) persistToDrive(ctx context.Context, v MRFReplicateEntr
|
|||||||
}
|
}
|
||||||
|
|
||||||
globalLocalDrivesMu.RLock()
|
globalLocalDrivesMu.RLock()
|
||||||
localDrives := cloneDrives(globalLocalDrives)
|
localDrives := cloneDrives(globalLocalDrivesMap)
|
||||||
globalLocalDrivesMu.RUnlock()
|
globalLocalDrivesMu.RUnlock()
|
||||||
|
|
||||||
for _, localDrive := range localDrives {
|
for _, localDrive := range localDrives {
|
||||||
@@ -3537,7 +3612,7 @@ func (p *ReplicationPool) loadMRF() (mrfRec MRFReplicateEntries, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
globalLocalDrivesMu.RLock()
|
globalLocalDrivesMu.RLock()
|
||||||
localDrives := cloneDrives(globalLocalDrives)
|
localDrives := cloneDrives(globalLocalDrivesMap)
|
||||||
globalLocalDrivesMu.RUnlock()
|
globalLocalDrivesMu.RUnlock()
|
||||||
|
|
||||||
for _, localDrive := range localDrives {
|
for _, localDrive := range localDrives {
|
||||||
@@ -3658,3 +3733,12 @@ func (p *ReplicationPool) getMRF(ctx context.Context, bucket string) (ch <-chan
|
|||||||
|
|
||||||
return mrfCh, nil
|
return mrfCh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateReplicationDestinationOptions is used to configure the validation of the replication destination.
|
||||||
|
// validateReplicationDestination uses this to configure the validation.
|
||||||
|
type validateReplicationDestinationOptions struct {
|
||||||
|
CheckRemoteBucket bool
|
||||||
|
CheckReady bool
|
||||||
|
|
||||||
|
checkReadyErr sync.Map
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
+14
-12
@@ -433,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)
|
||||||
@@ -686,16 +685,6 @@ func loadEnvVarsFromFiles() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if env.IsSet(kms.EnvKMSSecretKeyFile) {
|
|
||||||
kmsSecret, err := readFromSecret(env.Get(kms.EnvKMSSecretKeyFile, ""))
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(err, "Unable to read the KMS secret key inherited from secret file")
|
|
||||||
}
|
|
||||||
if kmsSecret != "" {
|
|
||||||
os.Setenv(kms.EnvKMSSecretKey, kmsSecret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if env.IsSet(config.EnvConfigEnvFile) {
|
if env.IsSet(config.EnvConfigEnvFile) {
|
||||||
ekvs, err := minioEnvironFromFile(env.Get(config.EnvConfigEnvFile, ""))
|
ekvs, err := minioEnvironFromFile(env.Get(config.EnvConfigEnvFile, ""))
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
@@ -835,7 +824,7 @@ func serverHandleEnvVars() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
globalDisableFreezeOnBoot = env.Get("_MINIO_DISABLE_API_FREEZE_ON_BOOT", "") == "true" || serverDebugLog
|
globalEnableSyncBoot = env.Get("MINIO_SYNC_BOOT", config.EnableOff) == config.EnableOn
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadRootCredentials() {
|
func loadRootCredentials() {
|
||||||
@@ -844,6 +833,7 @@ func loadRootCredentials() {
|
|||||||
// Check both cases and authenticate them if correctly defined
|
// Check both cases and authenticate them if correctly defined
|
||||||
var user, password string
|
var user, password string
|
||||||
var hasCredentials bool
|
var hasCredentials bool
|
||||||
|
var legacyCredentials bool
|
||||||
//nolint:gocritic
|
//nolint:gocritic
|
||||||
if env.IsSet(config.EnvRootUser) && env.IsSet(config.EnvRootPassword) {
|
if env.IsSet(config.EnvRootUser) && env.IsSet(config.EnvRootPassword) {
|
||||||
user = env.Get(config.EnvRootUser, "")
|
user = env.Get(config.EnvRootUser, "")
|
||||||
@@ -852,6 +842,7 @@ func loadRootCredentials() {
|
|||||||
} else if env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) {
|
} else if env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) {
|
||||||
user = env.Get(config.EnvAccessKey, "")
|
user = env.Get(config.EnvAccessKey, "")
|
||||||
password = env.Get(config.EnvSecretKey, "")
|
password = env.Get(config.EnvSecretKey, "")
|
||||||
|
legacyCredentials = true
|
||||||
hasCredentials = true
|
hasCredentials = true
|
||||||
} else if globalServerCtxt.RootUser != "" && globalServerCtxt.RootPwd != "" {
|
} else if globalServerCtxt.RootUser != "" && globalServerCtxt.RootPwd != "" {
|
||||||
user, password = globalServerCtxt.RootUser, globalServerCtxt.RootPwd
|
user, password = globalServerCtxt.RootUser, globalServerCtxt.RootPwd
|
||||||
@@ -860,8 +851,13 @@ func loadRootCredentials() {
|
|||||||
if hasCredentials {
|
if hasCredentials {
|
||||||
cred, err := auth.CreateCredentials(user, password)
|
cred, err := auth.CreateCredentials(user, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if legacyCredentials {
|
||||||
logger.Fatal(config.ErrInvalidCredentials(err),
|
logger.Fatal(config.ErrInvalidCredentials(err),
|
||||||
"Unable to validate credentials inherited from the shell environment")
|
"Unable to validate credentials inherited from the shell environment")
|
||||||
|
} else {
|
||||||
|
logger.Fatal(config.ErrInvalidRootUserCredentials(err),
|
||||||
|
"Unable to validate credentials inherited from the shell environment")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) {
|
if env.IsSet(config.EnvAccessKey) && env.IsSet(config.EnvSecretKey) {
|
||||||
msg := fmt.Sprintf("WARNING: %s and %s are deprecated.\n"+
|
msg := fmt.Sprintf("WARNING: %s and %s are deprecated.\n"+
|
||||||
@@ -875,6 +871,12 @@ func loadRootCredentials() {
|
|||||||
} else {
|
} else {
|
||||||
globalActiveCred = auth.DefaultCredentials
|
globalActiveCred = auth.DefaultCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
globalNodeAuthToken, err = authenticateNode(globalActiveCred.AccessKey, globalActiveCred.SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err, "Unable to generate internode credentials")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize KMS global variable after valiadating and loading the configuration.
|
// Initialize KMS global variable after valiadating and loading the configuration.
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func initHelp() {
|
|||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.SubnetSubSys,
|
Key: config.SubnetSubSys,
|
||||||
Type: "string",
|
Type: "string",
|
||||||
Description: "register the cluster to MinIO SUBNET",
|
Description: "register Enterprise license for the cluster",
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
|
|||||||
+42
-13
@@ -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"
|
||||||
@@ -226,7 +227,9 @@ func runDataScanner(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
binary.LittleEndian.PutUint64(tmp, cycleInfo.next)
|
binary.LittleEndian.PutUint64(tmp, cycleInfo.next)
|
||||||
tmp, _ = cycleInfo.MarshalMsg(tmp)
|
tmp, _ = cycleInfo.MarshalMsg(tmp)
|
||||||
err = saveConfig(ctx, objAPI, dataUsageBloomNamePath, tmp)
|
err = saveConfig(ctx, objAPI, dataUsageBloomNamePath, tmp)
|
||||||
scannerLogIf(ctx, err, dataUsageBloomNamePath)
|
if err != nil {
|
||||||
|
scannerLogIf(ctx, fmt.Errorf("%w, Object %s", err, dataUsageBloomNamePath))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,6 +253,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 +308,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 +333,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 +507,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 +672,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 +707,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 +743,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
|
||||||
@@ -772,7 +799,9 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
}, madmin.HealItemObject)
|
}, madmin.HealItemObject)
|
||||||
stopFn(int(ver.Size))
|
stopFn(int(ver.Size))
|
||||||
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
||||||
scannerLogIf(ctx, err, fiv.Name)
|
if err != nil {
|
||||||
|
scannerLogIf(ctx, fmt.Errorf("%w, Object %s/%s/%s", err, bucket, fiv.Name, ver.VersionID))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
successVersions++
|
successVersions++
|
||||||
@@ -1233,7 +1262,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)
|
||||||
@@ -1246,7 +1275,7 @@ func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer,
|
|||||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
ilmLogIf(ctx, err)
|
ilmLogIf(ctx, fmt.Errorf("expireTransitionedObject(%s, %s): %w", obj.Bucket, obj.Name, err))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
timeILM(1)
|
timeILM(1)
|
||||||
@@ -1299,7 +1328,7 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Assume it is still there.
|
// Assume it is still there.
|
||||||
ilmLogOnceIf(ctx, err, "non-transition-expiry")
|
ilmLogOnceIf(ctx, fmt.Errorf("DeleteObject(%s, %s): %w", obj.Bucket, obj.Name, err), "non-transition-expiry"+obj.Name)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if dobj.Name == "" {
|
if dobj.Name == "" {
|
||||||
|
|||||||
+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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,10 +61,9 @@ func NewDummyDataGen(totalLength, skipOffset int64) io.ReadSeeker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
skipOffset %= int64(len(alphabets))
|
skipOffset %= int64(len(alphabets))
|
||||||
as := make([]byte, 2*len(alphabets))
|
const multiply = 100
|
||||||
copy(as, alphabets)
|
as := bytes.Repeat(alphabets, multiply)
|
||||||
copy(as[len(alphabets):], alphabets)
|
b := as[skipOffset : skipOffset+int64(len(alphabets)*(multiply-1))]
|
||||||
b := as[skipOffset : skipOffset+int64(len(alphabets))]
|
|
||||||
return &DummyDataGen{
|
return &DummyDataGen{
|
||||||
length: totalLength,
|
length: totalLength,
|
||||||
b: b,
|
b: b,
|
||||||
|
|||||||
+29
-9
@@ -134,13 +134,18 @@ func DecryptETags(ctx context.Context, k *kms.KMS, objects []ObjectInfo) error {
|
|||||||
SSES3SinglePartObjects := make(map[int]bool)
|
SSES3SinglePartObjects := make(map[int]bool)
|
||||||
for i, object := range batch {
|
for i, object := range batch {
|
||||||
if kind, ok := crypto.IsEncrypted(object.UserDefined); ok && kind == crypto.S3 && !crypto.IsMultiPart(object.UserDefined) {
|
if kind, ok := crypto.IsEncrypted(object.UserDefined); ok && kind == crypto.S3 && !crypto.IsMultiPart(object.UserDefined) {
|
||||||
|
ETag, err := etag.Parse(object.ETag)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ETag.IsEncrypted() {
|
||||||
SSES3SinglePartObjects[i] = true
|
SSES3SinglePartObjects[i] = true
|
||||||
|
|
||||||
metadata = append(metadata, object.UserDefined)
|
metadata = append(metadata, object.UserDefined)
|
||||||
buckets = append(buckets, object.Bucket)
|
buckets = append(buckets, object.Bucket)
|
||||||
names = append(names, object.Name)
|
names = append(names, object.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If there are no SSE-S3 single-part objects
|
// If there are no SSE-S3 single-part objects
|
||||||
// we can skip the decryption process. However,
|
// we can skip the decryption process. However,
|
||||||
@@ -190,7 +195,7 @@ func DecryptETags(ctx context.Context, k *kms.KMS, objects []ObjectInfo) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if SSES3SinglePartObjects[i] && ETag.IsEncrypted() {
|
if SSES3SinglePartObjects[i] {
|
||||||
ETag, err = etag.Decrypt(keys[0][:], ETag)
|
ETag, err = etag.Decrypt(keys[0][:], ETag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1025,9 +1030,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 +1084,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 +1105,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 +1167,25 @@ 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 part > 0 && !crypto.SSEC.IsEncrypted(o.UserDefined) {
|
||||||
|
// already decrypted in ToObjectInfo for multipart objects
|
||||||
|
for _, pi := range o.Parts {
|
||||||
|
if pi.Number == part {
|
||||||
|
return pi.Checksums
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -147,6 +148,10 @@ func TestCommonTime(t *testing.T) {
|
|||||||
// TestListOnlineDisks - checks if listOnlineDisks and outDatedDisks
|
// TestListOnlineDisks - checks if listOnlineDisks and outDatedDisks
|
||||||
// are consistent with each other.
|
// are consistent with each other.
|
||||||
func TestListOnlineDisks(t *testing.T) {
|
func TestListOnlineDisks(t *testing.T) {
|
||||||
|
if runtime.GOOS == globalWindowsOSName {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -230,7 +235,7 @@ func TestListOnlineDisks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object := "object"
|
object := "object"
|
||||||
data := bytes.Repeat([]byte("a"), smallFileThreshold*16)
|
data := bytes.Repeat([]byte("a"), smallFileThreshold*32)
|
||||||
z := obj.(*erasureServerPools)
|
z := obj.(*erasureServerPools)
|
||||||
|
|
||||||
erasureDisks, err := z.GetDisks(0, 0)
|
erasureDisks, err := z.GetDisks(0, 0)
|
||||||
@@ -308,9 +313,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 +495,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 +557,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 +565,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 +637,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 +649,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
-46
@@ -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))
|
||||||
@@ -624,7 +629,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, v := range result.Before.Drives {
|
for i, v := range result.Before.Drives {
|
||||||
if v.Endpoint == disk.String() {
|
if v.Endpoint == disk.Endpoint().String() {
|
||||||
result.After.Drives[i].State = madmin.DriveStateOk
|
result.After.Drives[i].State = madmin.DriveStateOk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
+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 {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio/internal/amztime"
|
"github.com/minio/minio/internal/amztime"
|
||||||
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||||
"github.com/minio/minio/internal/bucket/replication"
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
"github.com/minio/minio/internal/crypto"
|
"github.com/minio/minio/internal/crypto"
|
||||||
"github.com/minio/minio/internal/hash/sha256"
|
"github.com/minio/minio/internal/hash/sha256"
|
||||||
@@ -173,6 +174,7 @@ func (fi FileInfo) ToObjectInfo(bucket, object string, versioned bool) ObjectInf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
objInfo.Checksum = fi.Checksum
|
objInfo.Checksum = fi.Checksum
|
||||||
|
objInfo.decryptPartsChecksums(nil)
|
||||||
objInfo.Inlined = fi.InlineData()
|
objInfo.Inlined = fi.InlineData()
|
||||||
// Success.
|
// Success.
|
||||||
return objInfo
|
return objInfo
|
||||||
@@ -456,6 +458,7 @@ func commonParity(parities []int, defaultParityCount int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listObjectParities(partsMetadata []FileInfo, errs []error) (parities []int) {
|
func listObjectParities(partsMetadata []FileInfo, errs []error) (parities []int) {
|
||||||
|
totalShards := len(partsMetadata)
|
||||||
parities = make([]int, len(partsMetadata))
|
parities = make([]int, len(partsMetadata))
|
||||||
for index, metadata := range partsMetadata {
|
for index, metadata := range partsMetadata {
|
||||||
if errs[index] != nil {
|
if errs[index] != nil {
|
||||||
@@ -466,9 +469,13 @@ func listObjectParities(partsMetadata []FileInfo, errs []error) (parities []int)
|
|||||||
parities[index] = -1
|
parities[index] = -1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
//nolint:gocritic
|
||||||
// Delete marker or zero byte objects take highest parity.
|
// Delete marker or zero byte objects take highest parity.
|
||||||
if metadata.Deleted || metadata.Size == 0 {
|
if metadata.Deleted || metadata.Size == 0 {
|
||||||
parities[index] = len(partsMetadata) / 2
|
parities[index] = totalShards / 2
|
||||||
|
} else if metadata.TransitionStatus == lifecycle.TransitionComplete {
|
||||||
|
// For tiered objects, read quorum is N/2+1 to ensure simple majority on xl.meta. It is not equal to EcM because the data integrity is entrusted with the warm tier.
|
||||||
|
parities[index] = totalShards - (totalShards/2 + 1)
|
||||||
} else {
|
} else {
|
||||||
parities[index] = metadata.Erasure.ParityBlocks
|
parities[index] = metadata.Erasure.ParityBlocks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -359,3 +361,124 @@ func TestSkipTierFreeVersion(t *testing.T) {
|
|||||||
t.Fatal("Expected SkipTierFreeVersion to be set on FileInfo but wasn't")
|
t.Fatal("Expected SkipTierFreeVersion to be set on FileInfo but wasn't")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListObjectParities(t *testing.T) {
|
||||||
|
mkMetaArr := func(N, parity, agree int) []FileInfo {
|
||||||
|
fi := newFileInfo("obj-1", N-parity, parity)
|
||||||
|
fi.TransitionTier = "WARM-TIER"
|
||||||
|
fi.TransitionedObjName = mustGetUUID()
|
||||||
|
fi.TransitionStatus = "complete"
|
||||||
|
fi.Size = 1 << 20
|
||||||
|
|
||||||
|
metaArr := make([]FileInfo, N)
|
||||||
|
for i := range N {
|
||||||
|
fi.Erasure.Index = i + 1
|
||||||
|
metaArr[i] = fi
|
||||||
|
if i < agree {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metaArr[i].TransitionTier, metaArr[i].TransitionedObjName = "", ""
|
||||||
|
metaArr[i].TransitionStatus = ""
|
||||||
|
}
|
||||||
|
return metaArr
|
||||||
|
}
|
||||||
|
mkParities := func(N, agreedParity, disagreedParity, agree int) []int {
|
||||||
|
ps := make([]int, N)
|
||||||
|
for i := range N {
|
||||||
|
if i < agree {
|
||||||
|
ps[i] = agreedParity
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ps[i] = disagreedParity // disagree
|
||||||
|
}
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
mkTest := func(N, parity, agree int) (res struct {
|
||||||
|
metaArr []FileInfo
|
||||||
|
errs []error
|
||||||
|
parities []int
|
||||||
|
parity int
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
res.metaArr = mkMetaArr(N, parity, agree)
|
||||||
|
res.parities = mkParities(N, N-(N/2+1), parity, agree)
|
||||||
|
res.errs = make([]error, N)
|
||||||
|
if agree >= N/2+1 { // simple majority consensus
|
||||||
|
res.parity = N - (N/2 + 1)
|
||||||
|
} else {
|
||||||
|
res.parity = -1
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
nonTieredTest := func(N, parity, agree int) (res struct {
|
||||||
|
metaArr []FileInfo
|
||||||
|
errs []error
|
||||||
|
parities []int
|
||||||
|
parity int
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
fi := newFileInfo("obj-1", N-parity, parity)
|
||||||
|
fi.Size = 1 << 20
|
||||||
|
metaArr := make([]FileInfo, N)
|
||||||
|
parities := make([]int, N)
|
||||||
|
for i := range N {
|
||||||
|
fi.Erasure.Index = i + 1
|
||||||
|
metaArr[i] = fi
|
||||||
|
parities[i] = parity
|
||||||
|
if i < agree {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metaArr[i].Erasure.Index = 0 // creates invalid fi on remaining drives
|
||||||
|
parities[i] = -1 // invalid fi are assigned parity -1
|
||||||
|
}
|
||||||
|
res.metaArr = metaArr
|
||||||
|
res.parities = parities
|
||||||
|
res.errs = make([]error, N)
|
||||||
|
if agree >= N-parity {
|
||||||
|
res.parity = parity
|
||||||
|
} else {
|
||||||
|
res.parity = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
metaArr []FileInfo
|
||||||
|
errs []error
|
||||||
|
parities []int
|
||||||
|
parity int
|
||||||
|
}{
|
||||||
|
// More than simple majority consensus
|
||||||
|
mkTest(15, 3, 11),
|
||||||
|
// No simple majority consensus
|
||||||
|
mkTest(15, 3, 7),
|
||||||
|
// Exact simple majority consensus
|
||||||
|
mkTest(15, 3, 8),
|
||||||
|
// More than simple majority consensus
|
||||||
|
mkTest(16, 4, 11),
|
||||||
|
// No simple majority consensus
|
||||||
|
mkTest(16, 4, 8),
|
||||||
|
// Exact simple majority consensus
|
||||||
|
mkTest(16, 4, 9),
|
||||||
|
// non-tiered object require read quorum of EcM
|
||||||
|
nonTieredTest(15, 3, 12),
|
||||||
|
// non-tiered object with fewer than EcM in consensus
|
||||||
|
nonTieredTest(15, 3, 11),
|
||||||
|
// non-tiered object require read quorum of EcM
|
||||||
|
nonTieredTest(16, 4, 12),
|
||||||
|
// non-tiered object with fewer than EcM in consensus
|
||||||
|
nonTieredTest(16, 4, 11),
|
||||||
|
}
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
|
||||||
|
if got := listObjectParities(test.metaArr, test.errs); !slices.Equal(got, test.parities) {
|
||||||
|
t.Fatalf("Expected parities %v but got %v", test.parities, got)
|
||||||
|
}
|
||||||
|
if got := commonParity(test.parities, len(test.metaArr)/2); got != test.parity {
|
||||||
|
t.Fatalf("Expected common parity %v but got %v", test.parity, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+28
-49
@@ -103,10 +103,9 @@ func (er erasureObjects) checkUploadIDExists(ctx context.Context, bucket, object
|
|||||||
return fi, partsMetadata, err
|
return fi, partsMetadata, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removes part.meta given by partName belonging to a multipart upload from minioMetaBucket
|
// cleanMultipartPath removes all extraneous files and parts from the multipart folder, this is used per CompleteMultipart.
|
||||||
func (er erasureObjects) removePartMeta(bucket, object, uploadID, dataDir string, partNumber int) {
|
// do not use this function outside of completeMultipartUpload()
|
||||||
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
|
func (er erasureObjects) cleanupMultipartPath(ctx context.Context, paths ...string) {
|
||||||
curpartPath := pathJoin(uploadIDPath, dataDir, fmt.Sprintf("part.%d", partNumber))
|
|
||||||
storageDisks := er.getDisks()
|
storageDisks := er.getDisks()
|
||||||
|
|
||||||
g := errgroup.WithNErrs(len(storageDisks))
|
g := errgroup.WithNErrs(len(storageDisks))
|
||||||
@@ -116,42 +115,7 @@ func (er erasureObjects) removePartMeta(bucket, object, uploadID, dataDir string
|
|||||||
}
|
}
|
||||||
index := index
|
index := index
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
_ = storageDisks[index].Delete(context.TODO(), minioMetaMultipartBucket, curpartPath+".meta", DeleteOptions{
|
_ = storageDisks[index].DeleteBulk(ctx, minioMetaMultipartBucket, paths...)
|
||||||
Recursive: false,
|
|
||||||
Immediate: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}, index)
|
|
||||||
}
|
|
||||||
g.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes part given by partName belonging to a multipart upload from minioMetaBucket
|
|
||||||
func (er erasureObjects) removeObjectPart(bucket, object, uploadID, dataDir string, partNumber int) {
|
|
||||||
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
|
|
||||||
curpartPath := pathJoin(uploadIDPath, dataDir, fmt.Sprintf("part.%d", partNumber))
|
|
||||||
storageDisks := er.getDisks()
|
|
||||||
|
|
||||||
g := errgroup.WithNErrs(len(storageDisks))
|
|
||||||
for index, disk := range storageDisks {
|
|
||||||
if disk == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
index := index
|
|
||||||
g.Go(func() error {
|
|
||||||
// Ignoring failure to remove parts that weren't present in CompleteMultipartUpload
|
|
||||||
// requests. xl.meta is the authoritative source of truth on which parts constitute
|
|
||||||
// the object. The presence of parts that don't belong in the object doesn't affect correctness.
|
|
||||||
_ = storageDisks[index].Delete(context.TODO(), minioMetaMultipartBucket, curpartPath, DeleteOptions{
|
|
||||||
Recursive: false,
|
|
||||||
Immediate: false,
|
|
||||||
})
|
|
||||||
_ = storageDisks[index].Delete(context.TODO(), minioMetaMultipartBucket, curpartPath+".meta", DeleteOptions{
|
|
||||||
Recursive: false,
|
|
||||||
Immediate: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}, index)
|
}, index)
|
||||||
}
|
}
|
||||||
@@ -159,7 +123,7 @@ func (er erasureObjects) removeObjectPart(bucket, object, uploadID, dataDir stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clean-up the old multipart uploads. Should be run in a Go routine.
|
// Clean-up the old multipart uploads. Should be run in a Go routine.
|
||||||
func (er erasureObjects) cleanupStaleUploads(ctx context.Context, expiry time.Duration) {
|
func (er erasureObjects) cleanupStaleUploads(ctx context.Context) {
|
||||||
// run multiple cleanup's local to this server.
|
// run multiple cleanup's local to this server.
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for _, disk := range er.getLocalDisks() {
|
for _, disk := range er.getLocalDisks() {
|
||||||
@@ -167,7 +131,7 @@ func (er erasureObjects) cleanupStaleUploads(ctx context.Context, expiry time.Du
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(disk StorageAPI) {
|
go func(disk StorageAPI) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
er.cleanupStaleUploadsOnDisk(ctx, disk, expiry)
|
er.cleanupStaleUploadsOnDisk(ctx, disk)
|
||||||
}(disk)
|
}(disk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +157,7 @@ func (er erasureObjects) deleteAll(ctx context.Context, bucket, prefix string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove the old multipart uploads on the given disk.
|
// Remove the old multipart uploads on the given disk.
|
||||||
func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk StorageAPI, expiry time.Duration) {
|
func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk StorageAPI) {
|
||||||
drivePath := disk.Endpoint().Path
|
drivePath := disk.Endpoint().Path
|
||||||
|
|
||||||
readDirFn(pathJoin(drivePath, minioMetaMultipartBucket), func(shaDir string, typ os.FileMode) error {
|
readDirFn(pathJoin(drivePath, minioMetaMultipartBucket), func(shaDir string, typ os.FileMode) error {
|
||||||
@@ -219,7 +183,7 @@ func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk Sto
|
|||||||
modTime = fi.ModTime
|
modTime = fi.ModTime
|
||||||
wait()
|
wait()
|
||||||
}
|
}
|
||||||
if time.Since(modTime) < expiry {
|
if time.Since(modTime) < globalAPIConfig.getStaleUploadsExpiry() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout())
|
w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout())
|
||||||
@@ -238,7 +202,7 @@ func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk Sto
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Modtime is returned in the Created field. See (*xlStorage).StatVol
|
// Modtime is returned in the Created field. See (*xlStorage).StatVol
|
||||||
if time.Since(vi.Created) < expiry {
|
if time.Since(vi.Created) < globalAPIConfig.getStaleUploadsExpiry() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout())
|
w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout())
|
||||||
@@ -267,7 +231,7 @@ func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk Sto
|
|||||||
w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout())
|
w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout())
|
||||||
return w.Run(func() error {
|
return w.Run(func() error {
|
||||||
wait := deleteMultipartCleanupSleeper.Timer(ctx)
|
wait := deleteMultipartCleanupSleeper.Timer(ctx)
|
||||||
if time.Since(vi.Created) > expiry {
|
if time.Since(vi.Created) > globalAPIConfig.getStaleUploadsExpiry() {
|
||||||
pathUUID := mustGetUUID()
|
pathUUID := mustGetUUID()
|
||||||
targetPath := pathJoin(drivePath, minioMetaTmpDeletedBucket, pathUUID)
|
targetPath := pathJoin(drivePath, minioMetaTmpDeletedBucket, pathUUID)
|
||||||
|
|
||||||
@@ -483,6 +447,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,6 +1263,14 @@ 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
|
||||||
var cs *hash.Checksum
|
var cs *hash.Checksum
|
||||||
@@ -1346,10 +1323,10 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
paths := make([]string, 0, len(currentFI.Parts))
|
||||||
// Remove parts that weren't present in CompleteMultipartUpload request.
|
// Remove parts that weren't present in CompleteMultipartUpload request.
|
||||||
for _, curpart := range currentFI.Parts {
|
for _, curpart := range currentFI.Parts {
|
||||||
// Remove part.meta which is not needed anymore.
|
paths = append(paths, pathJoin(uploadIDPath, currentFI.DataDir, fmt.Sprintf("part.%d.meta", curpart.Number)))
|
||||||
er.removePartMeta(bucket, object, uploadID, currentFI.DataDir, curpart.Number)
|
|
||||||
|
|
||||||
if objectPartIndex(fi.Parts, curpart.Number) == -1 {
|
if objectPartIndex(fi.Parts, curpart.Number) == -1 {
|
||||||
// Delete the missing part files. e.g,
|
// Delete the missing part files. e.g,
|
||||||
@@ -1358,10 +1335,12 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
// Request 3: PutObjectPart 2
|
// Request 3: PutObjectPart 2
|
||||||
// Request 4: CompleteMultipartUpload --part 2
|
// Request 4: CompleteMultipartUpload --part 2
|
||||||
// N.B. 1st part is not present. This part should be removed from the storage.
|
// N.B. 1st part is not present. This part should be removed from the storage.
|
||||||
er.removeObjectPart(bucket, object, uploadID, currentFI.DataDir, curpart.Number)
|
paths = append(paths, pathJoin(uploadIDPath, currentFI.DataDir, fmt.Sprintf("part.%d", curpart.Number)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
er.cleanupMultipartPath(ctx, paths...) // cleanup all part.N.meta, and skipped part.N's before final rename().
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
er.deleteAll(context.Background(), minioMetaMultipartBucket, uploadIDPath)
|
er.deleteAll(context.Background(), minioMetaMultipartBucket, uploadIDPath)
|
||||||
|
|||||||
+37
-39
@@ -20,6 +20,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -276,13 +277,13 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
if unlockOnDefer {
|
if unlockOnDefer {
|
||||||
unlockOnDefer = fi.InlineData()
|
unlockOnDefer = fi.InlineData() || len(fi.Data) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
pr, pw := xioutil.WaitPipe()
|
pr, pw := xioutil.WaitPipe()
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,6 +908,8 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
|||||||
}
|
}
|
||||||
|
|
||||||
rw.Lock()
|
rw.Lock()
|
||||||
|
// when its a versioned bucket and empty versionID - at totalResp == setDriveCount
|
||||||
|
// we must use rawFileInfo to resolve versions to figure out the latest version.
|
||||||
if opts.VersionID == "" && totalResp == er.setDriveCount {
|
if opts.VersionID == "" && totalResp == er.setDriveCount {
|
||||||
fi, onlineMeta, onlineDisks, modTime, etag, err = calcQuorum(pickLatestQuorumFilesInfo(ctx,
|
fi, onlineMeta, onlineDisks, modTime, etag, err = calcQuorum(pickLatestQuorumFilesInfo(ctx,
|
||||||
rawArr, errs, bucket, object, readData, opts.InclFreeVersions, true))
|
rawArr, errs, bucket, object, readData, opts.InclFreeVersions, true))
|
||||||
@@ -925,7 +917,7 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
|||||||
fi, onlineMeta, onlineDisks, modTime, etag, err = calcQuorum(metaArr, errs)
|
fi, onlineMeta, onlineDisks, modTime, etag, err = calcQuorum(metaArr, errs)
|
||||||
}
|
}
|
||||||
rw.Unlock()
|
rw.Unlock()
|
||||||
if err == nil && fi.InlineData() {
|
if err == nil && (fi.InlineData() || len(fi.Data) > 0) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1355,6 +1347,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
|
||||||
|
|
||||||
@@ -1403,7 +1401,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
|||||||
writers := make([]io.Writer, len(onlineDisks))
|
writers := make([]io.Writer, len(onlineDisks))
|
||||||
var inlineBuffers []*bytes.Buffer
|
var inlineBuffers []*bytes.Buffer
|
||||||
if shardFileSize >= 0 {
|
if shardFileSize >= 0 {
|
||||||
if !opts.Versioned && shardFileSize < inlineBlock {
|
if !opts.Versioned && shardFileSize <= inlineBlock {
|
||||||
inlineBuffers = make([]*bytes.Buffer, len(onlineDisks))
|
inlineBuffers = make([]*bytes.Buffer, len(onlineDisks))
|
||||||
} else if shardFileSize < inlineBlock/8 {
|
} else if shardFileSize < inlineBlock/8 {
|
||||||
inlineBuffers = make([]*bytes.Buffer, len(onlineDisks))
|
inlineBuffers = make([]*bytes.Buffer, len(onlineDisks))
|
||||||
@@ -1411,7 +1409,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
|||||||
} else {
|
} else {
|
||||||
// If compressed, use actual size to determine.
|
// If compressed, use actual size to determine.
|
||||||
if sz := erasure.ShardFileSize(data.ActualSize()); sz > 0 {
|
if sz := erasure.ShardFileSize(data.ActualSize()); sz > 0 {
|
||||||
if !opts.Versioned && sz < inlineBlock {
|
if !opts.Versioned && sz <= inlineBlock {
|
||||||
inlineBuffers = make([]*bytes.Buffer, len(onlineDisks))
|
inlineBuffers = make([]*bytes.Buffer, len(onlineDisks))
|
||||||
} else if sz < inlineBlock/8 {
|
} else if sz < inlineBlock/8 {
|
||||||
inlineBuffers = make([]*bytes.Buffer, len(onlineDisks))
|
inlineBuffers = make([]*bytes.Buffer, len(onlineDisks))
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ func (p *poolMeta) validate(pools []*erasureSets) (bool, error) {
|
|||||||
update = true
|
update = true
|
||||||
}
|
}
|
||||||
if ok && pi.completed {
|
if ok && pi.completed {
|
||||||
return false, fmt.Errorf("pool(%s) = %s is decommissioned, please remove from server command line", humanize.Ordinal(pi.position+1), k)
|
logger.LogIf(GlobalContext, "decommission", fmt.Errorf("pool(%s) = %s is decommissioned, please remove from server command line", humanize.Ordinal(pi.position+1), k))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1327,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,
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ func TestPoolMetaValidate(t *testing.T) {
|
|||||||
meta: nmeta1,
|
meta: nmeta1,
|
||||||
pools: pools,
|
pools: pools,
|
||||||
name: "Invalid-Completed-Pool-Not-Removed",
|
name: "Invalid-Completed-Pool-Not-Removed",
|
||||||
expectedErr: true,
|
expectedErr: false,
|
||||||
expectedUpdate: false,
|
expectedUpdate: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -119,11 +119,8 @@ func (z *erasureServerPools) loadRebalanceMeta(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
z.rebalMu.Lock()
|
z.rebalMu.Lock()
|
||||||
if len(r.PoolStats) == len(z.serverPools) {
|
|
||||||
z.rebalMeta = r
|
z.rebalMeta = r
|
||||||
} else {
|
|
||||||
z.updateRebalanceStats(ctx)
|
z.updateRebalanceStats(ctx)
|
||||||
}
|
|
||||||
z.rebalMu.Unlock()
|
z.rebalMu.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -147,24 +144,16 @@ func (z *erasureServerPools) updateRebalanceStats(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
lock := z.serverPools[0].NewNSLock(minioMetaBucket, rebalMetaName)
|
return z.rebalMeta.save(ctx, z.serverPools[0])
|
||||||
lkCtx, err := lock.GetLock(ctx, globalOperationTimeout)
|
|
||||||
if err != nil {
|
|
||||||
rebalanceLogIf(ctx, fmt.Errorf("failed to acquire write lock on %s/%s: %w", minioMetaBucket, rebalMetaName, err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer lock.Unlock(lkCtx)
|
|
||||||
|
|
||||||
ctx = lkCtx.Context()
|
|
||||||
|
|
||||||
noLockOpts := ObjectOptions{NoLock: true}
|
|
||||||
return z.rebalMeta.saveWithOpts(ctx, z.serverPools[0], noLockOpts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z *erasureServerPools) findIndex(index int) int {
|
func (z *erasureServerPools) findIndex(index int) int {
|
||||||
|
if z.rebalMeta == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
for i := 0; i < len(z.rebalMeta.PoolStats); i++ {
|
for i := 0; i < len(z.rebalMeta.PoolStats); i++ {
|
||||||
if i == index {
|
if i == index {
|
||||||
return index
|
return index
|
||||||
@@ -277,6 +266,10 @@ func (z *erasureServerPools) bucketRebalanceDone(bucket string, poolIdx int) {
|
|||||||
z.rebalMu.Lock()
|
z.rebalMu.Lock()
|
||||||
defer z.rebalMu.Unlock()
|
defer z.rebalMu.Unlock()
|
||||||
|
|
||||||
|
if z.rebalMeta == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ps := z.rebalMeta.PoolStats[poolIdx]
|
ps := z.rebalMeta.PoolStats[poolIdx]
|
||||||
if ps == nil {
|
if ps == nil {
|
||||||
return
|
return
|
||||||
@@ -331,6 +324,10 @@ func (r *rebalanceMeta) loadWithOpts(ctx context.Context, store objectIO, opts O
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *rebalanceMeta) saveWithOpts(ctx context.Context, store objectIO, opts ObjectOptions) error {
|
func (r *rebalanceMeta) saveWithOpts(ctx context.Context, store objectIO, opts ObjectOptions) error {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
data := make([]byte, 4, r.Msgsize()+4)
|
data := make([]byte, 4, r.Msgsize()+4)
|
||||||
|
|
||||||
// Initialize the header.
|
// Initialize the header.
|
||||||
@@ -353,8 +350,15 @@ func (z *erasureServerPools) IsRebalanceStarted() bool {
|
|||||||
z.rebalMu.RLock()
|
z.rebalMu.RLock()
|
||||||
defer z.rebalMu.RUnlock()
|
defer z.rebalMu.RUnlock()
|
||||||
|
|
||||||
if r := z.rebalMeta; r != nil {
|
r := z.rebalMeta
|
||||||
if r.StoppedAt.IsZero() {
|
if r == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !r.StoppedAt.IsZero() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, ps := range r.PoolStats {
|
||||||
|
if ps.Participating && ps.Info.Status != rebalCompleted {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +373,7 @@ func (z *erasureServerPools) IsPoolRebalancing(poolIndex int) bool {
|
|||||||
if !r.StoppedAt.IsZero() {
|
if !r.StoppedAt.IsZero() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
ps := z.rebalMeta.PoolStats[poolIndex]
|
ps := r.PoolStats[poolIndex]
|
||||||
return ps.Participating && ps.Info.Status == rebalStarted
|
return ps.Participating && ps.Info.Status == rebalStarted
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -794,8 +798,10 @@ func (z *erasureServerPools) saveRebalanceStats(ctx context.Context, poolIdx int
|
|||||||
case rebalSaveStoppedAt:
|
case rebalSaveStoppedAt:
|
||||||
r.StoppedAt = time.Now()
|
r.StoppedAt = time.Now()
|
||||||
case rebalSaveStats:
|
case rebalSaveStats:
|
||||||
|
if z.rebalMeta != nil {
|
||||||
r.PoolStats[poolIdx] = z.rebalMeta.PoolStats[poolIdx]
|
r.PoolStats[poolIdx] = z.rebalMeta.PoolStats[poolIdx]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
z.rebalMeta = r
|
z.rebalMeta = r
|
||||||
|
|
||||||
return z.rebalMeta.saveWithOpts(ctx, z.serverPools[0], noLockOpts)
|
return z.rebalMeta.saveWithOpts(ctx, z.serverPools[0], noLockOpts)
|
||||||
|
|||||||
+57
-18
@@ -168,7 +168,7 @@ func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServ
|
|||||||
|
|
||||||
if !globalIsDistErasure {
|
if !globalIsDistErasure {
|
||||||
globalLocalDrivesMu.Lock()
|
globalLocalDrivesMu.Lock()
|
||||||
globalLocalDrives = localDrives
|
globalLocalDrivesMap = make(map[string]StorageAPI, len(localDrives))
|
||||||
for _, drive := range localDrives {
|
for _, drive := range localDrives {
|
||||||
globalLocalDrivesMap[drive.Endpoint().String()] = drive
|
globalLocalDrivesMap[drive.Endpoint().String()] = drive
|
||||||
}
|
}
|
||||||
@@ -1526,15 +1526,13 @@ func (z *erasureServerPools) listObjectsGeneric(ctx context.Context, bucket, pre
|
|||||||
loi.NextMarker = last.Name
|
loi.NextMarker = last.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
if merged.lastSkippedEntry != "" {
|
if loi.IsTruncated && merged.lastSkippedEntry > loi.NextMarker {
|
||||||
if merged.lastSkippedEntry > loi.NextMarker {
|
// An object hidden by ILM was found during a truncated listing. Since the number of entries
|
||||||
// An object hidden by ILM was found during listing. Since the number of entries
|
// fetched from drives is limited by max-keys, we should use the last ILM filtered entry
|
||||||
// fetched from drives is limited, set IsTruncated to true to ask the s3 client
|
// as a continuation token if it is lexially higher than the last visible object so that the
|
||||||
// to continue listing if it wishes in order to find if there is more objects.
|
// next call of WalkDir() with the max-keys can reach new objects not seen previously.
|
||||||
loi.IsTruncated = true
|
|
||||||
loi.NextMarker = merged.lastSkippedEntry
|
loi.NextMarker = merged.lastSkippedEntry
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if loi.NextMarker != "" {
|
if loi.NextMarker != "" {
|
||||||
loi.NextMarker = opts.encodeMarker(loi.NextMarker)
|
loi.NextMarker = opts.encodeMarker(loi.NextMarker)
|
||||||
@@ -2008,12 +2006,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
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -2025,12 +2025,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2082,7 +2085,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)
|
||||||
@@ -2199,9 +2201,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 {
|
||||||
@@ -2212,6 +2213,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}:
|
||||||
@@ -2266,12 +2276,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
|
||||||
@@ -2327,12 +2341,18 @@ func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix str
|
|||||||
|
|
||||||
var poolErrs [][]error
|
var poolErrs [][]error
|
||||||
for idx, erasureSet := range z.serverPools {
|
for idx, erasureSet := range z.serverPools {
|
||||||
|
if opts.Pool != nil && *opts.Pool != idx {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if z.IsSuspended(idx) {
|
if z.IsSuspended(idx) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
errs := make([]error, len(erasureSet.sets))
|
errs := make([]error, len(erasureSet.sets))
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for idx, set := range erasureSet.sets {
|
for idx, set := range erasureSet.sets {
|
||||||
|
if opts.Set != nil && *opts.Set != idx {
|
||||||
|
continue
|
||||||
|
}
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(idx int, set *erasureObjects) {
|
go func(idx int, set *erasureObjects) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
@@ -2425,6 +2445,7 @@ const (
|
|||||||
type HealthOptions struct {
|
type HealthOptions struct {
|
||||||
Maintenance bool
|
Maintenance bool
|
||||||
DeploymentType string
|
DeploymentType string
|
||||||
|
NoLogging bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealthResult returns the current state of the system, also
|
// HealthResult returns the current state of the system, also
|
||||||
@@ -2449,6 +2470,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
|
||||||
@@ -2566,18 +2605,18 @@ 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 && !opts.NoLogging {
|
||||||
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 && !opts.NoLogging {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-40
@@ -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
|
||||||
@@ -125,13 +120,6 @@ func connectEndpoint(endpoint Endpoint) (StorageAPI, *formatErasureV3, error) {
|
|||||||
|
|
||||||
format, err := loadFormatErasure(disk, false)
|
format, err := loadFormatErasure(disk, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errUnformattedDisk) {
|
|
||||||
info, derr := disk.DiskInfo(context.TODO(), DiskInfoOptions{})
|
|
||||||
if derr != nil && info.RootDisk {
|
|
||||||
disk.Close()
|
|
||||||
return nil, nil, fmt.Errorf("Drive: %s is a root drive", disk)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
disk.Close()
|
disk.Close()
|
||||||
return nil, nil, fmt.Errorf("Drive: %s returned %w", disk, err) // make sure to '%w' to wrap the error
|
return nil, nil, fmt.Errorf("Drive: %s returned %w", disk, err) // make sure to '%w' to wrap the error
|
||||||
}
|
}
|
||||||
@@ -201,7 +189,7 @@ func (s *erasureSets) Legacy() (ok bool) {
|
|||||||
|
|
||||||
// connectDisks - attempt to connect all the endpoints, loads format
|
// connectDisks - attempt to connect all the endpoints, loads format
|
||||||
// and re-arranges the disks in proper position.
|
// and re-arranges the disks in proper position.
|
||||||
func (s *erasureSets) connectDisks() {
|
func (s *erasureSets) connectDisks(log bool) {
|
||||||
defer func() {
|
defer func() {
|
||||||
s.lastConnectDisksOpTime = time.Now()
|
s.lastConnectDisksOpTime = time.Now()
|
||||||
}()
|
}()
|
||||||
@@ -235,9 +223,11 @@ func (s *erasureSets) connectDisks() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if endpoint.IsLocal && errors.Is(err, errUnformattedDisk) {
|
if endpoint.IsLocal && errors.Is(err, errUnformattedDisk) {
|
||||||
globalBackgroundHealState.pushHealLocalDisks(endpoint)
|
globalBackgroundHealState.pushHealLocalDisks(endpoint)
|
||||||
} else {
|
} else if !errors.Is(err, errDriveIsRoot) {
|
||||||
|
if log {
|
||||||
printEndpointError(endpoint, err, true)
|
printEndpointError(endpoint, err, true)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if disk.IsLocal() && disk.Healing() != nil {
|
if disk.IsLocal() && disk.Healing() != nil {
|
||||||
@@ -272,13 +262,7 @@ func (s *erasureSets) connectDisks() {
|
|||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
globalLocalSetDrives[s.poolIndex][setIndex][diskIndex] = disk
|
globalLocalSetDrives[s.poolIndex][setIndex][diskIndex] = disk
|
||||||
}
|
}
|
||||||
for i, ldisk := range globalLocalDrives {
|
globalLocalDrivesMap[disk.Endpoint().String()] = disk
|
||||||
_, k, l := ldisk.GetDiskLoc()
|
|
||||||
if k == setIndex && l == diskIndex {
|
|
||||||
globalLocalDrives[i] = disk
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
globalLocalDrivesMu.Unlock()
|
globalLocalDrivesMu.Unlock()
|
||||||
}
|
}
|
||||||
s.erasureDisksMu.Unlock()
|
s.erasureDisksMu.Unlock()
|
||||||
@@ -297,7 +281,7 @@ func (s *erasureSets) monitorAndConnectEndpoints(ctx context.Context, monitorInt
|
|||||||
time.Sleep(time.Duration(r.Float64() * float64(time.Second)))
|
time.Sleep(time.Duration(r.Float64() * float64(time.Second)))
|
||||||
|
|
||||||
// Pre-emptively connect the disks if possible.
|
// Pre-emptively connect the disks if possible.
|
||||||
s.connectDisks()
|
s.connectDisks(false)
|
||||||
|
|
||||||
monitor := time.NewTimer(monitorInterval)
|
monitor := time.NewTimer(monitorInterval)
|
||||||
defer monitor.Stop()
|
defer monitor.Stop()
|
||||||
@@ -311,7 +295,7 @@ func (s *erasureSets) monitorAndConnectEndpoints(ctx context.Context, monitorInt
|
|||||||
console.Debugln("running drive monitoring")
|
console.Debugln("running drive monitoring")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.connectDisks()
|
s.connectDisks(true)
|
||||||
|
|
||||||
// Reset the timer for next interval
|
// Reset the timer for next interval
|
||||||
monitor.Reset(monitorInterval)
|
monitor.Reset(monitorInterval)
|
||||||
@@ -380,7 +364,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,
|
||||||
@@ -542,7 +525,7 @@ func (s *erasureSets) cleanupStaleUploads(ctx context.Context) {
|
|||||||
if set == nil {
|
if set == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
set.cleanupStaleUploads(ctx, globalAPIConfig.getStaleUploadsExpiry())
|
set.cleanupStaleUploads(ctx)
|
||||||
}(set)
|
}(set)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -662,14 +645,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1154,13 +1129,7 @@ func (s *erasureSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.H
|
|||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
globalLocalSetDrives[s.poolIndex][m][n] = disk
|
globalLocalSetDrives[s.poolIndex][m][n] = disk
|
||||||
}
|
}
|
||||||
for i, ldisk := range globalLocalDrives {
|
globalLocalDrivesMap[disk.Endpoint().String()] = disk
|
||||||
_, k, l := ldisk.GetDiskLoc()
|
|
||||||
if k == m && l == n {
|
|
||||||
globalLocalDrives[i] = disk
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
globalLocalDrivesMu.Unlock()
|
globalLocalDrivesMu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ func diskErrToDriveState(err error) (state string) {
|
|||||||
state = madmin.DriveStatePermission
|
state = madmin.DriveStatePermission
|
||||||
case errors.Is(err, errFaultyDisk):
|
case errors.Is(err, errFaultyDisk):
|
||||||
state = madmin.DriveStateFaulty
|
state = madmin.DriveStateFaulty
|
||||||
|
case errors.Is(err, errDriveIsRoot):
|
||||||
|
state = madmin.DriveStateRootMount
|
||||||
case err == nil:
|
case err == nil:
|
||||||
state = madmin.DriveStateOk
|
state = madmin.DriveStateOk
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
// 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/>.
|
||||||
|
|
||||||
|
//go:build fmtgen
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/klauspost/compress/zip"
|
||||||
|
"github.com/minio/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var fmtGenFlags = []cli.Flag{
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "parity",
|
||||||
|
Usage: "specify erasure code parity",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "deployment-id",
|
||||||
|
Usage: "deployment-id of the MinIO cluster for which format.json is needed",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "address",
|
||||||
|
Value: ":" + GlobalMinioDefaultPort,
|
||||||
|
Usage: "bind to a specific ADDRESS:PORT, ADDRESS can be an IP or hostname",
|
||||||
|
EnvVar: "MINIO_ADDRESS",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var fmtGenCmd = cli.Command{
|
||||||
|
Name: "fmt-gen",
|
||||||
|
Usage: "Generate format.json files for an erasure server pool",
|
||||||
|
Flags: append(fmtGenFlags, GlobalFlags...),
|
||||||
|
Action: fmtGenMain,
|
||||||
|
CustomHelpTemplate: `NAME:
|
||||||
|
{{.HelpName}} - {{.Usage}}
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
{{.HelpName}} {{if .VisibleFlags}}[FLAGS] {{end}}DIR1 [DIR2..]
|
||||||
|
{{.HelpName}} {{if .VisibleFlags}}[FLAGS] {{end}}DIR{1...64}
|
||||||
|
{{.HelpName}} {{if .VisibleFlags}}[FLAGS] {{end}}DIR{1...64} DIR{65...128}
|
||||||
|
|
||||||
|
DIR:
|
||||||
|
DIR points to a directory on a filesystem. When you want to combine
|
||||||
|
multiple drives into a single large system, pass one directory per
|
||||||
|
filesystem separated by space. You may also use a '...' convention
|
||||||
|
to abbreviate the directory arguments. Remote directories in a
|
||||||
|
distributed setup are encoded as HTTP(s) URIs.
|
||||||
|
{{if .VisibleFlags}}
|
||||||
|
FLAGS:
|
||||||
|
{{range .VisibleFlags}}{{.}}
|
||||||
|
{{end}}{{end}}
|
||||||
|
EXAMPLES:
|
||||||
|
1. Generate format.json.zip containing format.json files for all drives in a distributed MinIO server pool of 32 nodes with 32 drives each.
|
||||||
|
{{.Prompt}} {{.HelpName}} http://node{1...32}.example.com/mnt/export{1...32}
|
||||||
|
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func fmtGenMain(ctxt *cli.Context) {
|
||||||
|
deploymentID := ctxt.String("deployment-id")
|
||||||
|
err := buildServerCtxt(ctxt, &globalServerCtxt)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
handleCommonArgs(globalServerCtxt)
|
||||||
|
pools, _, err := createServerEndpoints(globalMinioAddr, globalServerCtxt.Layout.pools, globalServerCtxt.Layout.legacy)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zipFile, err := os.Create("format.json.zip")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create format.json.zip: %v", err)
|
||||||
|
}
|
||||||
|
defer zipFile.Close()
|
||||||
|
fmtZipW := zip.NewWriter(zipFile)
|
||||||
|
defer fmtZipW.Close()
|
||||||
|
for _, pool := range pools { // for each pool
|
||||||
|
setCount, setDriveCount := pool.SetCount, pool.DrivesPerSet
|
||||||
|
format := newFormatErasureV3(setCount, setDriveCount)
|
||||||
|
format.ID = deploymentID
|
||||||
|
for i := 0; i < setCount; i++ { // for each erasure set
|
||||||
|
for j := 0; j < setDriveCount; j++ {
|
||||||
|
newFormat := format.Clone()
|
||||||
|
newFormat.Erasure.This = format.Erasure.Sets[i][j]
|
||||||
|
if deploymentID != "" {
|
||||||
|
newFormat.ID = deploymentID
|
||||||
|
}
|
||||||
|
drive := pool.Endpoints[i*setDriveCount+j]
|
||||||
|
fmtBytes, err := json.Marshal(newFormat)
|
||||||
|
if err != nil {
|
||||||
|
//nolint:gocritic
|
||||||
|
log.Fatalf("failed to marshal format.json for %s: %v", drive.String(), err)
|
||||||
|
}
|
||||||
|
fmtJSON := filepath.Join(drive.Host, drive.Path, minioMetaBucket, "format.json")
|
||||||
|
embedFileInZip(fmtZipW, fmtJSON, fmtBytes, 0o600)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// Copyright (c) 2015-2022 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
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
@@ -15,9 +15,10 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
//go:build go1.19
|
//go:build !fmtgen
|
||||||
// +build go1.19
|
|
||||||
|
|
||||||
package fips
|
package cmd
|
||||||
|
|
||||||
const go19 = true
|
import "github.com/minio/cli"
|
||||||
|
|
||||||
|
var fmtGenCmd cli.Command
|
||||||
@@ -247,11 +247,14 @@ func guessIsRPCReq(req *http.Request) bool {
|
|||||||
if req == nil {
|
if req == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if req.Method == http.MethodGet && req.URL != nil && req.URL.Path == grid.RoutePath {
|
if req.Method == http.MethodGet && req.URL != nil {
|
||||||
|
switch req.URL.Path {
|
||||||
|
case grid.RoutePath, grid.RouteLockPath:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return req.Method == http.MethodPost &&
|
return (req.Method == http.MethodPost || req.Method == http.MethodGet) &&
|
||||||
strings.HasPrefix(req.URL.Path, minioReservedBucketPath+SlashSeparator)
|
strings.HasPrefix(req.URL.Path, minioReservedBucketPath+SlashSeparator)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +472,7 @@ func setBucketForwardingMiddleware(h http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
if globalDNSConfig == nil || !globalBucketFederation ||
|
if globalDNSConfig == nil || !globalBucketFederation ||
|
||||||
guessIsHealthCheckReq(r) || guessIsMetricsReq(r) ||
|
guessIsHealthCheckReq(r) || guessIsMetricsReq(r) ||
|
||||||
guessIsRPCReq(r) || guessIsLoginSTSReq(r) || isAdminReq(r) {
|
guessIsRPCReq(r) || guessIsLoginSTSReq(r) || isAdminReq(r) || isKMSReq(r) {
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,9 +51,10 @@ func TestGuessIsRPC(t *testing.T) {
|
|||||||
r = &http.Request{
|
r = &http.Request{
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
|
URL: u,
|
||||||
}
|
}
|
||||||
if guessIsRPCReq(r) {
|
if !guessIsRPCReq(r) {
|
||||||
t.Fatal("Test shouldn't report as net/rpc for a non net/rpc request.")
|
t.Fatal("Test shouldn't fail for a possible net/rpc request.")
|
||||||
}
|
}
|
||||||
r = &http.Request{
|
r = &http.Request{
|
||||||
Proto: "HTTP/1.1",
|
Proto: "HTTP/1.1",
|
||||||
@@ -63,6 +64,14 @@ func TestGuessIsRPC(t *testing.T) {
|
|||||||
if !guessIsRPCReq(r) {
|
if !guessIsRPCReq(r) {
|
||||||
t.Fatal("Grid RPC path not detected")
|
t.Fatal("Grid RPC path not detected")
|
||||||
}
|
}
|
||||||
|
r = &http.Request{
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: &url.URL{Path: grid.RouteLockPath},
|
||||||
|
}
|
||||||
|
if !guessIsRPCReq(r) {
|
||||||
|
t.Fatal("Grid RPC path not detected")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var isHTTPHeaderSizeTooLargeTests = []struct {
|
var isHTTPHeaderSizeTooLargeTests = []struct {
|
||||||
|
|||||||
+33
-10
@@ -150,6 +150,11 @@ type healEntryResult struct {
|
|||||||
|
|
||||||
// healErasureSet lists and heals all objects in a specific erasure set
|
// healErasureSet lists and heals all objects in a specific erasure set
|
||||||
func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string, tracker *healingTracker) error {
|
func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string, tracker *healingTracker) error {
|
||||||
|
bgSeq, found := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
|
||||||
|
if !found {
|
||||||
|
return errors.New("no local healing sequence initialized, unable to heal the drive")
|
||||||
|
}
|
||||||
|
|
||||||
scanMode := madmin.HealNormalScan
|
scanMode := madmin.HealNormalScan
|
||||||
|
|
||||||
// Make sure to copy since `buckets slice`
|
// Make sure to copy since `buckets slice`
|
||||||
@@ -163,8 +168,7 @@ 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})
|
if err := bgSeq.healBucket(objAPI, bucket, true); 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)
|
||||||
}
|
}
|
||||||
@@ -261,9 +265,7 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
tracker.setBucket(bucket)
|
tracker.setBucket(bucket)
|
||||||
// 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 := bgSeq.healBucket(objAPI, bucket, true); err != nil {
|
||||||
ScanMode: scanMode,
|
|
||||||
}); err != nil {
|
|
||||||
// Set this such that when we return this function
|
// Set this such that when we return this function
|
||||||
// we let the caller retry this disk again for the
|
// we let the caller retry this disk again for the
|
||||||
// buckets that failed healing.
|
// buckets that failed healing.
|
||||||
@@ -362,6 +364,7 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
case results <- result:
|
case results <- result:
|
||||||
|
bgSeq.countScanned(madmin.HealItemObject)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,8 +415,10 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
result = healEntryFailure(0)
|
result = healEntryFailure(0)
|
||||||
|
bgSeq.countFailed(madmin.HealItemObject)
|
||||||
healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s: %w", bucket, entry.name, err))
|
healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s: %w", bucket, entry.name, err))
|
||||||
} else {
|
} else {
|
||||||
|
bgSeq.countHealed(madmin.HealItemObject)
|
||||||
result = healEntrySuccess(uint64(res.ObjectSize))
|
result = healEntrySuccess(uint64(res.ObjectSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,6 +442,8 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var versionHealed bool
|
||||||
|
|
||||||
res, err := er.HealObject(ctx, bucket, encodedEntryName,
|
res, err := er.HealObject(ctx, bucket, encodedEntryName,
|
||||||
version.VersionID, madmin.HealOpts{
|
version.VersionID, madmin.HealOpts{
|
||||||
ScanMode: scanMode,
|
ScanMode: scanMode,
|
||||||
@@ -449,15 +456,24 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
versionNotFound++
|
versionNotFound++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// If not deleted, assume they failed.
|
} else {
|
||||||
|
// Look for the healing results
|
||||||
|
if res.After.Drives[tracker.DiskIndex].State == madmin.DriveStateOk {
|
||||||
|
versionHealed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if versionHealed {
|
||||||
|
bgSeq.countHealed(madmin.HealItemObject)
|
||||||
|
result = healEntrySuccess(uint64(version.Size))
|
||||||
|
} else {
|
||||||
|
bgSeq.countFailed(madmin.HealItemObject)
|
||||||
result = healEntryFailure(uint64(version.Size))
|
result = healEntryFailure(uint64(version.Size))
|
||||||
if version.VersionID != "" {
|
if version.VersionID != "" {
|
||||||
healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s-v(%s): %w", bucket, version.Name, version.VersionID, err))
|
healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s-v(%s): %w", bucket, version.Name, version.VersionID, err))
|
||||||
} else {
|
} else {
|
||||||
healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s: %w", bucket, version.Name, err))
|
healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s: %w", bucket, version.Name, err))
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
result = healEntrySuccess(uint64(res.ObjectSize))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !send(result) {
|
if !send(result) {
|
||||||
@@ -505,7 +521,11 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
jt.Take()
|
jt.Take()
|
||||||
go healEntry(bucket, *entry)
|
go healEntry(bucket, *entry)
|
||||||
},
|
},
|
||||||
finished: nil,
|
finished: func(errs []error) {
|
||||||
|
if countErrs(errs, nil) != len(errs) {
|
||||||
|
retErr = fmt.Errorf("one or more errors reported during listing: %v", errors.Join(errs...))
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
jt.Wait() // synchronize all the concurrent heal jobs
|
jt.Wait() // synchronize all the concurrent heal jobs
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -513,7 +533,10 @@ 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)
|
}
|
||||||
|
|
||||||
|
if retErr != nil {
|
||||||
|
healingLogIf(ctx, fmt.Errorf("listing failed with: %v on bucket: %v", retErr, bucket))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-7
@@ -163,7 +163,6 @@ type serverCtxt struct {
|
|||||||
MemLimit uint64
|
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
|
||||||
@@ -311,6 +310,7 @@ var (
|
|||||||
globalBootTime = UTCNow()
|
globalBootTime = UTCNow()
|
||||||
|
|
||||||
globalActiveCred auth.Credentials
|
globalActiveCred auth.Credentials
|
||||||
|
globalNodeAuthToken string
|
||||||
globalSiteReplicatorCred siteReplicatorCred
|
globalSiteReplicatorCred siteReplicatorCred
|
||||||
|
|
||||||
// Captures if root credentials are set via ENV.
|
// Captures if root credentials are set via ENV.
|
||||||
@@ -414,10 +414,9 @@ var (
|
|||||||
globalServiceFreezeCnt int32
|
globalServiceFreezeCnt int32
|
||||||
globalServiceFreezeMu sync.Mutex // Updates.
|
globalServiceFreezeMu sync.Mutex // Updates.
|
||||||
|
|
||||||
// List of local drives to this node, this is only set during server startup,
|
// Map of local drives to this node, this is set during server startup,
|
||||||
// and is only mutated by HealFormat. Hold globalLocalDrivesMu to access.
|
// disk reconnect and mutated by HealFormat. Hold globalLocalDrivesMu to access.
|
||||||
globalLocalDrives []StorageAPI
|
globalLocalDrivesMap map[string]StorageAPI
|
||||||
globalLocalDrivesMap = make(map[string]StorageAPI)
|
|
||||||
globalLocalDrivesMu sync.RWMutex
|
globalLocalDrivesMu sync.RWMutex
|
||||||
|
|
||||||
globalDriveMonitoring = env.Get("_MINIO_DRIVE_ACTIVE_MONITORING", config.EnableOn) == config.EnableOn
|
globalDriveMonitoring = env.Get("_MINIO_DRIVE_ACTIVE_MONITORING", config.EnableOn) == config.EnableOn
|
||||||
@@ -450,8 +449,8 @@ var (
|
|||||||
// dynamic sleeper for multipart expiration routine
|
// dynamic sleeper for multipart expiration routine
|
||||||
deleteMultipartCleanupSleeper = newDynamicSleeper(5, 25*time.Millisecond, false)
|
deleteMultipartCleanupSleeper = newDynamicSleeper(5, 25*time.Millisecond, false)
|
||||||
|
|
||||||
// Is _MINIO_DISABLE_API_FREEZE_ON_BOOT set?
|
// Is MINIO_SYNC_BOOT set?
|
||||||
globalDisableFreezeOnBoot bool
|
globalEnableSyncBoot bool
|
||||||
|
|
||||||
// Contains NIC interface name used for internode communication
|
// Contains NIC interface name used for internode communication
|
||||||
globalInternodeInterface string
|
globalInternodeInterface string
|
||||||
|
|||||||
+50
-8
@@ -31,9 +31,15 @@ import (
|
|||||||
// globalGrid is the global grid manager.
|
// globalGrid is the global grid manager.
|
||||||
var globalGrid atomic.Pointer[grid.Manager]
|
var globalGrid atomic.Pointer[grid.Manager]
|
||||||
|
|
||||||
|
// globalLockGrid is the global lock grid manager.
|
||||||
|
var globalLockGrid atomic.Pointer[grid.Manager]
|
||||||
|
|
||||||
// globalGridStart is a channel that will block startup of grid connections until closed.
|
// globalGridStart is a channel that will block startup of grid connections until closed.
|
||||||
var globalGridStart = make(chan struct{})
|
var globalGridStart = make(chan struct{})
|
||||||
|
|
||||||
|
// globalLockGridStart is a channel that will block startup of lock grid connections until closed.
|
||||||
|
var globalLockGridStart = make(chan struct{})
|
||||||
|
|
||||||
func initGlobalGrid(ctx context.Context, eps EndpointServerPools) error {
|
func initGlobalGrid(ctx context.Context, eps EndpointServerPools) error {
|
||||||
hosts, local := eps.GridHosts()
|
hosts, local := eps.GridHosts()
|
||||||
lookupHost := globalDNSCache.LookupHost
|
lookupHost := globalDNSCache.LookupHost
|
||||||
@@ -41,21 +47,24 @@ func initGlobalGrid(ctx context.Context, eps EndpointServerPools) error {
|
|||||||
// Pass Dialer for websocket grid, make sure we do not
|
// Pass Dialer for websocket grid, make sure we do not
|
||||||
// provide any DriveOPTimeout() function, as that is not
|
// provide any DriveOPTimeout() function, as that is not
|
||||||
// useful over persistent connections.
|
// useful over persistent connections.
|
||||||
Dialer: grid.ContextDialer(xhttp.DialContextWithLookupHost(lookupHost, xhttp.NewInternodeDialContext(rest.DefaultTimeout, globalTCPOptions.ForWebsocket()))),
|
Dialer: grid.ConnectWS(
|
||||||
Local: local,
|
grid.ContextDialer(xhttp.DialContextWithLookupHost(lookupHost, xhttp.NewInternodeDialContext(rest.DefaultTimeout, globalTCPOptions.ForWebsocket()))),
|
||||||
Hosts: hosts,
|
newCachedAuthToken(),
|
||||||
AddAuth: newCachedAuthToken(),
|
&tls.Config{
|
||||||
AuthRequest: storageServerRequestValidate,
|
|
||||||
BlockConnect: globalGridStart,
|
|
||||||
TLSConfig: &tls.Config{
|
|
||||||
RootCAs: globalRootCAs,
|
RootCAs: globalRootCAs,
|
||||||
CipherSuites: fips.TLSCiphers(),
|
CipherSuites: fips.TLSCiphers(),
|
||||||
CurvePreferences: fips.TLSCurveIDs(),
|
CurvePreferences: fips.TLSCurveIDs(),
|
||||||
},
|
}),
|
||||||
|
Local: local,
|
||||||
|
Hosts: hosts,
|
||||||
|
AuthToken: validateStorageRequestToken,
|
||||||
|
AuthFn: newCachedAuthToken(),
|
||||||
|
BlockConnect: globalGridStart,
|
||||||
// Record incoming and outgoing bytes.
|
// Record incoming and outgoing bytes.
|
||||||
Incoming: globalConnStats.incInternodeInputBytes,
|
Incoming: globalConnStats.incInternodeInputBytes,
|
||||||
Outgoing: globalConnStats.incInternodeOutputBytes,
|
Outgoing: globalConnStats.incInternodeOutputBytes,
|
||||||
TraceTo: globalTrace,
|
TraceTo: globalTrace,
|
||||||
|
RoutePath: grid.RoutePath,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -63,3 +72,36 @@ func initGlobalGrid(ctx context.Context, eps EndpointServerPools) error {
|
|||||||
globalGrid.Store(g)
|
globalGrid.Store(g)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initGlobalLockGrid(ctx context.Context, eps EndpointServerPools) error {
|
||||||
|
hosts, local := eps.GridHosts()
|
||||||
|
lookupHost := globalDNSCache.LookupHost
|
||||||
|
g, err := grid.NewManager(ctx, grid.ManagerOptions{
|
||||||
|
// Pass Dialer for websocket grid, make sure we do not
|
||||||
|
// provide any DriveOPTimeout() function, as that is not
|
||||||
|
// useful over persistent connections.
|
||||||
|
Dialer: grid.ConnectWSWithRoutePath(
|
||||||
|
grid.ContextDialer(xhttp.DialContextWithLookupHost(lookupHost, xhttp.NewInternodeDialContext(rest.DefaultTimeout, globalTCPOptions.ForWebsocket()))),
|
||||||
|
newCachedAuthToken(),
|
||||||
|
&tls.Config{
|
||||||
|
RootCAs: globalRootCAs,
|
||||||
|
CipherSuites: fips.TLSCiphers(),
|
||||||
|
CurvePreferences: fips.TLSCurveIDs(),
|
||||||
|
}, grid.RouteLockPath),
|
||||||
|
Local: local,
|
||||||
|
Hosts: hosts,
|
||||||
|
AuthToken: validateStorageRequestToken,
|
||||||
|
AuthFn: newCachedAuthToken(),
|
||||||
|
BlockConnect: globalGridStart,
|
||||||
|
// Record incoming and outgoing bytes.
|
||||||
|
Incoming: globalConnStats.incInternodeInputBytes,
|
||||||
|
Outgoing: globalConnStats.incInternodeOutputBytes,
|
||||||
|
TraceTo: globalTrace,
|
||||||
|
RoutePath: grid.RouteLockPath,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
globalLockGrid.Store(g)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+11
-4
@@ -46,6 +46,7 @@ type apiConfig struct {
|
|||||||
corsAllowOrigins []string
|
corsAllowOrigins []string
|
||||||
replicationPriority string
|
replicationPriority string
|
||||||
replicationMaxWorkers int
|
replicationMaxWorkers int
|
||||||
|
replicationMaxLWorkers int
|
||||||
transitionWorkers int
|
transitionWorkers int
|
||||||
|
|
||||||
staleUploadsExpiry time.Duration
|
staleUploadsExpiry time.Duration
|
||||||
@@ -170,11 +171,12 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int, legacy bool) {
|
|||||||
}
|
}
|
||||||
t.listQuorum = listQuorum
|
t.listQuorum = listQuorum
|
||||||
if globalReplicationPool != nil &&
|
if globalReplicationPool != nil &&
|
||||||
(cfg.ReplicationPriority != t.replicationPriority || cfg.ReplicationMaxWorkers != t.replicationMaxWorkers) {
|
(cfg.ReplicationPriority != t.replicationPriority || cfg.ReplicationMaxWorkers != t.replicationMaxWorkers || cfg.ReplicationMaxLWorkers != t.replicationMaxLWorkers) {
|
||||||
globalReplicationPool.ResizeWorkerPriority(cfg.ReplicationPriority, cfg.ReplicationMaxWorkers)
|
globalReplicationPool.ResizeWorkerPriority(cfg.ReplicationPriority, cfg.ReplicationMaxWorkers, cfg.ReplicationMaxLWorkers)
|
||||||
}
|
}
|
||||||
t.replicationPriority = cfg.ReplicationPriority
|
t.replicationPriority = cfg.ReplicationPriority
|
||||||
t.replicationMaxWorkers = cfg.ReplicationMaxWorkers
|
t.replicationMaxWorkers = cfg.ReplicationMaxWorkers
|
||||||
|
t.replicationMaxLWorkers = cfg.ReplicationMaxLWorkers
|
||||||
|
|
||||||
// N B api.transition_workers will be deprecated
|
// N B api.transition_workers will be deprecated
|
||||||
if globalTransitionState != nil {
|
if globalTransitionState != nil {
|
||||||
@@ -323,10 +325,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
|
||||||
}
|
}
|
||||||
@@ -334,6 +335,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
|
||||||
}
|
}
|
||||||
@@ -349,12 +351,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
|
||||||
@@ -364,6 +368,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.
|
||||||
@@ -380,12 +385,14 @@ func (t *apiConfig) getReplicationOpts() replicationPoolOpts {
|
|||||||
return replicationPoolOpts{
|
return replicationPoolOpts{
|
||||||
Priority: "auto",
|
Priority: "auto",
|
||||||
MaxWorkers: WorkerMaxLimit,
|
MaxWorkers: WorkerMaxLimit,
|
||||||
|
MaxLWorkers: LargeWorkerCount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return replicationPoolOpts{
|
return replicationPoolOpts{
|
||||||
Priority: t.replicationPriority,
|
Priority: t.replicationPriority,
|
||||||
MaxWorkers: t.replicationMaxWorkers,
|
MaxWorkers: t.replicationMaxWorkers,
|
||||||
|
MaxLWorkers: t.replicationMaxLWorkers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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, ",")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,14 +29,35 @@ import (
|
|||||||
|
|
||||||
const unavailable = "offline"
|
const unavailable = "offline"
|
||||||
|
|
||||||
// ClusterCheckHandler returns if the server is ready for requests.
|
func checkHealth(w http.ResponseWriter) ObjectLayer {
|
||||||
func ClusterCheckHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "ClusterCheckHandler")
|
|
||||||
|
|
||||||
objLayer := newObjectLayerFn()
|
objLayer := newObjectLayerFn()
|
||||||
if objLayer == nil {
|
if objLayer == nil {
|
||||||
w.Header().Set(xhttp.MinIOServerStatus, unavailable)
|
w.Header().Set(xhttp.MinIOServerStatus, unavailable)
|
||||||
writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
|
writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !globalBucketMetadataSys.Initialized() {
|
||||||
|
w.Header().Set(xhttp.MinIOServerStatus, "bucket-metadata-offline")
|
||||||
|
writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !globalIAMSys.Initialized() {
|
||||||
|
w.Header().Set(xhttp.MinIOServerStatus, "iam-offline")
|
||||||
|
writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return objLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClusterCheckHandler returns if the server is ready for requests.
|
||||||
|
func ClusterCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "ClusterCheckHandler")
|
||||||
|
|
||||||
|
objLayer := checkHealth(w)
|
||||||
|
if objLayer == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,10 +93,8 @@ func ClusterCheckHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
func ClusterReadCheckHandler(w http.ResponseWriter, r *http.Request) {
|
func ClusterReadCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ClusterReadCheckHandler")
|
ctx := newContext(r, w, "ClusterReadCheckHandler")
|
||||||
|
|
||||||
objLayer := newObjectLayerFn()
|
objLayer := checkHealth(w)
|
||||||
if objLayer == nil {
|
if objLayer == nil {
|
||||||
w.Header().Set(xhttp.MinIOServerStatus, unavailable)
|
|
||||||
writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-7
@@ -27,10 +27,6 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
apiGetObject = "GetObject"
|
|
||||||
)
|
|
||||||
|
|
||||||
// connStats - Network statistics
|
// connStats - Network statistics
|
||||||
// Count total input/output transferred bytes during
|
// Count total input/output transferred bytes during
|
||||||
// the server's life.
|
// the server's life.
|
||||||
@@ -132,7 +128,7 @@ func (bh *bucketHTTPStats) updateHTTPStats(bucket, api string, w *xhttp.Response
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if w != nil && api == apiGetObject {
|
if w != nil {
|
||||||
// Increment the prometheus http request response histogram with API, Bucket
|
// Increment the prometheus http request response histogram with API, Bucket
|
||||||
bucketHTTPRequestsDuration.With(prometheus.Labels{
|
bucketHTTPRequestsDuration.With(prometheus.Labels{
|
||||||
"api": api,
|
"api": api,
|
||||||
@@ -437,9 +433,7 @@ func (st *HTTPStats) updateStats(api string, w *xhttp.ResponseRecorder) {
|
|||||||
st.totalS3Requests.Inc(api)
|
st.totalS3Requests.Inc(api)
|
||||||
|
|
||||||
// Increment the prometheus http request response histogram with appropriate label
|
// Increment the prometheus http request response histogram with appropriate label
|
||||||
if api == apiGetObject {
|
|
||||||
httpRequestsDuration.With(prometheus.Labels{"api": api}).Observe(w.TimeToFirstByte.Seconds())
|
httpRequestsDuration.With(prometheus.Labels{"api": api}).Observe(w.TimeToFirstByte.Seconds())
|
||||||
}
|
|
||||||
|
|
||||||
code := w.StatusCode
|
code := w.StatusCode
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-74
@@ -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,79 +435,48 @@ 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.
|
const (
|
||||||
func (iamOS *IAMObjectStore) PurgeExpiredSTS(ctx context.Context) error {
|
maxIAMLoadOpTime = 5 * time.Second
|
||||||
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, firstTime bool) error {
|
||||||
|
bootstrapTraceMsgFirstTime := func(s string) {
|
||||||
|
if firstTime {
|
||||||
|
bootstrapTraceMsg(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if iamOS.objAPI == nil {
|
if iamOS.objAPI == nil {
|
||||||
return errServerNotInitialized
|
return errServerNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrapTraceMsg("loading all IAM items")
|
bootstrapTraceMsgFirstTime("loading all IAM items")
|
||||||
|
|
||||||
|
setDefaultCannedPolicies(cache.iamPolicyDocsMap)
|
||||||
|
|
||||||
|
listStartTime := UTCNow()
|
||||||
listedConfigItems, err := iamOS.listAllIAMConfigItems(ctx)
|
listedConfigItems, err := iamOS.listAllIAMConfigItems(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to list IAM data: %w", err)
|
return fmt.Errorf("unable to list IAM data: %w", err)
|
||||||
}
|
}
|
||||||
|
if took := time.Since(listStartTime); took > maxIAMLoadOpTime {
|
||||||
|
var s strings.Builder
|
||||||
|
for k, v := range listedConfigItems {
|
||||||
|
s.WriteString(fmt.Sprintf(" %s: %d items\n", k, len(v)))
|
||||||
|
}
|
||||||
|
logger.Info("listAllIAMConfigItems took %.2fs with contents:\n%s", took.Seconds(), s.String())
|
||||||
|
}
|
||||||
|
|
||||||
// Loads things in the same order as `LoadIAMCache()`
|
// Loads things in the same order as `LoadIAMCache()`
|
||||||
|
|
||||||
bootstrapTraceMsg("loading policy documents")
|
bootstrapTraceMsgFirstTime("loading policy documents")
|
||||||
|
|
||||||
|
policyLoadStartTime := UTCNow()
|
||||||
policiesList := listedConfigItems[policiesListKey]
|
policiesList := listedConfigItems[policiesListKey]
|
||||||
for _, item := range policiesList {
|
for _, item := range policiesList {
|
||||||
policyName := path.Dir(item)
|
policyName := path.Dir(item)
|
||||||
@@ -518,58 +484,88 @@ func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iam
|
|||||||
return fmt.Errorf("unable to load the policy doc `%s`: %w", policyName, err)
|
return fmt.Errorf("unable to load the policy doc `%s`: %w", policyName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setDefaultCannedPolicies(cache.iamPolicyDocsMap)
|
if took := time.Since(policyLoadStartTime); took > maxIAMLoadOpTime {
|
||||||
|
logger.Info("Policy docs load took %.2fs (for %d items)", took.Seconds(), len(policiesList))
|
||||||
|
}
|
||||||
|
|
||||||
if iamOS.usersSysType == MinIOUsersSysType {
|
if iamOS.usersSysType == MinIOUsersSysType {
|
||||||
bootstrapTraceMsg("loading regular IAM users")
|
bootstrapTraceMsgFirstTime("loading regular IAM users")
|
||||||
|
regUsersLoadStartTime := UTCNow()
|
||||||
regUsersList := listedConfigItems[usersListKey]
|
regUsersList := listedConfigItems[usersListKey]
|
||||||
for _, item := range regUsersList {
|
for _, item := range regUsersList {
|
||||||
userName := path.Dir(item)
|
userName := path.Dir(item)
|
||||||
if err := iamOS.loadUser(ctx, userName, regUser, cache.iamUsersMap); err != nil && err != errNoSuchUser {
|
if err := iamOS.loadUser(ctx, userName, regUser, cache.iamUsersMap); err != nil && err != errNoSuchUser {
|
||||||
return fmt.Errorf("unable to load the user `%s`: %w", userName, err)
|
return fmt.Errorf("unable to load the user: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if took := time.Since(regUsersLoadStartTime); took > maxIAMLoadOpTime {
|
||||||
|
actualLoaded := len(cache.iamUsersMap)
|
||||||
|
logger.Info("Reg. users load took %.2fs (for %d items with %d expired items)", took.Seconds(),
|
||||||
|
len(regUsersList), len(regUsersList)-actualLoaded)
|
||||||
|
}
|
||||||
|
|
||||||
bootstrapTraceMsg("loading regular IAM groups")
|
bootstrapTraceMsgFirstTime("loading regular IAM groups")
|
||||||
|
groupsLoadStartTime := UTCNow()
|
||||||
groupsList := listedConfigItems[groupsListKey]
|
groupsList := listedConfigItems[groupsListKey]
|
||||||
for _, item := range groupsList {
|
for _, item := range groupsList {
|
||||||
group := path.Dir(item)
|
group := path.Dir(item)
|
||||||
if err := iamOS.loadGroup(ctx, group, cache.iamGroupsMap); err != nil && err != errNoSuchGroup {
|
if err := iamOS.loadGroup(ctx, group, cache.iamGroupsMap); err != nil && err != errNoSuchGroup {
|
||||||
return fmt.Errorf("unable to load the group `%s`: %w", group, err)
|
return fmt.Errorf("unable to load the group: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if took := time.Since(groupsLoadStartTime); took > maxIAMLoadOpTime {
|
||||||
|
logger.Info("Groups load took %.2fs (for %d items)", took.Seconds(), len(groupsList))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrapTraceMsg("loading user policy mapping")
|
bootstrapTraceMsgFirstTime("loading user policy mapping")
|
||||||
|
userPolicyMappingLoadStartTime := UTCNow()
|
||||||
userPolicyMappingsList := listedConfigItems[policyDBUsersListKey]
|
userPolicyMappingsList := listedConfigItems[policyDBUsersListKey]
|
||||||
for _, item := range userPolicyMappingsList {
|
for _, item := range userPolicyMappingsList {
|
||||||
userName := strings.TrimSuffix(item, ".json")
|
userName := strings.TrimSuffix(item, ".json")
|
||||||
if err := iamOS.loadMappedPolicy(ctx, userName, regUser, false, cache.iamUserPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
|
if err := iamOS.loadMappedPolicy(ctx, userName, regUser, false, cache.iamUserPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
|
||||||
return fmt.Errorf("unable to load the policy mapping for the user `%s`: %w", userName, err)
|
return fmt.Errorf("unable to load the policy mapping for the user: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if took := time.Since(userPolicyMappingLoadStartTime); took > maxIAMLoadOpTime {
|
||||||
|
logger.Info("User policy mappings load took %.2fs (for %d items)", took.Seconds(), len(userPolicyMappingsList))
|
||||||
|
}
|
||||||
|
|
||||||
bootstrapTraceMsg("loading group policy mapping")
|
bootstrapTraceMsgFirstTime("loading group policy mapping")
|
||||||
|
groupPolicyMappingLoadStartTime := UTCNow()
|
||||||
groupPolicyMappingsList := listedConfigItems[policyDBGroupsListKey]
|
groupPolicyMappingsList := listedConfigItems[policyDBGroupsListKey]
|
||||||
for _, item := range groupPolicyMappingsList {
|
for _, item := range groupPolicyMappingsList {
|
||||||
groupName := strings.TrimSuffix(item, ".json")
|
groupName := strings.TrimSuffix(item, ".json")
|
||||||
if err := iamOS.loadMappedPolicy(ctx, groupName, regUser, true, cache.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
|
if err := iamOS.loadMappedPolicy(ctx, groupName, regUser, true, cache.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) {
|
||||||
return fmt.Errorf("unable to load the policy mapping for the group `%s`: %w", groupName, err)
|
return fmt.Errorf("unable to load the policy mapping for the group: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if took := time.Since(groupPolicyMappingLoadStartTime); took > maxIAMLoadOpTime {
|
||||||
|
logger.Info("Group policy mappings load took %.2fs (for %d items)", took.Seconds(), len(groupPolicyMappingsList))
|
||||||
|
}
|
||||||
|
|
||||||
bootstrapTraceMsg("loading service accounts")
|
bootstrapTraceMsgFirstTime("loading service accounts")
|
||||||
|
svcAccLoadStartTime := UTCNow()
|
||||||
svcAccList := listedConfigItems[svcAccListKey]
|
svcAccList := listedConfigItems[svcAccListKey]
|
||||||
svcUsersMap := make(map[string]UserIdentity, len(svcAccList))
|
svcUsersMap := make(map[string]UserIdentity, len(svcAccList))
|
||||||
for _, item := range svcAccList {
|
for _, item := range svcAccList {
|
||||||
userName := path.Dir(item)
|
userName := path.Dir(item)
|
||||||
if err := iamOS.loadUser(ctx, userName, svcUser, svcUsersMap); err != nil && err != errNoSuchUser {
|
if err := iamOS.loadUser(ctx, userName, svcUser, svcUsersMap); err != nil && err != errNoSuchUser {
|
||||||
return fmt.Errorf("unable to load the service account `%s`: %w", userName, err)
|
return fmt.Errorf("unable to load the service account: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if took := time.Since(svcAccLoadStartTime); took > maxIAMLoadOpTime {
|
||||||
|
logger.Info("Service accounts load took %.2fs (for %d items with %d expired items)", took.Seconds(),
|
||||||
|
len(svcAccList), len(svcAccList)-len(svcUsersMap))
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrapTraceMsg("loading STS account policy mapping")
|
||||||
|
stsPolicyMappingLoadStartTime := UTCNow()
|
||||||
|
var stsPolicyMappingsCount int
|
||||||
for _, svcAcc := range svcUsersMap {
|
for _, svcAcc := range svcUsersMap {
|
||||||
svcParent := svcAcc.Credentials.ParentUser
|
svcParent := svcAcc.Credentials.ParentUser
|
||||||
if _, ok := cache.iamUsersMap[svcParent]; !ok {
|
if _, ok := cache.iamUsersMap[svcParent]; !ok {
|
||||||
|
stsPolicyMappingsCount++
|
||||||
// If a service account's parent user is not in iamUsersMap, the
|
// If a service account's parent user is not in iamUsersMap, the
|
||||||
// parent is an STS account. Such accounts may have a policy mapped
|
// parent is an STS account. Such accounts may have a policy mapped
|
||||||
// on the parent user, so we load them. This is not needed for the
|
// on the parent user, so we load them. This is not needed for the
|
||||||
@@ -584,16 +580,59 @@ func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iam
|
|||||||
// OIDC/AssumeRoleWithCustomToken/AssumeRoleWithCertificate).
|
// OIDC/AssumeRoleWithCustomToken/AssumeRoleWithCertificate).
|
||||||
err := iamOS.loadMappedPolicy(ctx, svcParent, stsUser, false, cache.iamSTSPolicyMap)
|
err := iamOS.loadMappedPolicy(ctx, svcParent, stsUser, false, cache.iamSTSPolicyMap)
|
||||||
if err != nil && !errors.Is(err, errNoSuchPolicy) {
|
if err != nil && !errors.Is(err, errNoSuchPolicy) {
|
||||||
return fmt.Errorf("unable to load the policy mapping for the STS user `%s`: %w", svcParent, err)
|
return fmt.Errorf("unable to load the policy mapping for the STS user: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if took := time.Since(stsPolicyMappingLoadStartTime); took > maxIAMLoadOpTime {
|
||||||
|
logger.Info("STS policy mappings load took %.2fs (for %d items)", took.Seconds(), stsPolicyMappingsCount)
|
||||||
|
}
|
||||||
|
|
||||||
// Copy svcUsersMap to cache.iamUsersMap
|
// Copy svcUsersMap to cache.iamUsersMap
|
||||||
for k, v := range svcUsersMap {
|
for k, v := range svcUsersMap {
|
||||||
cache.iamUsersMap[k] = v
|
cache.iamUsersMap[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+278
-95
@@ -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 (
|
||||||
@@ -429,8 +431,41 @@ func (c *iamCache) policyDBGet(store *IAMStoreSys, name string, isGroup bool) ([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// returned policy could be empty
|
// returned policy could be empty, we use set to de-duplicate.
|
||||||
policies := mp.toSlice()
|
policies := set.CreateStringSet(mp.toSlice()...)
|
||||||
|
|
||||||
|
for _, group := range u.Credentials.Groups {
|
||||||
|
if store.getUsersSysType() == MinIOUsersSysType {
|
||||||
|
g, ok := c.iamGroupsMap[group]
|
||||||
|
if !ok {
|
||||||
|
if err := store.loadGroup(context.Background(), group, c.iamGroupsMap); err != nil {
|
||||||
|
return nil, time.Time{}, err
|
||||||
|
}
|
||||||
|
g, ok = c.iamGroupsMap[group]
|
||||||
|
if !ok {
|
||||||
|
return nil, time.Time{}, errNoSuchGroup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group is disabled, so we return no policy - this
|
||||||
|
// ensures the request is denied.
|
||||||
|
if g.Status == statusDisabled {
|
||||||
|
return nil, time.Time{}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
policy, ok := c.iamGroupPolicyMap.Load(group)
|
||||||
|
if !ok {
|
||||||
|
if err := store.loadMappedPolicyWithRetry(context.TODO(), group, regUser, true, c.iamGroupPolicyMap, 3); err != nil && !errors.Is(err, errNoSuchPolicy) {
|
||||||
|
return nil, time.Time{}, err
|
||||||
|
}
|
||||||
|
policy, _ = c.iamGroupPolicyMap.Load(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range policy.toSlice() {
|
||||||
|
policies.Add(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, group := range c.iamUserGroupMemberships[name].ToSlice() {
|
for _, group := range c.iamUserGroupMemberships[name].ToSlice() {
|
||||||
if store.getUsersSysType() == MinIOUsersSysType {
|
if store.getUsersSysType() == MinIOUsersSysType {
|
||||||
@@ -460,10 +495,12 @@ func (c *iamCache) policyDBGet(store *IAMStoreSys, name string, isGroup bool) ([
|
|||||||
policy, _ = c.iamGroupPolicyMap.Load(group)
|
policy, _ = c.iamGroupPolicyMap.Load(group)
|
||||||
}
|
}
|
||||||
|
|
||||||
policies = append(policies, policy.toSlice()...)
|
for _, p := range policy.toSlice() {
|
||||||
|
policies.Add(p)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return policies, mp.UpdatedAt, nil
|
return policies.ToSlice(), mp.UpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *iamCache) updateUserWithClaims(key string, u UserIdentity) error {
|
func (c *iamCache) updateUserWithClaims(key string, u UserIdentity) error {
|
||||||
@@ -532,38 +569,28 @@ 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 {
|
||||||
bootstrapTraceMsg := func(s string) {
|
bootstrapTraceMsgFirstTime := func(s string) {
|
||||||
if firstTime {
|
if firstTime {
|
||||||
bootstrapTraceMsg(s)
|
bootstrapTraceMsg(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bootstrapTraceMsg("loading IAM data")
|
bootstrapTraceMsgFirstTime("loading IAM data")
|
||||||
|
|
||||||
newCache := newIamCache()
|
newCache := newIamCache()
|
||||||
|
|
||||||
loadedAt := time.Now()
|
loadedAt := time.Now()
|
||||||
|
|
||||||
if iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore); ok {
|
if iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore); ok {
|
||||||
err := iamOS.loadAllFromObjStore(ctx, newCache)
|
err := iamOS.loadAllFromObjStore(ctx, newCache, firstTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Only non-object IAM store (i.e. only etcd backend).
|
||||||
bootstrapTraceMsg("loading policy documents")
|
bootstrapTraceMsgFirstTime("loading policy documents")
|
||||||
if err := store.loadPolicyDocs(ctx, newCache.iamPolicyDocsMap); err != nil {
|
if err := store.loadPolicyDocs(ctx, newCache.iamPolicyDocsMap); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -572,29 +599,29 @@ func (store *IAMStoreSys) LoadIAMCache(ctx context.Context, firstTime bool) erro
|
|||||||
setDefaultCannedPolicies(newCache.iamPolicyDocsMap)
|
setDefaultCannedPolicies(newCache.iamPolicyDocsMap)
|
||||||
|
|
||||||
if store.getUsersSysType() == MinIOUsersSysType {
|
if store.getUsersSysType() == MinIOUsersSysType {
|
||||||
bootstrapTraceMsg("loading regular users")
|
bootstrapTraceMsgFirstTime("loading regular users")
|
||||||
if err := store.loadUsers(ctx, regUser, newCache.iamUsersMap); err != nil {
|
if err := store.loadUsers(ctx, regUser, newCache.iamUsersMap); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
bootstrapTraceMsg("loading regular groups")
|
bootstrapTraceMsgFirstTime("loading regular groups")
|
||||||
if err := store.loadGroups(ctx, newCache.iamGroupsMap); err != nil {
|
if err := store.loadGroups(ctx, newCache.iamGroupsMap); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrapTraceMsg("loading user policy mapping")
|
bootstrapTraceMsgFirstTime("loading user policy mapping")
|
||||||
// load polices mapped to users
|
// load polices mapped to users
|
||||||
if err := store.loadMappedPolicies(ctx, regUser, false, newCache.iamUserPolicyMap); err != nil {
|
if err := store.loadMappedPolicies(ctx, regUser, false, newCache.iamUserPolicyMap); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrapTraceMsg("loading group policy mapping")
|
bootstrapTraceMsgFirstTime("loading group policy mapping")
|
||||||
// load policies mapped to groups
|
// load policies mapped to groups
|
||||||
if err := store.loadMappedPolicies(ctx, regUser, true, newCache.iamGroupPolicyMap); err != nil {
|
if err := store.loadMappedPolicies(ctx, regUser, true, newCache.iamGroupPolicyMap); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrapTraceMsg("loading service accounts")
|
bootstrapTraceMsgFirstTime("loading service accounts")
|
||||||
// load service accounts
|
// load service accounts
|
||||||
if err := store.loadUsers(ctx, svcUser, newCache.iamUsersMap); err != nil {
|
if err := store.loadUsers(ctx, svcUser, newCache.iamUsersMap); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -643,6 +670,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.
|
||||||
@@ -943,12 +972,7 @@ func (store *IAMStoreSys) GetGroupDescription(group string) (gd madmin.GroupDesc
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListGroups - lists groups. Since this is not going to be a frequent
|
func (store *IAMStoreSys) updateGroups(ctx context.Context, cache *iamCache) (res []string, err error) {
|
||||||
// operation, we fetch this info from storage, and refresh the cache as well.
|
|
||||||
func (store *IAMStoreSys) ListGroups(ctx context.Context) (res []string, err error) {
|
|
||||||
cache := store.lock()
|
|
||||||
defer store.unlock()
|
|
||||||
|
|
||||||
if store.getUsersSysType() == MinIOUsersSysType {
|
if store.getUsersSysType() == MinIOUsersSysType {
|
||||||
m := map[string]GroupInfo{}
|
m := map[string]GroupInfo{}
|
||||||
err = store.loadGroups(ctx, m)
|
err = store.loadGroups(ctx, m)
|
||||||
@@ -976,7 +1000,16 @@ func (store *IAMStoreSys) ListGroups(ctx context.Context) (res []string, err err
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListGroups - lists groups. Since this is not going to be a frequent
|
||||||
|
// operation, we fetch this info from storage, and refresh the cache as well.
|
||||||
|
func (store *IAMStoreSys) ListGroups(ctx context.Context) (res []string, err error) {
|
||||||
|
cache := store.lock()
|
||||||
|
defer store.unlock()
|
||||||
|
|
||||||
|
return store.updateGroups(ctx, cache)
|
||||||
}
|
}
|
||||||
|
|
||||||
// listGroups - lists groups - fetch groups from cache
|
// listGroups - lists groups - fetch groups from cache
|
||||||
@@ -1451,16 +1484,51 @@ func filterPolicies(cache *iamCache, policyName string, bucketName string) (stri
|
|||||||
return strings.Join(policies, ","), policy.MergePolicies(toMerge...)
|
return strings.Join(policies, ","), policy.MergePolicies(toMerge...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterPolicies - accepts a comma separated list of policy names as a string
|
// MergePolicies - accepts a comma separated list of policy names as a string
|
||||||
// and bucket and returns only policies that currently exist in MinIO. If
|
// and returns only policies that currently exist in MinIO. It includes hot loading
|
||||||
// bucketName is non-empty, additionally filters policies matching the bucket.
|
// of policies if not in the memory
|
||||||
// The first returned value is the list of currently existing policies, and the
|
func (store *IAMStoreSys) MergePolicies(policyName string) (string, policy.Policy) {
|
||||||
// second is their combined policy definition.
|
var policies []string
|
||||||
func (store *IAMStoreSys) FilterPolicies(policyName string, bucketName string) (string, policy.Policy) {
|
var missingPolicies []string
|
||||||
cache := store.rlock()
|
var toMerge []policy.Policy
|
||||||
defer store.runlock()
|
|
||||||
|
|
||||||
return filterPolicies(cache, policyName, bucketName)
|
cache := store.rlock()
|
||||||
|
for _, policy := range newMappedPolicy(policyName).toSlice() {
|
||||||
|
if policy == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, found := cache.iamPolicyDocsMap[policy]
|
||||||
|
if !found {
|
||||||
|
missingPolicies = append(missingPolicies, policy)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
policies = append(policies, policy)
|
||||||
|
toMerge = append(toMerge, p.Policy)
|
||||||
|
}
|
||||||
|
store.runlock()
|
||||||
|
|
||||||
|
if len(missingPolicies) > 0 {
|
||||||
|
m := make(map[string]PolicyDoc)
|
||||||
|
for _, policy := range missingPolicies {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
_ = store.loadPolicyDoc(ctx, policy, m)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
cache := store.lock()
|
||||||
|
for policy, p := range m {
|
||||||
|
cache.iamPolicyDocsMap[policy] = p
|
||||||
|
}
|
||||||
|
store.unlock()
|
||||||
|
|
||||||
|
for policy, p := range m {
|
||||||
|
policies = append(policies, policy)
|
||||||
|
toMerge = append(toMerge, p.Policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(policies, ","), policy.MergePolicies(toMerge...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketUsers - returns users (not STS or service accounts) that have access
|
// GetBucketUsers - returns users (not STS or service accounts) that have access
|
||||||
@@ -1810,6 +1878,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 +1954,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
|
||||||
}
|
}
|
||||||
@@ -1911,6 +1981,11 @@ func (store *IAMStoreSys) GetAllParentUsers() map[string]ParentUserInfo {
|
|||||||
cache := store.rlock()
|
cache := store.rlock()
|
||||||
defer store.runlock()
|
defer store.runlock()
|
||||||
|
|
||||||
|
return store.getParentUsers(cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assumes store is locked by caller.
|
||||||
|
func (store *IAMStoreSys) getParentUsers(cache *iamCache) map[string]ParentUserInfo {
|
||||||
res := map[string]ParentUserInfo{}
|
res := map[string]ParentUserInfo{}
|
||||||
for _, ui := range cache.iamUsersMap {
|
for _, ui := range cache.iamUsersMap {
|
||||||
cred := ui.Credentials
|
cred := ui.Credentials
|
||||||
@@ -1981,50 +2056,104 @@ func (store *IAMStoreSys) GetAllParentUsers() map[string]ParentUserInfo {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assumes store is locked by caller. If users is empty, returns all user mappings.
|
// GetAllSTSUserMappings - Loads all STS user policy mappings from storage and
|
||||||
func (store *IAMStoreSys) listUserPolicyMappings(cache *iamCache, users []string,
|
// returns them. Also gets any STS users that do not have policy mappings but have
|
||||||
userPredicate func(string) bool,
|
// Service Accounts or STS keys (This is useful if the user is part of a group)
|
||||||
|
func (store *IAMStoreSys) GetAllSTSUserMappings(userPredicate func(string) bool) (map[string]string, error) {
|
||||||
|
cache := store.rlock()
|
||||||
|
defer store.runlock()
|
||||||
|
|
||||||
|
stsMap := make(map[string]string)
|
||||||
|
m := xsync.NewMapOf[string, MappedPolicy]()
|
||||||
|
if err := store.loadMappedPolicies(context.Background(), stsUser, false, m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Range(func(user string, mappedPolicy MappedPolicy) bool {
|
||||||
|
if userPredicate != nil && !userPredicate(user) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
stsMap[user] = mappedPolicy.Policies
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
for user := range store.getParentUsers(cache) {
|
||||||
|
if _, ok := stsMap[user]; !ok {
|
||||||
|
if userPredicate != nil && !userPredicate(user) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stsMap[user] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stsMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes store is locked by caller. If userMap is empty, returns all user mappings.
|
||||||
|
func (store *IAMStoreSys) listUserPolicyMappings(cache *iamCache, userMap map[string]set.StringSet,
|
||||||
|
userPredicate func(string) bool, decodeFunc func(string) string,
|
||||||
) []madmin.UserPolicyEntities {
|
) []madmin.UserPolicyEntities {
|
||||||
|
stsMap := xsync.NewMapOf[string, MappedPolicy]()
|
||||||
|
resMap := make(map[string]madmin.UserPolicyEntities, len(userMap))
|
||||||
|
|
||||||
|
for user, groupSet := range userMap {
|
||||||
|
// Attempt to load parent user mapping for STS accounts
|
||||||
|
store.loadMappedPolicy(context.TODO(), user, stsUser, false, stsMap)
|
||||||
|
decodeUser := user
|
||||||
|
if decodeFunc != nil {
|
||||||
|
decodeUser = decodeFunc(user)
|
||||||
|
}
|
||||||
|
blankEntities := madmin.UserPolicyEntities{User: decodeUser}
|
||||||
|
if !groupSet.IsEmpty() {
|
||||||
|
blankEntities.MemberOfMappings = store.listGroupPolicyMappings(cache, groupSet, nil, decodeFunc)
|
||||||
|
}
|
||||||
|
resMap[user] = blankEntities
|
||||||
|
}
|
||||||
|
|
||||||
var r []madmin.UserPolicyEntities
|
var r []madmin.UserPolicyEntities
|
||||||
usersSet := set.CreateStringSet(users...)
|
|
||||||
cache.iamUserPolicyMap.Range(func(user string, mappedPolicy MappedPolicy) bool {
|
cache.iamUserPolicyMap.Range(func(user string, mappedPolicy MappedPolicy) bool {
|
||||||
if userPredicate != nil && !userPredicate(user) {
|
if userPredicate != nil && !userPredicate(user) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !usersSet.IsEmpty() && !usersSet.Contains(user) {
|
entitiesWithMemberOf, ok := resMap[user]
|
||||||
|
if !ok {
|
||||||
|
if len(userMap) > 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
decodeUser := user
|
||||||
|
if decodeFunc != nil {
|
||||||
|
decodeUser = decodeFunc(user)
|
||||||
|
}
|
||||||
|
entitiesWithMemberOf = madmin.UserPolicyEntities{User: decodeUser}
|
||||||
|
}
|
||||||
|
|
||||||
ps := mappedPolicy.toSlice()
|
ps := mappedPolicy.toSlice()
|
||||||
sort.Strings(ps)
|
sort.Strings(ps)
|
||||||
r = append(r, madmin.UserPolicyEntities{
|
entitiesWithMemberOf.Policies = ps
|
||||||
User: user,
|
resMap[user] = entitiesWithMemberOf
|
||||||
Policies: ps,
|
|
||||||
})
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
stsMap := xsync.NewMapOf[string, MappedPolicy]()
|
|
||||||
for _, user := range users {
|
|
||||||
// Attempt to load parent user mapping for STS accounts
|
|
||||||
store.loadMappedPolicy(context.TODO(), user, stsUser, false, stsMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
stsMap.Range(func(user string, mappedPolicy MappedPolicy) bool {
|
stsMap.Range(func(user string, mappedPolicy MappedPolicy) bool {
|
||||||
if userPredicate != nil && !userPredicate(user) {
|
if userPredicate != nil && !userPredicate(user) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entitiesWithMemberOf := resMap[user]
|
||||||
|
|
||||||
ps := mappedPolicy.toSlice()
|
ps := mappedPolicy.toSlice()
|
||||||
sort.Strings(ps)
|
sort.Strings(ps)
|
||||||
r = append(r, madmin.UserPolicyEntities{
|
entitiesWithMemberOf.Policies = ps
|
||||||
User: user,
|
resMap[user] = entitiesWithMemberOf
|
||||||
Policies: ps,
|
|
||||||
})
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for _, v := range resMap {
|
||||||
|
if v.Policies != nil || v.MemberOfMappings != nil {
|
||||||
|
r = append(r, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sort.Slice(r, func(i, j int) bool {
|
sort.Slice(r, func(i, j int) bool {
|
||||||
return r[i].User < r[j].User
|
return r[i].User < r[j].User
|
||||||
})
|
})
|
||||||
@@ -2033,11 +2162,11 @@ func (store *IAMStoreSys) listUserPolicyMappings(cache *iamCache, users []string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Assumes store is locked by caller. If groups is empty, returns all group mappings.
|
// Assumes store is locked by caller. If groups is empty, returns all group mappings.
|
||||||
func (store *IAMStoreSys) listGroupPolicyMappings(cache *iamCache, groups []string,
|
func (store *IAMStoreSys) listGroupPolicyMappings(cache *iamCache, groupsSet set.StringSet,
|
||||||
groupPredicate func(string) bool,
|
groupPredicate func(string) bool, decodeFunc func(string) string,
|
||||||
) []madmin.GroupPolicyEntities {
|
) []madmin.GroupPolicyEntities {
|
||||||
var r []madmin.GroupPolicyEntities
|
var r []madmin.GroupPolicyEntities
|
||||||
groupsSet := set.CreateStringSet(groups...)
|
|
||||||
cache.iamGroupPolicyMap.Range(func(group string, mappedPolicy MappedPolicy) bool {
|
cache.iamGroupPolicyMap.Range(func(group string, mappedPolicy MappedPolicy) bool {
|
||||||
if groupPredicate != nil && !groupPredicate(group) {
|
if groupPredicate != nil && !groupPredicate(group) {
|
||||||
return true
|
return true
|
||||||
@@ -2047,10 +2176,15 @@ func (store *IAMStoreSys) listGroupPolicyMappings(cache *iamCache, groups []stri
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decodeGroup := group
|
||||||
|
if decodeFunc != nil {
|
||||||
|
decodeGroup = decodeFunc(group)
|
||||||
|
}
|
||||||
|
|
||||||
ps := mappedPolicy.toSlice()
|
ps := mappedPolicy.toSlice()
|
||||||
sort.Strings(ps)
|
sort.Strings(ps)
|
||||||
r = append(r, madmin.GroupPolicyEntities{
|
r = append(r, madmin.GroupPolicyEntities{
|
||||||
Group: group,
|
Group: decodeGroup,
|
||||||
Policies: ps,
|
Policies: ps,
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
@@ -2064,17 +2198,20 @@ func (store *IAMStoreSys) listGroupPolicyMappings(cache *iamCache, groups []stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Assumes store is locked by caller. If policies is empty, returns all policy mappings.
|
// Assumes store is locked by caller. If policies is empty, returns all policy mappings.
|
||||||
func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, policies []string,
|
func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, queryPolSet set.StringSet,
|
||||||
userPredicate, groupPredicate func(string) bool,
|
userPredicate, groupPredicate func(string) bool, decodeFunc func(string) string,
|
||||||
) []madmin.PolicyEntities {
|
) []madmin.PolicyEntities {
|
||||||
queryPolSet := set.CreateStringSet(policies...)
|
|
||||||
|
|
||||||
policyToUsersMap := make(map[string]set.StringSet)
|
policyToUsersMap := make(map[string]set.StringSet)
|
||||||
cache.iamUserPolicyMap.Range(func(user string, mappedPolicy MappedPolicy) bool {
|
cache.iamUserPolicyMap.Range(func(user string, mappedPolicy MappedPolicy) bool {
|
||||||
if userPredicate != nil && !userPredicate(user) {
|
if userPredicate != nil && !userPredicate(user) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decodeUser := user
|
||||||
|
if decodeFunc != nil {
|
||||||
|
decodeUser = decodeFunc(user)
|
||||||
|
}
|
||||||
|
|
||||||
commonPolicySet := mappedPolicy.policySet()
|
commonPolicySet := mappedPolicy.policySet()
|
||||||
if !queryPolSet.IsEmpty() {
|
if !queryPolSet.IsEmpty() {
|
||||||
commonPolicySet = commonPolicySet.Intersection(queryPolSet)
|
commonPolicySet = commonPolicySet.Intersection(queryPolSet)
|
||||||
@@ -2082,9 +2219,9 @@ func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, policies []string,
|
|||||||
for _, policy := range commonPolicySet.ToSlice() {
|
for _, policy := range commonPolicySet.ToSlice() {
|
||||||
s, ok := policyToUsersMap[policy]
|
s, ok := policyToUsersMap[policy]
|
||||||
if !ok {
|
if !ok {
|
||||||
policyToUsersMap[policy] = set.CreateStringSet(user)
|
policyToUsersMap[policy] = set.CreateStringSet(decodeUser)
|
||||||
} else {
|
} else {
|
||||||
s.Add(user)
|
s.Add(decodeUser)
|
||||||
policyToUsersMap[policy] = s
|
policyToUsersMap[policy] = s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2098,6 +2235,11 @@ func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, policies []string,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decodeUser := user
|
||||||
|
if decodeFunc != nil {
|
||||||
|
decodeUser = decodeFunc(user)
|
||||||
|
}
|
||||||
|
|
||||||
var mappedPolicy MappedPolicy
|
var mappedPolicy MappedPolicy
|
||||||
store.loadIAMConfig(context.Background(), &mappedPolicy, getMappedPolicyPath(user, stsUser, false))
|
store.loadIAMConfig(context.Background(), &mappedPolicy, getMappedPolicyPath(user, stsUser, false))
|
||||||
|
|
||||||
@@ -2108,9 +2250,9 @@ func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, policies []string,
|
|||||||
for _, policy := range commonPolicySet.ToSlice() {
|
for _, policy := range commonPolicySet.ToSlice() {
|
||||||
s, ok := policyToUsersMap[policy]
|
s, ok := policyToUsersMap[policy]
|
||||||
if !ok {
|
if !ok {
|
||||||
policyToUsersMap[policy] = set.CreateStringSet(user)
|
policyToUsersMap[policy] = set.CreateStringSet(decodeUser)
|
||||||
} else {
|
} else {
|
||||||
s.Add(user)
|
s.Add(decodeUser)
|
||||||
policyToUsersMap[policy] = s
|
policyToUsersMap[policy] = s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2125,6 +2267,11 @@ func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, policies []string,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decodeUser := user
|
||||||
|
if decodeFunc != nil {
|
||||||
|
decodeUser = decodeFunc(user)
|
||||||
|
}
|
||||||
|
|
||||||
commonPolicySet := mappedPolicy.policySet()
|
commonPolicySet := mappedPolicy.policySet()
|
||||||
if !queryPolSet.IsEmpty() {
|
if !queryPolSet.IsEmpty() {
|
||||||
commonPolicySet = commonPolicySet.Intersection(queryPolSet)
|
commonPolicySet = commonPolicySet.Intersection(queryPolSet)
|
||||||
@@ -2132,9 +2279,9 @@ func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, policies []string,
|
|||||||
for _, policy := range commonPolicySet.ToSlice() {
|
for _, policy := range commonPolicySet.ToSlice() {
|
||||||
s, ok := policyToUsersMap[policy]
|
s, ok := policyToUsersMap[policy]
|
||||||
if !ok {
|
if !ok {
|
||||||
policyToUsersMap[policy] = set.CreateStringSet(user)
|
policyToUsersMap[policy] = set.CreateStringSet(decodeUser)
|
||||||
} else {
|
} else {
|
||||||
s.Add(user)
|
s.Add(decodeUser)
|
||||||
policyToUsersMap[policy] = s
|
policyToUsersMap[policy] = s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2149,6 +2296,11 @@ func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, policies []string,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decodeGroup := group
|
||||||
|
if decodeFunc != nil {
|
||||||
|
decodeGroup = decodeFunc(group)
|
||||||
|
}
|
||||||
|
|
||||||
commonPolicySet := mappedPolicy.policySet()
|
commonPolicySet := mappedPolicy.policySet()
|
||||||
if !queryPolSet.IsEmpty() {
|
if !queryPolSet.IsEmpty() {
|
||||||
commonPolicySet = commonPolicySet.Intersection(queryPolSet)
|
commonPolicySet = commonPolicySet.Intersection(queryPolSet)
|
||||||
@@ -2156,9 +2308,9 @@ func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, policies []string,
|
|||||||
for _, policy := range commonPolicySet.ToSlice() {
|
for _, policy := range commonPolicySet.ToSlice() {
|
||||||
s, ok := policyToGroupsMap[policy]
|
s, ok := policyToGroupsMap[policy]
|
||||||
if !ok {
|
if !ok {
|
||||||
policyToGroupsMap[policy] = set.CreateStringSet(group)
|
policyToGroupsMap[policy] = set.CreateStringSet(decodeGroup)
|
||||||
} else {
|
} else {
|
||||||
s.Add(group)
|
s.Add(decodeGroup)
|
||||||
policyToGroupsMap[policy] = s
|
policyToGroupsMap[policy] = s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2198,24 +2350,24 @@ func (store *IAMStoreSys) listPolicyMappings(cache *iamCache, policies []string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListPolicyMappings - return users/groups mapped to policies.
|
// ListPolicyMappings - return users/groups mapped to policies.
|
||||||
func (store *IAMStoreSys) ListPolicyMappings(q madmin.PolicyEntitiesQuery,
|
func (store *IAMStoreSys) ListPolicyMappings(q cleanEntitiesQuery,
|
||||||
userPredicate, groupPredicate func(string) bool,
|
userPredicate, groupPredicate func(string) bool, decodeFunc func(string) string,
|
||||||
) madmin.PolicyEntitiesResult {
|
) madmin.PolicyEntitiesResult {
|
||||||
cache := store.rlock()
|
cache := store.rlock()
|
||||||
defer store.runlock()
|
defer store.runlock()
|
||||||
|
|
||||||
var result madmin.PolicyEntitiesResult
|
var result madmin.PolicyEntitiesResult
|
||||||
|
|
||||||
isAllPoliciesQuery := len(q.Users) == 0 && len(q.Groups) == 0 && len(q.Policy) == 0
|
isAllPoliciesQuery := len(q.Users) == 0 && len(q.Groups) == 0 && len(q.Policies) == 0
|
||||||
|
|
||||||
if len(q.Users) > 0 {
|
if len(q.Users) > 0 {
|
||||||
result.UserMappings = store.listUserPolicyMappings(cache, q.Users, userPredicate)
|
result.UserMappings = store.listUserPolicyMappings(cache, q.Users, userPredicate, decodeFunc)
|
||||||
}
|
}
|
||||||
if len(q.Groups) > 0 {
|
if len(q.Groups) > 0 {
|
||||||
result.GroupMappings = store.listGroupPolicyMappings(cache, q.Groups, groupPredicate)
|
result.GroupMappings = store.listGroupPolicyMappings(cache, q.Groups, groupPredicate, decodeFunc)
|
||||||
}
|
}
|
||||||
if len(q.Policy) > 0 || isAllPoliciesQuery {
|
if len(q.Policies) > 0 || isAllPoliciesQuery {
|
||||||
result.PolicyMappings = store.listPolicyMappings(cache, q.Policy, userPredicate, groupPredicate)
|
result.PolicyMappings = store.listPolicyMappings(cache, q.Policies, userPredicate, groupPredicate, decodeFunc)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -2572,37 +2724,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 +2769,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 +2782,42 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
load := len(cache.iamGroupsMap) == 0
|
||||||
|
if store.getUsersSysType() == LDAPUsersSysType && cache.iamGroupPolicyMap.Size() == 0 {
|
||||||
|
load = true
|
||||||
|
}
|
||||||
|
if load {
|
||||||
|
if _, err = store.updateGroups(ctx, cache); err != nil {
|
||||||
|
return "done", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.buildUserGroupMemberships()
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|||||||
+147
-93
@@ -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
|
||||||
@@ -177,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{}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +217,9 @@ func (sys *IAMSys) Load(ctx context.Context, firstTime bool) error {
|
|||||||
|
|
||||||
if firstTime {
|
if firstTime {
|
||||||
bootstrapTraceMsg(fmt.Sprintf("globalIAMSys.Load(): (duration: %s)", loadDuration))
|
bootstrapTraceMsg(fmt.Sprintf("globalIAMSys.Load(): (duration: %s)", loadDuration))
|
||||||
|
if globalIsDistErasure {
|
||||||
|
logger.Info("IAM load(startup) finished. (duration: %s)", loadDuration)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -314,6 +318,24 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cache := sys.store.lock()
|
||||||
|
setDefaultCannedPolicies(cache.iamPolicyDocsMap)
|
||||||
|
sys.store.unlock()
|
||||||
|
|
||||||
|
// Load RoleARNs
|
||||||
|
sys.rolesMap = make(map[arn.ARN]string)
|
||||||
|
|
||||||
|
// From OpenID
|
||||||
|
if riMap := sys.OpenIDConfig.GetRoleInfo(); riMap != nil {
|
||||||
|
sys.validateAndAddRolePolicyMappings(ctx, riMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// From AuthN plugin if enabled.
|
||||||
|
if authn := newGlobalAuthNPluginFn(); authn != nil {
|
||||||
|
riMap := authn.GetRoleInfo()
|
||||||
|
sys.validateAndAddRolePolicyMappings(ctx, riMap)
|
||||||
|
}
|
||||||
|
|
||||||
// Load IAM data from storage.
|
// Load IAM data from storage.
|
||||||
for {
|
for {
|
||||||
if err := sys.Load(retryCtx, true); err != nil {
|
if err := sys.Load(retryCtx, true); err != nil {
|
||||||
@@ -333,25 +355,13 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
|||||||
|
|
||||||
go sys.periodicRoutines(ctx, refreshInterval)
|
go sys.periodicRoutines(ctx, refreshInterval)
|
||||||
|
|
||||||
// Load RoleARNs
|
|
||||||
sys.rolesMap = make(map[arn.ARN]string)
|
|
||||||
|
|
||||||
// From OpenID
|
|
||||||
if riMap := sys.OpenIDConfig.GetRoleInfo(); riMap != nil {
|
|
||||||
sys.validateAndAddRolePolicyMappings(ctx, riMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// From AuthN plugin if enabled.
|
|
||||||
if authn := newGlobalAuthNPluginFn(); authn != nil {
|
|
||||||
riMap := authn.GetRoleInfo()
|
|
||||||
sys.validateAndAddRolePolicyMappings(ctx, riMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
sys.printIAMRoles()
|
sys.printIAMRoles()
|
||||||
|
|
||||||
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)
|
||||||
@@ -384,7 +394,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()
|
||||||
|
|
||||||
@@ -394,24 +403,12 @@ func (sys *IAMSys) periodicRoutines(ctx context.Context, baseInterval time.Durat
|
|||||||
// Load all IAM items (except STS creds) periodically.
|
// Load all IAM items (except STS creds) periodically.
|
||||||
refreshStart := time.Now()
|
refreshStart := time.Now()
|
||||||
if err := sys.Load(ctx, false); err != nil {
|
if err := sys.Load(ctx, false); err != nil {
|
||||||
iamLogIf(ctx, fmt.Errorf("Failure in periodic refresh for IAM (took %.2fs): %v", time.Since(refreshStart).Seconds(), err), logger.WarningKind)
|
iamLogIf(ctx, fmt.Errorf("Failure in periodic refresh for IAM (duration: %s): %v", time.Since(refreshStart), err), logger.WarningKind)
|
||||||
} else {
|
} else {
|
||||||
took := time.Since(refreshStart).Seconds()
|
took := time.Since(refreshStart).Seconds()
|
||||||
if took > maxDurationSecondsForLog {
|
if took > maxDurationSecondsForLog {
|
||||||
// Log if we took a lot of time to load.
|
// Log if we took a lot of time to load.
|
||||||
logger.Info("IAM refresh took %.2fs", took)
|
logger.Info("IAM refresh took (duration: %s)", took)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,7 +443,7 @@ func (sys *IAMSys) validateAndAddRolePolicyMappings(ctx context.Context, m map[a
|
|||||||
// running server by creating the policies after start up.
|
// running server by creating the policies after start up.
|
||||||
for arn, rolePolicies := range m {
|
for arn, rolePolicies := range m {
|
||||||
specifiedPoliciesSet := newMappedPolicy(rolePolicies).policySet()
|
specifiedPoliciesSet := newMappedPolicy(rolePolicies).policySet()
|
||||||
validPolicies, _ := sys.store.FilterPolicies(rolePolicies, "")
|
validPolicies, _ := sys.store.MergePolicies(rolePolicies)
|
||||||
knownPoliciesSet := newMappedPolicy(validPolicies).policySet()
|
knownPoliciesSet := newMappedPolicy(validPolicies).policySet()
|
||||||
unknownPoliciesSet := specifiedPoliciesSet.Difference(knownPoliciesSet)
|
unknownPoliciesSet := specifiedPoliciesSet.Difference(knownPoliciesSet)
|
||||||
if len(unknownPoliciesSet) > 0 {
|
if len(unknownPoliciesSet) > 0 {
|
||||||
@@ -682,7 +679,7 @@ func (sys *IAMSys) CurrentPolicies(policyName string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
policies, _ := sys.store.FilterPolicies(policyName, "")
|
policies, _ := sys.store.MergePolicies(policyName)
|
||||||
return policies
|
return policies
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,11 +793,15 @@ func (sys *IAMSys) ListLDAPUsers(ctx context.Context) (map[string]madmin.UserInf
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-sys.configLoaded:
|
case <-sys.configLoaded:
|
||||||
ldapUsers := make(map[string]madmin.UserInfo)
|
stsMap, err := sys.store.GetAllSTSUserMappings(sys.LDAPConfig.IsLDAPUserDN)
|
||||||
for user, policy := range sys.store.GetUsersWithMappedPolicies() {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ldapUsers := make(map[string]madmin.UserInfo, len(stsMap))
|
||||||
|
for user, policy := range stsMap {
|
||||||
ldapUsers[user] = madmin.UserInfo{
|
ldapUsers[user] = madmin.UserInfo{
|
||||||
PolicyName: policy,
|
PolicyName: policy,
|
||||||
Status: madmin.AccountEnabled,
|
Status: statusEnabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ldapUsers, nil
|
return ldapUsers, nil
|
||||||
@@ -809,6 +810,57 @@ func (sys *IAMSys) ListLDAPUsers(ctx context.Context) (map[string]madmin.UserInf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type cleanEntitiesQuery struct {
|
||||||
|
Users map[string]set.StringSet
|
||||||
|
Groups set.StringSet
|
||||||
|
Policies set.StringSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// createCleanEntitiesQuery - maps users to their groups and normalizes user or group DNs if ldap.
|
||||||
|
func (sys *IAMSys) createCleanEntitiesQuery(q madmin.PolicyEntitiesQuery, ldap bool) cleanEntitiesQuery {
|
||||||
|
cleanQ := cleanEntitiesQuery{
|
||||||
|
Users: make(map[string]set.StringSet),
|
||||||
|
Groups: set.CreateStringSet(q.Groups...),
|
||||||
|
Policies: set.CreateStringSet(q.Policy...),
|
||||||
|
}
|
||||||
|
|
||||||
|
if ldap {
|
||||||
|
// Validate and normalize users, then fetch and normalize their groups
|
||||||
|
// Also include unvalidated users for backward compatibility.
|
||||||
|
for _, user := range q.Users {
|
||||||
|
lookupRes, actualGroups, _ := sys.LDAPConfig.GetValidatedDNWithGroups(user)
|
||||||
|
if lookupRes != nil {
|
||||||
|
groupSet := set.CreateStringSet(actualGroups...)
|
||||||
|
|
||||||
|
// duplicates can be overwritten, fetched groups should be identical.
|
||||||
|
cleanQ.Users[lookupRes.NormDN] = groupSet
|
||||||
|
}
|
||||||
|
// Search for non-normalized DN as well for backward compatibility.
|
||||||
|
if _, ok := cleanQ.Users[user]; !ok {
|
||||||
|
cleanQ.Users[user] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and normalize groups.
|
||||||
|
for _, group := range q.Groups {
|
||||||
|
lookupRes, underDN, _ := sys.LDAPConfig.GetValidatedGroupDN(nil, group)
|
||||||
|
if lookupRes != nil && underDN {
|
||||||
|
cleanQ.Groups.Add(lookupRes.NormDN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, user := range q.Users {
|
||||||
|
info, err := sys.store.GetUserInfo(user)
|
||||||
|
var groupSet set.StringSet
|
||||||
|
if err == nil {
|
||||||
|
groupSet = set.CreateStringSet(info.MemberOf...)
|
||||||
|
}
|
||||||
|
cleanQ.Users[user] = groupSet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleanQ
|
||||||
|
}
|
||||||
|
|
||||||
// QueryLDAPPolicyEntities - queries policy associations for LDAP users/groups/policies.
|
// QueryLDAPPolicyEntities - queries policy associations for LDAP users/groups/policies.
|
||||||
func (sys *IAMSys) QueryLDAPPolicyEntities(ctx context.Context, q madmin.PolicyEntitiesQuery) (*madmin.PolicyEntitiesResult, error) {
|
func (sys *IAMSys) QueryLDAPPolicyEntities(ctx context.Context, q madmin.PolicyEntitiesQuery) (*madmin.PolicyEntitiesResult, error) {
|
||||||
if !sys.Initialized() {
|
if !sys.Initialized() {
|
||||||
@@ -821,7 +873,8 @@ func (sys *IAMSys) QueryLDAPPolicyEntities(ctx context.Context, q madmin.PolicyE
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-sys.configLoaded:
|
case <-sys.configLoaded:
|
||||||
pe := sys.store.ListPolicyMappings(q, sys.LDAPConfig.IsLDAPUserDN, sys.LDAPConfig.IsLDAPGroupDN)
|
cleanQuery := sys.createCleanEntitiesQuery(q, true)
|
||||||
|
pe := sys.store.ListPolicyMappings(cleanQuery, sys.LDAPConfig.IsLDAPUserDN, sys.LDAPConfig.IsLDAPGroupDN, sys.LDAPConfig.DecodeDN)
|
||||||
pe.Timestamp = UTCNow()
|
pe.Timestamp = UTCNow()
|
||||||
return &pe, nil
|
return &pe, nil
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -895,6 +948,7 @@ func (sys *IAMSys) QueryPolicyEntities(ctx context.Context, q madmin.PolicyEntit
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-sys.configLoaded:
|
case <-sys.configLoaded:
|
||||||
|
cleanQuery := sys.createCleanEntitiesQuery(q, false)
|
||||||
var userPredicate, groupPredicate func(string) bool
|
var userPredicate, groupPredicate func(string) bool
|
||||||
if sys.LDAPConfig.Enabled() {
|
if sys.LDAPConfig.Enabled() {
|
||||||
userPredicate = func(s string) bool {
|
userPredicate = func(s string) bool {
|
||||||
@@ -904,7 +958,7 @@ func (sys *IAMSys) QueryPolicyEntities(ctx context.Context, q madmin.PolicyEntit
|
|||||||
return !sys.LDAPConfig.IsLDAPGroupDN(s)
|
return !sys.LDAPConfig.IsLDAPGroupDN(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pe := sys.store.ListPolicyMappings(q, userPredicate, groupPredicate)
|
pe := sys.store.ListPolicyMappings(cleanQuery, userPredicate, groupPredicate, nil)
|
||||||
pe.Timestamp = UTCNow()
|
pe.Timestamp = UTCNow()
|
||||||
return &pe, nil
|
return &pe, nil
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -1388,11 +1442,13 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
|
|||||||
// 1. Collect all LDAP users with active creds.
|
// 1. Collect all LDAP users with active creds.
|
||||||
allCreds := sys.store.GetSTSAndServiceAccounts()
|
allCreds := sys.store.GetSTSAndServiceAccounts()
|
||||||
// List of unique LDAP (parent) user DNs that have active creds
|
// List of unique LDAP (parent) user DNs that have active creds
|
||||||
var parentUsers []string
|
var parentUserActualDNList []string
|
||||||
// Map of LDAP user to list of active credential objects
|
// Map of LDAP user (internal representation) to list of active credential objects
|
||||||
parentUserToCredsMap := make(map[string][]auth.Credentials)
|
parentUserToCredsMap := make(map[string][]auth.Credentials)
|
||||||
// DN to ldap username mapping for each LDAP user
|
// DN to ldap username mapping for each LDAP user
|
||||||
parentUserToLDAPUsernameMap := make(map[string]string)
|
actualDNToLDAPUsernameMap := make(map[string]string)
|
||||||
|
// External (actual) LDAP DN to internal normalized representation
|
||||||
|
actualDNToParentUserMap := make(map[string]string)
|
||||||
for _, cred := range allCreds {
|
for _, cred := range allCreds {
|
||||||
// Expired credentials don't need parent user updates.
|
// Expired credentials don't need parent user updates.
|
||||||
if cred.IsExpired() {
|
if cred.IsExpired() {
|
||||||
@@ -1435,25 +1491,28 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ldapUsername, ok := jwtClaims.Lookup(ldapUserN)
|
ldapUsername, okUserN := jwtClaims.Lookup(ldapUserN)
|
||||||
if !ok {
|
ldapActualDN, okDN := jwtClaims.Lookup(ldapActualUser)
|
||||||
|
if !okUserN || !okDN {
|
||||||
// skip this cred - we dont have the
|
// skip this cred - we dont have the
|
||||||
// username info needed
|
// username info needed
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect each new cred.ParentUser into parentUsers
|
// Collect each new cred.ParentUser into parentUsers
|
||||||
parentUsers = append(parentUsers, cred.ParentUser)
|
parentUserActualDNList = append(parentUserActualDNList, ldapActualDN)
|
||||||
|
|
||||||
// Update the ldapUsernameMap
|
// Update the ldapUsernameMap
|
||||||
parentUserToLDAPUsernameMap[cred.ParentUser] = ldapUsername
|
actualDNToLDAPUsernameMap[ldapActualDN] = ldapUsername
|
||||||
|
|
||||||
|
// Update the actualDNToParentUserMap
|
||||||
|
actualDNToParentUserMap[ldapActualDN] = cred.ParentUser
|
||||||
}
|
}
|
||||||
parentUserToCredsMap[cred.ParentUser] = append(parentUserToCredsMap[cred.ParentUser], cred)
|
parentUserToCredsMap[cred.ParentUser] = append(parentUserToCredsMap[cred.ParentUser], cred)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Query LDAP server for groups of the LDAP users collected.
|
// 2. Query LDAP server for groups of the LDAP users collected.
|
||||||
updatedGroups, err := sys.LDAPConfig.LookupGroupMemberships(parentUsers, parentUserToLDAPUsernameMap)
|
updatedGroups, err := sys.LDAPConfig.LookupGroupMemberships(parentUserActualDNList, actualDNToLDAPUsernameMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log and return on error - perhaps it'll work the next time.
|
// Log and return on error - perhaps it'll work the next time.
|
||||||
iamLogIf(GlobalContext, err)
|
iamLogIf(GlobalContext, err)
|
||||||
@@ -1461,8 +1520,9 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Update creds for those users whose groups are changed
|
// 3. Update creds for those users whose groups are changed
|
||||||
for _, parentUser := range parentUsers {
|
for _, parentActualDN := range parentUserActualDNList {
|
||||||
currGroupsSet := updatedGroups[parentUser]
|
currGroupsSet := updatedGroups[parentActualDN]
|
||||||
|
parentUser := actualDNToParentUserMap[parentActualDN]
|
||||||
currGroups := currGroupsSet.ToSlice()
|
currGroups := currGroupsSet.ToSlice()
|
||||||
for _, cred := range parentUserToCredsMap[parentUser] {
|
for _, cred := range parentUserToCredsMap[parentUser] {
|
||||||
gSet := set.CreateStringSet(cred.Groups...)
|
gSet := set.CreateStringSet(cred.Groups...)
|
||||||
@@ -1520,11 +1580,11 @@ func (sys *IAMSys) NormalizeLDAPAccessKeypairs(ctx context.Context, accessKeyMap
|
|||||||
// server and is under a configured base DN.
|
// server and is under a configured base DN.
|
||||||
validatedParent, isUnderBaseDN, err := sys.LDAPConfig.GetValidatedUserDN(conn, parent)
|
validatedParent, isUnderBaseDN, err := sys.LDAPConfig.GetValidatedUserDN(conn, parent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
collectedErrors = append(collectedErrors, fmt.Errorf("could not validate `%s` exists in LDAP directory: %w", parent, err))
|
collectedErrors = append(collectedErrors, fmt.Errorf("could not validate parent exists in LDAP directory: %w", err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if validatedParent == nil || !isUnderBaseDN {
|
if validatedParent == nil || !isUnderBaseDN {
|
||||||
err := fmt.Errorf("DN `%s` was not found in the LDAP directory", parent)
|
err := fmt.Errorf("DN parent was not found in the LDAP directory")
|
||||||
collectedErrors = append(collectedErrors, err)
|
collectedErrors = append(collectedErrors, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1539,11 +1599,11 @@ func (sys *IAMSys) NormalizeLDAPAccessKeypairs(ctx context.Context, accessKeyMap
|
|||||||
// configured base DN.
|
// configured base DN.
|
||||||
validatedGroup, _, err := sys.LDAPConfig.GetValidatedGroupDN(conn, group)
|
validatedGroup, _, err := sys.LDAPConfig.GetValidatedGroupDN(conn, group)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
collectedErrors = append(collectedErrors, fmt.Errorf("could not validate `%s` exists in LDAP directory: %w", group, err))
|
collectedErrors = append(collectedErrors, fmt.Errorf("could not validate group exists in LDAP directory: %w", err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if validatedGroup == nil {
|
if validatedGroup == nil {
|
||||||
err := fmt.Errorf("DN `%s` was not found in the LDAP directory", group)
|
err := fmt.Errorf("DN group was not found in the LDAP directory")
|
||||||
collectedErrors = append(collectedErrors, err)
|
collectedErrors = append(collectedErrors, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1578,20 +1638,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
|
||||||
@@ -1602,7 +1648,6 @@ func (sys *IAMSys) getStoredLDAPPolicyMappingKeys(ctx context.Context, isGroup b
|
|||||||
entityKeysInStorage.Add(k)
|
entityKeysInStorage.Add(k)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return entityKeysInStorage
|
return entityKeysInStorage
|
||||||
}
|
}
|
||||||
@@ -1648,7 +1693,7 @@ func (sys *IAMSys) NormalizeLDAPMappingImport(ctx context.Context, isGroup bool,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if validatedDN == nil || !underBaseDN {
|
if validatedDN == nil || !underBaseDN {
|
||||||
err := fmt.Errorf("DN `%s` was not found in the LDAP directory", k)
|
err := fmt.Errorf("DN was not found in the LDAP directory")
|
||||||
collectedErrors = append(collectedErrors, err)
|
collectedErrors = append(collectedErrors, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1728,31 +1773,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.
|
||||||
@@ -1976,7 +2036,7 @@ func (sys *IAMSys) PolicyDBUpdateLDAP(ctx context.Context, isAttach bool,
|
|||||||
if dnResult == nil {
|
if dnResult == nil {
|
||||||
// dn not found - still attempt to detach if provided user is a DN.
|
// dn not found - still attempt to detach if provided user is a DN.
|
||||||
if !isAttach && sys.LDAPConfig.IsLDAPUserDN(r.User) {
|
if !isAttach && sys.LDAPConfig.IsLDAPUserDN(r.User) {
|
||||||
dn = r.User
|
dn = sys.LDAPConfig.QuickNormalizeDN(r.User)
|
||||||
} else {
|
} else {
|
||||||
err = errNoSuchUser
|
err = errNoSuchUser
|
||||||
return
|
return
|
||||||
@@ -1993,7 +2053,7 @@ func (sys *IAMSys) PolicyDBUpdateLDAP(ctx context.Context, isAttach bool,
|
|||||||
}
|
}
|
||||||
if dnResult == nil || !underBaseDN {
|
if dnResult == nil || !underBaseDN {
|
||||||
if !isAttach {
|
if !isAttach {
|
||||||
dn = r.Group
|
dn = sys.LDAPConfig.QuickNormalizeDN(r.Group)
|
||||||
} else {
|
} else {
|
||||||
err = errNoSuchGroup
|
err = errNoSuchGroup
|
||||||
return
|
return
|
||||||
@@ -2128,7 +2188,7 @@ func (sys *IAMSys) IsAllowedServiceAccount(args policy.Args, parentUser string)
|
|||||||
var combinedPolicy policy.Policy
|
var combinedPolicy policy.Policy
|
||||||
// Policies were found, evaluate all of them.
|
// Policies were found, evaluate all of them.
|
||||||
if !isOwnerDerived {
|
if !isOwnerDerived {
|
||||||
availablePoliciesStr, c := sys.store.FilterPolicies(strings.Join(svcPolicies, ","), "")
|
availablePoliciesStr, c := sys.store.MergePolicies(strings.Join(svcPolicies, ","))
|
||||||
if availablePoliciesStr == "" {
|
if availablePoliciesStr == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -2220,22 +2280,16 @@ func (sys *IAMSys) IsAllowedSTS(args policy.Args, parentUser string) bool {
|
|||||||
// 2. Combine the mapped policies into a single combined policy.
|
// 2. Combine the mapped policies into a single combined policy.
|
||||||
|
|
||||||
var combinedPolicy policy.Policy
|
var combinedPolicy policy.Policy
|
||||||
|
// Policies were found, evaluate all of them.
|
||||||
if !isOwnerDerived {
|
if !isOwnerDerived {
|
||||||
var err error
|
availablePoliciesStr, c := sys.store.MergePolicies(strings.Join(policies, ","))
|
||||||
combinedPolicy, err = sys.store.GetPolicy(strings.Join(policies, ","))
|
if availablePoliciesStr == "" {
|
||||||
if errors.Is(err, errNoSuchPolicy) {
|
|
||||||
for _, pname := range policies {
|
|
||||||
_, err := sys.store.GetPolicy(pname)
|
|
||||||
if errors.Is(err, errNoSuchPolicy) {
|
|
||||||
// all policies presented in the claim should exist
|
// all policies presented in the claim should exist
|
||||||
iamLogIf(GlobalContext, fmt.Errorf("expected policy (%s) missing from the JWT claim %s, rejecting the request", pname, iamPolicyClaimNameOpenID()))
|
iamLogIf(GlobalContext, fmt.Errorf("expected policy (%s) missing from the JWT claim %s, rejecting the request", policies, iamPolicyClaimNameOpenID()))
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
iamLogIf(GlobalContext, fmt.Errorf("all policies were unexpectedly present!"))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
combinedPolicy = c
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. If an inline session-policy is present, evaluate it.
|
// 3. If an inline session-policy is present, evaluate it.
|
||||||
@@ -2356,7 +2410,7 @@ func isAllowedBySessionPolicy(args policy.Args) (hasSessionPolicy bool, isAllowe
|
|||||||
|
|
||||||
// GetCombinedPolicy returns a combined policy combining all policies
|
// GetCombinedPolicy returns a combined policy combining all policies
|
||||||
func (sys *IAMSys) GetCombinedPolicy(policies ...string) policy.Policy {
|
func (sys *IAMSys) GetCombinedPolicy(policies ...string) policy.Policy {
|
||||||
_, policy := sys.store.FilterPolicies(strings.Join(policies, ","), "")
|
_, policy := sys.store.MergePolicies(strings.Join(policies, ","))
|
||||||
return policy
|
return policy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-34
@@ -24,10 +24,8 @@ import (
|
|||||||
|
|
||||||
jwtgo "github.com/golang-jwt/jwt/v4"
|
jwtgo "github.com/golang-jwt/jwt/v4"
|
||||||
jwtreq "github.com/golang-jwt/jwt/v4/request"
|
jwtreq "github.com/golang-jwt/jwt/v4/request"
|
||||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
|
||||||
"github.com/minio/minio/internal/auth"
|
"github.com/minio/minio/internal/auth"
|
||||||
xjwt "github.com/minio/minio/internal/jwt"
|
xjwt "github.com/minio/minio/internal/jwt"
|
||||||
"github.com/minio/minio/internal/logger"
|
|
||||||
"github.com/minio/pkg/v3/policy"
|
"github.com/minio/pkg/v3/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,8 +35,8 @@ const (
|
|||||||
// Default JWT token for web handlers is one day.
|
// Default JWT token for web handlers is one day.
|
||||||
defaultJWTExpiry = 24 * time.Hour
|
defaultJWTExpiry = 24 * time.Hour
|
||||||
|
|
||||||
// Inter-node JWT token expiry is 15 minutes.
|
// Inter-node JWT token expiry is 100 years approx.
|
||||||
defaultInterNodeJWTExpiry = 15 * time.Minute
|
defaultInterNodeJWTExpiry = 100 * 365 * 24 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -50,17 +48,10 @@ var (
|
|||||||
errMalformedAuth = errors.New("Malformed authentication input")
|
errMalformedAuth = errors.New("Malformed authentication input")
|
||||||
)
|
)
|
||||||
|
|
||||||
type cacheKey struct {
|
func authenticateNode(accessKey, secretKey string) (string, error) {
|
||||||
accessKey, secretKey, audience string
|
|
||||||
}
|
|
||||||
|
|
||||||
var cacheLRU = expirable.NewLRU[cacheKey, string](1000, nil, 15*time.Second)
|
|
||||||
|
|
||||||
func authenticateNode(accessKey, secretKey, audience string) (string, error) {
|
|
||||||
claims := xjwt.NewStandardClaims()
|
claims := xjwt.NewStandardClaims()
|
||||||
claims.SetExpiry(UTCNow().Add(defaultInterNodeJWTExpiry))
|
claims.SetExpiry(UTCNow().Add(defaultInterNodeJWTExpiry))
|
||||||
claims.SetAccessKey(accessKey)
|
claims.SetAccessKey(accessKey)
|
||||||
claims.SetAudience(audience)
|
|
||||||
|
|
||||||
jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims)
|
jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims)
|
||||||
return jwt.SignedString([]byte(secretKey))
|
return jwt.SignedString([]byte(secretKey))
|
||||||
@@ -141,27 +132,9 @@ func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, b
|
|||||||
return claims, groups, owner, nil
|
return claims, groups, owner, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newCachedAuthToken returns a token that is cached up to 15 seconds.
|
// newCachedAuthToken returns the cached token.
|
||||||
// If globalActiveCred is updated it is reflected at once.
|
func newCachedAuthToken() func() string {
|
||||||
func newCachedAuthToken() func(audience string) string {
|
return func() string {
|
||||||
fn := func(accessKey, secretKey, audience string) (s string, err error) {
|
return globalNodeAuthToken
|
||||||
k := cacheKey{accessKey: accessKey, secretKey: secretKey, audience: audience}
|
|
||||||
|
|
||||||
var ok bool
|
|
||||||
s, ok = cacheLRU.Get(k)
|
|
||||||
if !ok {
|
|
||||||
s, err = authenticateNode(accessKey, secretKey, audience)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
cacheLRU.Add(k, s)
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
return func(audience string) string {
|
|
||||||
cred := globalActiveCred
|
|
||||||
token, err := fn(cred.AccessKey, cred.SecretKey, audience)
|
|
||||||
logger.CriticalIf(GlobalContext, err)
|
|
||||||
return token
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -107,7 +107,7 @@ func BenchmarkParseJWTStandardClaims(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
creds := globalActiveCred
|
creds := globalActiveCred
|
||||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey, "")
|
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ func BenchmarkParseJWTMapClaims(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
creds := globalActiveCred
|
creds := globalActiveCred
|
||||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey, "")
|
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,7 @@ func BenchmarkAuthenticateNode(b *testing.B) {
|
|||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
fn(creds.AccessKey, creds.SecretKey, "aud")
|
fn(creds.AccessKey, creds.SecretKey)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
b.Run("cached", func(b *testing.B) {
|
b.Run("cached", func(b *testing.B) {
|
||||||
@@ -184,7 +184,7 @@ func BenchmarkAuthenticateNode(b *testing.B) {
|
|||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
fn("aud")
|
fn()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-7
@@ -24,6 +24,7 @@ import (
|
|||||||
|
|
||||||
"github.com/minio/kms-go/kes"
|
"github.com/minio/kms-go/kes"
|
||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
|
"github.com/minio/minio/internal/auth"
|
||||||
"github.com/minio/minio/internal/kms"
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/v3/policy"
|
"github.com/minio/pkg/v3/policy"
|
||||||
@@ -56,7 +57,7 @@ func (a kmsAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
writeSuccessResponseJSON(w, resp)
|
writeSuccessResponseJSON(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMSMetricsHandler - POST /minio/kms/v1/metrics
|
// KMSMetricsHandler - GET /minio/kms/v1/metrics
|
||||||
func (a kmsAPIHandlers) KMSMetricsHandler(w http.ResponseWriter, r *http.Request) {
|
func (a kmsAPIHandlers) KMSMetricsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "KMSMetrics")
|
ctx := newContext(r, w, "KMSMetrics")
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
@@ -83,7 +84,7 @@ func (a kmsAPIHandlers) KMSMetricsHandler(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMSAPIsHandler - POST /minio/kms/v1/apis
|
// KMSAPIsHandler - GET /minio/kms/v1/apis
|
||||||
func (a kmsAPIHandlers) KMSAPIsHandler(w http.ResponseWriter, r *http.Request) {
|
func (a kmsAPIHandlers) KMSAPIsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "KMSAPIs")
|
ctx := newContext(r, w, "KMSAPIs")
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
@@ -114,7 +115,7 @@ type versionResponse struct {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMSVersionHandler - POST /minio/kms/v1/version
|
// KMSVersionHandler - GET /minio/kms/v1/version
|
||||||
func (a kmsAPIHandlers) KMSVersionHandler(w http.ResponseWriter, r *http.Request) {
|
func (a kmsAPIHandlers) KMSVersionHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "KMSVersion")
|
ctx := newContext(r, w, "KMSVersion")
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
@@ -159,7 +160,20 @@ func (a kmsAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := GlobalKMS.CreateKey(ctx, &kms.CreateKeyRequest{Name: r.Form.Get("key-id")}); err != nil {
|
keyID := r.Form.Get("key-id")
|
||||||
|
|
||||||
|
// Ensure policy allows the user to create this key name
|
||||||
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !checkKMSActionAllowed(r, owner, cred, policy.KMSCreateKeyAction, keyID) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := GlobalKMS.CreateKey(ctx, &kms.CreateKeyRequest{Name: keyID}); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -171,6 +185,9 @@ func (a kmsAPIHandlers) KMSListKeysHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
ctx := newContext(r, w, "KMSListKeys")
|
ctx := newContext(r, w, "KMSListKeys")
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
// This only checks if the action (kms:ListKeys) is allowed, it does not check
|
||||||
|
// each key name against the policy's Resources. We check that below, once
|
||||||
|
// we have the list of key names from the KMS.
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSListKeysAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSListKeysAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -180,7 +197,7 @@ func (a kmsAPIHandlers) KMSListKeysHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
names, _, err := GlobalKMS.ListKeyNames(ctx, &kms.ListRequest{
|
allKeyNames, _, err := GlobalKMS.ListKeyNames(ctx, &kms.ListRequest{
|
||||||
Prefix: r.Form.Get("pattern"),
|
Prefix: r.Form.Get("pattern"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -188,8 +205,24 @@ func (a kmsAPIHandlers) KMSListKeysHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
values := make([]kes.KeyInfo, 0, len(names))
|
// Get the cred and owner for checking authz below.
|
||||||
for _, name := range names {
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we have all the key names, for each of them, check whether the policy grants permission for
|
||||||
|
// the user to list it.
|
||||||
|
keyNames := []string{}
|
||||||
|
for _, name := range allKeyNames {
|
||||||
|
if checkKMSActionAllowed(r, owner, cred, policy.KMSListKeysAction, name) {
|
||||||
|
keyNames = append(keyNames, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
values := make([]kes.KeyInfo, 0, len(keyNames))
|
||||||
|
for _, name := range keyNames {
|
||||||
values = append(values, kes.KeyInfo{
|
values = append(values, kes.KeyInfo{
|
||||||
Name: name,
|
Name: name,
|
||||||
})
|
})
|
||||||
@@ -224,6 +257,17 @@ func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
KeyID: keyID,
|
KeyID: keyID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure policy allows the user to get this key's status
|
||||||
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !checkKMSActionAllowed(r, owner, cred, policy.KMSKeyStatusAction, keyID) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
kmsContext := kms.Context{"MinIO admin API": "KMSKeyStatusHandler"} // Context for a test key operation
|
kmsContext := kms.Context{"MinIO admin API": "KMSKeyStatusHandler"} // Context for a test key operation
|
||||||
// 1. Generate a new key using the KMS.
|
// 1. Generate a new key using the KMS.
|
||||||
key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{Name: keyID, AssociatedData: kmsContext})
|
key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{Name: keyID, AssociatedData: kmsContext})
|
||||||
@@ -274,3 +318,16 @@ func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
writeSuccessResponseJSON(w, resp)
|
writeSuccessResponseJSON(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkKMSActionAllowed checks for authorization for a specific action on a resource.
|
||||||
|
func checkKMSActionAllowed(r *http.Request, owner bool, cred auth.Credentials, action policy.KMSAction, resource string) bool {
|
||||||
|
return globalIAMSys.IsAllowed(policy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
|
Action: policy.Action(action),
|
||||||
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: cred.Claims,
|
||||||
|
BucketName: resource, // overloading BucketName as that's what the policy engine uses to assemble a Resource.
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,851 @@
|
|||||||
|
// 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"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/madmin-go/v3"
|
||||||
|
"github.com/minio/minio/internal/kms"
|
||||||
|
"github.com/minio/pkg/v3/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// KMS API paths
|
||||||
|
// For example: /minio/kms/v1/key/list?pattern=*
|
||||||
|
kmsURL = kmsPathPrefix + kmsAPIVersionPrefix
|
||||||
|
kmsStatusPath = kmsURL + "/status"
|
||||||
|
kmsMetricsPath = kmsURL + "/metrics"
|
||||||
|
kmsAPIsPath = kmsURL + "/apis"
|
||||||
|
kmsVersionPath = kmsURL + "/version"
|
||||||
|
kmsKeyCreatePath = kmsURL + "/key/create"
|
||||||
|
kmsKeyListPath = kmsURL + "/key/list"
|
||||||
|
kmsKeyStatusPath = kmsURL + "/key/status"
|
||||||
|
|
||||||
|
// Admin API paths
|
||||||
|
// For example: /minio/admin/v3/kms/status
|
||||||
|
adminURL = adminPathPrefix + adminAPIVersionPrefix
|
||||||
|
kmsAdminStatusPath = adminURL + "/kms/status"
|
||||||
|
kmsAdminKeyStatusPath = adminURL + "/kms/key/status"
|
||||||
|
kmsAdminKeyCreate = adminURL + "/kms/key/create"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
userAccessKey = "miniofakeuseraccesskey"
|
||||||
|
userSecretKey = "miniofakeusersecret"
|
||||||
|
)
|
||||||
|
|
||||||
|
type kmsTestCase struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
query map[string]string
|
||||||
|
|
||||||
|
// User credentials and policy for request
|
||||||
|
policy string
|
||||||
|
asRoot bool
|
||||||
|
|
||||||
|
// Wanted in response.
|
||||||
|
wantStatusCode int
|
||||||
|
wantKeyNames []string
|
||||||
|
wantResp []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKMSHandlersCreateKey(t *testing.T) {
|
||||||
|
adminTestBed, tearDown := setupKMSTest(t, true)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
tests := []kmsTestCase{
|
||||||
|
// Create key test
|
||||||
|
{
|
||||||
|
name: "create key as user with no policy want forbidden",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsKeyCreatePath,
|
||||||
|
query: map[string]string{"key-id": "new-test-key"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create key as user with no resources specified want success",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsKeyCreatePath,
|
||||||
|
query: map[string]string{"key-id": "new-test-key"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:CreateKey"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create key as user set policy to allow want success",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsKeyCreatePath,
|
||||||
|
query: map[string]string{"key-id": "second-new-test-key"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:CreateKey"],
|
||||||
|
"Resource": ["arn:minio:kms:::second-new-test-*"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create key as user set policy to non matching resource want forbidden",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsKeyCreatePath,
|
||||||
|
query: map[string]string{"key-id": "third-new-test-key"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:CreateKey"],
|
||||||
|
"Resource": ["arn:minio:kms:::non-matching-key-name"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for testNum, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
|
||||||
|
execKMSTest(t, test, adminTestBed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKMSHandlersKeyStatus(t *testing.T) {
|
||||||
|
adminTestBed, tearDown := setupKMSTest(t, true)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
tests := []kmsTestCase{
|
||||||
|
{
|
||||||
|
name: "create a first key root user",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsKeyCreatePath,
|
||||||
|
query: map[string]string{"key-id": "abc-test-key"},
|
||||||
|
asRoot: true,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key status as root want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyStatusPath,
|
||||||
|
query: map[string]string{"key-id": "abc-test-key"},
|
||||||
|
asRoot: true,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"abc-test-key"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key status as user no policy want forbidden",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyStatusPath,
|
||||||
|
query: map[string]string{"key-id": "abc-test-key"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key status as user legacy no resources specified want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyStatusPath,
|
||||||
|
query: map[string]string{"key-id": "abc-test-key"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:KeyStatus"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"abc-test-key"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key status as user set policy to allow only one key",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyStatusPath,
|
||||||
|
query: map[string]string{"key-id": "abc-test-key"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:KeyStatus"],
|
||||||
|
"Resource": ["arn:minio:kms:::abc-test-*"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"abc-test-key"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key status as user set policy to allow non-matching key",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyStatusPath,
|
||||||
|
query: map[string]string{"key-id": "abc-test-key"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:KeyStatus"],
|
||||||
|
"Resource": ["arn:minio:kms:::xyz-test-key"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for testNum, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
|
||||||
|
execKMSTest(t, test, adminTestBed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKMSHandlersAPIs(t *testing.T) {
|
||||||
|
adminTestBed, tearDown := setupKMSTest(t, true)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
tests := []kmsTestCase{
|
||||||
|
// Version test
|
||||||
|
{
|
||||||
|
name: "version as root want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsVersionPath,
|
||||||
|
asRoot: true,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"version"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "version as user with no policy want forbidden",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsVersionPath,
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "version as user with policy ignores resource want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsVersionPath,
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:Version"],
|
||||||
|
"Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"version"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// APIs test
|
||||||
|
{
|
||||||
|
name: "apis as root want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsAPIsPath,
|
||||||
|
asRoot: true,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"stub/path"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "apis as user with no policy want forbidden",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsAPIsPath,
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "apis as user with policy ignores resource want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsAPIsPath,
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:API"],
|
||||||
|
"Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"stub/path"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Metrics test
|
||||||
|
{
|
||||||
|
name: "metrics as root want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsMetricsPath,
|
||||||
|
asRoot: true,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"kms"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "metrics as user with no policy want forbidden",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsMetricsPath,
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "metrics as user with policy ignores resource want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsMetricsPath,
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:Metrics"],
|
||||||
|
"Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"kms"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Status tests
|
||||||
|
{
|
||||||
|
name: "status as root want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsStatusPath,
|
||||||
|
asRoot: true,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"MinIO builtin"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status as user with no policy want forbidden",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsStatusPath,
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status as user with policy ignores resource want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsStatusPath,
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:Status"],
|
||||||
|
"Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"]}`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"MinIO builtin"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for testNum, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
|
||||||
|
execKMSTest(t, test, adminTestBed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKMSHandlersListKeys(t *testing.T) {
|
||||||
|
adminTestBed, tearDown := setupKMSTest(t, true)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
tests := []kmsTestCase{
|
||||||
|
{
|
||||||
|
name: "create a first key root user",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsKeyCreatePath,
|
||||||
|
query: map[string]string{"key-id": "abc-test-key"},
|
||||||
|
asRoot: true,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create a second key root user",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsKeyCreatePath,
|
||||||
|
query: map[string]string{"key-id": "xyz-test-key"},
|
||||||
|
asRoot: true,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
|
||||||
|
// List keys tests
|
||||||
|
{
|
||||||
|
name: "list keys as root want all to be returned",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyListPath,
|
||||||
|
query: map[string]string{"pattern": "*"},
|
||||||
|
asRoot: true,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantKeyNames: []string{"default-test-key", "abc-test-key", "xyz-test-key"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list keys as user with no policy want forbidden",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyListPath,
|
||||||
|
query: map[string]string{"pattern": "*"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list keys as user with no resources specified want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyListPath,
|
||||||
|
query: map[string]string{"pattern": "*"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:ListKeys"]
|
||||||
|
}`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantKeyNames: []string{"default-test-key", "abc-test-key", "xyz-test-key"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list keys as user set policy resource to allow only one key",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyListPath,
|
||||||
|
query: map[string]string{"pattern": "*"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:ListKeys"],
|
||||||
|
"Resource": ["arn:minio:kms:::abc*"]}`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantKeyNames: []string{"abc-test-key"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list keys as user set policy to allow only one key, use pattern that includes correct key",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyListPath,
|
||||||
|
query: map[string]string{"pattern": "abc*"},
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:ListKeys"],
|
||||||
|
"Resource": ["arn:minio:kms:::abc*"]}`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantKeyNames: []string{"abc-test-key"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list keys as user set policy to allow only one key, use pattern that excludes correct key",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyListPath,
|
||||||
|
query: map[string]string{"pattern": "xyz*"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:ListKeys"],
|
||||||
|
"Resource": ["arn:minio:kms:::abc*"]}`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantKeyNames: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list keys as user set policy that has no matching key resources",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyListPath,
|
||||||
|
query: map[string]string{"pattern": "*"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["kms:ListKeys"],
|
||||||
|
"Resource": ["arn:minio:kms:::nonematch*"]}`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantKeyNames: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list keys as user set policy that allows listing but denies specific keys",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyListPath,
|
||||||
|
query: map[string]string{"pattern": "*"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
// It looks like this should allow listing any key that isn't "default-test-key", however
|
||||||
|
// the policy engine matches all Deny statements first, without regard to Resources (for KMS).
|
||||||
|
// This is for backwards compatibility where historically KMS statements ignored Resources.
|
||||||
|
policy: `{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": ["kms:ListKeys"]
|
||||||
|
},{
|
||||||
|
"Effect": "Deny",
|
||||||
|
"Action": ["kms:ListKeys"],
|
||||||
|
"Resource": ["arn:minio:kms:::default-test-key"]
|
||||||
|
}`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for testNum, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
|
||||||
|
execKMSTest(t, test, adminTestBed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKMSHandlerAdminAPI(t *testing.T) {
|
||||||
|
adminTestBed, tearDown := setupKMSTest(t, true)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
tests := []kmsTestCase{
|
||||||
|
// Create key tests
|
||||||
|
{
|
||||||
|
name: "create a key root user",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsAdminKeyCreate,
|
||||||
|
query: map[string]string{"key-id": "abc-test-key"},
|
||||||
|
asRoot: true,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create key as user with no policy want forbidden",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsAdminKeyCreate,
|
||||||
|
query: map[string]string{"key-id": "new-test-key"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create key as user with no resources specified want success",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsAdminKeyCreate,
|
||||||
|
query: map[string]string{"key-id": "new-test-key"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["admin:KMSCreateKey"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create key as user set policy to non matching resource want success",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsAdminKeyCreate,
|
||||||
|
query: map[string]string{"key-id": "third-new-test-key"},
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
// Admin actions ignore Resources
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["admin:KMSCreateKey"],
|
||||||
|
"Resource": ["arn:minio:kms:::this-is-disregarded"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Status tests
|
||||||
|
{
|
||||||
|
name: "status as root want success",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsAdminStatusPath,
|
||||||
|
asRoot: true,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"MinIO builtin"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status as user with no policy want forbidden",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsAdminStatusPath,
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status as user with policy ignores resource want success",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsAdminStatusPath,
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["admin:KMSKeyStatus"],
|
||||||
|
"Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"MinIO builtin"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Key status tests
|
||||||
|
{
|
||||||
|
name: "key status as root want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsAdminKeyStatusPath,
|
||||||
|
asRoot: true,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"key-id"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key status as user with no policy want forbidden",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsAdminKeyStatusPath,
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
wantResp: []string{"AccessDenied"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key status as user with policy ignores resource want success",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsAdminKeyStatusPath,
|
||||||
|
asRoot: false,
|
||||||
|
|
||||||
|
policy: `{"Effect": "Allow",
|
||||||
|
"Action": ["admin:KMSKeyStatus"],
|
||||||
|
"Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
|
||||||
|
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantResp: []string{"key-id"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for testNum, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
|
||||||
|
execKMSTest(t, test, adminTestBed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// execKMSTest runs a single test case for KMS handlers
|
||||||
|
func execKMSTest(t *testing.T, test kmsTestCase, adminTestBed *adminErasureTestBed) {
|
||||||
|
var accessKey, secretKey string
|
||||||
|
if test.asRoot {
|
||||||
|
accessKey, secretKey = globalActiveCred.AccessKey, globalActiveCred.SecretKey
|
||||||
|
} else {
|
||||||
|
setupKMSUser(t, userAccessKey, userSecretKey, test.policy)
|
||||||
|
accessKey = userAccessKey
|
||||||
|
secretKey = userSecretKey
|
||||||
|
}
|
||||||
|
|
||||||
|
req := buildKMSRequest(t, test.method, test.path, accessKey, secretKey, test.query)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
adminTestBed.router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
t.Logf("HTTP req: %s, resp code: %d, resp body: %s", req.URL.String(), rec.Code, rec.Body.String())
|
||||||
|
|
||||||
|
// Check status code
|
||||||
|
if rec.Code != test.wantStatusCode {
|
||||||
|
t.Errorf("want status code %d, got %d", test.wantStatusCode, rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check returned key list is correct
|
||||||
|
if test.wantKeyNames != nil {
|
||||||
|
gotKeyNames := keyNamesFromListKeysResp(t, rec.Body.Bytes())
|
||||||
|
if len(test.wantKeyNames) != len(gotKeyNames) {
|
||||||
|
t.Fatalf("want keys len: %d, got len: %d", len(test.wantKeyNames), len(gotKeyNames))
|
||||||
|
}
|
||||||
|
for i, wantKeyName := range test.wantKeyNames {
|
||||||
|
if gotKeyNames[i] != wantKeyName {
|
||||||
|
t.Fatalf("want key name %s, in position %d, got %s", wantKeyName, i, gotKeyNames[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check generic text in the response
|
||||||
|
if test.wantResp != nil {
|
||||||
|
for _, want := range test.wantResp {
|
||||||
|
if !strings.Contains(rec.Body.String(), want) {
|
||||||
|
t.Fatalf("want response to contain %s, got %s", want, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestKMSHandlerNotConfiguredOrInvalidCreds tests KMS handlers for situations where KMS is not configured
|
||||||
|
// or invalid credentials are provided.
|
||||||
|
func TestKMSHandlerNotConfiguredOrInvalidCreds(t *testing.T) {
|
||||||
|
adminTestBed, tearDown := setupKMSTest(t, false)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
query map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "GET status",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsStatusPath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GET metrics",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsMetricsPath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GET apis",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsAPIsPath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GET version",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsVersionPath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST key create",
|
||||||
|
method: http.MethodPost,
|
||||||
|
path: kmsKeyCreatePath,
|
||||||
|
query: map[string]string{"key-id": "master-key-id"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GET key list",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyListPath,
|
||||||
|
query: map[string]string{"pattern": "*"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GET key status",
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: kmsKeyStatusPath,
|
||||||
|
query: map[string]string{"key-id": "master-key-id"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test when the GlobalKMS is not configured
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name+" not configured", func(t *testing.T) {
|
||||||
|
req := buildKMSRequest(t, test.method, test.path, "", "", test.query)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
adminTestBed.router.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusNotImplemented {
|
||||||
|
t.Errorf("want status code %d, got %d", http.StatusNotImplemented, rec.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test when the GlobalKMS is configured but the credentials are invalid
|
||||||
|
GlobalKMS = kms.NewStub("default-test-key")
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name+" invalid credentials", func(t *testing.T) {
|
||||||
|
req := buildKMSRequest(t, test.method, test.path, userAccessKey, userSecretKey, test.query)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
adminTestBed.router.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusForbidden {
|
||||||
|
t.Errorf("want status code %d, got %d", http.StatusForbidden, rec.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupKMSTest(t *testing.T, enableKMS bool) (*adminErasureTestBed, func()) {
|
||||||
|
adminTestBed, err := prepareAdminErasureTestBed(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
registerKMSRouter(adminTestBed.router)
|
||||||
|
|
||||||
|
if enableKMS {
|
||||||
|
GlobalKMS = kms.NewStub("default-test-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
tearDown := func() {
|
||||||
|
adminTestBed.TearDown()
|
||||||
|
GlobalKMS = nil
|
||||||
|
}
|
||||||
|
return adminTestBed, tearDown
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildKMSRequest(t *testing.T, method, path, accessKey, secretKey string, query map[string]string) *http.Request {
|
||||||
|
if len(query) > 0 {
|
||||||
|
queryVal := url.Values{}
|
||||||
|
for k, v := range query {
|
||||||
|
queryVal.Add(k, v)
|
||||||
|
}
|
||||||
|
path = path + "?" + queryVal.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessKey == "" && secretKey == "" {
|
||||||
|
accessKey = globalActiveCred.AccessKey
|
||||||
|
secretKey = globalActiveCred.SecretKey
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := newTestSignedRequestV4(method, path, 0, nil, accessKey, secretKey, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupKMSUser is a test helper that creates a new user with the provided access key and secret key
|
||||||
|
// and applies the given policy to the user.
|
||||||
|
func setupKMSUser(t *testing.T, accessKey, secretKey, p string) {
|
||||||
|
ctx := context.Background()
|
||||||
|
createUserParams := madmin.AddOrUpdateUserReq{
|
||||||
|
SecretKey: secretKey,
|
||||||
|
Status: madmin.AccountEnabled,
|
||||||
|
}
|
||||||
|
_, err := globalIAMSys.CreateUser(ctx, accessKey, createUserParams)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testKMSPolicyName := "testKMSPolicy"
|
||||||
|
if p != "" {
|
||||||
|
p = `{"Version":"2012-10-17","Statement":[` + p + `]}`
|
||||||
|
policyData, err := policy.ParseConfig(strings.NewReader(p))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = globalIAMSys.SetPolicy(ctx, testKMSPolicyName, *policyData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = globalIAMSys.PolicyDBSet(ctx, accessKey, testKMSPolicyName, regUser, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = globalIAMSys.DeletePolicy(ctx, testKMSPolicyName, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = globalIAMSys.PolicyDBSet(ctx, accessKey, "", regUser, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyNamesFromListKeysResp(t *testing.T, b []byte) []string {
|
||||||
|
var keyInfos []madmin.KMSKeyInfo
|
||||||
|
err := json.Unmarshal(b, &keyInfos)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot unmarshal '%s', err: %v", b, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotKeyNames []string
|
||||||
|
for _, keyInfo := range keyInfos {
|
||||||
|
gotKeyNames = append(gotKeyNames, keyInfo.Name)
|
||||||
|
}
|
||||||
|
return gotKeyNames
|
||||||
|
}
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
// Copyright (c) 2015-2023 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"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
licUpdateCycle = 24 * time.Hour * 30
|
|
||||||
licRenewPath = "/api/cluster/renew-license"
|
|
||||||
)
|
|
||||||
|
|
||||||
// initlicenseUpdateJob start the periodic license update job in the background.
|
|
||||||
func initLicenseUpdateJob(ctx context.Context, objAPI ObjectLayer) {
|
|
||||||
go func() {
|
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
// Leader node (that successfully acquires the lock inside licenceUpdaterLoop)
|
|
||||||
// will keep performing the license update. If the leader goes down for some
|
|
||||||
// reason, the lock will be released and another node will acquire it and
|
|
||||||
// take over because of this loop.
|
|
||||||
for {
|
|
||||||
licenceUpdaterLoop(ctx, objAPI)
|
|
||||||
|
|
||||||
// license update stopped for some reason.
|
|
||||||
// sleep for some time and try again.
|
|
||||||
duration := time.Duration(r.Float64() * float64(time.Hour))
|
|
||||||
if duration < time.Second {
|
|
||||||
// Make sure to sleep at least a second to avoid high CPU ticks.
|
|
||||||
duration = time.Second
|
|
||||||
}
|
|
||||||
time.Sleep(duration)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func licenceUpdaterLoop(ctx context.Context, objAPI ObjectLayer) {
|
|
||||||
ctx, cancel := globalLeaderLock.GetLock(ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
licenseUpdateTimer := time.NewTimer(licUpdateCycle)
|
|
||||||
defer licenseUpdateTimer.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-licenseUpdateTimer.C:
|
|
||||||
|
|
||||||
if globalSubnetConfig.Registered() {
|
|
||||||
performLicenseUpdate(ctx, objAPI)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the timer for next cycle.
|
|
||||||
licenseUpdateTimer.Reset(licUpdateCycle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func performLicenseUpdate(ctx context.Context, objectAPI ObjectLayer) {
|
|
||||||
// the subnet license renewal api renews the license only
|
|
||||||
// if required e.g. when it is expiring soon
|
|
||||||
url := globalSubnetConfig.BaseURL + licRenewPath
|
|
||||||
|
|
||||||
resp, err := globalSubnetConfig.Post(url, nil)
|
|
||||||
if err != nil {
|
|
||||||
subnetLogIf(ctx, fmt.Errorf("error from %s: %w", url, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r := gjson.Parse(resp).Get("license_v2")
|
|
||||||
if r.Index == 0 {
|
|
||||||
internalLogIf(ctx, fmt.Errorf("license not found in response from %s", url))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lic := r.String()
|
|
||||||
if lic == globalSubnetConfig.License {
|
|
||||||
// license hasn't changed.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
kv := "subnet license=" + lic
|
|
||||||
result, err := setConfigKV(ctx, objectAPI, []byte(kv))
|
|
||||||
if err != nil {
|
|
||||||
internalLogIf(ctx, fmt.Errorf("error setting subnet license config: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.Dynamic {
|
|
||||||
if err := applyDynamicConfigForSubSys(GlobalContext, objectAPI, result.Cfg, result.SubSys); err != nil {
|
|
||||||
subnetLogIf(ctx, fmt.Errorf("error applying subnet dynamic config: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
globalNotificationSys.SignalConfigReload(result.SubSys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+16
-12
@@ -34,8 +34,8 @@ type lockRequesterInfo struct {
|
|||||||
Name string // name of the resource lock was requested for
|
Name string // name of the resource lock was requested for
|
||||||
Writer bool // Bool whether write or read lock.
|
Writer bool // Bool whether write or read lock.
|
||||||
UID string // UID to uniquely identify request of client.
|
UID string // UID to uniquely identify request of client.
|
||||||
Timestamp time.Time // Timestamp set at the time of initialization.
|
Timestamp int64 // Timestamp set at the time of initialization.
|
||||||
TimeLastRefresh time.Time // Timestamp for last lock refresh.
|
TimeLastRefresh int64 // Timestamp for last lock refresh.
|
||||||
Source string // Contains line, function and filename requesting the lock.
|
Source string // Contains line, function and filename requesting the lock.
|
||||||
Group bool // indicates if it was a group lock.
|
Group bool // indicates if it was a group lock.
|
||||||
Owner string // Owner represents the UUID of the owner who originally requested the lock.
|
Owner string // Owner represents the UUID of the owner who originally requested the lock.
|
||||||
@@ -87,6 +87,7 @@ func (l *localLocker) Lock(ctx context.Context, args dsync.LockArgs) (reply bool
|
|||||||
|
|
||||||
// No locks held on the all resources, so claim write
|
// No locks held on the all resources, so claim write
|
||||||
// lock on all resources at once.
|
// lock on all resources at once.
|
||||||
|
now := UTCNow()
|
||||||
for i, resource := range args.Resources {
|
for i, resource := range args.Resources {
|
||||||
l.lockMap[resource] = []lockRequesterInfo{
|
l.lockMap[resource] = []lockRequesterInfo{
|
||||||
{
|
{
|
||||||
@@ -95,10 +96,10 @@ func (l *localLocker) Lock(ctx context.Context, args dsync.LockArgs) (reply bool
|
|||||||
Source: args.Source,
|
Source: args.Source,
|
||||||
Owner: args.Owner,
|
Owner: args.Owner,
|
||||||
UID: args.UID,
|
UID: args.UID,
|
||||||
Timestamp: UTCNow(),
|
Timestamp: now.UnixNano(),
|
||||||
TimeLastRefresh: UTCNow(),
|
TimeLastRefresh: now.UnixNano(),
|
||||||
Group: len(args.Resources) > 1,
|
Group: len(args.Resources) > 1,
|
||||||
Quorum: args.Quorum,
|
Quorum: *args.Quorum,
|
||||||
idx: i,
|
idx: i,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -108,7 +109,7 @@ func (l *localLocker) Lock(ctx context.Context, args dsync.LockArgs) (reply bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
func formatUUID(s string, idx int) string {
|
func formatUUID(s string, idx int) string {
|
||||||
return s + strconv.Itoa(idx)
|
return concat(s, strconv.Itoa(idx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *localLocker) Unlock(_ context.Context, args dsync.LockArgs) (reply bool, err error) {
|
func (l *localLocker) Unlock(_ context.Context, args dsync.LockArgs) (reply bool, err error) {
|
||||||
@@ -167,17 +168,19 @@ func (l *localLocker) RLock(ctx context.Context, args dsync.LockArgs) (reply boo
|
|||||||
l.mutex.Lock()
|
l.mutex.Lock()
|
||||||
defer l.mutex.Unlock()
|
defer l.mutex.Unlock()
|
||||||
resource := args.Resources[0]
|
resource := args.Resources[0]
|
||||||
|
now := UTCNow()
|
||||||
lrInfo := lockRequesterInfo{
|
lrInfo := lockRequesterInfo{
|
||||||
Name: resource,
|
Name: resource,
|
||||||
Writer: false,
|
Writer: false,
|
||||||
Source: args.Source,
|
Source: args.Source,
|
||||||
Owner: args.Owner,
|
Owner: args.Owner,
|
||||||
UID: args.UID,
|
UID: args.UID,
|
||||||
Timestamp: UTCNow(),
|
Timestamp: now.UnixNano(),
|
||||||
TimeLastRefresh: UTCNow(),
|
TimeLastRefresh: now.UnixNano(),
|
||||||
Quorum: args.Quorum,
|
Quorum: *args.Quorum,
|
||||||
}
|
}
|
||||||
if lri, ok := l.lockMap[resource]; ok {
|
lri, ok := l.lockMap[resource]
|
||||||
|
if ok {
|
||||||
if reply = !isWriteLock(lri); reply {
|
if reply = !isWriteLock(lri); reply {
|
||||||
// Unless there is a write lock
|
// Unless there is a write lock
|
||||||
l.lockMap[resource] = append(l.lockMap[resource], lrInfo)
|
l.lockMap[resource] = append(l.lockMap[resource], lrInfo)
|
||||||
@@ -351,9 +354,10 @@ func (l *localLocker) Refresh(ctx context.Context, args dsync.LockArgs) (refresh
|
|||||||
delete(l.lockUID, formatUUID(args.UID, idx))
|
delete(l.lockUID, formatUUID(args.UID, idx))
|
||||||
return idx > 0, nil
|
return idx > 0, nil
|
||||||
}
|
}
|
||||||
|
now := UTCNow()
|
||||||
for i := range lris {
|
for i := range lris {
|
||||||
if lris[i].UID == args.UID {
|
if lris[i].UID == args.UID {
|
||||||
lris[i].TimeLastRefresh = UTCNow()
|
lris[i].TimeLastRefresh = now.UnixNano()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
idx++
|
idx++
|
||||||
@@ -376,7 +380,7 @@ func (l *localLocker) expireOldLocks(interval time.Duration) {
|
|||||||
modified := false
|
modified := false
|
||||||
for i := 0; i < len(lris); {
|
for i := 0; i < len(lris); {
|
||||||
lri := &lris[i]
|
lri := &lris[i]
|
||||||
if time.Since(lri.TimeLastRefresh) > interval {
|
if time.Since(time.Unix(0, lri.TimeLastRefresh)) > interval {
|
||||||
delete(l.lockUID, formatUUID(lri.UID, lri.idx))
|
delete(l.lockUID, formatUUID(lri.UID, lri.idx))
|
||||||
if len(lris) == 1 {
|
if len(lris) == 1 {
|
||||||
// Remove the write lock.
|
// Remove the write lock.
|
||||||
|
|||||||
@@ -204,13 +204,13 @@ func (z *lockRequesterInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "Timestamp":
|
case "Timestamp":
|
||||||
z.Timestamp, err = dc.ReadTime()
|
z.Timestamp, err = dc.ReadInt64()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "Timestamp")
|
err = msgp.WrapError(err, "Timestamp")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "TimeLastRefresh":
|
case "TimeLastRefresh":
|
||||||
z.TimeLastRefresh, err = dc.ReadTime()
|
z.TimeLastRefresh, err = dc.ReadInt64()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "TimeLastRefresh")
|
err = msgp.WrapError(err, "TimeLastRefresh")
|
||||||
return
|
return
|
||||||
@@ -288,7 +288,7 @@ func (z *lockRequesterInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = en.WriteTime(z.Timestamp)
|
err = en.WriteInt64(z.Timestamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "Timestamp")
|
err = msgp.WrapError(err, "Timestamp")
|
||||||
return
|
return
|
||||||
@@ -298,7 +298,7 @@ func (z *lockRequesterInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = en.WriteTime(z.TimeLastRefresh)
|
err = en.WriteInt64(z.TimeLastRefresh)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "TimeLastRefresh")
|
err = msgp.WrapError(err, "TimeLastRefresh")
|
||||||
return
|
return
|
||||||
@@ -361,10 +361,10 @@ func (z *lockRequesterInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
o = msgp.AppendString(o, z.UID)
|
o = msgp.AppendString(o, z.UID)
|
||||||
// string "Timestamp"
|
// string "Timestamp"
|
||||||
o = append(o, 0xa9, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70)
|
o = append(o, 0xa9, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70)
|
||||||
o = msgp.AppendTime(o, z.Timestamp)
|
o = msgp.AppendInt64(o, z.Timestamp)
|
||||||
// string "TimeLastRefresh"
|
// string "TimeLastRefresh"
|
||||||
o = append(o, 0xaf, 0x54, 0x69, 0x6d, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68)
|
o = append(o, 0xaf, 0x54, 0x69, 0x6d, 0x65, 0x4c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68)
|
||||||
o = msgp.AppendTime(o, z.TimeLastRefresh)
|
o = msgp.AppendInt64(o, z.TimeLastRefresh)
|
||||||
// string "Source"
|
// string "Source"
|
||||||
o = append(o, 0xa6, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65)
|
o = append(o, 0xa6, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65)
|
||||||
o = msgp.AppendString(o, z.Source)
|
o = msgp.AppendString(o, z.Source)
|
||||||
@@ -417,13 +417,13 @@ func (z *lockRequesterInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "Timestamp":
|
case "Timestamp":
|
||||||
z.Timestamp, bts, err = msgp.ReadTimeBytes(bts)
|
z.Timestamp, bts, err = msgp.ReadInt64Bytes(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "Timestamp")
|
err = msgp.WrapError(err, "Timestamp")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "TimeLastRefresh":
|
case "TimeLastRefresh":
|
||||||
z.TimeLastRefresh, bts, err = msgp.ReadTimeBytes(bts)
|
z.TimeLastRefresh, bts, err = msgp.ReadInt64Bytes(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "TimeLastRefresh")
|
err = msgp.WrapError(err, "TimeLastRefresh")
|
||||||
return
|
return
|
||||||
@@ -466,7 +466,7 @@ func (z *lockRequesterInfo) 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 *lockRequesterInfo) Msgsize() (s int) {
|
func (z *lockRequesterInfo) Msgsize() (s int) {
|
||||||
s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 7 + msgp.BoolSize + 4 + msgp.StringPrefixSize + len(z.UID) + 10 + msgp.TimeSize + 16 + msgp.TimeSize + 7 + msgp.StringPrefixSize + len(z.Source) + 6 + msgp.BoolSize + 6 + msgp.StringPrefixSize + len(z.Owner) + 7 + msgp.IntSize
|
s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 7 + msgp.BoolSize + 4 + msgp.StringPrefixSize + len(z.UID) + 10 + msgp.Int64Size + 16 + msgp.Int64Size + 7 + msgp.StringPrefixSize + len(z.Source) + 6 + msgp.BoolSize + 6 + msgp.StringPrefixSize + len(z.Owner) + 7 + msgp.IntSize
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-11
@@ -32,6 +32,7 @@ import (
|
|||||||
func TestLocalLockerExpire(t *testing.T) {
|
func TestLocalLockerExpire(t *testing.T) {
|
||||||
wResources := make([]string, 1000)
|
wResources := make([]string, 1000)
|
||||||
rResources := make([]string, 1000)
|
rResources := make([]string, 1000)
|
||||||
|
quorum := 0
|
||||||
l := newLocker()
|
l := newLocker()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
for i := range wResources {
|
for i := range wResources {
|
||||||
@@ -40,7 +41,7 @@ func TestLocalLockerExpire(t *testing.T) {
|
|||||||
Resources: []string{mustGetUUID()},
|
Resources: []string{mustGetUUID()},
|
||||||
Source: t.Name(),
|
Source: t.Name(),
|
||||||
Owner: "owner",
|
Owner: "owner",
|
||||||
Quorum: 0,
|
Quorum: &quorum,
|
||||||
}
|
}
|
||||||
ok, err := l.Lock(ctx, arg)
|
ok, err := l.Lock(ctx, arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -58,14 +59,14 @@ func TestLocalLockerExpire(t *testing.T) {
|
|||||||
Resources: []string{name},
|
Resources: []string{name},
|
||||||
Source: t.Name(),
|
Source: t.Name(),
|
||||||
Owner: "owner",
|
Owner: "owner",
|
||||||
Quorum: 0,
|
Quorum: &quorum,
|
||||||
}
|
}
|
||||||
ok, err := l.RLock(ctx, arg)
|
ok, err := l.RLock(ctx, arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("did not get write lock")
|
t.Fatal("did not get read lock")
|
||||||
}
|
}
|
||||||
// RLock twice
|
// RLock twice
|
||||||
ok, err = l.RLock(ctx, arg)
|
ok, err = l.RLock(ctx, arg)
|
||||||
@@ -112,6 +113,7 @@ func TestLocalLockerUnlock(t *testing.T) {
|
|||||||
rUIDs := make([]string, 0, n*2)
|
rUIDs := make([]string, 0, n*2)
|
||||||
l := newLocker()
|
l := newLocker()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
quorum := 0
|
||||||
for i := range wResources {
|
for i := range wResources {
|
||||||
names := [m]string{}
|
names := [m]string{}
|
||||||
for j := range names {
|
for j := range names {
|
||||||
@@ -123,7 +125,7 @@ func TestLocalLockerUnlock(t *testing.T) {
|
|||||||
Resources: names[:],
|
Resources: names[:],
|
||||||
Source: t.Name(),
|
Source: t.Name(),
|
||||||
Owner: "owner",
|
Owner: "owner",
|
||||||
Quorum: 0,
|
Quorum: &quorum,
|
||||||
}
|
}
|
||||||
ok, err := l.Lock(ctx, arg)
|
ok, err := l.Lock(ctx, arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -144,7 +146,7 @@ func TestLocalLockerUnlock(t *testing.T) {
|
|||||||
Resources: []string{name},
|
Resources: []string{name},
|
||||||
Source: t.Name(),
|
Source: t.Name(),
|
||||||
Owner: "owner",
|
Owner: "owner",
|
||||||
Quorum: 0,
|
Quorum: &quorum,
|
||||||
}
|
}
|
||||||
ok, err := l.RLock(ctx, arg)
|
ok, err := l.RLock(ctx, arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -183,7 +185,7 @@ func TestLocalLockerUnlock(t *testing.T) {
|
|||||||
Resources: []string{name},
|
Resources: []string{name},
|
||||||
Source: t.Name(),
|
Source: t.Name(),
|
||||||
Owner: "owner",
|
Owner: "owner",
|
||||||
Quorum: 0,
|
Quorum: &quorum,
|
||||||
}
|
}
|
||||||
ok, err := l.RUnlock(ctx, arg)
|
ok, err := l.RUnlock(ctx, arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -203,6 +205,7 @@ func TestLocalLockerUnlock(t *testing.T) {
|
|||||||
if len(l.lockUID) != len(rResources)+len(wResources)*m {
|
if len(l.lockUID) != len(rResources)+len(wResources)*m {
|
||||||
t.Fatalf("lockUID len, got %d, want %d + %d", len(l.lockUID), len(rResources), len(wResources)*m)
|
t.Fatalf("lockUID len, got %d, want %d + %d", len(l.lockUID), len(rResources), len(wResources)*m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RUnlock again, different uids
|
// RUnlock again, different uids
|
||||||
for i, name := range rResources {
|
for i, name := range rResources {
|
||||||
arg := dsync.LockArgs{
|
arg := dsync.LockArgs{
|
||||||
@@ -210,7 +213,7 @@ func TestLocalLockerUnlock(t *testing.T) {
|
|||||||
Resources: []string{name},
|
Resources: []string{name},
|
||||||
Source: "minio",
|
Source: "minio",
|
||||||
Owner: "owner",
|
Owner: "owner",
|
||||||
Quorum: 0,
|
Quorum: &quorum,
|
||||||
}
|
}
|
||||||
ok, err := l.RUnlock(ctx, arg)
|
ok, err := l.RUnlock(ctx, arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -238,7 +241,7 @@ func TestLocalLockerUnlock(t *testing.T) {
|
|||||||
Resources: names[:],
|
Resources: names[:],
|
||||||
Source: "minio",
|
Source: "minio",
|
||||||
Owner: "owner",
|
Owner: "owner",
|
||||||
Quorum: 0,
|
Quorum: &quorum,
|
||||||
}
|
}
|
||||||
ok, err := l.Unlock(ctx, arg)
|
ok, err := l.Unlock(ctx, arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -261,6 +264,7 @@ func TestLocalLockerUnlock(t *testing.T) {
|
|||||||
|
|
||||||
func Test_localLocker_expireOldLocksExpire(t *testing.T) {
|
func Test_localLocker_expireOldLocksExpire(t *testing.T) {
|
||||||
rng := rand.New(rand.NewSource(0))
|
rng := rand.New(rand.NewSource(0))
|
||||||
|
quorum := 0
|
||||||
// Numbers of unique locks
|
// Numbers of unique locks
|
||||||
for _, locks := range []int{100, 1000, 1e6} {
|
for _, locks := range []int{100, 1000, 1e6} {
|
||||||
if testing.Short() && locks > 100 {
|
if testing.Short() && locks > 100 {
|
||||||
@@ -289,7 +293,7 @@ func Test_localLocker_expireOldLocksExpire(t *testing.T) {
|
|||||||
Resources: res,
|
Resources: res,
|
||||||
Source: hex.EncodeToString(tmp[:8]),
|
Source: hex.EncodeToString(tmp[:8]),
|
||||||
Owner: hex.EncodeToString(tmp[8:]),
|
Owner: hex.EncodeToString(tmp[8:]),
|
||||||
Quorum: 0,
|
Quorum: &quorum,
|
||||||
})
|
})
|
||||||
if !ok || err != nil {
|
if !ok || err != nil {
|
||||||
t.Fatal("failed:", err, ok)
|
t.Fatal("failed:", err, ok)
|
||||||
@@ -311,7 +315,7 @@ func Test_localLocker_expireOldLocksExpire(t *testing.T) {
|
|||||||
for _, v := range l.lockMap {
|
for _, v := range l.lockMap {
|
||||||
for i := range v {
|
for i := range v {
|
||||||
if rng.Intn(2) == 0 {
|
if rng.Intn(2) == 0 {
|
||||||
v[i].TimeLastRefresh = expired
|
v[i].TimeLastRefresh = expired.UnixNano()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,6 +351,7 @@ func Test_localLocker_expireOldLocksExpire(t *testing.T) {
|
|||||||
|
|
||||||
func Test_localLocker_RUnlock(t *testing.T) {
|
func Test_localLocker_RUnlock(t *testing.T) {
|
||||||
rng := rand.New(rand.NewSource(0))
|
rng := rand.New(rand.NewSource(0))
|
||||||
|
quorum := 0
|
||||||
// Numbers of unique locks
|
// Numbers of unique locks
|
||||||
for _, locks := range []int{1, 100, 1000, 1e6} {
|
for _, locks := range []int{1, 100, 1000, 1e6} {
|
||||||
if testing.Short() && locks > 100 {
|
if testing.Short() && locks > 100 {
|
||||||
@@ -375,7 +380,7 @@ func Test_localLocker_RUnlock(t *testing.T) {
|
|||||||
Resources: res,
|
Resources: res,
|
||||||
Source: hex.EncodeToString(tmp[:8]),
|
Source: hex.EncodeToString(tmp[:8]),
|
||||||
Owner: hex.EncodeToString(tmp[8:]),
|
Owner: hex.EncodeToString(tmp[8:]),
|
||||||
Quorum: 0,
|
Quorum: &quorum,
|
||||||
})
|
})
|
||||||
if !ok || err != nil {
|
if !ok || err != nil {
|
||||||
t.Fatal("failed:", err, ok)
|
t.Fatal("failed:", err, ok)
|
||||||
|
|||||||
@@ -107,5 +107,5 @@ func newLockAPI(endpoint Endpoint) dsync.NetLocker {
|
|||||||
|
|
||||||
// Returns a lock rest client.
|
// Returns a lock rest client.
|
||||||
func newlockRESTClient(ep Endpoint) *lockRESTClient {
|
func newlockRESTClient(ep Endpoint) *lockRESTClient {
|
||||||
return &lockRESTClient{globalGrid.Load().Connection(ep.GridHost())}
|
return &lockRESTClient{globalLockGrid.Load().Connection(ep.GridHost())}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func TestLockRESTlient(t *testing.T) {
|
|||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
err = initGlobalGrid(ctx, []PoolEndpoints{{Endpoints: Endpoints{endpoint, endpointLocal}}})
|
err = initGlobalLockGrid(ctx, []PoolEndpoints{{Endpoints: Endpoints{endpoint, endpointLocal}}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func createLockTestServer(ctx context.Context, t *testing.T) (string, *lockRESTS
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
creds := globalActiveCred
|
creds := globalActiveCred
|
||||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey, "")
|
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -63,15 +63,15 @@ func TestLockRpcServerRemoveEntry(t *testing.T) {
|
|||||||
Owner: "owner",
|
Owner: "owner",
|
||||||
Writer: true,
|
Writer: true,
|
||||||
UID: "0123-4567",
|
UID: "0123-4567",
|
||||||
Timestamp: UTCNow(),
|
Timestamp: UTCNow().UnixNano(),
|
||||||
TimeLastRefresh: UTCNow(),
|
TimeLastRefresh: UTCNow().UnixNano(),
|
||||||
}
|
}
|
||||||
lockRequesterInfo2 := lockRequesterInfo{
|
lockRequesterInfo2 := lockRequesterInfo{
|
||||||
Owner: "owner",
|
Owner: "owner",
|
||||||
Writer: true,
|
Writer: true,
|
||||||
UID: "89ab-cdef",
|
UID: "89ab-cdef",
|
||||||
Timestamp: UTCNow(),
|
Timestamp: UTCNow().UnixNano(),
|
||||||
TimeLastRefresh: UTCNow(),
|
TimeLastRefresh: UTCNow().UnixNano(),
|
||||||
}
|
}
|
||||||
|
|
||||||
locker.ll.lockMap["name"] = []lockRequesterInfo{
|
locker.ll.lockMap["name"] = []lockRequesterInfo{
|
||||||
|
|||||||
@@ -111,17 +111,17 @@ func newLockHandler(h grid.HandlerID) *grid.SingleHandler[*dsync.LockArgs, *dsyn
|
|||||||
}
|
}
|
||||||
|
|
||||||
// registerLockRESTHandlers - register lock rest router.
|
// registerLockRESTHandlers - register lock rest router.
|
||||||
func registerLockRESTHandlers() {
|
func registerLockRESTHandlers(gm *grid.Manager) {
|
||||||
lockServer := &lockRESTServer{
|
lockServer := &lockRESTServer{
|
||||||
ll: newLocker(),
|
ll: newLocker(),
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.FatalIf(lockRPCForceUnlock.Register(globalGrid.Load(), lockServer.ForceUnlockHandler), "unable to register handler")
|
logger.FatalIf(lockRPCForceUnlock.Register(gm, lockServer.ForceUnlockHandler), "unable to register handler")
|
||||||
logger.FatalIf(lockRPCRefresh.Register(globalGrid.Load(), lockServer.RefreshHandler), "unable to register handler")
|
logger.FatalIf(lockRPCRefresh.Register(gm, lockServer.RefreshHandler), "unable to register handler")
|
||||||
logger.FatalIf(lockRPCLock.Register(globalGrid.Load(), lockServer.LockHandler), "unable to register handler")
|
logger.FatalIf(lockRPCLock.Register(gm, lockServer.LockHandler), "unable to register handler")
|
||||||
logger.FatalIf(lockRPCUnlock.Register(globalGrid.Load(), lockServer.UnlockHandler), "unable to register handler")
|
logger.FatalIf(lockRPCUnlock.Register(gm, lockServer.UnlockHandler), "unable to register handler")
|
||||||
logger.FatalIf(lockRPCRLock.Register(globalGrid.Load(), lockServer.RLockHandler), "unable to register handler")
|
logger.FatalIf(lockRPCRLock.Register(gm, lockServer.RLockHandler), "unable to register handler")
|
||||||
logger.FatalIf(lockRPCRUnlock.Register(globalGrid.Load(), lockServer.RUnlockHandler), "unable to register handler")
|
logger.FatalIf(lockRPCRUnlock.Register(gm, lockServer.RUnlockHandler), "unable to register handler")
|
||||||
|
|
||||||
globalLockServer = lockServer.ll
|
globalLockServer = lockServer.ll
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -20,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...)
|
||||||
@@ -180,10 +184,6 @@ func etcdLogOnceIf(ctx context.Context, err error, id string, errKind ...interfa
|
|||||||
logger.LogOnceIf(ctx, "etcd", err, id, errKind...)
|
logger.LogOnceIf(ctx, "etcd", err, id, errKind...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func subnetLogIf(ctx context.Context, err error, errKind ...interface{}) {
|
|
||||||
logger.LogIf(ctx, "subnet", err, errKind...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func metricsLogIf(ctx context.Context, err error, errKind ...interface{}) {
|
func metricsLogIf(ctx context.Context, err error, errKind ...interface{}) {
|
||||||
logger.LogIf(ctx, "metrics", err, errKind...)
|
logger.LogIf(ctx, "metrics", err, errKind...)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -107,6 +107,11 @@ func newApp(name string) *cli.App {
|
|||||||
|
|
||||||
// registerCommand registers a cli command.
|
// registerCommand registers a cli command.
|
||||||
registerCommand := func(command cli.Command) {
|
registerCommand := func(command cli.Command) {
|
||||||
|
// avoid registering commands which are not being built (via
|
||||||
|
// go:build tags)
|
||||||
|
if command.Name == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
commands = append(commands, command)
|
commands = append(commands, command)
|
||||||
commandsTree.Insert(command.Name)
|
commandsTree.Insert(command.Name)
|
||||||
}
|
}
|
||||||
@@ -134,6 +139,7 @@ func newApp(name string) *cli.App {
|
|||||||
|
|
||||||
// Register all commands.
|
// Register all commands.
|
||||||
registerCommand(serverCmd)
|
registerCommand(serverCmd)
|
||||||
|
registerCommand(fmtGenCmd)
|
||||||
|
|
||||||
// Set up app.
|
// Set up app.
|
||||||
cli.HelpFlag = cli.BoolFlag{
|
cli.HelpFlag = cli.BoolFlag{
|
||||||
@@ -189,12 +195,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++
|
||||||
@@ -536,6 +532,9 @@ func (m *metaCacheEntriesSorted) fileInfoVersions(bucket, prefix, delimiter, aft
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, version := range fiVersions {
|
for _, version := range fiVersions {
|
||||||
|
if !version.VersionPurgeStatus().Empty() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
versioned := vcfg != nil && vcfg.Versioned(entry.name)
|
versioned := vcfg != nil && vcfg.Versioned(entry.name)
|
||||||
versions = append(versions, version.ToObjectInfo(bucket, entry.name, versioned))
|
versions = append(versions, version.ToObjectInfo(bucket, entry.name, versioned))
|
||||||
}
|
}
|
||||||
@@ -597,7 +596,7 @@ func (m *metaCacheEntriesSorted) fileInfos(bucket, prefix, delimiter string) (ob
|
|||||||
}
|
}
|
||||||
|
|
||||||
fi, err := entry.fileInfo(bucket)
|
fi, err := entry.fileInfo(bucket)
|
||||||
if err == nil {
|
if err == nil && fi.VersionPurgeStatus().Empty() {
|
||||||
versioned := vcfg != nil && vcfg.Versioned(entry.name)
|
versioned := vcfg != nil && vcfg.Versioned(entry.name)
|
||||||
objects = append(objects, fi.ToObjectInfo(bucket, entry.name, versioned))
|
objects = append(objects, fi.ToObjectInfo(bucket, entry.name, versioned))
|
||||||
}
|
}
|
||||||
@@ -752,7 +751,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 +761,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
|
||||||
|
|||||||
@@ -153,19 +153,7 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
|
|||||||
} else {
|
} else {
|
||||||
// Continue listing
|
// Continue listing
|
||||||
o.ID = c.id
|
o.ID = c.id
|
||||||
go func(meta metacache) {
|
go c.keepAlive(ctx, rpc)
|
||||||
// Continuously update while we wait.
|
|
||||||
t := time.NewTicker(metacacheMaxClientWait / 10)
|
|
||||||
defer t.Stop()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
// Request is done, stop updating.
|
|
||||||
return
|
|
||||||
case <-t.C:
|
|
||||||
meta.lastHandout = time.Now()
|
|
||||||
meta, _ = rpc.UpdateMetacacheListing(ctx, meta)
|
|
||||||
}
|
|
||||||
}(*c)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,6 +207,9 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
|
|||||||
o.ID = ""
|
o.ID = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if contextCanceled(ctx) {
|
||||||
|
return entries, ctx.Err()
|
||||||
|
}
|
||||||
// Do listing in-place.
|
// Do listing in-place.
|
||||||
// Create output for our results.
|
// Create output for our results.
|
||||||
// Create filter for results.
|
// Create filter for results.
|
||||||
@@ -449,5 +440,10 @@ func (z *erasureServerPools) listAndSave(ctx context.Context, o *listPathOptions
|
|||||||
xioutil.SafeClose(saveCh)
|
xioutil.SafeClose(saveCh)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return filteredResults()
|
entries, err = filteredResults()
|
||||||
|
if err == nil {
|
||||||
|
// Check if listing recorded an error.
|
||||||
|
err = meta.getErr()
|
||||||
|
}
|
||||||
|
return entries, err
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user