mirror of
https://github.com/pgsty/minio.git
synced 2026-08-10 16:23:28 +03:00
Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 703f51164d | |||
| b8dde47d4e | |||
| 7fa3e39f85 | |||
| 4df7a3aa8f | |||
| 64a8f2e554 | |||
| f4fd4ea66d | |||
| 712fe1a8df | |||
| 4a319bedc9 | |||
| bdb3db6dad | |||
| abb385af41 | |||
| 4ee62606e4 | |||
| 079d64c801 | |||
| dcc000ae2c | |||
| c5d19ecebb | |||
| ed29a525b3 | |||
| 020c46cd3c | |||
| 827004cd6d | |||
| 779ec8f0d4 | |||
| 3d0f513ee2 | |||
| 4b6eadbd80 | |||
| 6f47414b23 | |||
| 224a27992a | |||
| 232544e1d8 | |||
| dbcb71828d | |||
| b9196757fd | |||
| b4ac53d157 | |||
| 4952bdb770 | |||
| 00b2ef2932 | |||
| 4536ecfaa4 | |||
| 43a7402968 | |||
| 330dca9a35 | |||
| ddd137d317 | |||
| 06ddd8770e | |||
| 16f8cf1c52 | |||
| 01e520eb23 | |||
| 02f770a0c0 | |||
| 2f4c79bc0f | |||
| 969ee7dfbe | |||
| 5f0b086b05 | |||
| 68b004a48f | |||
| 54ecce66f0 | |||
| 2b008c598b | |||
| 86d02b17cf | |||
| c1a95a70ac | |||
| f246c9053f | |||
| 9cdd204ae4 | |||
| 7b3eb9f7f8 | |||
| d56ef8dbe1 | |||
| a248ed5ff5 | |||
| 5bb31e4883 | |||
| aff2a76d80 | |||
| eddbe6bca2 | |||
| 734d1e320a | |||
| b8dab7b1a9 | |||
| abd6bf060d | |||
| f0d4ef604c | |||
| 2712f75762 | |||
| 4c46668da8 | |||
| 02e93fd6ba | |||
| 366876e98b | |||
| d202fdd022 | |||
| c07e5b49d4 | |||
| 9a39f8ad4d | |||
| 7e0c1c9413 | |||
| 485d833cd7 | |||
| e8a476ef5a | |||
| 267f0ecea2 | |||
| 4ee3434854 | |||
| 0e9854372e | |||
| b5177993b3 | |||
| 55f5c18fd9 | |||
| 8ce101c174 | |||
| 4972735507 | |||
| e6ca6de194 | |||
| cefc43e4da | |||
| 25e34fda5f | |||
| 4208d7af5a | |||
| 8d42f37e4b | |||
| 7cb4b5c636 | |||
| 1615920f48 | |||
| 7ee42b3ff5 | |||
| a6f1e727fb | |||
| c1fc7779ca | |||
| b3ab7546ee | |||
| ad88a81e3d | |||
| c4239ced22 | |||
| f85c28e960 | |||
| f7e176d4ca | |||
| 72a0d14195 | |||
| 6abe4128d7 | |||
| ed5ed7e490 | |||
| 51410c9023 | |||
| 96ca402dcd | |||
| 3da7c9cce3 | |||
| a14e19ec54 | |||
| e091dde041 | |||
| d10bb7e1b6 | |||
| 7ebceacac6 | |||
| 1593cb615d | |||
| 86a41d1631 | |||
| d4157b819c | |||
| e0aceca1b7 | |||
| 87804624fe | |||
| e029f8a9d7 | |||
| 1bc6681176 | |||
| 28322124e2 | |||
| cbfe9de3e7 | |||
| dc86b8d9d4 | |||
| ba70118e2b | |||
| cb1d3e50f7 | |||
| ded0b19d97 | |||
| d0bb3dd136 | |||
| ab7714b01e | |||
| e5b18df6db | |||
| 0abfd1bcb1 | |||
| 6186d11761 | |||
| e8b457e8a6 | |||
| afea40cc0f | |||
| 402b798f1b | |||
| 4759532e90 | |||
| 7f1e1713ab | |||
| b2c5819dbc | |||
| 2b0156b1fc | |||
| f6f0807c86 | |||
| 0c53d86017 | |||
| 6a6ee46d76 | |||
| 974cbb3bb7 |
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x]
|
||||
go-version: [1.23.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x]
|
||||
go-version: [1.23.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x]
|
||||
go-version: [1.23.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x]
|
||||
go-version: [1.23.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Resiliency Functional Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||
# updated.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.23.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
- name: Build on ${{ matrix.os }}
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GO111MODULE: on
|
||||
run: |
|
||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||
make test-resiliency
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x]
|
||||
go-version: [1.23.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -39,3 +39,4 @@ jobs:
|
||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||
make verify
|
||||
make test-timeout
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
# are turned off - i.e. if ldap="", then ldap server is not enabled for
|
||||
# the tests.
|
||||
matrix:
|
||||
go-version: [1.22.x]
|
||||
go-version: [1.23.x]
|
||||
ldap: ["", "localhost:389"]
|
||||
etcd: ["", "http://localhost:2379"]
|
||||
openid: ["", "http://127.0.0.1:5556/dex"]
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: setup-go-step
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22.x
|
||||
go-version: 1.23.x
|
||||
|
||||
- name: github sha short
|
||||
id: vars
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x]
|
||||
go-version: [1.23.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -40,6 +40,7 @@ jobs:
|
||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||
make test-ilm
|
||||
make test-ilm-transition
|
||||
|
||||
- name: Test PBAC
|
||||
run: |
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x]
|
||||
go-version: [1.23.x]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -15,6 +15,9 @@ docker volume rm $(docker volume ls -f dangling=true) || true
|
||||
## change working directory
|
||||
cd .github/workflows/mint
|
||||
|
||||
## always pull latest
|
||||
docker pull docker.io/minio/mint:edge
|
||||
|
||||
docker-compose -f minio-${MODE}.yaml up -d
|
||||
sleep 1m
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x]
|
||||
go-version: [1.23.x]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22.7
|
||||
go-version: 1.23.5
|
||||
- name: Get official govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
shell: bash
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.22-alpine as build
|
||||
FROM golang:1.23-alpine as build
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG RELEASE
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.22-alpine AS build
|
||||
FROM golang:1.23-alpine AS build
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG RELEASE
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
FROM golang:1.22-alpine AS build
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG RELEASE
|
||||
|
||||
ENV GOPATH=/go
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
# Install curl and minisign
|
||||
RUN apk add -U --no-cache ca-certificates && \
|
||||
apk add -U --no-cache curl && \
|
||||
go install aead.dev/minisign/cmd/minisign@v0.2.1
|
||||
|
||||
# Download minio binary and signature files
|
||||
RUN curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips -o /go/bin/minio && \
|
||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.minisig -o /go/bin/minio.minisig && \
|
||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.sha256sum -o /go/bin/minio.sha256sum && \
|
||||
chmod +x /go/bin/minio
|
||||
|
||||
# Download mc binary and signature files
|
||||
RUN curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.fips -o /go/bin/mc && \
|
||||
curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.fips.minisig -o /go/bin/mc.minisig && \
|
||||
curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.fips.sha256sum -o /go/bin/mc.sha256sum && \
|
||||
chmod +x /go/bin/mc
|
||||
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
curl -L -s -q https://github.com/moparisthebest/static-curl/releases/latest/download/curl-${TARGETARCH} -o /go/bin/curl; \
|
||||
chmod +x /go/bin/curl; \
|
||||
fi
|
||||
|
||||
# Verify binary signature using public key "RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGavRUN"
|
||||
RUN minisign -Vqm /go/bin/minio -x /go/bin/minio.minisig -P RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav
|
||||
|
||||
FROM registry.access.redhat.com/ubi9/ubi-micro:latest
|
||||
|
||||
ARG RELEASE
|
||||
|
||||
LABEL name="MinIO" \
|
||||
vendor="MinIO Inc <dev@min.io>" \
|
||||
maintainer="MinIO Inc <dev@min.io>" \
|
||||
version="${RELEASE}" \
|
||||
release="${RELEASE}" \
|
||||
summary="MinIO is a High Performance Object Storage, API compatible with Amazon S3 cloud storage service." \
|
||||
description="MinIO object storage is fundamentally different. Designed for performance and the S3 API, it is 100% open-source. MinIO is ideal for large, private cloud environments with stringent security requirements and delivers mission-critical availability across a diverse range of workloads."
|
||||
|
||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
MINIO_SECRET_KEY_FILE=secret_key \
|
||||
MINIO_ROOT_USER_FILE=access_key \
|
||||
MINIO_ROOT_PASSWORD_FILE=secret_key \
|
||||
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
|
||||
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
|
||||
MINIO_CONFIG_ENV_FILE=config.env
|
||||
|
||||
RUN chmod -R 777 /usr/bin
|
||||
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=build /go/bin/minio* /usr/bin/
|
||||
COPY --from=build /go/bin/mc* /usr/bin/
|
||||
COPY --from=build /go/bin/cur* /usr/bin/
|
||||
|
||||
COPY CREDITS /licenses/CREDITS
|
||||
COPY LICENSE /licenses/LICENSE
|
||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
|
||||
|
||||
EXPOSE 9000
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
CMD ["minio"]
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.22-alpine AS build
|
||||
FROM golang:1.23-alpine AS build
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG RELEASE
|
||||
|
||||
@@ -24,7 +24,7 @@ help: ## print this help
|
||||
getdeps: ## fetch necessary dependencies
|
||||
@mkdir -p ${GOPATH}/bin
|
||||
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOLANGCI_DIR)
|
||||
@echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.1.10-0.20240227114326-6d6f813fff1b
|
||||
@echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.2.5
|
||||
@echo "Installing stringer" && go install -v golang.org/x/tools/cmd/stringer@latest
|
||||
|
||||
crosscompile: ## cross compile minio
|
||||
@@ -60,6 +60,10 @@ test-ilm: install-race
|
||||
@echo "Running ILM tests"
|
||||
@env bash $(PWD)/docs/bucket/replication/setup_ilm_expiry_replication.sh
|
||||
|
||||
test-ilm-transition: install-race
|
||||
@echo "Running ILM tiering tests with healing"
|
||||
@env bash $(PWD)/docs/bucket/lifecycle/setup_ilm_transition.sh
|
||||
|
||||
test-pbac: install-race
|
||||
@echo "Running bucket policies tests"
|
||||
@env bash $(PWD)/docs/iam/policies/pbac-tests.sh
|
||||
@@ -145,6 +149,10 @@ test-multipart: install-race ## test multipart
|
||||
@echo "Test multipart behavior when part files are missing"
|
||||
@(env bash $(PWD)/buildscripts/multipart-quorum-test.sh)
|
||||
|
||||
test-timeout: install-race ## test multipart
|
||||
@echo "Test server timeout"
|
||||
@(env bash $(PWD)/buildscripts/test-timeout.sh)
|
||||
|
||||
verify: install-race ## verify minio various setups
|
||||
@echo "Verifying build with race"
|
||||
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||
@@ -209,6 +217,10 @@ docker: build ## builds minio docker container
|
||||
@echo "Building minio docker image '$(TAG)'"
|
||||
@docker build -q --no-cache -t $(TAG) . -f Dockerfile
|
||||
|
||||
test-resiliency: build
|
||||
@echo "Running resiliency tests"
|
||||
@(DOCKER_COMPOSE_FILE=$(PWD)/docs/resiliency/docker-compose.yaml env bash $(PWD)/docs/resiliency/resiliency-tests.sh)
|
||||
|
||||
install-race: checks build-debugging ## builds minio to $(PWD)
|
||||
@echo "Building minio binary with -race to './minio'"
|
||||
@GORACE=history_size=7 CGO_ENABLED=1 go build -tags kqueue,dev -race -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
[](https://min.io)
|
||||
|
||||
MinIO is a High Performance Object Storage released under GNU Affero General Public License v3.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads.
|
||||
MinIO is a High Performance Object Storage released under GNU Affero General Public License v3.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads. To learn more about what MinIO is doing for AI storage, go to [AI storage documentation](https://min.io/solutions/object-storage-for-ai).
|
||||
|
||||
This README provides quickstart instructions on running MinIO on bare metal hardware, including container-based installations. For Kubernetes environments, use the [MinIO Kubernetes Operator](https://github.com/minio/operator/blob/master/README.md).
|
||||
|
||||
@@ -93,7 +93,6 @@ The following table lists supported architectures. Replace the `wget` URL with t
|
||||
| 64-bit Intel/AMD | <https://dl.min.io/server/minio/release/linux-amd64/minio> |
|
||||
| 64-bit ARM | <https://dl.min.io/server/minio/release/linux-arm64/minio> |
|
||||
| 64-bit PowerPC LE (ppc64le) | <https://dl.min.io/server/minio/release/linux-ppc64le/minio> |
|
||||
| IBM Z-Series (S390X) | <https://dl.min.io/server/minio/release/linux-s390x/minio> |
|
||||
|
||||
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to <http://127.0.0.1:9000> and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#
|
||||
# This script assumes that LDAP server is at:
|
||||
#
|
||||
# `localhost:1389`
|
||||
# `localhost:389`
|
||||
#
|
||||
# if this is not the case, set the environment variable
|
||||
# `_MINIO_LDAP_TEST_SERVER`.
|
||||
@@ -41,7 +41,7 @@ __init__() {
|
||||
fi
|
||||
|
||||
if [ -z "$_MINIO_LDAP_TEST_SERVER" ]; then
|
||||
export _MINIO_LDAP_TEST_SERVER=localhost:1389
|
||||
export _MINIO_LDAP_TEST_SERVER=localhost:389
|
||||
echo "Using default LDAP endpoint: $_MINIO_LDAP_TEST_SERVER"
|
||||
fi
|
||||
|
||||
@@ -58,7 +58,7 @@ create_iam_content_in_old_minio() {
|
||||
mc alias set old-minio http://localhost:9000 minioadmin minioadmin
|
||||
mc ready old-minio
|
||||
mc idp ldap add old-minio \
|
||||
server_addr=localhost:1389 \
|
||||
server_addr=localhost:389 \
|
||||
server_insecure=on \
|
||||
lookup_bind_dn=cn=admin,dc=min,dc=io \
|
||||
lookup_bind_password=admin \
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -n "$TEST_DEBUG" ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
||||
MINIO=("$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server)
|
||||
|
||||
if [ ! -x "$PWD/minio" ]; then
|
||||
echo "minio executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$PWD/minio" ]; then
|
||||
echo "minio executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
trap 'catch $LINENO' ERR
|
||||
|
||||
function purge() {
|
||||
rm -rf "$1"
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2120
|
||||
catch() {
|
||||
if [ $# -ne 0 ]; then
|
||||
echo "error on line $1"
|
||||
fi
|
||||
|
||||
echo "Cleaning up instances of MinIO"
|
||||
pkill minio || true
|
||||
pkill -9 minio || true
|
||||
purge "$WORK_DIR"
|
||||
if [ $# -ne 0 ]; then
|
||||
exit $#
|
||||
fi
|
||||
}
|
||||
|
||||
catch
|
||||
|
||||
function gen_put_request() {
|
||||
hdr_sleep=$1
|
||||
body_sleep=$2
|
||||
|
||||
echo "PUT /testbucket/testobject HTTP/1.1"
|
||||
sleep $hdr_sleep
|
||||
echo "Host: foo-header"
|
||||
echo "User-Agent: curl/8.2.1"
|
||||
echo "Accept: */*"
|
||||
echo "Content-Length: 30"
|
||||
echo ""
|
||||
|
||||
sleep $body_sleep
|
||||
echo "random line 0"
|
||||
echo "random line 1"
|
||||
echo ""
|
||||
echo ""
|
||||
}
|
||||
|
||||
function send_put_object_request() {
|
||||
hdr_timeout=$1
|
||||
body_timeout=$2
|
||||
|
||||
start=$(date +%s)
|
||||
timeout 5m bash -c "gen_put_request $hdr_timeout $body_timeout | netcat 127.0.0.1 $start_port | read" || return -1
|
||||
[ $(($(date +%s) - start)) -gt $((srv_hdr_timeout + srv_idle_timeout + 1)) ] && return -1
|
||||
return 0
|
||||
}
|
||||
|
||||
function test_minio_with_timeout() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ROOT_USER=minio
|
||||
export MINIO_ROOT_PASSWORD=minio123
|
||||
export MC_HOST_minio="http://minio:minio123@127.0.0.1:${start_port}/"
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
mkdir ${WORK_DIR}
|
||||
C_PWD=${PWD}
|
||||
if [ ! -x "$PWD/mc" ]; then
|
||||
MC_BUILD_DIR="mc-$RANDOM"
|
||||
if ! git clone --quiet https://github.com/minio/mc "$MC_BUILD_DIR"; then
|
||||
echo "failed to download https://github.com/minio/mc"
|
||||
purge "${MC_BUILD_DIR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(cd "${MC_BUILD_DIR}" && go build -o "$C_PWD/mc")
|
||||
|
||||
# remove mc source.
|
||||
purge "${MC_BUILD_DIR}"
|
||||
fi
|
||||
|
||||
"${MINIO[@]}" --address ":$start_port" --read-header-timeout ${srv_hdr_timeout}s --idle-timeout ${srv_idle_timeout}s "${WORK_DIR}/disk/" >"${WORK_DIR}/server1.log" 2>&1 &
|
||||
pid=$!
|
||||
disown $pid
|
||||
sleep 1
|
||||
|
||||
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||
echo "server1 log:"
|
||||
cat "${WORK_DIR}/server1.log"
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
"${PWD}/mc" mb minio/testbucket
|
||||
"${PWD}/mc" anonymous set public minio/testbucket
|
||||
|
||||
# slow header writing
|
||||
send_put_object_request 20 0 && exit -1
|
||||
"${PWD}/mc" stat minio/testbucket/testobject && exit -1
|
||||
|
||||
# quick header write and slow bodywrite
|
||||
send_put_object_request 0 40 && exit -1
|
||||
"${PWD}/mc" stat minio/testbucket/testobject && exit -1
|
||||
|
||||
# quick header and body write
|
||||
send_put_object_request 1 1 || exit -1
|
||||
"${PWD}/mc" stat minio/testbucket/testobject || exit -1
|
||||
}
|
||||
|
||||
function main() {
|
||||
export start_port=$(shuf -i 10000-65000 -n 1)
|
||||
export srv_hdr_timeout=5
|
||||
export srv_idle_timeout=5
|
||||
export -f gen_put_request
|
||||
|
||||
test_minio_with_timeout ${start_port}
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -259,7 +259,7 @@ func (a adminAPIHandlers) RebalanceStart(w http.ResponseWriter, r *http.Request)
|
||||
if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
|
||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||
if proxyEp.Endpoint.Host == ep.Host {
|
||||
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
||||
if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -330,7 +330,7 @@ func (a adminAPIHandlers) RebalanceStatus(w http.ResponseWriter, r *http.Request
|
||||
if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
|
||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||
if proxyEp.Endpoint.Host == ep.Host {
|
||||
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
||||
if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -384,7 +384,7 @@ func proxyDecommissionRequest(ctx context.Context, defaultEndPoint Endpoint, w h
|
||||
}
|
||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||
if proxyEp.Endpoint.Host == host && !proxyEp.IsLocal {
|
||||
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
||||
if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
+42
-46
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config/dns"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
xldap "github.com/minio/pkg/v3/ldap"
|
||||
"github.com/minio/pkg/v3/policy"
|
||||
@@ -65,6 +66,17 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// This API only supports removal of internal users not service accounts.
|
||||
ok, _, err = globalIAMSys.IsServiceAccount(accessKey)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// When the user is root credential you are not allowed to
|
||||
// remove the root user. Also you cannot delete yourself.
|
||||
if accessKey == globalActiveCred.AccessKey || accessKey == cred.AccessKey {
|
||||
@@ -1244,6 +1256,7 @@ func (a adminAPIHandlers) ListAccessKeysBulk(w http.ResponseWriter, r *http.Requ
|
||||
for user := range users {
|
||||
checkedUserList = append(checkedUserList, user)
|
||||
}
|
||||
checkedUserList = append(checkedUserList, globalActiveCred.AccessKey)
|
||||
} else {
|
||||
for _, user := range users {
|
||||
// Validate the user
|
||||
@@ -1441,7 +1454,12 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
var buf []byte
|
||||
switch {
|
||||
case accountName == globalActiveCred.AccessKey:
|
||||
case accountName == globalActiveCred.AccessKey || newGlobalAuthZPluginFn() != nil:
|
||||
// For owner account and when plugin authZ is configured always set
|
||||
// effective policy as `consoleAdmin`.
|
||||
//
|
||||
// In the latter case, we let the UI render everything, but individual
|
||||
// actions would fail if not permitted by the external authZ service.
|
||||
for _, policy := range policy.DefaultPolicies {
|
||||
if policy.Name == "consoleAdmin" {
|
||||
effectivePolicy = policy.Definition
|
||||
@@ -1562,6 +1580,7 @@ func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Reques
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errTooManyPolicies), r.URL)
|
||||
return
|
||||
}
|
||||
setReqInfoPolicyName(ctx, name)
|
||||
|
||||
policyDoc, err := globalIAMSys.InfoPolicy(name)
|
||||
if err != nil {
|
||||
@@ -1665,6 +1684,7 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
vars := mux.Vars(r)
|
||||
policyName := vars["name"]
|
||||
setReqInfoPolicyName(ctx, policyName)
|
||||
|
||||
if err := globalIAMSys.DeletePolicy(ctx, policyName, true); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
@@ -1697,6 +1717,13 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
setReqInfoPolicyName(ctx, policyName)
|
||||
|
||||
// Reject policy names with commas.
|
||||
if strings.Contains(policyName, ",") {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyInvalidName), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Error out if Content-Length is missing.
|
||||
if r.ContentLength <= 0 {
|
||||
@@ -1762,6 +1789,7 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
|
||||
policyName := vars["policyName"]
|
||||
entityName := vars["userOrGroup"]
|
||||
isGroup := vars["isGroup"] == "true"
|
||||
setReqInfoPolicyName(ctx, policyName)
|
||||
|
||||
if !isGroup {
|
||||
ok, _, err := globalIAMSys.IsTempUser(entityName)
|
||||
@@ -1847,7 +1875,7 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
|
||||
}))
|
||||
}
|
||||
|
||||
// ListPolicyMappingEntities - GET /minio/admin/v3/idp/builtin/polciy-entities?policy=xxx&user=xxx&group=xxx
|
||||
// ListPolicyMappingEntities - GET /minio/admin/v3/idp/builtin/policy-entities?policy=xxx&user=xxx&group=xxx
|
||||
func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
@@ -1949,6 +1977,7 @@ func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *ht
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
setReqInfoPolicyName(ctx, strings.Join(addedOrRemoved, ","))
|
||||
|
||||
respBody := madmin.PolicyAssociationResp{
|
||||
UpdatedAt: updatedAt,
|
||||
@@ -2003,6 +2032,7 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
// Get current object layer instance.
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ExportIAMAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
// Initialize a zip writer which will provide a zipped content
|
||||
@@ -2225,17 +2255,13 @@ func (a adminAPIHandlers) ImportIAMV2(w http.ResponseWriter, r *http.Request) {
|
||||
func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiVer string) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
// Validate signature, permissions and get current object layer instance.
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ImportIAMAction)
|
||||
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
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
@@ -2325,38 +2351,12 @@ func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiV
|
||||
return
|
||||
}
|
||||
|
||||
if (cred.IsTemp() || cred.IsServiceAccount()) && cred.ParentUser == accessKey {
|
||||
// Incoming access key matches parent user then we should
|
||||
// reject password change requests.
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if accessKey has beginning and end space characters, this only applies to new users.
|
||||
if !exists && hasSpaceBE(accessKey) {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
checkDenyOnly := false
|
||||
if accessKey == cred.AccessKey {
|
||||
// Check that there is no explicit deny - otherwise it's allowed
|
||||
// to change one's own password.
|
||||
checkDenyOnly = true
|
||||
}
|
||||
|
||||
if !globalIAMSys.IsAllowed(policy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
Action: policy.CreateUserAdminAction,
|
||||
ConditionValues: getConditionValues(r, "", cred),
|
||||
IsOwner: owner,
|
||||
Claims: cred.Claims,
|
||||
DenyOnly: checkDenyOnly,
|
||||
}) {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allUsersFile, accessKey), r.URL)
|
||||
return
|
||||
}
|
||||
if _, err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil {
|
||||
failed.Users = append(failed.Users, madmin.IAMErrEntity{Name: accessKey, Error: err})
|
||||
} else {
|
||||
@@ -2456,17 +2456,6 @@ func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiV
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
if !globalIAMSys.IsAllowed(policy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
Action: policy.CreateServiceAccountAdminAction,
|
||||
ConditionValues: getConditionValues(r, "", cred),
|
||||
IsOwner: owner,
|
||||
Claims: cred.Claims,
|
||||
}) {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allSvcAcctsFile, user), r.URL)
|
||||
return
|
||||
}
|
||||
updateReq := true
|
||||
_, _, err = globalIAMSys.GetServiceAccount(ctx, svcAcctReq.AccessKey)
|
||||
if err != nil {
|
||||
@@ -2795,3 +2784,10 @@ func commonAddServiceAccount(r *http.Request, ldap bool) (context.Context, auth.
|
||||
|
||||
return ctx, cred, opts, createReq, targetUser, APIError{}
|
||||
}
|
||||
|
||||
// setReqInfoPolicyName will set the given policyName as a tag on the context's request info,
|
||||
// so that it appears in audit logs.
|
||||
func setReqInfoPolicyName(ctx context.Context, policyName string) {
|
||||
reqInfo := logger.GetReqInfo(ctx)
|
||||
reqInfo.SetTags("policyName", policyName)
|
||||
}
|
||||
|
||||
@@ -716,6 +716,12 @@ func (s *TestSuiteIAM) TestCannedPolicies(c *check) {
|
||||
c.Fatalf("policy info err: %v", err)
|
||||
}
|
||||
|
||||
// Check that policy with comma is rejected.
|
||||
err = s.adm.AddCannedPolicy(ctx, "invalid,policy", policyBytes)
|
||||
if err == nil {
|
||||
c.Fatalf("invalid policy created successfully")
|
||||
}
|
||||
|
||||
infoStr := string(info)
|
||||
if !strings.Contains(infoStr, `"s3:PutObject"`) || !strings.Contains(infoStr, ":"+bucket+"/") {
|
||||
c.Fatalf("policy contains unexpected content!")
|
||||
|
||||
+61
-33
@@ -93,11 +93,18 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
||||
return
|
||||
}
|
||||
|
||||
if globalInplaceUpdateDisabled || currentReleaseTime.IsZero() {
|
||||
if globalInplaceUpdateDisabled {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if currentReleaseTime.IsZero() || currentReleaseTime.Equal(timeSentinel) {
|
||||
apiErr := errorCodes.ToAPIErr(ErrMethodNotAllowed)
|
||||
apiErr.Description = fmt.Sprintf("unable to perform in-place update, release time is unrecognized: %s", currentReleaseTime)
|
||||
writeErrorResponseJSON(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
updateURL := vars["updateURL"]
|
||||
dryRun := r.Form.Get("dry-run") == "true"
|
||||
@@ -110,6 +117,11 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
}
|
||||
|
||||
local := globalLocalNodeName
|
||||
if local == "" {
|
||||
local = "127.0.0.1"
|
||||
}
|
||||
|
||||
u, err := url.Parse(updateURL)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
@@ -128,6 +140,39 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
||||
return
|
||||
}
|
||||
|
||||
updateStatus := madmin.ServerUpdateStatusV2{
|
||||
DryRun: dryRun,
|
||||
Results: make([]madmin.ServerPeerUpdateStatus, 0, len(globalNotificationSys.allPeerClients)),
|
||||
}
|
||||
peerResults := make(map[string]madmin.ServerPeerUpdateStatus, len(globalNotificationSys.allPeerClients))
|
||||
failedClients := make(map[int]bool, len(globalNotificationSys.allPeerClients))
|
||||
|
||||
if lrTime.Sub(currentReleaseTime) <= 0 {
|
||||
updateStatus.Results = append(updateStatus.Results, madmin.ServerPeerUpdateStatus{
|
||||
Host: local,
|
||||
Err: fmt.Sprintf("server is running the latest version: %s", Version),
|
||||
CurrentVersion: Version,
|
||||
})
|
||||
|
||||
for _, client := range globalNotificationSys.peerClients {
|
||||
updateStatus.Results = append(updateStatus.Results, madmin.ServerPeerUpdateStatus{
|
||||
Host: client.String(),
|
||||
Err: fmt.Sprintf("server is running the latest version: %s", Version),
|
||||
CurrentVersion: Version,
|
||||
})
|
||||
}
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(updateStatus)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
return
|
||||
}
|
||||
|
||||
u.Path = path.Dir(u.Path) + SlashSeparator + releaseInfo
|
||||
// Download Binary Once
|
||||
binC, bin, err := downloadBinary(u, mode)
|
||||
@@ -137,16 +182,6 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
||||
return
|
||||
}
|
||||
|
||||
updateStatus := madmin.ServerUpdateStatusV2{DryRun: dryRun}
|
||||
peerResults := make(map[string]madmin.ServerPeerUpdateStatus)
|
||||
|
||||
local := globalLocalNodeName
|
||||
if local == "" {
|
||||
local = "127.0.0.1"
|
||||
}
|
||||
|
||||
failedClients := make(map[int]struct{})
|
||||
|
||||
if globalIsDistErasure {
|
||||
// Push binary to other servers
|
||||
for idx, nerr := range globalNotificationSys.VerifyBinary(ctx, u, sha256Sum, releaseInfo, binC) {
|
||||
@@ -156,7 +191,7 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
||||
Err: nerr.Err.Error(),
|
||||
CurrentVersion: Version,
|
||||
}
|
||||
failedClients[idx] = struct{}{}
|
||||
failedClients[idx] = true
|
||||
} else {
|
||||
peerResults[nerr.Host.String()] = madmin.ServerPeerUpdateStatus{
|
||||
Host: nerr.Host.String(),
|
||||
@@ -167,25 +202,17 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
}
|
||||
|
||||
if lrTime.Sub(currentReleaseTime) > 0 {
|
||||
if err = verifyBinary(u, sha256Sum, releaseInfo, mode, bytes.NewReader(bin)); err != nil {
|
||||
peerResults[local] = madmin.ServerPeerUpdateStatus{
|
||||
Host: local,
|
||||
Err: err.Error(),
|
||||
CurrentVersion: Version,
|
||||
}
|
||||
} else {
|
||||
peerResults[local] = madmin.ServerPeerUpdateStatus{
|
||||
Host: local,
|
||||
CurrentVersion: Version,
|
||||
UpdatedVersion: lrTime.Format(MinioReleaseTagTimeLayout),
|
||||
}
|
||||
if err = verifyBinary(u, sha256Sum, releaseInfo, mode, bytes.NewReader(bin)); err != nil {
|
||||
peerResults[local] = madmin.ServerPeerUpdateStatus{
|
||||
Host: local,
|
||||
Err: err.Error(),
|
||||
CurrentVersion: Version,
|
||||
}
|
||||
} else {
|
||||
peerResults[local] = madmin.ServerPeerUpdateStatus{
|
||||
Host: local,
|
||||
Err: fmt.Sprintf("server is already running the latest version: %s", Version),
|
||||
CurrentVersion: Version,
|
||||
UpdatedVersion: lrTime.Format(MinioReleaseTagTimeLayout),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,8 +220,7 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
||||
if globalIsDistErasure {
|
||||
ng := WithNPeers(len(globalNotificationSys.peerClients))
|
||||
for idx, client := range globalNotificationSys.peerClients {
|
||||
_, ok := failedClients[idx]
|
||||
if ok {
|
||||
if failedClients[idx] {
|
||||
continue
|
||||
}
|
||||
client := client
|
||||
@@ -240,14 +266,14 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
||||
startTime := time.Now().Add(restartUpdateDelay)
|
||||
ng := WithNPeers(len(globalNotificationSys.peerClients))
|
||||
for idx, client := range globalNotificationSys.peerClients {
|
||||
_, ok := failedClients[idx]
|
||||
if ok {
|
||||
if failedClients[idx] {
|
||||
continue
|
||||
}
|
||||
client := client
|
||||
ng.Go(ctx, func() error {
|
||||
prs, ok := peerResults[client.String()]
|
||||
if ok && prs.CurrentVersion != prs.UpdatedVersion && prs.UpdatedVersion != "" {
|
||||
// We restart only on success, not for any failures.
|
||||
if ok && prs.Err == "" {
|
||||
return client.SignalService(serviceRestart, "", dryRun, &startTime)
|
||||
}
|
||||
return nil
|
||||
@@ -284,7 +310,9 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
|
||||
if !dryRun {
|
||||
if lrTime.Sub(currentReleaseTime) > 0 {
|
||||
prs, ok := peerResults[local]
|
||||
// We restart only on success, not for any failures.
|
||||
if ok && prs.Err == "" {
|
||||
globalServiceSignalCh <- serviceRestart
|
||||
}
|
||||
}
|
||||
@@ -1292,7 +1320,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Analyze the heal token and route the request accordingly
|
||||
token, success := proxyRequestByToken(ctx, w, r, hip.clientToken)
|
||||
token, _, success := proxyRequestByToken(ctx, w, r, hip.clientToken, false)
|
||||
if success {
|
||||
return
|
||||
}
|
||||
|
||||
+11
-19
@@ -806,18 +806,20 @@ func (h *healSequence) healDiskMeta(objAPI ObjectLayer) error {
|
||||
return h.healMinioSysMeta(objAPI, minioConfigPrefix)()
|
||||
}
|
||||
|
||||
func (h *healSequence) healItems(objAPI ObjectLayer, bucketsOnly bool) error {
|
||||
func (h *healSequence) healItems(objAPI ObjectLayer) error {
|
||||
if h.clientToken == bgHealingUUID {
|
||||
// For background heal do nothing.
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := h.healDiskMeta(objAPI); err != nil {
|
||||
return err
|
||||
if h.bucket == "" { // heal internal meta only during a site-wide heal
|
||||
if err := h.healDiskMeta(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Heal buckets and objects
|
||||
return h.healBuckets(objAPI, bucketsOnly)
|
||||
return h.healBuckets(objAPI)
|
||||
}
|
||||
|
||||
// traverseAndHeal - traverses on-disk data and performs healing
|
||||
@@ -828,8 +830,7 @@ func (h *healSequence) healItems(objAPI ObjectLayer, bucketsOnly bool) error {
|
||||
// has to wait until a safe point is reached, such as between scanning
|
||||
// two objects.
|
||||
func (h *healSequence) traverseAndHeal(objAPI ObjectLayer) {
|
||||
bucketsOnly := false // Heals buckets and objects also.
|
||||
h.traverseAndHealDoneCh <- h.healItems(objAPI, bucketsOnly)
|
||||
h.traverseAndHealDoneCh <- h.healItems(objAPI)
|
||||
xioutil.SafeClose(h.traverseAndHealDoneCh)
|
||||
}
|
||||
|
||||
@@ -840,6 +841,7 @@ func (h *healSequence) healMinioSysMeta(objAPI ObjectLayer, metaPrefix string) f
|
||||
// NOTE: Healing on meta is run regardless
|
||||
// of any bucket being selected, this is to ensure that
|
||||
// meta are always upto date and correct.
|
||||
h.settings.Recursive = true
|
||||
return objAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, h.settings, func(bucket, object, versionID string, scanMode madmin.HealScanMode) error {
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
@@ -856,14 +858,14 @@ func (h *healSequence) healMinioSysMeta(objAPI ObjectLayer, metaPrefix string) f
|
||||
}
|
||||
|
||||
// healBuckets - check for all buckets heal or just particular bucket.
|
||||
func (h *healSequence) healBuckets(objAPI ObjectLayer, bucketsOnly bool) error {
|
||||
func (h *healSequence) healBuckets(objAPI ObjectLayer) error {
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
|
||||
// 1. If a bucket was specified, heal only the bucket.
|
||||
if h.bucket != "" {
|
||||
return h.healBucket(objAPI, h.bucket, bucketsOnly)
|
||||
return h.healBucket(objAPI, h.bucket, false)
|
||||
}
|
||||
|
||||
buckets, err := objAPI.ListBuckets(h.ctx, BucketOptions{})
|
||||
@@ -877,7 +879,7 @@ func (h *healSequence) healBuckets(objAPI ObjectLayer, bucketsOnly bool) error {
|
||||
})
|
||||
|
||||
for _, bucket := range buckets {
|
||||
if err = h.healBucket(objAPI, bucket.Name, bucketsOnly); err != nil {
|
||||
if err = h.healBucket(objAPI, bucket.Name, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -895,16 +897,6 @@ func (h *healSequence) healBucket(objAPI ObjectLayer, bucket string, bucketsOnly
|
||||
return nil
|
||||
}
|
||||
|
||||
if !h.settings.Recursive {
|
||||
if h.object != "" {
|
||||
if err := h.healObject(bucket, h.object, "", h.settings.ScanMode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := objAPI.HealObjects(h.ctx, bucket, h.object, h.settings, h.healObject); err != nil {
|
||||
return errFnHealFromAPIErr(h.ctx, err)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ type DeletedObject struct {
|
||||
DeleteMarkerMTime DeleteMarkerMTime `xml:"-"`
|
||||
// MinIO extensions to support delete marker replication
|
||||
ReplicationState ReplicationState `xml:"-"`
|
||||
|
||||
found bool // the object was found during deletion
|
||||
}
|
||||
|
||||
// DeleteMarkerMTime is an embedded type containing time.Time for XML marshal
|
||||
|
||||
+8
-2
@@ -213,6 +213,7 @@ const (
|
||||
ErrPolicyAlreadyAttached
|
||||
ErrPolicyNotAttached
|
||||
ErrExcessData
|
||||
ErrPolicyInvalidName
|
||||
// Add new error codes here.
|
||||
|
||||
// SSE-S3/SSE-KMS related API errors
|
||||
@@ -561,6 +562,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "More data provided than indicated content length",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrPolicyInvalidName: {
|
||||
Code: "PolicyInvalidName",
|
||||
Description: "Policy name may not contain comma",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrPolicyTooLarge: {
|
||||
Code: "PolicyTooLarge",
|
||||
Description: "Policy exceeds the maximum allowed document size.",
|
||||
@@ -623,7 +629,7 @@ var errorCodes = errorCodeMap{
|
||||
},
|
||||
ErrMissingContentMD5: {
|
||||
Code: "MissingContentMD5",
|
||||
Description: "Missing required header for this request: Content-Md5.",
|
||||
Description: "Missing or invalid required header for this request: Content-Md5 or Amz-Content-Checksum",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingSecurityHeader: {
|
||||
@@ -978,7 +984,7 @@ var errorCodes = errorCodeMap{
|
||||
},
|
||||
ErrReplicationNoExistingObjects: {
|
||||
Code: "XMinioReplicationNoExistingObjects",
|
||||
Description: "No matching ExistingsObjects rule enabled",
|
||||
Description: "No matching ExistingObjects rule enabled",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrRemoteTargetDenyAddError: {
|
||||
|
||||
+21
-17
@@ -166,10 +166,11 @@ type Part struct {
|
||||
Size int64
|
||||
|
||||
// Checksum values
|
||||
ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"`
|
||||
ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"`
|
||||
ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"`
|
||||
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
|
||||
ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"`
|
||||
ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"`
|
||||
ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"`
|
||||
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
|
||||
ChecksumCRC64NVME string `xml:",omitempty"`
|
||||
}
|
||||
|
||||
// ListPartsResponse - format for list parts response.
|
||||
@@ -192,6 +193,8 @@ type ListPartsResponse struct {
|
||||
IsTruncated bool
|
||||
|
||||
ChecksumAlgorithm string
|
||||
ChecksumType string
|
||||
|
||||
// List of parts.
|
||||
Parts []Part `xml:"Part"`
|
||||
}
|
||||
@@ -413,10 +416,11 @@ type CompleteMultipartUploadResponse struct {
|
||||
Key string
|
||||
ETag string
|
||||
|
||||
ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"`
|
||||
ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"`
|
||||
ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"`
|
||||
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
|
||||
ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"`
|
||||
ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"`
|
||||
ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"`
|
||||
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
|
||||
ChecksumCRC64NVME string `xml:",omitempty"`
|
||||
}
|
||||
|
||||
// DeleteError structure.
|
||||
@@ -593,8 +597,6 @@ func generateListVersionsResponse(ctx context.Context, bucket, prefix, marker, v
|
||||
for k, v := range cleanReservedKeys(object.UserDefined) {
|
||||
content.UserMetadata.Set(k, v)
|
||||
}
|
||||
|
||||
content.UserMetadata.Set("expires", object.Expires.Format(http.TimeFormat))
|
||||
content.Internal = &ObjectInternalInfo{
|
||||
K: object.DataBlocks,
|
||||
M: object.ParityBlocks,
|
||||
@@ -729,7 +731,6 @@ func generateListObjectsV2Response(ctx context.Context, bucket, prefix, token, n
|
||||
for k, v := range cleanReservedKeys(object.UserDefined) {
|
||||
content.UserMetadata.Set(k, v)
|
||||
}
|
||||
content.UserMetadata.Set("expires", object.Expires.Format(http.TimeFormat))
|
||||
content.Internal = &ObjectInternalInfo{
|
||||
K: object.DataBlocks,
|
||||
M: object.ParityBlocks,
|
||||
@@ -790,17 +791,18 @@ func generateInitiateMultipartUploadResponse(bucket, key, uploadID string) Initi
|
||||
|
||||
// generates CompleteMultipartUploadResponse for given bucket, key, location and ETag.
|
||||
func generateCompleteMultipartUploadResponse(bucket, key, location string, oi ObjectInfo, h http.Header) CompleteMultipartUploadResponse {
|
||||
cs := oi.decryptChecksums(0, h)
|
||||
cs, _ := oi.decryptChecksums(0, h)
|
||||
c := CompleteMultipartUploadResponse{
|
||||
Location: location,
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
// AWS S3 quotes the ETag in XML, make sure we are compatible here.
|
||||
ETag: "\"" + oi.ETag + "\"",
|
||||
ChecksumSHA1: cs[hash.ChecksumSHA1.String()],
|
||||
ChecksumSHA256: cs[hash.ChecksumSHA256.String()],
|
||||
ChecksumCRC32: cs[hash.ChecksumCRC32.String()],
|
||||
ChecksumCRC32C: cs[hash.ChecksumCRC32C.String()],
|
||||
ETag: "\"" + oi.ETag + "\"",
|
||||
ChecksumSHA1: cs[hash.ChecksumSHA1.String()],
|
||||
ChecksumSHA256: cs[hash.ChecksumSHA256.String()],
|
||||
ChecksumCRC32: cs[hash.ChecksumCRC32.String()],
|
||||
ChecksumCRC32C: cs[hash.ChecksumCRC32C.String()],
|
||||
ChecksumCRC64NVME: cs[hash.ChecksumCRC64NVME.String()],
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -828,6 +830,7 @@ func generateListPartsResponse(partsInfo ListPartsInfo, encodingType string) Lis
|
||||
listPartsResponse.IsTruncated = partsInfo.IsTruncated
|
||||
listPartsResponse.NextPartNumberMarker = partsInfo.NextPartNumberMarker
|
||||
listPartsResponse.ChecksumAlgorithm = partsInfo.ChecksumAlgorithm
|
||||
listPartsResponse.ChecksumType = partsInfo.ChecksumType
|
||||
|
||||
listPartsResponse.Parts = make([]Part, len(partsInfo.Parts))
|
||||
for index, part := range partsInfo.Parts {
|
||||
@@ -840,6 +843,7 @@ func generateListPartsResponse(partsInfo ListPartsInfo, encodingType string) Lis
|
||||
newPart.ChecksumCRC32C = part.ChecksumCRC32C
|
||||
newPart.ChecksumSHA1 = part.ChecksumSHA1
|
||||
newPart.ChecksumSHA256 = part.ChecksumSHA256
|
||||
newPart.ChecksumCRC64NVME = part.ChecksumCRC64NVME
|
||||
listPartsResponse.Parts[index] = newPart
|
||||
}
|
||||
return listPartsResponse
|
||||
|
||||
+200
-199
File diff suppressed because one or more lines are too long
@@ -223,9 +223,6 @@ func (h *healingTracker) updateProgress(success, skipped bool, bytes uint64) {
|
||||
// update will update the tracker on the disk.
|
||||
// If the tracker has been deleted an error is returned.
|
||||
func (h *healingTracker) update(ctx context.Context) error {
|
||||
if h.disk.Healing() == nil {
|
||||
return fmt.Errorf("healingTracker: drive %q is not marked as healing", h.ID)
|
||||
}
|
||||
h.mu.Lock()
|
||||
if h.ID == "" || h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
|
||||
h.ID, _ = h.disk.GetDiskID()
|
||||
|
||||
+13
-10
@@ -289,6 +289,16 @@ type BatchJobExpire struct {
|
||||
|
||||
var _ yaml.Unmarshaler = &BatchJobExpire{}
|
||||
|
||||
// RedactSensitive will redact any sensitive information in b.
|
||||
func (r *BatchJobExpire) RedactSensitive() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if r.NotificationCfg.Token != "" {
|
||||
r.NotificationCfg.Token = redactedText
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalYAML - BatchJobExpire extends default unmarshal to extract line, col information.
|
||||
func (r *BatchJobExpire) UnmarshalYAML(val *yaml.Node) error {
|
||||
type expireJob BatchJobExpire
|
||||
@@ -435,14 +445,14 @@ func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo
|
||||
oiCache.Add(od, &exp.ObjectInfo)
|
||||
}
|
||||
|
||||
var done bool
|
||||
// DeleteObject(deletePrefix: true) to expire all versions of an object
|
||||
for _, exp := range toExpireAll {
|
||||
var success bool
|
||||
for attempts := 1; attempts <= retryAttempts; attempts++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
done = true
|
||||
ri.trackMultipleObjectVersions(exp, success)
|
||||
return
|
||||
default:
|
||||
}
|
||||
stopFn := globalBatchJobsMetrics.trace(batchJobMetricExpire, ri.JobID, attempts)
|
||||
@@ -459,14 +469,7 @@ func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo
|
||||
break
|
||||
}
|
||||
}
|
||||
ri.trackMultipleObjectVersions(r.Bucket, exp, success)
|
||||
if done {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if done {
|
||||
return
|
||||
ri.trackMultipleObjectVersions(exp, success)
|
||||
}
|
||||
|
||||
// DeleteMultiple objects
|
||||
|
||||
+76
-20
@@ -61,6 +61,8 @@ var globalBatchConfig batch.Config
|
||||
const (
|
||||
// Keep the completed/failed job stats 3 days before removing it
|
||||
oldJobsExpiration = 3 * 24 * time.Hour
|
||||
|
||||
redactedText = "**REDACTED**"
|
||||
)
|
||||
|
||||
// BatchJobRequest this is an internal data structure not for external consumption.
|
||||
@@ -74,6 +76,29 @@ type BatchJobRequest struct {
|
||||
ctx context.Context `msg:"-"`
|
||||
}
|
||||
|
||||
// RedactSensitive will redact any sensitive information in b.
|
||||
func (j *BatchJobRequest) RedactSensitive() {
|
||||
j.Replicate.RedactSensitive()
|
||||
j.Expire.RedactSensitive()
|
||||
j.KeyRotate.RedactSensitive()
|
||||
}
|
||||
|
||||
// RedactSensitive will redact any sensitive information in b.
|
||||
func (r *BatchJobReplicateV1) RedactSensitive() {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if r.Target.Creds.SecretKey != "" {
|
||||
r.Target.Creds.SecretKey = redactedText
|
||||
}
|
||||
if r.Target.Creds.SessionToken != "" {
|
||||
r.Target.Creds.SessionToken = redactedText
|
||||
}
|
||||
}
|
||||
|
||||
// RedactSensitive will redact any sensitive information in b.
|
||||
func (r *BatchJobKeyRotateV1) RedactSensitive() {}
|
||||
|
||||
func notifyEndpoint(ctx context.Context, ri *batchJobInfo, endpoint, token string) error {
|
||||
if endpoint == "" {
|
||||
return nil
|
||||
@@ -301,12 +326,12 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
||||
return true
|
||||
}
|
||||
|
||||
if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.Before(oi.ModTime) {
|
||||
if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.After(oi.ModTime) {
|
||||
// skip all objects that are created before the specified time.
|
||||
return true
|
||||
}
|
||||
|
||||
if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.After(oi.ModTime) {
|
||||
if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.Before(oi.ModTime) {
|
||||
// skip all objects that are created after the specified time.
|
||||
return true
|
||||
}
|
||||
@@ -597,12 +622,12 @@ func (r BatchJobReplicateV1) writeAsArchive(ctx context.Context, objAPI ObjectLa
|
||||
},
|
||||
}
|
||||
|
||||
opts, err := batchReplicationOpts(ctx, "", gr.ObjInfo)
|
||||
opts, _, err := batchReplicationOpts(ctx, "", gr.ObjInfo)
|
||||
if err != nil {
|
||||
batchLogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: I am not sure we read it back, but we aren't sending whether checksums are single/multipart.
|
||||
for k, vals := range opts.Header() {
|
||||
for _, v := range vals {
|
||||
snowballObj.Headers.Add(k, v)
|
||||
@@ -687,14 +712,14 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
|
||||
return err
|
||||
}
|
||||
|
||||
putOpts, err := batchReplicationOpts(ctx, "", objInfo)
|
||||
putOpts, isMP, err := batchReplicationOpts(ctx, "", objInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
|
||||
putOpts.Internal = miniogo.AdvancedPutOptions{}
|
||||
}
|
||||
if objInfo.isMultipart() {
|
||||
if isMP {
|
||||
if err := replicateObjectWithMultipart(ctx, c, tgtBucket, pathJoin(tgtPrefix, objInfo.Name), rd, objInfo, putOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -971,7 +996,7 @@ func (ri *batchJobInfo) updateAfter(ctx context.Context, api ObjectLayer, durati
|
||||
// Note: to be used only with batch jobs that affect multiple versions through
|
||||
// a single action. e.g batch-expire has an option to expire all versions of an
|
||||
// object which matches the given filters.
|
||||
func (ri *batchJobInfo) trackMultipleObjectVersions(bucket string, info ObjectInfo, success bool) {
|
||||
func (ri *batchJobInfo) trackMultipleObjectVersions(info ObjectInfo, success bool) {
|
||||
if success {
|
||||
ri.Objects += int64(info.NumVersions)
|
||||
} else {
|
||||
@@ -1047,12 +1072,12 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
||||
return false
|
||||
}
|
||||
|
||||
if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.Before(info.ModTime) {
|
||||
if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.After(info.ModTime) {
|
||||
// skip all objects that are created before the specified time.
|
||||
return false
|
||||
}
|
||||
|
||||
if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.After(info.ModTime) {
|
||||
if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.Before(info.ModTime) {
|
||||
// skip all objects that are created after the specified time.
|
||||
return false
|
||||
}
|
||||
@@ -1551,11 +1576,11 @@ func (j *BatchJobRequest) load(ctx context.Context, api ObjectLayer, name string
|
||||
return err
|
||||
}
|
||||
|
||||
func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions, err error) {
|
||||
func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions, isMP bool, err error) {
|
||||
// TODO: support custom storage class for remote replication
|
||||
putOpts, err = putReplicationOpts(ctx, "", objInfo, 0)
|
||||
putOpts, isMP, err = putReplicationOpts(ctx, "", objInfo)
|
||||
if err != nil {
|
||||
return putOpts, err
|
||||
return putOpts, isMP, err
|
||||
}
|
||||
putOpts.Internal = miniogo.AdvancedPutOptions{
|
||||
SourceVersionID: objInfo.VersionID,
|
||||
@@ -1563,7 +1588,7 @@ func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (p
|
||||
SourceETag: objInfo.ETag,
|
||||
ReplicationRequest: true,
|
||||
}
|
||||
return putOpts, nil
|
||||
return putOpts, isMP, nil
|
||||
}
|
||||
|
||||
// ListBatchJobs - lists all currently active batch jobs, optionally takes {jobType}
|
||||
@@ -1577,9 +1602,6 @@ func (a adminAPIHandlers) ListBatchJobs(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
jobType := r.Form.Get("jobType")
|
||||
if jobType == "" {
|
||||
jobType = string(madmin.BatchJobReplicate)
|
||||
}
|
||||
|
||||
resultCh := make(chan itemOrErr[ObjectInfo])
|
||||
|
||||
@@ -1608,7 +1630,7 @@ func (a adminAPIHandlers) ListBatchJobs(w http.ResponseWriter, r *http.Request)
|
||||
continue
|
||||
}
|
||||
|
||||
if jobType == string(req.Type()) {
|
||||
if jobType == string(req.Type()) || jobType == "" {
|
||||
listResult.Jobs = append(listResult.Jobs, madmin.BatchJobResult{
|
||||
ID: req.ID,
|
||||
Type: req.Type(),
|
||||
@@ -1698,6 +1720,8 @@ func (a adminAPIHandlers) DescribeBatchJob(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
// Remove sensitive fields.
|
||||
req.RedactSensitive()
|
||||
buf, err := yaml.Marshal(req)
|
||||
if err != nil {
|
||||
batchLogIf(ctx, err)
|
||||
@@ -1805,7 +1829,7 @@ func (a adminAPIHandlers) CancelBatchJob(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if _, success := proxyRequestByToken(ctx, w, r, jobID); success {
|
||||
if _, proxied, _ := proxyRequestByToken(ctx, w, r, jobID, true); proxied {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1989,7 +2013,6 @@ func (j *BatchJobPool) AddWorker() {
|
||||
}
|
||||
}
|
||||
}
|
||||
job.delete(j.ctx, j.objLayer)
|
||||
j.canceler(job.ID, false)
|
||||
case <-j.workerKillCh:
|
||||
return
|
||||
@@ -2050,7 +2073,9 @@ func (j *BatchJobPool) canceler(jobID string, cancel bool) error {
|
||||
canceler()
|
||||
}
|
||||
}
|
||||
delete(j.jobCancelers, jobID)
|
||||
if cancel {
|
||||
delete(j.jobCancelers, jobID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2176,11 +2201,42 @@ func (m *batchJobMetrics) purgeJobMetrics() {
|
||||
m.RUnlock()
|
||||
for _, jobID := range toDeleteJobMetrics {
|
||||
m.delete(jobID)
|
||||
j := BatchJobRequest{
|
||||
ID: jobID,
|
||||
}
|
||||
j.delete(GlobalContext, newObjectLayerFn())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// load metrics from disk on startup
|
||||
func (m *batchJobMetrics) init(ctx context.Context, objectAPI ObjectLayer) error {
|
||||
resultCh := make(chan itemOrErr[ObjectInfo])
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
if err := objectAPI.Walk(ctx, minioMetaBucket, batchJobReportsPrefix, resultCh, WalkOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for result := range resultCh {
|
||||
if result.Err != nil {
|
||||
return result.Err
|
||||
}
|
||||
ri := &batchJobInfo{}
|
||||
if err := ri.loadByPath(ctx, objectAPI, result.Item.Name); err != nil {
|
||||
if !errors.Is(err, errNoSuchJob) {
|
||||
batchLogIf(ctx, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
m.metrics[ri.JobID] = ri
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *batchJobMetrics) delete(jobID string) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
@@ -20,6 +20,7 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"hash"
|
||||
"io"
|
||||
"sync"
|
||||
@@ -37,12 +38,22 @@ type streamingBitrotWriter struct {
|
||||
shardSize int64
|
||||
canClose *sync.WaitGroup
|
||||
byteBuf []byte
|
||||
finished bool
|
||||
}
|
||||
|
||||
func (b *streamingBitrotWriter) Write(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if b.finished {
|
||||
return 0, errors.New("bitrot write not allowed")
|
||||
}
|
||||
if int64(len(p)) > b.shardSize {
|
||||
return 0, errors.New("unexpected bitrot buffer size")
|
||||
}
|
||||
if int64(len(p)) < b.shardSize {
|
||||
b.finished = true
|
||||
}
|
||||
b.h.Reset()
|
||||
b.h.Write(p)
|
||||
hashBytes := b.h.Sum(nil)
|
||||
|
||||
@@ -183,7 +183,12 @@ var binaryChecksum = getBinaryChecksum()
|
||||
|
||||
func getBinaryChecksum() string {
|
||||
mw := md5.New()
|
||||
b, err := os.Open(os.Args[0])
|
||||
binPath, err := os.Executable()
|
||||
if err != nil {
|
||||
logger.Error("Calculating checksum failed: %s", err)
|
||||
return "00000000000000000000000000000000"
|
||||
}
|
||||
b, err := os.Open(binPath)
|
||||
if err != nil {
|
||||
logger.Error("Calculating checksum failed: %s", err)
|
||||
return "00000000000000000000000000000000"
|
||||
|
||||
@@ -429,7 +429,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
|
||||
// Content-Md5 is required should be set
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
if _, ok := r.Header[xhttp.ContentMD5]; !ok {
|
||||
if !validateLengthAndChecksum(r) {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1338,6 +1338,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
}
|
||||
opts.EncryptFn = metadataEncrypter(objectEncryptionKey)
|
||||
pReader, err = pReader.WithEncryption(hashReader, &objectEncryptionKey)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
@@ -1809,6 +1810,10 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
return
|
||||
}
|
||||
|
||||
// Audit log tags.
|
||||
reqInfo := logger.GetReqInfo(ctx)
|
||||
reqInfo.SetTags("retention", config.String())
|
||||
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
|
||||
@@ -19,7 +19,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -53,7 +52,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
||||
bucket := vars["bucket"]
|
||||
|
||||
// PutBucketLifecycle always needs a Content-Md5
|
||||
if _, ok := r.Header[xhttp.ContentMD5]; !ok {
|
||||
if !validateLengthAndChecksum(r) {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -70,7 +69,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
bucketLifecycle, err := lifecycle.ParseLifecycleConfigWithID(io.LimitReader(r.Body, r.ContentLength))
|
||||
bucketLifecycle, err := lifecycle.ParseLifecycleConfigWithID(r.Body)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
|
||||
+12
-7
@@ -71,7 +71,7 @@ func NewLifecycleSys() *LifecycleSys {
|
||||
return &LifecycleSys{}
|
||||
}
|
||||
|
||||
func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event string, metadata map[string]string) madmin.TraceInfo {
|
||||
func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event string, metadata map[string]string, err string) madmin.TraceInfo {
|
||||
sz, _ := oi.GetActualSize()
|
||||
return madmin.TraceInfo{
|
||||
TraceType: madmin.TraceILM,
|
||||
@@ -81,18 +81,22 @@ func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event
|
||||
Duration: duration,
|
||||
Path: pathJoin(oi.Bucket, oi.Name),
|
||||
Bytes: sz,
|
||||
Error: "",
|
||||
Error: err,
|
||||
Message: getSource(4),
|
||||
Custom: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
func (sys *LifecycleSys) trace(oi ObjectInfo) func(event string, metadata map[string]string) {
|
||||
func (sys *LifecycleSys) trace(oi ObjectInfo) func(event string, metadata map[string]string, err error) {
|
||||
startTime := time.Now()
|
||||
return func(event string, metadata map[string]string) {
|
||||
return func(event string, metadata map[string]string, err error) {
|
||||
duration := time.Since(startTime)
|
||||
if globalTrace.NumSubscribers(madmin.TraceILM) > 0 {
|
||||
globalTrace.Publish(ilmTrace(startTime, duration, oi, event, metadata))
|
||||
e := ""
|
||||
if err != nil {
|
||||
e = err.Error()
|
||||
}
|
||||
globalTrace.Publish(ilmTrace(startTime, duration, oi, event, metadata, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,7 +352,7 @@ func (es *expiryState) Worker(input <-chan expiryOp) {
|
||||
traceFn := globalLifecycleSys.trace(oi)
|
||||
if !oi.TransitionedObject.FreeVersion {
|
||||
// nothing to be done
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
ignoreNotFoundErr := func(err error) error {
|
||||
@@ -362,7 +366,8 @@ func (es *expiryState) Worker(input <-chan expiryOp) {
|
||||
err := deleteObjectFromRemoteTier(es.ctx, oi.TransitionedObject.Name, oi.TransitionedObject.VersionID, oi.TransitionedObject.Tier)
|
||||
if ignoreNotFoundErr(err) != nil {
|
||||
transitionLogIf(es.ctx, err)
|
||||
return
|
||||
traceFn(ILMFreeVersionDelete, nil, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove this free version
|
||||
|
||||
@@ -243,26 +243,26 @@ func parseRequestToken(token string) (subToken string, nodeIndex int) {
|
||||
return subToken, nodeIndex
|
||||
}
|
||||
|
||||
func proxyRequestByToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token string) (string, bool) {
|
||||
subToken, nodeIndex := parseRequestToken(token)
|
||||
if nodeIndex > 0 {
|
||||
return subToken, proxyRequestByNodeIndex(ctx, w, r, nodeIndex)
|
||||
func proxyRequestByToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token string, returnErr bool) (subToken string, proxied bool, success bool) {
|
||||
var nodeIndex int
|
||||
if subToken, nodeIndex = parseRequestToken(token); nodeIndex >= 0 {
|
||||
proxied, success = proxyRequestByNodeIndex(ctx, w, r, nodeIndex, returnErr)
|
||||
}
|
||||
return subToken, false
|
||||
return
|
||||
}
|
||||
|
||||
func proxyRequestByNodeIndex(ctx context.Context, w http.ResponseWriter, r *http.Request, index int) (success bool) {
|
||||
func proxyRequestByNodeIndex(ctx context.Context, w http.ResponseWriter, r *http.Request, index int, returnErr bool) (proxied, success bool) {
|
||||
if len(globalProxyEndpoints) == 0 {
|
||||
return false
|
||||
return
|
||||
}
|
||||
if index < 0 || index >= len(globalProxyEndpoints) {
|
||||
return false
|
||||
return
|
||||
}
|
||||
ep := globalProxyEndpoints[index]
|
||||
if ep.IsLocal {
|
||||
return false
|
||||
return
|
||||
}
|
||||
return proxyRequest(ctx, w, r, ep)
|
||||
return true, proxyRequest(ctx, w, r, ep, returnErr)
|
||||
}
|
||||
|
||||
// ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
|
||||
|
||||
+100
-35
@@ -49,12 +49,11 @@ import (
|
||||
"github.com/minio/minio/internal/hash"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
xioutil "github.com/minio/minio/internal/ioutil"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/once"
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
"github.com/zeebo/xxh3"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -775,13 +774,14 @@ func (m caseInsensitiveMap) Lookup(key string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, partNum int) (putOpts minio.PutObjectOptions, err error) {
|
||||
func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts minio.PutObjectOptions, isMP bool, err error) {
|
||||
meta := make(map[string]string)
|
||||
isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined)
|
||||
|
||||
for k, v := range objInfo.UserDefined {
|
||||
_, isValidSSEHeader := validSSEReplicationHeaders[k]
|
||||
// In case of SSE-C objects copy the allowed internal headers as well
|
||||
if !isSSEC || !slices.Contains(maps.Keys(validSSEReplicationHeaders), k) {
|
||||
if !isSSEC || !isValidSSEHeader {
|
||||
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
||||
continue
|
||||
}
|
||||
@@ -789,24 +789,28 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
||||
continue
|
||||
}
|
||||
}
|
||||
if slices.Contains(maps.Keys(validSSEReplicationHeaders), k) {
|
||||
if isValidSSEHeader {
|
||||
meta[validSSEReplicationHeaders[k]] = v
|
||||
} else {
|
||||
meta[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
isMP = objInfo.isMultipart()
|
||||
if len(objInfo.Checksum) > 0 {
|
||||
// Add encrypted CRC to metadata for SSE-C objects.
|
||||
if isSSEC {
|
||||
meta[ReplicationSsecChecksumHeader] = base64.StdEncoding.EncodeToString(objInfo.Checksum)
|
||||
} else {
|
||||
for _, pi := range objInfo.Parts {
|
||||
if pi.Number == partNum {
|
||||
for k, v := range pi.Checksums {
|
||||
meta[k] = v
|
||||
}
|
||||
}
|
||||
cs, mp := getCRCMeta(objInfo, 0, nil)
|
||||
// Set object checksum.
|
||||
for k, v := range cs {
|
||||
meta[k] = v
|
||||
}
|
||||
isMP = mp
|
||||
if !objInfo.isMultipart() && cs[xhttp.AmzChecksumType] == xhttp.AmzChecksumTypeFullObject {
|
||||
// For objects where checksum is full object, it will be the same.
|
||||
// Therefore, we use the cheaper PutObject replication.
|
||||
isMP = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -837,7 +841,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
||||
if tagTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp]; ok {
|
||||
tagTimestamp, err = time.Parse(time.RFC3339Nano, tagTmstampStr)
|
||||
if err != nil {
|
||||
return putOpts, err
|
||||
return putOpts, false, err
|
||||
}
|
||||
}
|
||||
putOpts.Internal.TaggingTimestamp = tagTimestamp
|
||||
@@ -861,7 +865,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
||||
if retainDateStr, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok {
|
||||
rdate, err := amztime.ISO8601Parse(retainDateStr)
|
||||
if err != nil {
|
||||
return putOpts, err
|
||||
return putOpts, false, err
|
||||
}
|
||||
putOpts.RetainUntilDate = rdate
|
||||
// set retention timestamp in opts
|
||||
@@ -869,7 +873,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
||||
if retainTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok {
|
||||
retTimestamp, err = time.Parse(time.RFC3339Nano, retainTmstampStr)
|
||||
if err != nil {
|
||||
return putOpts, err
|
||||
return putOpts, false, err
|
||||
}
|
||||
}
|
||||
putOpts.Internal.RetentionTimestamp = retTimestamp
|
||||
@@ -881,7 +885,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
||||
if lholdTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp]; ok {
|
||||
lholdTimestamp, err = time.Parse(time.RFC3339Nano, lholdTmstampStr)
|
||||
if err != nil {
|
||||
return putOpts, err
|
||||
return putOpts, false, err
|
||||
}
|
||||
}
|
||||
putOpts.Internal.LegalholdTimestamp = lholdTimestamp
|
||||
@@ -889,6 +893,24 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
||||
if crypto.S3.IsEncrypted(objInfo.UserDefined) {
|
||||
putOpts.ServerSideEncryption = encrypt.NewSSE()
|
||||
}
|
||||
|
||||
if crypto.S3KMS.IsEncrypted(objInfo.UserDefined) {
|
||||
// If KMS key ID replication is enabled (as by default)
|
||||
// we include the object's KMS key ID. In any case, we
|
||||
// always set the SSE-KMS header. If no KMS key ID is
|
||||
// specified, MinIO is supposed to use whatever default
|
||||
// config applies on the site or bucket.
|
||||
var keyID string
|
||||
if kms.ReplicateKeyID() {
|
||||
keyID = objInfo.KMSKeyID()
|
||||
}
|
||||
|
||||
sseEnc, err := encrypt.NewSSEKMS(keyID, nil)
|
||||
if err != nil {
|
||||
return putOpts, false, err
|
||||
}
|
||||
putOpts.ServerSideEncryption = sseEnc
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -946,7 +968,11 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati
|
||||
}
|
||||
|
||||
t, _ := tags.ParseObjectTags(oi1.UserTags)
|
||||
if (oi2.UserTagCount > 0 && !reflect.DeepEqual(oi2.UserTags, t.ToMap())) || (oi2.UserTagCount != len(t.ToMap())) {
|
||||
oi2Map := make(map[string]string)
|
||||
for k, v := range oi2.UserTags {
|
||||
oi2Map[k] = v
|
||||
}
|
||||
if (oi2.UserTagCount > 0 && !reflect.DeepEqual(oi2Map, t.ToMap())) || (oi2.UserTagCount != len(t.ToMap())) {
|
||||
return replicateMetadata
|
||||
}
|
||||
|
||||
@@ -1274,7 +1300,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
||||
// use core client to avoid doing multipart on PUT
|
||||
c := &minio.Core{Client: tgt.Client}
|
||||
|
||||
putOpts, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo, 0)
|
||||
putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo)
|
||||
if err != nil {
|
||||
replLogIf(ctx, fmt.Errorf("failure setting options for replication bucket:%s err:%w", bucket, err))
|
||||
sendEvent(eventArgs{
|
||||
@@ -1306,7 +1332,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
||||
defer cancel()
|
||||
}
|
||||
r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
|
||||
if objInfo.isMultipart() {
|
||||
if isMP {
|
||||
rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts)
|
||||
} else {
|
||||
_, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts)
|
||||
@@ -1435,13 +1461,14 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
||||
}
|
||||
rinfo.Duration = time.Since(startTime)
|
||||
}()
|
||||
|
||||
oi, cerr := tgt.StatObject(ctx, tgt.Bucket, object, minio.StatObjectOptions{
|
||||
sOpts := minio.StatObjectOptions{
|
||||
VersionID: objInfo.VersionID,
|
||||
Internal: minio.AdvancedGetOptions{
|
||||
ReplicationProxyRequest: "false",
|
||||
},
|
||||
})
|
||||
}
|
||||
sOpts.Set(xhttp.AmzTagDirective, "ACCESS")
|
||||
oi, cerr := tgt.StatObject(ctx, tgt.Bucket, object, sOpts)
|
||||
if cerr == nil {
|
||||
rAction = getReplicationAction(objInfo, oi, ri.OpType)
|
||||
rinfo.ReplicationStatus = replication.Completed
|
||||
@@ -1526,19 +1553,30 @@ applyAction:
|
||||
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
|
||||
},
|
||||
}
|
||||
if tagTmStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp]; ok {
|
||||
// default timestamps to ModTime unless present in metadata
|
||||
lkMap := caseInsensitiveMap(objInfo.UserDefined)
|
||||
if _, ok := lkMap.Lookup(xhttp.AmzObjectLockLegalHold); ok {
|
||||
dstOpts.Internal.LegalholdTimestamp = objInfo.ModTime
|
||||
}
|
||||
if _, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok {
|
||||
dstOpts.Internal.RetentionTimestamp = objInfo.ModTime
|
||||
}
|
||||
if objInfo.UserTags != "" {
|
||||
dstOpts.Internal.TaggingTimestamp = objInfo.ModTime
|
||||
}
|
||||
if tagTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + TaggingTimestamp); ok {
|
||||
ondiskTimestamp, err := time.Parse(time.RFC3339, tagTmStr)
|
||||
if err == nil {
|
||||
dstOpts.Internal.TaggingTimestamp = ondiskTimestamp
|
||||
}
|
||||
}
|
||||
if retTmStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok {
|
||||
if retTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp); ok {
|
||||
ondiskTimestamp, err := time.Parse(time.RFC3339, retTmStr)
|
||||
if err == nil {
|
||||
dstOpts.Internal.RetentionTimestamp = ondiskTimestamp
|
||||
}
|
||||
}
|
||||
if lholdTmStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp]; ok {
|
||||
if lholdTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp); ok {
|
||||
ondiskTimestamp, err := time.Parse(time.RFC3339, lholdTmStr)
|
||||
if err == nil {
|
||||
dstOpts.Internal.LegalholdTimestamp = ondiskTimestamp
|
||||
@@ -1549,8 +1587,7 @@ applyAction:
|
||||
replLogIf(ctx, fmt.Errorf("unable to replicate metadata for object %s/%s(%s) to target %s: %w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), rinfo.Err))
|
||||
}
|
||||
} else {
|
||||
var putOpts minio.PutObjectOptions
|
||||
putOpts, err = putReplicationOpts(ctx, tgt.StorageClass, objInfo, 0)
|
||||
putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo)
|
||||
if err != nil {
|
||||
replLogIf(ctx, fmt.Errorf("failed to set replicate options for object %s/%s(%s) (target %s) err:%w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), err))
|
||||
sendEvent(eventArgs{
|
||||
@@ -1581,7 +1618,7 @@ applyAction:
|
||||
defer cancel()
|
||||
}
|
||||
r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
|
||||
if objInfo.isMultipart() {
|
||||
if isMP {
|
||||
rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts)
|
||||
} else {
|
||||
_, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts)
|
||||
@@ -1659,7 +1696,8 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
||||
cHeader := http.Header{}
|
||||
cHeader.Add(xhttp.MinIOSourceReplicationRequest, "true")
|
||||
if !isSSEC {
|
||||
for k, v := range partInfo.Checksums {
|
||||
cs, _ := getCRCMeta(objInfo, partInfo.Number, nil)
|
||||
for k, v := range cs {
|
||||
cHeader.Add(k, v)
|
||||
}
|
||||
}
|
||||
@@ -1683,12 +1721,13 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
||||
return fmt.Errorf("ssec(%t): Part size mismatch: got %d, want %d", isSSEC, pInfo.Size, size)
|
||||
}
|
||||
uploadedParts = append(uploadedParts, minio.CompletePart{
|
||||
PartNumber: pInfo.PartNumber,
|
||||
ETag: pInfo.ETag,
|
||||
ChecksumCRC32: pInfo.ChecksumCRC32,
|
||||
ChecksumCRC32C: pInfo.ChecksumCRC32C,
|
||||
ChecksumSHA1: pInfo.ChecksumSHA1,
|
||||
ChecksumSHA256: pInfo.ChecksumSHA256,
|
||||
PartNumber: pInfo.PartNumber,
|
||||
ETag: pInfo.ETag,
|
||||
ChecksumCRC32: pInfo.ChecksumCRC32,
|
||||
ChecksumCRC32C: pInfo.ChecksumCRC32C,
|
||||
ChecksumSHA1: pInfo.ChecksumSHA1,
|
||||
ChecksumSHA256: pInfo.ChecksumSHA256,
|
||||
ChecksumCRC64NVME: pInfo.ChecksumCRC64NVME,
|
||||
})
|
||||
}
|
||||
userMeta := map[string]string{
|
||||
@@ -1701,6 +1740,13 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
||||
// really big value but its okay on heavily loaded systems. This is just tail end timeout.
|
||||
cctx, ccancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
defer ccancel()
|
||||
|
||||
if len(objInfo.Checksum) > 0 {
|
||||
cs, _ := getCRCMeta(objInfo, 0, nil)
|
||||
for k, v := range cs {
|
||||
userMeta[k] = strings.Split(v, "-")[0]
|
||||
}
|
||||
}
|
||||
_, err = c.CompleteMultipartUpload(cctx, bucket, object, uploadID, uploadedParts, minio.PutObjectOptions{
|
||||
UserMetadata: userMeta,
|
||||
Internal: minio.AdvancedPutOptions{
|
||||
@@ -3746,3 +3792,22 @@ type validateReplicationDestinationOptions struct {
|
||||
|
||||
checkReadyErr sync.Map
|
||||
}
|
||||
|
||||
func getCRCMeta(oi ObjectInfo, partNum int, h http.Header) (cs map[string]string, isMP bool) {
|
||||
meta := make(map[string]string)
|
||||
cs, isMP = oi.decryptChecksums(partNum, h)
|
||||
for k, v := range cs {
|
||||
cksum := hash.NewChecksumString(k, v)
|
||||
if cksum == nil {
|
||||
continue
|
||||
}
|
||||
if cksum.Valid() {
|
||||
meta[cksum.Type.Key()] = v
|
||||
}
|
||||
if isMP && partNum == 0 {
|
||||
meta[xhttp.AmzChecksumType] = cksum.Type.ObjType()
|
||||
meta[xhttp.AmzChecksumAlgo] = cksum.Type.String()
|
||||
}
|
||||
}
|
||||
return meta, isMP
|
||||
}
|
||||
|
||||
+7
-7
@@ -108,18 +108,18 @@ func (l ReplicationLastHour) merge(o ReplicationLastHour) (merged ReplicationLas
|
||||
|
||||
// Add a new duration data
|
||||
func (l *ReplicationLastHour) addsize(sz int64) {
|
||||
min := time.Now().Unix() / 60
|
||||
l.forwardTo(min)
|
||||
winIdx := min % 60
|
||||
l.Totals[winIdx].merge(AccElem{Total: min, Size: sz, N: 1})
|
||||
l.LastMin = min
|
||||
minutes := time.Now().Unix() / 60
|
||||
l.forwardTo(minutes)
|
||||
winIdx := minutes % 60
|
||||
l.Totals[winIdx].merge(AccElem{Total: minutes, Size: sz, N: 1})
|
||||
l.LastMin = minutes
|
||||
}
|
||||
|
||||
// Merge all recorded counts of last hour into one
|
||||
func (l *ReplicationLastHour) getTotal() AccElem {
|
||||
var res AccElem
|
||||
min := time.Now().Unix() / 60
|
||||
l.forwardTo(min)
|
||||
minutes := time.Now().Unix() / 60
|
||||
l.forwardTo(minutes)
|
||||
for _, elem := range l.Totals[:] {
|
||||
res.merge(elem)
|
||||
}
|
||||
|
||||
@@ -91,9 +91,6 @@ func init() {
|
||||
logger.Init(GOPATH, GOROOT)
|
||||
logger.RegisterError(config.FmtError)
|
||||
|
||||
globalBatchJobsMetrics = batchJobMetrics{metrics: make(map[string]*batchJobInfo)}
|
||||
go globalBatchJobsMetrics.purgeJobMetrics()
|
||||
|
||||
t, _ := minioVersionToReleaseTime(Version)
|
||||
if !t.IsZero() {
|
||||
globalVersionUnix = uint64(t.Unix())
|
||||
|
||||
+6
-3
@@ -1325,10 +1325,13 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
|
||||
dobj, err = objLayer.DeleteObject(ctx, obj.Bucket, encodeDirObject(obj.Name), opts)
|
||||
if err != nil {
|
||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||
traceFn(ILMExpiry, nil, nil)
|
||||
return false
|
||||
}
|
||||
// Assume it is still there.
|
||||
ilmLogOnceIf(ctx, fmt.Errorf("DeleteObject(%s, %s): %w", obj.Bucket, obj.Name, err), "non-transition-expiry"+obj.Name)
|
||||
err := fmt.Errorf("DeleteObject(%s, %s): %w", obj.Bucket, obj.Name, err)
|
||||
ilmLogOnceIf(ctx, err, "non-transition-expiry"+obj.Name)
|
||||
traceFn(ILMExpiry, nil, err)
|
||||
return false
|
||||
}
|
||||
if dobj.Name == "" {
|
||||
@@ -1549,7 +1552,7 @@ const (
|
||||
ILMTransition = " ilm:transition"
|
||||
)
|
||||
|
||||
func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string, tags map[string]string, traceFn func(event string, metadata map[string]string)) {
|
||||
func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string, tags map[string]string, traceFn func(event string, metadata map[string]string, err error)) {
|
||||
var apiName string
|
||||
switch event {
|
||||
case ILMExpiry:
|
||||
@@ -1567,5 +1570,5 @@ func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string, tags ma
|
||||
VersionID: oi.VersionID,
|
||||
Tags: tags,
|
||||
})
|
||||
traceFn(event, tags)
|
||||
traceFn(event, tags, nil)
|
||||
}
|
||||
|
||||
+48
-283
@@ -39,6 +39,8 @@ import (
|
||||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
//msgp:clearomitted
|
||||
|
||||
//go:generate msgp -file $GOFILE -unexported
|
||||
|
||||
// dataUsageHash is the hash type used.
|
||||
@@ -57,15 +59,14 @@ type versionsHistogram [dataUsageVersionLen]uint64
|
||||
type dataUsageEntry struct {
|
||||
Children dataUsageHashMap `msg:"ch"`
|
||||
// These fields do no include any children.
|
||||
Size int64 `msg:"sz"`
|
||||
Objects uint64 `msg:"os"`
|
||||
Versions uint64 `msg:"vs"` // Versions that are not delete markers.
|
||||
DeleteMarkers uint64 `msg:"dms"`
|
||||
ObjSizes sizeHistogram `msg:"szs"`
|
||||
ObjVersions versionsHistogram `msg:"vh"`
|
||||
ReplicationStats *replicationAllStats `msg:"rs,omitempty"`
|
||||
AllTierStats *allTierStats `msg:"ats,omitempty"`
|
||||
Compacted bool `msg:"c"`
|
||||
Size int64 `msg:"sz"`
|
||||
Objects uint64 `msg:"os"`
|
||||
Versions uint64 `msg:"vs"` // Versions that are not delete markers.
|
||||
DeleteMarkers uint64 `msg:"dms"`
|
||||
ObjSizes sizeHistogram `msg:"szs"`
|
||||
ObjVersions versionsHistogram `msg:"vh"`
|
||||
AllTierStats *allTierStats `msg:"ats,omitempty"`
|
||||
Compacted bool `msg:"c"`
|
||||
}
|
||||
|
||||
// allTierStats is a collection of per-tier stats across all configured remote
|
||||
@@ -135,93 +136,6 @@ func (ts tierStats) add(u tierStats) tierStats {
|
||||
}
|
||||
}
|
||||
|
||||
//msgp:tuple replicationStatsV1
|
||||
type replicationStatsV1 struct {
|
||||
PendingSize uint64
|
||||
ReplicatedSize uint64
|
||||
FailedSize uint64
|
||||
ReplicaSize uint64
|
||||
FailedCount uint64
|
||||
PendingCount uint64
|
||||
MissedThresholdSize uint64
|
||||
AfterThresholdSize uint64
|
||||
MissedThresholdCount uint64
|
||||
AfterThresholdCount uint64
|
||||
}
|
||||
|
||||
func (rsv1 replicationStatsV1) Empty() bool {
|
||||
return rsv1.ReplicatedSize == 0 &&
|
||||
rsv1.FailedSize == 0 &&
|
||||
rsv1.FailedCount == 0
|
||||
}
|
||||
|
||||
//msgp:tuple replicationStats
|
||||
type replicationStats struct {
|
||||
PendingSize uint64
|
||||
ReplicatedSize uint64
|
||||
FailedSize uint64
|
||||
FailedCount uint64
|
||||
PendingCount uint64
|
||||
MissedThresholdSize uint64
|
||||
AfterThresholdSize uint64
|
||||
MissedThresholdCount uint64
|
||||
AfterThresholdCount uint64
|
||||
ReplicatedCount uint64
|
||||
}
|
||||
|
||||
func (rs replicationStats) Empty() bool {
|
||||
return rs.ReplicatedSize == 0 &&
|
||||
rs.FailedSize == 0 &&
|
||||
rs.FailedCount == 0
|
||||
}
|
||||
|
||||
type replicationAllStats struct {
|
||||
Targets map[string]replicationStats `msg:"t,omitempty"`
|
||||
ReplicaSize uint64 `msg:"r,omitempty"`
|
||||
ReplicaCount uint64 `msg:"rc,omitempty"`
|
||||
}
|
||||
|
||||
//msgp:tuple replicationAllStatsV1
|
||||
type replicationAllStatsV1 struct {
|
||||
Targets map[string]replicationStats
|
||||
ReplicaSize uint64 `msg:"ReplicaSize,omitempty"`
|
||||
ReplicaCount uint64 `msg:"ReplicaCount,omitempty"`
|
||||
}
|
||||
|
||||
// empty returns true if the replicationAllStats is empty (contains no entries).
|
||||
func (r *replicationAllStats) empty() bool {
|
||||
if r == nil {
|
||||
return true
|
||||
}
|
||||
if r.ReplicaSize != 0 || r.ReplicaCount != 0 {
|
||||
return false
|
||||
}
|
||||
for _, v := range r.Targets {
|
||||
if !v.Empty() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// clone creates a deep-copy clone.
|
||||
func (r *replicationAllStats) clone() *replicationAllStats {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shallow copy
|
||||
dst := *r
|
||||
|
||||
// Copy individual targets.
|
||||
dst.Targets = make(map[string]replicationStats, len(r.Targets))
|
||||
for k, v := range r.Targets {
|
||||
dst.Targets[k] = v
|
||||
}
|
||||
|
||||
return &dst
|
||||
}
|
||||
|
||||
//msgp:encode ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 dataUsageEntryV7
|
||||
//msgp:marshal ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 dataUsageEntryV7
|
||||
|
||||
@@ -237,62 +151,54 @@ type dataUsageEntryV2 struct {
|
||||
//msgp:tuple dataUsageEntryV3
|
||||
type dataUsageEntryV3 struct {
|
||||
// These fields do no include any children.
|
||||
Size int64
|
||||
ReplicatedSize uint64
|
||||
ReplicationPendingSize uint64
|
||||
ReplicationFailedSize uint64
|
||||
ReplicaSize uint64
|
||||
Objects uint64
|
||||
ObjSizes sizeHistogram
|
||||
Children dataUsageHashMap
|
||||
Size int64
|
||||
Objects uint64
|
||||
ObjSizes sizeHistogram
|
||||
Children dataUsageHashMap
|
||||
}
|
||||
|
||||
//msgp:tuple dataUsageEntryV4
|
||||
type dataUsageEntryV4 struct {
|
||||
Children dataUsageHashMap
|
||||
// These fields do no include any children.
|
||||
Size int64
|
||||
Objects uint64
|
||||
ObjSizes sizeHistogram
|
||||
ReplicationStats replicationStatsV1
|
||||
Size int64
|
||||
Objects uint64
|
||||
ObjSizes sizeHistogram
|
||||
}
|
||||
|
||||
//msgp:tuple dataUsageEntryV5
|
||||
type dataUsageEntryV5 struct {
|
||||
Children dataUsageHashMap
|
||||
// These fields do no include any children.
|
||||
Size int64
|
||||
Objects uint64
|
||||
Versions uint64 // Versions that are not delete markers.
|
||||
ObjSizes sizeHistogram
|
||||
ReplicationStats *replicationStatsV1
|
||||
Compacted bool
|
||||
Size int64
|
||||
Objects uint64
|
||||
Versions uint64 // Versions that are not delete markers.
|
||||
ObjSizes sizeHistogram
|
||||
Compacted bool
|
||||
}
|
||||
|
||||
//msgp:tuple dataUsageEntryV6
|
||||
type dataUsageEntryV6 struct {
|
||||
Children dataUsageHashMap
|
||||
// These fields do no include any children.
|
||||
Size int64
|
||||
Objects uint64
|
||||
Versions uint64 // Versions that are not delete markers.
|
||||
ObjSizes sizeHistogram
|
||||
ReplicationStats *replicationAllStatsV1
|
||||
Compacted bool
|
||||
Size int64
|
||||
Objects uint64
|
||||
Versions uint64 // Versions that are not delete markers.
|
||||
ObjSizes sizeHistogram
|
||||
Compacted bool
|
||||
}
|
||||
|
||||
type dataUsageEntryV7 struct {
|
||||
Children dataUsageHashMap `msg:"ch"`
|
||||
// These fields do no include any children.
|
||||
Size int64 `msg:"sz"`
|
||||
Objects uint64 `msg:"os"`
|
||||
Versions uint64 `msg:"vs"` // Versions that are not delete markers.
|
||||
DeleteMarkers uint64 `msg:"dms"`
|
||||
ObjSizes sizeHistogramV1 `msg:"szs"`
|
||||
ObjVersions versionsHistogram `msg:"vh"`
|
||||
ReplicationStats *replicationAllStats `msg:"rs,omitempty"`
|
||||
AllTierStats *allTierStats `msg:"ats,omitempty"`
|
||||
Compacted bool `msg:"c"`
|
||||
Size int64 `msg:"sz"`
|
||||
Objects uint64 `msg:"os"`
|
||||
Versions uint64 `msg:"vs"` // Versions that are not delete markers.
|
||||
DeleteMarkers uint64 `msg:"dms"`
|
||||
ObjSizes sizeHistogramV1 `msg:"szs"`
|
||||
ObjVersions versionsHistogram `msg:"vh"`
|
||||
AllTierStats *allTierStats `msg:"ats,omitempty"`
|
||||
Compacted bool `msg:"c"`
|
||||
}
|
||||
|
||||
// dataUsageCache contains a cache of data usage entries latest version.
|
||||
@@ -373,29 +279,6 @@ func (e *dataUsageEntry) addSizes(summary sizeSummary) {
|
||||
e.ObjSizes.add(summary.totalSize)
|
||||
e.ObjVersions.add(summary.versions)
|
||||
|
||||
if e.ReplicationStats == nil {
|
||||
e.ReplicationStats = &replicationAllStats{
|
||||
Targets: make(map[string]replicationStats),
|
||||
}
|
||||
} else if e.ReplicationStats.Targets == nil {
|
||||
e.ReplicationStats.Targets = make(map[string]replicationStats)
|
||||
}
|
||||
e.ReplicationStats.ReplicaSize += uint64(summary.replicaSize)
|
||||
e.ReplicationStats.ReplicaCount += uint64(summary.replicaCount)
|
||||
|
||||
for arn, st := range summary.replTargetStats {
|
||||
tgtStat, ok := e.ReplicationStats.Targets[arn]
|
||||
if !ok {
|
||||
tgtStat = replicationStats{}
|
||||
}
|
||||
tgtStat.PendingSize += uint64(st.pendingSize)
|
||||
tgtStat.FailedSize += uint64(st.failedSize)
|
||||
tgtStat.ReplicatedSize += uint64(st.replicatedSize)
|
||||
tgtStat.ReplicatedCount += uint64(st.replicatedCount)
|
||||
tgtStat.FailedCount += st.failedCount
|
||||
tgtStat.PendingCount += st.pendingCount
|
||||
e.ReplicationStats.Targets[arn] = tgtStat
|
||||
}
|
||||
if len(summary.tiers) != 0 {
|
||||
if e.AllTierStats == nil {
|
||||
e.AllTierStats = newAllTierStats()
|
||||
@@ -410,26 +293,6 @@ func (e *dataUsageEntry) merge(other dataUsageEntry) {
|
||||
e.Versions += other.Versions
|
||||
e.DeleteMarkers += other.DeleteMarkers
|
||||
e.Size += other.Size
|
||||
if other.ReplicationStats != nil {
|
||||
if e.ReplicationStats == nil {
|
||||
e.ReplicationStats = &replicationAllStats{Targets: make(map[string]replicationStats)}
|
||||
} else if e.ReplicationStats.Targets == nil {
|
||||
e.ReplicationStats.Targets = make(map[string]replicationStats)
|
||||
}
|
||||
e.ReplicationStats.ReplicaSize += other.ReplicationStats.ReplicaSize
|
||||
e.ReplicationStats.ReplicaCount += other.ReplicationStats.ReplicaCount
|
||||
for arn, stat := range other.ReplicationStats.Targets {
|
||||
st := e.ReplicationStats.Targets[arn]
|
||||
e.ReplicationStats.Targets[arn] = replicationStats{
|
||||
PendingSize: stat.PendingSize + st.PendingSize,
|
||||
FailedSize: stat.FailedSize + st.FailedSize,
|
||||
ReplicatedSize: stat.ReplicatedSize + st.ReplicatedSize,
|
||||
PendingCount: stat.PendingCount + st.PendingCount,
|
||||
FailedCount: stat.FailedCount + st.FailedCount,
|
||||
ReplicatedCount: stat.ReplicatedCount + st.ReplicatedCount,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, v := range other.ObjSizes[:] {
|
||||
e.ObjSizes[i] += v
|
||||
@@ -490,10 +353,7 @@ func (e dataUsageEntry) clone() dataUsageEntry {
|
||||
}
|
||||
e.Children = ch
|
||||
}
|
||||
if e.ReplicationStats != nil {
|
||||
// Clone ReplicationStats
|
||||
e.ReplicationStats = e.ReplicationStats.clone()
|
||||
}
|
||||
|
||||
if e.AllTierStats != nil {
|
||||
e.AllTierStats = e.AllTierStats.clone()
|
||||
}
|
||||
@@ -931,22 +791,6 @@ func (d *dataUsageCache) bucketsUsageInfo(buckets []BucketInfo) map[string]Bucke
|
||||
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
||||
ObjectVersionsHistogram: flat.ObjVersions.toMap(),
|
||||
}
|
||||
if flat.ReplicationStats != nil {
|
||||
bui.ReplicaSize = flat.ReplicationStats.ReplicaSize
|
||||
bui.ReplicaCount = flat.ReplicationStats.ReplicaCount
|
||||
|
||||
bui.ReplicationInfo = make(map[string]BucketTargetUsageInfo, len(flat.ReplicationStats.Targets))
|
||||
for arn, stat := range flat.ReplicationStats.Targets {
|
||||
bui.ReplicationInfo[arn] = BucketTargetUsageInfo{
|
||||
ReplicationPendingSize: stat.PendingSize,
|
||||
ReplicatedSize: stat.ReplicatedSize,
|
||||
ReplicationFailedSize: stat.FailedSize,
|
||||
ReplicationPendingCount: stat.PendingCount,
|
||||
ReplicationFailedCount: stat.FailedCount,
|
||||
ReplicatedCount: stat.ReplicatedCount,
|
||||
}
|
||||
}
|
||||
}
|
||||
dst[bucket.Name] = bui
|
||||
}
|
||||
return dst
|
||||
@@ -959,9 +803,6 @@ func (d *dataUsageCache) sizeRecursive(path string) *dataUsageEntry {
|
||||
return root
|
||||
}
|
||||
flat := d.flatten(*root)
|
||||
if flat.ReplicationStats.empty() {
|
||||
flat.ReplicationStats = nil
|
||||
}
|
||||
return &flat
|
||||
}
|
||||
|
||||
@@ -1238,20 +1079,6 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
|
||||
ObjSizes: v.ObjSizes,
|
||||
Children: v.Children,
|
||||
}
|
||||
if v.ReplicatedSize > 0 || v.ReplicaSize > 0 || v.ReplicationFailedSize > 0 || v.ReplicationPendingSize > 0 {
|
||||
cfg, _ := getReplicationConfig(GlobalContext, d.Info.Name)
|
||||
if cfg != nil && cfg.RoleArn != "" {
|
||||
due.ReplicationStats = &replicationAllStats{
|
||||
Targets: make(map[string]replicationStats),
|
||||
}
|
||||
due.ReplicationStats.ReplicaSize = v.ReplicaSize
|
||||
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
|
||||
ReplicatedSize: v.ReplicatedSize,
|
||||
FailedSize: v.ReplicationFailedSize,
|
||||
PendingSize: v.ReplicationPendingSize,
|
||||
}
|
||||
}
|
||||
}
|
||||
due.Compacted = len(due.Children) == 0 && k != d.Info.Name
|
||||
|
||||
d.Cache[k] = due
|
||||
@@ -1277,36 +1104,10 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
|
||||
ObjSizes: v.ObjSizes,
|
||||
Children: v.Children,
|
||||
}
|
||||
empty := replicationStatsV1{}
|
||||
|
||||
if v.ReplicationStats != empty {
|
||||
cfg, _ := getReplicationConfig(GlobalContext, d.Info.Name)
|
||||
if cfg != nil && cfg.RoleArn != "" {
|
||||
due.ReplicationStats = &replicationAllStats{
|
||||
Targets: make(map[string]replicationStats),
|
||||
}
|
||||
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
|
||||
ReplicatedSize: v.ReplicationStats.ReplicatedSize,
|
||||
FailedSize: v.ReplicationStats.FailedSize,
|
||||
FailedCount: v.ReplicationStats.FailedCount,
|
||||
PendingSize: v.ReplicationStats.PendingSize,
|
||||
PendingCount: v.ReplicationStats.PendingCount,
|
||||
}
|
||||
due.ReplicationStats.ReplicaSize = v.ReplicationStats.ReplicaSize
|
||||
}
|
||||
}
|
||||
due.Compacted = len(due.Children) == 0 && k != d.Info.Name
|
||||
|
||||
d.Cache[k] = due
|
||||
}
|
||||
|
||||
// Populate compacted value and remove unneeded replica stats.
|
||||
for k, e := range d.Cache {
|
||||
if e.ReplicationStats != nil && len(e.ReplicationStats.Targets) == 0 {
|
||||
e.ReplicationStats = nil
|
||||
}
|
||||
d.Cache[k] = e
|
||||
}
|
||||
return nil
|
||||
case dataUsageCacheVerV5:
|
||||
// Zstd compressed.
|
||||
@@ -1328,36 +1129,10 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
|
||||
ObjSizes: v.ObjSizes,
|
||||
Children: v.Children,
|
||||
}
|
||||
if v.ReplicationStats != nil && !v.ReplicationStats.Empty() {
|
||||
cfg, _ := getReplicationConfig(GlobalContext, d.Info.Name)
|
||||
if cfg != nil && cfg.RoleArn != "" {
|
||||
due.ReplicationStats = &replicationAllStats{
|
||||
Targets: make(map[string]replicationStats),
|
||||
}
|
||||
d.Info.replication = replicationConfig{Config: cfg}
|
||||
|
||||
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
|
||||
ReplicatedSize: v.ReplicationStats.ReplicatedSize,
|
||||
FailedSize: v.ReplicationStats.FailedSize,
|
||||
FailedCount: v.ReplicationStats.FailedCount,
|
||||
PendingSize: v.ReplicationStats.PendingSize,
|
||||
PendingCount: v.ReplicationStats.PendingCount,
|
||||
}
|
||||
due.ReplicationStats.ReplicaSize = v.ReplicationStats.ReplicaSize
|
||||
}
|
||||
}
|
||||
due.Compacted = len(due.Children) == 0 && k != d.Info.Name
|
||||
|
||||
d.Cache[k] = due
|
||||
}
|
||||
|
||||
// Populate compacted value and remove unneeded replica stats.
|
||||
for k, e := range d.Cache {
|
||||
if e.ReplicationStats != nil && len(e.ReplicationStats.Targets) == 0 {
|
||||
e.ReplicationStats = nil
|
||||
}
|
||||
d.Cache[k] = e
|
||||
}
|
||||
return nil
|
||||
case dataUsageCacheVerV6:
|
||||
// Zstd compressed.
|
||||
@@ -1373,22 +1148,13 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
|
||||
d.Info = dold.Info
|
||||
d.Cache = make(map[string]dataUsageEntry, len(dold.Cache))
|
||||
for k, v := range dold.Cache {
|
||||
var replicationStats *replicationAllStats
|
||||
if v.ReplicationStats != nil {
|
||||
replicationStats = &replicationAllStats{
|
||||
Targets: v.ReplicationStats.Targets,
|
||||
ReplicaSize: v.ReplicationStats.ReplicaSize,
|
||||
ReplicaCount: v.ReplicationStats.ReplicaCount,
|
||||
}
|
||||
}
|
||||
due := dataUsageEntry{
|
||||
Children: v.Children,
|
||||
Size: v.Size,
|
||||
Objects: v.Objects,
|
||||
Versions: v.Versions,
|
||||
ObjSizes: v.ObjSizes,
|
||||
ReplicationStats: replicationStats,
|
||||
Compacted: v.Compacted,
|
||||
Children: v.Children,
|
||||
Size: v.Size,
|
||||
Objects: v.Objects,
|
||||
Versions: v.Versions,
|
||||
ObjSizes: v.ObjSizes,
|
||||
Compacted: v.Compacted,
|
||||
}
|
||||
d.Cache[k] = due
|
||||
}
|
||||
@@ -1410,13 +1176,12 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
|
||||
var szHist sizeHistogram
|
||||
szHist.mergeV1(v.ObjSizes)
|
||||
d.Cache[k] = dataUsageEntry{
|
||||
Children: v.Children,
|
||||
Size: v.Size,
|
||||
Objects: v.Objects,
|
||||
Versions: v.Versions,
|
||||
ObjSizes: szHist,
|
||||
ReplicationStats: v.ReplicationStats,
|
||||
Compacted: v.Compacted,
|
||||
Children: v.Children,
|
||||
Size: v.Size,
|
||||
Objects: v.Objects,
|
||||
Versions: v.Versions,
|
||||
ObjSizes: szHist,
|
||||
Compacted: v.Compacted,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+615
-1330
File diff suppressed because it is too large
Load Diff
@@ -519,458 +519,6 @@ func BenchmarkDecodedataUsageEntry(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshalreplicationAllStats(t *testing.T) {
|
||||
v := replicationAllStats{}
|
||||
bts, err := v.MarshalMsg(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
left, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||
}
|
||||
|
||||
left, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalMsgreplicationAllStats(b *testing.B) {
|
||||
v := replicationAllStats{}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.MarshalMsg(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendMsgreplicationAllStats(b *testing.B) {
|
||||
v := replicationAllStats{}
|
||||
bts := make([]byte, 0, v.Msgsize())
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalreplicationAllStats(b *testing.B) {
|
||||
v := replicationAllStats{}
|
||||
bts, _ := v.MarshalMsg(nil)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeDecodereplicationAllStats(t *testing.T) {
|
||||
v := replicationAllStats{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
|
||||
m := v.Msgsize()
|
||||
if buf.Len() > m {
|
||||
t.Log("WARNING: TestEncodeDecodereplicationAllStats Msgsize() is inaccurate")
|
||||
}
|
||||
|
||||
vn := replicationAllStats{}
|
||||
err := msgp.Decode(&buf, &vn)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
msgp.Encode(&buf, &v)
|
||||
err = msgp.NewReader(&buf).Skip()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncodereplicationAllStats(b *testing.B) {
|
||||
v := replicationAllStats{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
en := msgp.NewWriter(msgp.Nowhere)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.EncodeMsg(en)
|
||||
}
|
||||
en.Flush()
|
||||
}
|
||||
|
||||
func BenchmarkDecodereplicationAllStats(b *testing.B) {
|
||||
v := replicationAllStats{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||
dc := msgp.NewReader(rd)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := v.DecodeMsg(dc)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshalreplicationAllStatsV1(t *testing.T) {
|
||||
v := replicationAllStatsV1{}
|
||||
bts, err := v.MarshalMsg(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
left, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||
}
|
||||
|
||||
left, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalMsgreplicationAllStatsV1(b *testing.B) {
|
||||
v := replicationAllStatsV1{}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.MarshalMsg(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendMsgreplicationAllStatsV1(b *testing.B) {
|
||||
v := replicationAllStatsV1{}
|
||||
bts := make([]byte, 0, v.Msgsize())
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalreplicationAllStatsV1(b *testing.B) {
|
||||
v := replicationAllStatsV1{}
|
||||
bts, _ := v.MarshalMsg(nil)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeDecodereplicationAllStatsV1(t *testing.T) {
|
||||
v := replicationAllStatsV1{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
|
||||
m := v.Msgsize()
|
||||
if buf.Len() > m {
|
||||
t.Log("WARNING: TestEncodeDecodereplicationAllStatsV1 Msgsize() is inaccurate")
|
||||
}
|
||||
|
||||
vn := replicationAllStatsV1{}
|
||||
err := msgp.Decode(&buf, &vn)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
msgp.Encode(&buf, &v)
|
||||
err = msgp.NewReader(&buf).Skip()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncodereplicationAllStatsV1(b *testing.B) {
|
||||
v := replicationAllStatsV1{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
en := msgp.NewWriter(msgp.Nowhere)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.EncodeMsg(en)
|
||||
}
|
||||
en.Flush()
|
||||
}
|
||||
|
||||
func BenchmarkDecodereplicationAllStatsV1(b *testing.B) {
|
||||
v := replicationAllStatsV1{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||
dc := msgp.NewReader(rd)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := v.DecodeMsg(dc)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshalreplicationStats(t *testing.T) {
|
||||
v := replicationStats{}
|
||||
bts, err := v.MarshalMsg(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
left, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||
}
|
||||
|
||||
left, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalMsgreplicationStats(b *testing.B) {
|
||||
v := replicationStats{}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.MarshalMsg(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendMsgreplicationStats(b *testing.B) {
|
||||
v := replicationStats{}
|
||||
bts := make([]byte, 0, v.Msgsize())
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalreplicationStats(b *testing.B) {
|
||||
v := replicationStats{}
|
||||
bts, _ := v.MarshalMsg(nil)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeDecodereplicationStats(t *testing.T) {
|
||||
v := replicationStats{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
|
||||
m := v.Msgsize()
|
||||
if buf.Len() > m {
|
||||
t.Log("WARNING: TestEncodeDecodereplicationStats Msgsize() is inaccurate")
|
||||
}
|
||||
|
||||
vn := replicationStats{}
|
||||
err := msgp.Decode(&buf, &vn)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
msgp.Encode(&buf, &v)
|
||||
err = msgp.NewReader(&buf).Skip()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncodereplicationStats(b *testing.B) {
|
||||
v := replicationStats{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
en := msgp.NewWriter(msgp.Nowhere)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.EncodeMsg(en)
|
||||
}
|
||||
en.Flush()
|
||||
}
|
||||
|
||||
func BenchmarkDecodereplicationStats(b *testing.B) {
|
||||
v := replicationStats{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||
dc := msgp.NewReader(rd)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := v.DecodeMsg(dc)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshalreplicationStatsV1(t *testing.T) {
|
||||
v := replicationStatsV1{}
|
||||
bts, err := v.MarshalMsg(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
left, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||
}
|
||||
|
||||
left, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalMsgreplicationStatsV1(b *testing.B) {
|
||||
v := replicationStatsV1{}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.MarshalMsg(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendMsgreplicationStatsV1(b *testing.B) {
|
||||
v := replicationStatsV1{}
|
||||
bts := make([]byte, 0, v.Msgsize())
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalreplicationStatsV1(b *testing.B) {
|
||||
v := replicationStatsV1{}
|
||||
bts, _ := v.MarshalMsg(nil)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeDecodereplicationStatsV1(t *testing.T) {
|
||||
v := replicationStatsV1{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
|
||||
m := v.Msgsize()
|
||||
if buf.Len() > m {
|
||||
t.Log("WARNING: TestEncodeDecodereplicationStatsV1 Msgsize() is inaccurate")
|
||||
}
|
||||
|
||||
vn := replicationStatsV1{}
|
||||
err := msgp.Decode(&buf, &vn)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
msgp.Encode(&buf, &v)
|
||||
err = msgp.NewReader(&buf).Skip()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncodereplicationStatsV1(b *testing.B) {
|
||||
v := replicationStatsV1{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
en := msgp.NewWriter(msgp.Nowhere)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.EncodeMsg(en)
|
||||
}
|
||||
en.Flush()
|
||||
}
|
||||
|
||||
func BenchmarkDecodereplicationStatsV1(b *testing.B) {
|
||||
v := replicationStatsV1{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||
dc := msgp.NewReader(rd)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := v.DecodeMsg(dc)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshalsizeHistogram(t *testing.T) {
|
||||
v := sizeHistogram{}
|
||||
bts, err := v.MarshalMsg(nil)
|
||||
|
||||
@@ -587,17 +587,6 @@ func TestDataUsageCacheSerialize(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e := want.find("abucket/dir2")
|
||||
e.ReplicationStats = &replicationAllStats{
|
||||
Targets: map[string]replicationStats{
|
||||
"arn": {
|
||||
PendingSize: 1,
|
||||
ReplicatedSize: 2,
|
||||
FailedSize: 3,
|
||||
FailedCount: 5,
|
||||
PendingCount: 6,
|
||||
},
|
||||
},
|
||||
}
|
||||
want.replace("abucket/dir2", "", *e)
|
||||
var buf bytes.Buffer
|
||||
err = want.serializeTo(&buf)
|
||||
|
||||
@@ -117,12 +117,12 @@ func (dt *dynamicTimeout) logEntry(duration time.Duration) {
|
||||
// adjust changes the value of the dynamic timeout based on the
|
||||
// previous results
|
||||
func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
|
||||
failures, max := 0, time.Duration(0)
|
||||
failures, maxDur := 0, time.Duration(0)
|
||||
for _, dur := range entries[:] {
|
||||
if dur == maxDuration {
|
||||
failures++
|
||||
} else if dur > max {
|
||||
max = dur
|
||||
} else if dur > maxDur {
|
||||
maxDur = dur
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,12 +144,12 @@ func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
|
||||
} else if failPct < dynamicTimeoutDecreaseThresholdPct {
|
||||
// We are hitting the timeout relatively few times,
|
||||
// so decrease the timeout towards 25 % of maximum time spent.
|
||||
max = max * 125 / 100
|
||||
maxDur = maxDur * 125 / 100
|
||||
|
||||
timeout := atomic.LoadInt64(&dt.timeout)
|
||||
if max < time.Duration(timeout) {
|
||||
if maxDur < time.Duration(timeout) {
|
||||
// Move 50% toward the max.
|
||||
timeout = (int64(max) + timeout) / 2
|
||||
timeout = (int64(maxDur) + timeout) / 2
|
||||
}
|
||||
if timeout < dt.minimum {
|
||||
timeout = dt.minimum
|
||||
|
||||
@@ -109,7 +109,7 @@ func kmsKeyIDFromMetadata(metadata map[string]string) string {
|
||||
// be AWS S3 compliant.
|
||||
//
|
||||
// DecryptETags uses a KMS bulk decryption API, if available, which
|
||||
// is more efficient than decrypting ETags sequentually.
|
||||
// is more efficient than decrypting ETags sequentially.
|
||||
func DecryptETags(ctx context.Context, k *kms.KMS, objects []ObjectInfo) error {
|
||||
const BatchSize = 250 // We process the objects in batches - 250 is a reasonable default.
|
||||
var (
|
||||
@@ -1099,7 +1099,9 @@ func (o *ObjectInfo) decryptPartsChecksums(h http.Header) {
|
||||
if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
|
||||
decrypted, err := o.metadataDecrypter(h)("object-checksum", data)
|
||||
if err != nil {
|
||||
encLogIf(GlobalContext, err)
|
||||
if !errors.Is(err, crypto.ErrSecretKeyMismatch) {
|
||||
encLogIf(GlobalContext, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
data = decrypted
|
||||
@@ -1153,16 +1155,17 @@ func (o *ObjectInfo) metadataEncryptFn(headers http.Header) (objectMetaEncryptFn
|
||||
|
||||
// decryptChecksums will attempt to decode checksums and return it/them if set.
|
||||
// if part > 0, and we have the checksum for the part that will be returned.
|
||||
func (o *ObjectInfo) decryptChecksums(part int, h http.Header) map[string]string {
|
||||
// Returns whether the checksum (main part 0) is a multipart checksum.
|
||||
func (o *ObjectInfo) decryptChecksums(part int, h http.Header) (cs map[string]string, isMP bool) {
|
||||
data := o.Checksum
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
return nil, false
|
||||
}
|
||||
if part > 0 && !crypto.SSEC.IsEncrypted(o.UserDefined) {
|
||||
// already decrypted in ToObjectInfo for multipart objects
|
||||
for _, pi := range o.Parts {
|
||||
if pi.Number == part {
|
||||
return pi.Checksums
|
||||
return pi.Checksums, true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1172,7 +1175,7 @@ func (o *ObjectInfo) decryptChecksums(part int, h http.Header) map[string]string
|
||||
if err != crypto.ErrSecretKeyMismatch {
|
||||
encLogIf(GlobalContext, err)
|
||||
}
|
||||
return nil
|
||||
return nil, part > 0
|
||||
}
|
||||
data = decrypted
|
||||
}
|
||||
|
||||
+3
-3
@@ -26,6 +26,7 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -38,7 +39,6 @@ import (
|
||||
"github.com/minio/minio/internal/mountinfo"
|
||||
"github.com/minio/pkg/v3/env"
|
||||
xnet "github.com/minio/pkg/v3/net"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// EndpointType - enum for endpoint type.
|
||||
@@ -1194,7 +1194,7 @@ func GetProxyEndpointLocalIndex(proxyEps []ProxyEndpoint) int {
|
||||
}
|
||||
|
||||
// GetProxyEndpoints - get all endpoints that can be used to proxy list request.
|
||||
func GetProxyEndpoints(endpointServerPools EndpointServerPools) []ProxyEndpoint {
|
||||
func GetProxyEndpoints(endpointServerPools EndpointServerPools, transport http.RoundTripper) []ProxyEndpoint {
|
||||
var proxyEps []ProxyEndpoint
|
||||
|
||||
proxyEpSet := set.NewStringSet()
|
||||
@@ -1213,7 +1213,7 @@ func GetProxyEndpoints(endpointServerPools EndpointServerPools) []ProxyEndpoint
|
||||
|
||||
proxyEps = append(proxyEps, ProxyEndpoint{
|
||||
Endpoint: endpoint,
|
||||
Transport: globalRemoteTargetTransport,
|
||||
Transport: transport,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,23 +277,21 @@ 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
|
||||
func countPartNotSuccess(partErrs []int) (c int) {
|
||||
for _, pe := range partErrs {
|
||||
if pe != checkPartSuccess {
|
||||
c++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// disksWithAllParts - This function needs to be called with
|
||||
// []StorageAPI returned by listOnlineDisks. Returns,
|
||||
//
|
||||
// - disks which have all parts specified in the latest xl.meta.
|
||||
//
|
||||
// - slice of errors about the state of data files on disk - can have
|
||||
// a not-found error or a hash-mismatch error.
|
||||
func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo,
|
||||
errs []error, latestMeta FileInfo, bucket, object string,
|
||||
// checkObjectWithAllParts sets partsMetadata and onlineDisks when xl.meta is inexistant/corrupted or outdated
|
||||
// it also checks if the status of each part (corrupted, missing, ok) in each drive
|
||||
func checkObjectWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo,
|
||||
errs []error, latestMeta FileInfo, filterByETag bool, bucket, object string,
|
||||
scanMode madmin.HealScanMode,
|
||||
) (availableDisks []StorageAPI, dataErrsByDisk map[int][]int, dataErrsByPart map[int][]int) {
|
||||
availableDisks = make([]StorageAPI, len(onlineDisks))
|
||||
|
||||
) (dataErrsByDisk map[int][]int, dataErrsByPart map[int][]int) {
|
||||
dataErrsByDisk = make(map[int][]int, len(onlineDisks))
|
||||
for i := range onlineDisks {
|
||||
dataErrsByDisk[i] = make([]int, len(latestMeta.Parts))
|
||||
@@ -334,20 +332,28 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
||||
|
||||
metaErrs := make([]error, len(errs))
|
||||
|
||||
for i, onlineDisk := range onlineDisks {
|
||||
for i := range onlineDisks {
|
||||
if errs[i] != nil {
|
||||
metaErrs[i] = errs[i]
|
||||
continue
|
||||
}
|
||||
if onlineDisk == OfflineDisk {
|
||||
if onlineDisks[i] == OfflineDisk {
|
||||
metaErrs[i] = errDiskNotFound
|
||||
continue
|
||||
}
|
||||
|
||||
meta := partsMetadata[i]
|
||||
if !meta.ModTime.Equal(latestMeta.ModTime) || meta.DataDir != latestMeta.DataDir {
|
||||
corrupted := false
|
||||
if filterByETag {
|
||||
corrupted = meta.Metadata["etag"] != latestMeta.Metadata["etag"]
|
||||
} else {
|
||||
corrupted = !meta.ModTime.Equal(latestMeta.ModTime) || meta.DataDir != latestMeta.DataDir
|
||||
}
|
||||
|
||||
if corrupted {
|
||||
metaErrs[i] = errFileCorrupt
|
||||
partsMetadata[i] = FileInfo{}
|
||||
onlineDisks[i] = nil
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -355,6 +361,7 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
||||
if !meta.IsValid() {
|
||||
partsMetadata[i] = FileInfo{}
|
||||
metaErrs[i] = errFileCorrupt
|
||||
onlineDisks[i] = nil
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -365,6 +372,7 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
||||
// might have the right erasure distribution.
|
||||
partsMetadata[i] = FileInfo{}
|
||||
metaErrs[i] = errFileCorrupt
|
||||
onlineDisks[i] = nil
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -385,8 +393,8 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
||||
if metaErrs[i] != nil {
|
||||
continue
|
||||
}
|
||||
meta := partsMetadata[i]
|
||||
|
||||
meta := partsMetadata[i]
|
||||
if meta.Deleted || meta.IsRemote() {
|
||||
continue
|
||||
}
|
||||
@@ -408,7 +416,6 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
||||
verifyResp *CheckPartsResp
|
||||
)
|
||||
|
||||
meta.DataDir = latestMeta.DataDir
|
||||
switch scanMode {
|
||||
case madmin.HealDeepScan:
|
||||
// disk has a valid xl.meta but may not have all the
|
||||
@@ -434,16 +441,5 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
||||
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.
|
||||
availableDisks[i] = onlineDisk
|
||||
} else {
|
||||
// upon errors just make that disk's fileinfo invalid
|
||||
partsMetadata[i] = FileInfo{}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -313,11 +313,11 @@ func TestListOnlineDisks(t *testing.T) {
|
||||
t.Fatalf("Expected modTime to be equal to %v but was found to be %v",
|
||||
test.expectedTime, modTime)
|
||||
}
|
||||
availableDisks, _, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||
test.errs, fi, bucket, object, madmin.HealDeepScan)
|
||||
_, _ = checkObjectWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||
test.errs, fi, false, bucket, object, madmin.HealDeepScan)
|
||||
|
||||
if test._tamperBackend != noTamper {
|
||||
if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil {
|
||||
if tamperedIndex != -1 && onlineDisks[tamperedIndex] != nil {
|
||||
t.Fatalf("Drive (%v) with part.1 missing is not a drive with available data",
|
||||
erasureDisks[tamperedIndex])
|
||||
}
|
||||
@@ -495,11 +495,11 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
|
||||
test.expectedTime, modTime)
|
||||
}
|
||||
|
||||
availableDisks, _, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||
test.errs, fi, bucket, object, madmin.HealDeepScan)
|
||||
_, _ = checkObjectWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||
test.errs, fi, false, bucket, object, madmin.HealDeepScan)
|
||||
|
||||
if test._tamperBackend != noTamper {
|
||||
if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil {
|
||||
if tamperedIndex != -1 && onlineDisks[tamperedIndex] != nil {
|
||||
t.Fatalf("Drive (%v) with part.1 missing is not a drive with available data",
|
||||
erasureDisks[tamperedIndex])
|
||||
}
|
||||
@@ -545,6 +545,7 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
|
||||
// Test 1: Test that all disks are returned without any failures with
|
||||
// unmodified meta data
|
||||
erasureDisks = s.getDisks()
|
||||
partsMetadata, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read xl meta data %v", err)
|
||||
@@ -557,14 +558,10 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
|
||||
erasureDisks, _, _ = listOnlineDisks(erasureDisks, partsMetadata, errs, readQuorum)
|
||||
|
||||
filteredDisks, _, dataErrsPerDisk := disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||
errs, fi, bucket, object, madmin.HealDeepScan)
|
||||
dataErrsPerDisk, _ := checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||
errs, fi, false, bucket, object, madmin.HealDeepScan)
|
||||
|
||||
if len(filteredDisks) != len(erasureDisks) {
|
||||
t.Errorf("Unexpected number of drives: %d", len(filteredDisks))
|
||||
}
|
||||
|
||||
for diskIndex, disk := range filteredDisks {
|
||||
for diskIndex, disk := range erasureDisks {
|
||||
if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
|
||||
t.Errorf("Unexpected error: %v", dataErrsPerDisk[diskIndex])
|
||||
}
|
||||
@@ -575,17 +572,15 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test 2: Not synchronized modtime
|
||||
erasureDisks = s.getDisks()
|
||||
partsMetadataBackup := partsMetadata[0]
|
||||
partsMetadata[0].ModTime = partsMetadata[0].ModTime.Add(-1 * time.Hour)
|
||||
|
||||
errs = make([]error, len(erasureDisks))
|
||||
filteredDisks, _, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||
errs, fi, bucket, object, madmin.HealDeepScan)
|
||||
_, _ = checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||
errs, fi, false, bucket, object, madmin.HealDeepScan)
|
||||
|
||||
if len(filteredDisks) != len(erasureDisks) {
|
||||
t.Errorf("Unexpected number of drives: %d", len(filteredDisks))
|
||||
}
|
||||
for diskIndex, disk := range filteredDisks {
|
||||
for diskIndex, disk := range erasureDisks {
|
||||
if diskIndex == 0 && disk != nil {
|
||||
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
|
||||
}
|
||||
@@ -596,17 +591,15 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
partsMetadata[0] = partsMetadataBackup // Revert before going to the next test
|
||||
|
||||
// Test 3: Not synchronized DataDir
|
||||
erasureDisks = s.getDisks()
|
||||
partsMetadataBackup = partsMetadata[1]
|
||||
partsMetadata[1].DataDir = "foo-random"
|
||||
|
||||
errs = make([]error, len(erasureDisks))
|
||||
filteredDisks, _, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||
errs, fi, bucket, object, madmin.HealDeepScan)
|
||||
_, _ = checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||
errs, fi, false, bucket, object, madmin.HealDeepScan)
|
||||
|
||||
if len(filteredDisks) != len(erasureDisks) {
|
||||
t.Errorf("Unexpected number of drives: %d", len(filteredDisks))
|
||||
}
|
||||
for diskIndex, disk := range filteredDisks {
|
||||
for diskIndex, disk := range erasureDisks {
|
||||
if diskIndex == 1 && disk != nil {
|
||||
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
|
||||
}
|
||||
@@ -617,6 +610,7 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
partsMetadata[1] = partsMetadataBackup // Revert before going to the next test
|
||||
|
||||
// Test 4: key = disk index, value = part name with hash mismatch
|
||||
erasureDisks = s.getDisks()
|
||||
diskFailures := make(map[int]string)
|
||||
diskFailures[0] = "part.1"
|
||||
diskFailures[3] = "part.1"
|
||||
@@ -637,29 +631,18 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
}
|
||||
|
||||
errs = make([]error, len(erasureDisks))
|
||||
filteredDisks, dataErrsPerDisk, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||
errs, fi, bucket, object, madmin.HealDeepScan)
|
||||
dataErrsPerDisk, _ = checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||
errs, fi, false, bucket, object, madmin.HealDeepScan)
|
||||
|
||||
if len(filteredDisks) != len(erasureDisks) {
|
||||
t.Errorf("Unexpected number of drives: %d", len(filteredDisks))
|
||||
}
|
||||
|
||||
for diskIndex, disk := range filteredDisks {
|
||||
for diskIndex := range erasureDisks {
|
||||
if _, ok := diskFailures[diskIndex]; ok {
|
||||
if disk != nil {
|
||||
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
|
||||
}
|
||||
if !partNeedsHealing(dataErrsPerDisk[diskIndex]) {
|
||||
t.Errorf("Disk expected to be healed, driveIndex: %d", diskIndex)
|
||||
}
|
||||
} else {
|
||||
if disk == nil {
|
||||
t.Errorf("Drive erroneously filtered, driveIndex: %d", diskIndex)
|
||||
}
|
||||
if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
|
||||
t.Errorf("Disk not expected to be healed, driveIndex: %d", diskIndex)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+116
-82
@@ -32,7 +32,7 @@ import (
|
||||
"github.com/minio/minio/internal/grid"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/v3/sync/errgroup"
|
||||
"golang.org/x/exp/slices"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=healingMetric -trimprefix=healingMetric $GOFILE
|
||||
@@ -45,7 +45,8 @@ const (
|
||||
healingMetricCheckAbandonedParts
|
||||
)
|
||||
|
||||
func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string, scanMode madmin.HealScanMode, healEntry func(string, metaCacheEntry, madmin.HealScanMode) error) error {
|
||||
// List a prefix or a single object versions and heal
|
||||
func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string, recursive bool, scanMode madmin.HealScanMode, healEntry func(string, metaCacheEntry, madmin.HealScanMode) error) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
@@ -78,11 +79,14 @@ func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string,
|
||||
bucket: bucket,
|
||||
path: path,
|
||||
filterPrefix: filterPrefix,
|
||||
recursive: true,
|
||||
recursive: recursive,
|
||||
forwardTo: "",
|
||||
minDisks: 1,
|
||||
reportNotFound: false,
|
||||
agreed: func(entry metaCacheEntry) {
|
||||
if !recursive && prefix != entry.name {
|
||||
return
|
||||
}
|
||||
if err := healEntry(bucket, entry, scanMode); err != nil {
|
||||
cancel()
|
||||
}
|
||||
@@ -94,7 +98,9 @@ func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string,
|
||||
// proceed to heal nonetheless.
|
||||
entry, _ = entries.firstFound()
|
||||
}
|
||||
|
||||
if !recursive && prefix != entry.name {
|
||||
return
|
||||
}
|
||||
if err := healEntry(bucket, *entry, scanMode); err != nil {
|
||||
cancel()
|
||||
return
|
||||
@@ -112,9 +118,8 @@ func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string,
|
||||
|
||||
// listAllBuckets lists all buckets from all disks. It also
|
||||
// returns the occurrence of each buckets in all disks
|
||||
func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets map[string]VolInfo, readQuorum int) error {
|
||||
func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets *xsync.MapOf[string, VolInfo], readQuorum int) error {
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
var mu sync.Mutex
|
||||
for index := range storageDisks {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
@@ -126,6 +131,7 @@ func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, volInfo := range volsInfo {
|
||||
// StorageAPI can send volume names which are
|
||||
// incompatible with buckets - these are
|
||||
@@ -133,53 +139,75 @@ func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets
|
||||
if isReservedOrInvalidBucket(volInfo.Name, false) {
|
||||
continue
|
||||
}
|
||||
mu.Lock()
|
||||
if _, ok := healBuckets[volInfo.Name]; !ok {
|
||||
healBuckets[volInfo.Name] = volInfo
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
healBuckets.Compute(volInfo.Name, func(oldValue VolInfo, loaded bool) (newValue VolInfo, del bool) {
|
||||
if loaded {
|
||||
newValue = oldValue
|
||||
newValue.count = oldValue.count + 1
|
||||
return newValue, false
|
||||
}
|
||||
return VolInfo{
|
||||
Name: volInfo.Name,
|
||||
Created: volInfo.Created,
|
||||
count: 1,
|
||||
}, false
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
return reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, readQuorum)
|
||||
|
||||
if err := reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, readQuorum); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
healBuckets.Range(func(volName string, volInfo VolInfo) bool {
|
||||
if volInfo.count < readQuorum {
|
||||
healBuckets.Delete(volName)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var errLegacyXLMeta = errors.New("legacy XL meta")
|
||||
|
||||
var errOutdatedXLMeta = errors.New("outdated XL meta")
|
||||
|
||||
var errPartMissingOrCorrupt = errors.New("part missing or corrupt")
|
||||
var (
|
||||
errLegacyXLMeta = errors.New("legacy XL meta")
|
||||
errOutdatedXLMeta = errors.New("outdated XL meta")
|
||||
errPartCorrupt = errors.New("part corrupt")
|
||||
errPartMissing = errors.New("part missing")
|
||||
)
|
||||
|
||||
// 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.
|
||||
func shouldHealObjectOnDisk(erErr error, partsErrs []int, meta FileInfo, latestMeta FileInfo) (bool, error) {
|
||||
func shouldHealObjectOnDisk(erErr error, partsErrs []int, meta FileInfo, latestMeta FileInfo) (bool, bool, error) {
|
||||
if errors.Is(erErr, errFileNotFound) || errors.Is(erErr, errFileVersionNotFound) || errors.Is(erErr, errFileCorrupt) {
|
||||
return true, erErr
|
||||
return true, true, erErr
|
||||
}
|
||||
if erErr == nil {
|
||||
if meta.XLV1 {
|
||||
// Legacy means heal always
|
||||
// always check first.
|
||||
return true, errLegacyXLMeta
|
||||
return true, true, errLegacyXLMeta
|
||||
}
|
||||
if !latestMeta.Equals(meta) {
|
||||
return true, errOutdatedXLMeta
|
||||
return true, true, errOutdatedXLMeta
|
||||
}
|
||||
if !meta.Deleted && !meta.IsRemote() {
|
||||
// If xl.meta was read fine but there may be problem with the part.N files.
|
||||
for _, partErr := range partsErrs {
|
||||
if slices.Contains([]int{
|
||||
checkPartFileNotFound,
|
||||
checkPartFileCorrupt,
|
||||
}, partErr) {
|
||||
return true, errPartMissingOrCorrupt
|
||||
if partErr == checkPartFileNotFound {
|
||||
return true, false, errPartMissing
|
||||
}
|
||||
if partErr == checkPartFileCorrupt {
|
||||
return true, false, errPartCorrupt
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
return false, false, nil
|
||||
}
|
||||
return false, erErr
|
||||
return false, false, erErr
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -234,11 +262,12 @@ func (er *erasureObjects) auditHealObject(ctx context.Context, bucket, object, v
|
||||
}
|
||||
|
||||
b, a := result.GetCorruptedCounts()
|
||||
if b == a {
|
||||
if b > 0 && b == a {
|
||||
opts.Error = fmt.Sprintf("unable to heal %d corrupted blocks on drives", b)
|
||||
}
|
||||
|
||||
b, a = result.GetMissingCounts()
|
||||
if b == a {
|
||||
if b > 0 && b == a {
|
||||
opts.Error = fmt.Sprintf("unable to heal %d missing blocks on drives", b)
|
||||
}
|
||||
|
||||
@@ -253,6 +282,21 @@ func (er *erasureObjects) auditHealObject(ctx context.Context, bucket, object, v
|
||||
auditLogInternal(ctx, opts)
|
||||
}
|
||||
|
||||
func objectErrToDriveState(reason error) string {
|
||||
switch {
|
||||
case reason == nil:
|
||||
return madmin.DriveStateOk
|
||||
case IsErr(reason, errDiskNotFound):
|
||||
return madmin.DriveStateOffline
|
||||
case IsErr(reason, errFileNotFound, errFileVersionNotFound, errVolumeNotFound, errPartMissing, errOutdatedXLMeta, errLegacyXLMeta):
|
||||
return madmin.DriveStateMissing
|
||||
case IsErr(reason, errFileCorrupt, errPartCorrupt):
|
||||
return madmin.DriveStateCorrupt
|
||||
default:
|
||||
return fmt.Sprintf("%s (%s)", madmin.DriveStateUnknown, reason.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Heals an object by re-writing corrupt/missing erasure blocks.
|
||||
func (er *erasureObjects) healObject(ctx context.Context, bucket string, object string, versionID string, opts madmin.HealOpts) (result madmin.HealResultItem, err error) {
|
||||
dryRun := opts.DryRun
|
||||
@@ -327,26 +371,20 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
|
||||
// List of disks having latest version of the object xl.meta
|
||||
// (by modtime).
|
||||
onlineDisks, modTime, etag := listOnlineDisks(storageDisks, partsMetadata, errs, readQuorum)
|
||||
onlineDisks, quorumModTime, quorumETag := listOnlineDisks(storageDisks, partsMetadata, errs, readQuorum)
|
||||
|
||||
// Latest FileInfo for reference. If a valid metadata is not
|
||||
// present, it is as good as object not found.
|
||||
latestMeta, err := pickValidFileInfo(ctx, partsMetadata, modTime, etag, readQuorum)
|
||||
latestMeta, err := pickValidFileInfo(ctx, partsMetadata, quorumModTime, quorumETag, readQuorum)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// List of disks having all parts as per latest metadata.
|
||||
// NOTE: do not pass in latestDisks to diskWithAllParts since
|
||||
// the diskWithAllParts needs to reach the drive to ensure
|
||||
// validity of the metadata content, we should make sure that
|
||||
// we pass in disks as is for it to be verified. Once verified
|
||||
// the disksWithAllParts() returns the actual disks that can be
|
||||
// used here for reconstruction. This is done to ensure that
|
||||
// we do not skip drives that have inconsistent metadata to be
|
||||
// skipped from purging when they are stale.
|
||||
availableDisks, dataErrsByDisk, dataErrsByPart := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||
errs, latestMeta, bucket, object, scanMode)
|
||||
// No modtime quorum
|
||||
filterDisksByETag := quorumETag != ""
|
||||
|
||||
dataErrsByDisk, dataErrsByPart := checkObjectWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||
errs, latestMeta, filterDisksByETag, bucket, object, scanMode)
|
||||
|
||||
var erasure Erasure
|
||||
if !latestMeta.Deleted && !latestMeta.IsRemote() {
|
||||
@@ -367,26 +405,18 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
// data state and a list of outdated disks on which data needs
|
||||
// to be healed.
|
||||
outDatedDisks := make([]StorageAPI, len(storageDisks))
|
||||
disksToHealCount := 0
|
||||
for i := range availableDisks {
|
||||
yes, reason := shouldHealObjectOnDisk(errs[i], dataErrsByDisk[i], partsMetadata[i], latestMeta)
|
||||
disksToHealCount, xlMetaToHealCount := 0, 0
|
||||
for i := range onlineDisks {
|
||||
yes, isMeta, reason := shouldHealObjectOnDisk(errs[i], dataErrsByDisk[i], partsMetadata[i], latestMeta)
|
||||
if yes {
|
||||
outDatedDisks[i] = storageDisks[i]
|
||||
disksToHealCount++
|
||||
if isMeta {
|
||||
xlMetaToHealCount++
|
||||
}
|
||||
}
|
||||
|
||||
driveState := ""
|
||||
switch {
|
||||
case reason == nil:
|
||||
driveState = madmin.DriveStateOk
|
||||
case IsErr(reason, errDiskNotFound):
|
||||
driveState = madmin.DriveStateOffline
|
||||
case IsErr(reason, errFileNotFound, errFileVersionNotFound, errVolumeNotFound, errPartMissingOrCorrupt, errOutdatedXLMeta, errLegacyXLMeta):
|
||||
driveState = madmin.DriveStateMissing
|
||||
default:
|
||||
// all remaining cases imply corrupt data/metadata
|
||||
driveState = madmin.DriveStateCorrupt
|
||||
}
|
||||
driveState := objectErrToDriveState(reason)
|
||||
|
||||
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
@@ -400,16 +430,6 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
})
|
||||
}
|
||||
|
||||
if isAllNotFound(errs) {
|
||||
// File is fully gone, fileInfo is empty.
|
||||
err := errFileNotFound
|
||||
if versionID != "" {
|
||||
err = errFileVersionNotFound
|
||||
}
|
||||
return er.defaultHealResult(FileInfo{}, storageDisks, storageEndpoints, errs,
|
||||
bucket, object, versionID), err
|
||||
}
|
||||
|
||||
if disksToHealCount == 0 {
|
||||
// Nothing to heal!
|
||||
return result, nil
|
||||
@@ -421,7 +441,24 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if !latestMeta.XLV1 && !latestMeta.Deleted && disksToHealCount > latestMeta.Erasure.ParityBlocks {
|
||||
cannotHeal := !latestMeta.XLV1 && !latestMeta.Deleted && xlMetaToHealCount > latestMeta.Erasure.ParityBlocks
|
||||
if cannotHeal && quorumETag != "" {
|
||||
// This is an object that is supposed to be removed by the dangling code
|
||||
// but we noticed that ETag is the same for all objects, let's give it a shot
|
||||
cannotHeal = false
|
||||
}
|
||||
|
||||
if !latestMeta.Deleted && !latestMeta.IsRemote() {
|
||||
// check if there is a part that lost its quorum
|
||||
for _, partErrs := range dataErrsByPart {
|
||||
if countPartNotSuccess(partErrs) > latestMeta.Erasure.ParityBlocks {
|
||||
cannotHeal = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cannotHeal {
|
||||
// Allow for dangling deletes, on versions that have DataDir missing etc.
|
||||
// this would end up restoring the correct readable versions.
|
||||
m, err := er.deleteIfDangling(ctx, bucket, object, partsMetadata, errs, dataErrsByPart, ObjectOptions{
|
||||
@@ -459,16 +496,15 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
tmpID := mustGetUUID()
|
||||
migrateDataDir := mustGetUUID()
|
||||
|
||||
// Reorder so that we have data disks first and parity disks next.
|
||||
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(availableDisks) {
|
||||
err := fmt.Errorf("unexpected file distribution (%v) from available disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
|
||||
latestMeta.Erasure.Distribution, availableDisks, bucket, object, versionID)
|
||||
healingLogOnceIf(ctx, err, "heal-object-available-disks")
|
||||
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(onlineDisks) {
|
||||
err := fmt.Errorf("unexpected file distribution (%v) from online disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
|
||||
latestMeta.Erasure.Distribution, onlineDisks, bucket, object, versionID)
|
||||
healingLogOnceIf(ctx, err, "heal-object-online-disks")
|
||||
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
|
||||
bucket, object, versionID), err
|
||||
}
|
||||
|
||||
latestDisks := shuffleDisks(availableDisks, latestMeta.Erasure.Distribution)
|
||||
latestDisks := shuffleDisks(onlineDisks, latestMeta.Erasure.Distribution)
|
||||
|
||||
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(outDatedDisks) {
|
||||
err := fmt.Errorf("unexpected file distribution (%v) from outdated disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
|
||||
@@ -539,6 +575,10 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
thisPartErrs := shuffleCheckParts(dataErrsByPart[partIndex], latestMeta.Erasure.Distribution)
|
||||
if thisPartErrs[i] != checkPartSuccess {
|
||||
continue
|
||||
}
|
||||
checksumInfo := copyPartsMetadata[i].Erasure.GetChecksumInfo(partNumber)
|
||||
partPath := pathJoin(object, srcDataDir, fmt.Sprintf("part.%d", partNumber))
|
||||
readers[i] = newBitrotReader(disk, copyPartsMetadata[i].Data, bucket, partPath, tillOffset, checksumAlgo,
|
||||
@@ -806,13 +846,7 @@ func (er *erasureObjects) defaultHealResult(lfi FileInfo, storageDisks []Storage
|
||||
})
|
||||
continue
|
||||
}
|
||||
driveState := madmin.DriveStateCorrupt
|
||||
switch errs[index] {
|
||||
case errFileNotFound, errVolumeNotFound:
|
||||
driveState = madmin.DriveStateMissing
|
||||
case nil:
|
||||
driveState = madmin.DriveStateOk
|
||||
}
|
||||
driveState := objectErrToDriveState(errs[index])
|
||||
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: storageEndpoints[index].String(),
|
||||
|
||||
@@ -733,7 +733,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
||||
t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions)
|
||||
}
|
||||
|
||||
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Remove: true},
|
||||
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Recursive: true, Remove: true},
|
||||
func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
|
||||
_, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
|
||||
return err
|
||||
@@ -780,7 +780,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
||||
t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions)
|
||||
}
|
||||
|
||||
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Remove: true},
|
||||
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Recursive: true, Remove: true},
|
||||
func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
|
||||
_, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
|
||||
return err
|
||||
@@ -829,7 +829,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
||||
t.Fatalf("Expected versions 3, got %d", fileInfoPreHeal.NumVersions)
|
||||
}
|
||||
|
||||
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Remove: true},
|
||||
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Recursive: true, Remove: true},
|
||||
func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
|
||||
_, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
|
||||
return err
|
||||
|
||||
@@ -52,15 +52,15 @@ func reduceCommonVersions(diskVersions [][]byte, writeQuorum int) (versions []by
|
||||
}
|
||||
|
||||
var commonVersions uint64
|
||||
max := 0
|
||||
maxCnt := 0
|
||||
for versions, count := range diskVersionsCount {
|
||||
if max < count {
|
||||
max = count
|
||||
if maxCnt < count {
|
||||
maxCnt = count
|
||||
commonVersions = versions
|
||||
}
|
||||
}
|
||||
|
||||
if max >= writeQuorum {
|
||||
if maxCnt >= writeQuorum {
|
||||
for _, versions := range diskVersions {
|
||||
if binary.BigEndian.Uint64(versions) == commonVersions {
|
||||
return versions
|
||||
@@ -80,15 +80,15 @@ func reduceCommonDataDir(dataDirs []string, writeQuorum int) (dataDir string) {
|
||||
dataDirsCount[ddir]++
|
||||
}
|
||||
|
||||
max := 0
|
||||
maxCnt := 0
|
||||
for ddir, count := range dataDirsCount {
|
||||
if max < count {
|
||||
max = count
|
||||
if maxCnt < count {
|
||||
maxCnt = count
|
||||
dataDir = ddir
|
||||
}
|
||||
}
|
||||
|
||||
if max >= writeQuorum {
|
||||
if maxCnt >= writeQuorum {
|
||||
return dataDir
|
||||
}
|
||||
|
||||
@@ -115,20 +115,20 @@ func reduceErrs(errs []error, ignoredErrs []error) (maxCount int, maxErr error)
|
||||
errorCounts[err]++
|
||||
}
|
||||
|
||||
max := 0
|
||||
maxCnt := 0
|
||||
for err, count := range errorCounts {
|
||||
switch {
|
||||
case max < count:
|
||||
max = count
|
||||
case maxCnt < count:
|
||||
maxCnt = count
|
||||
maxErr = err
|
||||
|
||||
// Prefer `nil` over other error values with the same
|
||||
// number of occurrences.
|
||||
case max == count && err == nil:
|
||||
case maxCnt == count && err == nil:
|
||||
maxErr = err
|
||||
}
|
||||
}
|
||||
return max, maxErr
|
||||
return maxCnt, maxErr
|
||||
}
|
||||
|
||||
// reduceQuorumErrs behaves like reduceErrs by only for returning
|
||||
@@ -295,34 +295,34 @@ func shuffleDisksAndPartsMetadata(disks []StorageAPI, partsMetadata []FileInfo,
|
||||
return shuffledDisks, shuffledPartsMetadata
|
||||
}
|
||||
|
||||
// Return shuffled partsMetadata depending on distribution.
|
||||
func shufflePartsMetadata(partsMetadata []FileInfo, distribution []int) (shuffledPartsMetadata []FileInfo) {
|
||||
func shuffleWithDist[T any](input []T, distribution []int) []T {
|
||||
if distribution == nil {
|
||||
return partsMetadata
|
||||
return input
|
||||
}
|
||||
shuffledPartsMetadata = make([]FileInfo, len(partsMetadata))
|
||||
// Shuffle slice xl metadata for expected distribution.
|
||||
for index := range partsMetadata {
|
||||
shuffled := make([]T, len(input))
|
||||
for index := range input {
|
||||
blockIndex := distribution[index]
|
||||
shuffledPartsMetadata[blockIndex-1] = partsMetadata[index]
|
||||
shuffled[blockIndex-1] = input[index]
|
||||
}
|
||||
return shuffledPartsMetadata
|
||||
return shuffled
|
||||
}
|
||||
|
||||
// Return shuffled partsMetadata depending on distribution.
|
||||
func shufflePartsMetadata(partsMetadata []FileInfo, distribution []int) []FileInfo {
|
||||
return shuffleWithDist[FileInfo](partsMetadata, distribution)
|
||||
}
|
||||
|
||||
// shuffleCheckParts - shuffle CheckParts slice depending on the
|
||||
// erasure distribution.
|
||||
func shuffleCheckParts(parts []int, distribution []int) []int {
|
||||
return shuffleWithDist[int](parts, distribution)
|
||||
}
|
||||
|
||||
// shuffleDisks - shuffle input disks slice depending on the
|
||||
// erasure distribution. Return shuffled slice of disks with
|
||||
// their expected distribution.
|
||||
func shuffleDisks(disks []StorageAPI, distribution []int) (shuffledDisks []StorageAPI) {
|
||||
if distribution == nil {
|
||||
return disks
|
||||
}
|
||||
shuffledDisks = make([]StorageAPI, len(disks))
|
||||
// Shuffle disks for expected distribution.
|
||||
for index := range disks {
|
||||
blockIndex := distribution[index]
|
||||
shuffledDisks[blockIndex-1] = disks[index]
|
||||
}
|
||||
return shuffledDisks
|
||||
func shuffleDisks(disks []StorageAPI, distribution []int) []StorageAPI {
|
||||
return shuffleWithDist[StorageAPI](disks, distribution)
|
||||
}
|
||||
|
||||
// evalDisks - returns a new slice of disks where nil is set if
|
||||
|
||||
@@ -514,8 +514,10 @@ func listObjectParities(partsMetadata []FileInfo, errs []error) (parities []int)
|
||||
if metadata.Deleted || metadata.Size == 0 {
|
||||
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)
|
||||
// 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.
|
||||
// However, we never go below EcM, in case of a EcM=EcN setup.
|
||||
parities[index] = max(totalShards-(totalShards/2+1), metadata.Erasure.ParityBlocks)
|
||||
} else {
|
||||
parities[index] = metadata.Erasure.ParityBlocks
|
||||
}
|
||||
|
||||
+85
-92
@@ -480,6 +480,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
||||
|
||||
if opts.WantChecksum != nil && opts.WantChecksum.Type.IsSet() {
|
||||
userDefined[hash.MinIOMultipartChecksum] = opts.WantChecksum.Type.String()
|
||||
userDefined[hash.MinIOMultipartChecksumType] = opts.WantChecksum.Type.ObjType()
|
||||
}
|
||||
|
||||
modTime := opts.MTime
|
||||
@@ -508,6 +509,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
||||
return &NewMultipartUploadResult{
|
||||
UploadID: uploadID,
|
||||
ChecksumAlgo: userDefined[hash.MinIOMultipartChecksum],
|
||||
ChecksumType: userDefined[hash.MinIOMultipartChecksumType],
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -526,6 +528,14 @@ func (er erasureObjects) NewMultipartUpload(ctx context.Context, bucket, object
|
||||
|
||||
// renamePart - renames multipart part to its relevant location under uploadID.
|
||||
func (er erasureObjects) renamePart(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, dstBucket, dstEntry string, optsMeta []byte, writeQuorum int) ([]StorageAPI, error) {
|
||||
paths := []string{
|
||||
dstEntry,
|
||||
dstEntry + ".meta",
|
||||
}
|
||||
|
||||
// cleanup existing paths first across all drives.
|
||||
er.cleanupMultipartPath(ctx, paths...)
|
||||
|
||||
g := errgroup.WithNErrs(len(disks))
|
||||
|
||||
// Rename file on all underlying storage disks.
|
||||
@@ -542,11 +552,6 @@ func (er erasureObjects) renamePart(ctx context.Context, disks []StorageAPI, src
|
||||
// Wait for all renames to finish.
|
||||
errs := g.Wait()
|
||||
|
||||
paths := []string{
|
||||
dstEntry,
|
||||
dstEntry + ".meta",
|
||||
}
|
||||
|
||||
err := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||
if err != nil {
|
||||
er.cleanupMultipartPath(ctx, paths...)
|
||||
@@ -574,19 +579,9 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
return pi, toObjectErr(errInvalidArgument)
|
||||
}
|
||||
|
||||
// Read lock for upload id.
|
||||
// Only held while reading the upload metadata.
|
||||
uploadIDRLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
rlkctx, err := uploadIDRLock.GetRLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return PartInfo{}, err
|
||||
}
|
||||
rctx := rlkctx.Context()
|
||||
defer uploadIDRLock.RUnlock(rlkctx)
|
||||
|
||||
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
|
||||
// Validates if upload ID exists.
|
||||
fi, _, err := er.checkUploadIDExists(rctx, bucket, object, uploadID, true)
|
||||
fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, true)
|
||||
if err != nil {
|
||||
if errors.Is(err, errVolumeNotFound) {
|
||||
return pi, toObjectErr(err, bucket)
|
||||
@@ -633,17 +628,13 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
switch size := data.Size(); {
|
||||
case size == 0:
|
||||
buffer = make([]byte, 1) // Allocate at least a byte to reach EOF
|
||||
case size == -1:
|
||||
if size := data.ActualSize(); size > 0 && size < fi.Erasure.BlockSize {
|
||||
// Account for padding and forced compression overhead and encryption.
|
||||
buffer = make([]byte, data.ActualSize()+256+32+32, data.ActualSize()*2+512)
|
||||
case size >= fi.Erasure.BlockSize || size == -1:
|
||||
if int64(globalBytePoolCap.Load().Width()) < fi.Erasure.BlockSize {
|
||||
buffer = make([]byte, fi.Erasure.BlockSize, 2*fi.Erasure.BlockSize)
|
||||
} else {
|
||||
buffer = globalBytePoolCap.Load().Get()
|
||||
defer globalBytePoolCap.Load().Put(buffer)
|
||||
}
|
||||
case size >= fi.Erasure.BlockSize:
|
||||
buffer = globalBytePoolCap.Load().Get()
|
||||
defer globalBytePoolCap.Load().Put(buffer)
|
||||
case size < fi.Erasure.BlockSize:
|
||||
// No need to allocate fully fi.Erasure.BlockSize buffer if the incoming data is smaller.
|
||||
buffer = make([]byte, size, 2*size+int64(fi.Erasure.ParityBlocks+fi.Erasure.DataBlocks-1))
|
||||
@@ -741,10 +732,7 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
||||
}
|
||||
|
||||
// Write lock for this part ID, only hold it if we are planning to read from the
|
||||
// stream avoid any concurrent updates.
|
||||
//
|
||||
// Must be held throughout this call.
|
||||
// Serialize concurrent part uploads.
|
||||
partIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID, strconv.Itoa(partID)))
|
||||
plkctx, err := partIDLock.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
@@ -754,6 +742,15 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
ctx = plkctx.Context()
|
||||
defer partIDLock.Unlock(plkctx)
|
||||
|
||||
// Read lock for upload id, only held while reading the upload metadata.
|
||||
uploadIDRLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
rlkctx, err := uploadIDRLock.GetRLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return PartInfo{}, err
|
||||
}
|
||||
ctx = rlkctx.Context()
|
||||
defer uploadIDRLock.RUnlock(rlkctx)
|
||||
|
||||
onlineDisks, err = er.renamePart(ctx, onlineDisks, minioMetaTmpBucket, tmpPartPath, minioMetaMultipartBucket, partPath, partFI, writeQuorum)
|
||||
if err != nil {
|
||||
if errors.Is(err, errFileNotFound) {
|
||||
@@ -770,15 +767,16 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
|
||||
// Return success.
|
||||
return PartInfo{
|
||||
PartNumber: partInfo.Number,
|
||||
ETag: partInfo.ETag,
|
||||
LastModified: partInfo.ModTime,
|
||||
Size: partInfo.Size,
|
||||
ActualSize: partInfo.ActualSize,
|
||||
ChecksumCRC32: partInfo.Checksums["CRC32"],
|
||||
ChecksumCRC32C: partInfo.Checksums["CRC32C"],
|
||||
ChecksumSHA1: partInfo.Checksums["SHA1"],
|
||||
ChecksumSHA256: partInfo.Checksums["SHA256"],
|
||||
PartNumber: partInfo.Number,
|
||||
ETag: partInfo.ETag,
|
||||
LastModified: partInfo.ModTime,
|
||||
Size: partInfo.Size,
|
||||
ActualSize: partInfo.ActualSize,
|
||||
ChecksumCRC32: partInfo.Checksums["CRC32"],
|
||||
ChecksumCRC32C: partInfo.Checksums["CRC32C"],
|
||||
ChecksumSHA1: partInfo.Checksums["SHA1"],
|
||||
ChecksumSHA256: partInfo.Checksums["SHA256"],
|
||||
ChecksumCRC64NVME: partInfo.Checksums["CRC64NVME"],
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -798,14 +796,6 @@ func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, u
|
||||
UploadID: uploadID,
|
||||
}
|
||||
|
||||
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return MultipartInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer uploadIDLock.RUnlock(lkctx)
|
||||
|
||||
fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
|
||||
if err != nil {
|
||||
if errors.Is(err, errVolumeNotFound) {
|
||||
@@ -885,14 +875,6 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
||||
auditObjectErasureSet(ctx, "ListObjectParts", object, &er)
|
||||
}
|
||||
|
||||
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return ListPartsInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer uploadIDLock.RUnlock(lkctx)
|
||||
|
||||
fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
|
||||
if err != nil {
|
||||
return result, toObjectErr(err, bucket, object, uploadID)
|
||||
@@ -916,6 +898,7 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
||||
result.PartNumberMarker = partNumberMarker
|
||||
result.UserDefined = cloneMSS(fi.Metadata)
|
||||
result.ChecksumAlgorithm = fi.Metadata[hash.MinIOMultipartChecksum]
|
||||
result.ChecksumType = fi.Metadata[hash.MinIOMultipartChecksumType]
|
||||
|
||||
if maxParts == 0 {
|
||||
return result, nil
|
||||
@@ -962,15 +945,16 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
||||
count := maxParts
|
||||
for _, objPart := range objParts {
|
||||
result.Parts = append(result.Parts, PartInfo{
|
||||
PartNumber: objPart.Number,
|
||||
LastModified: objPart.ModTime,
|
||||
ETag: objPart.ETag,
|
||||
Size: objPart.Size,
|
||||
ActualSize: objPart.ActualSize,
|
||||
ChecksumCRC32: objPart.Checksums["CRC32"],
|
||||
ChecksumCRC32C: objPart.Checksums["CRC32C"],
|
||||
ChecksumSHA1: objPart.Checksums["SHA1"],
|
||||
ChecksumSHA256: objPart.Checksums["SHA256"],
|
||||
PartNumber: objPart.Number,
|
||||
LastModified: objPart.ModTime,
|
||||
ETag: objPart.ETag,
|
||||
Size: objPart.Size,
|
||||
ActualSize: objPart.ActualSize,
|
||||
ChecksumCRC32: objPart.Checksums["CRC32"],
|
||||
ChecksumCRC32C: objPart.Checksums["CRC32C"],
|
||||
ChecksumSHA1: objPart.Checksums["SHA1"],
|
||||
ChecksumSHA256: objPart.Checksums["SHA256"],
|
||||
ChecksumCRC64NVME: objPart.Checksums["CRC64NVME"],
|
||||
})
|
||||
count--
|
||||
if count == 0 {
|
||||
@@ -1115,16 +1099,6 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
}
|
||||
}
|
||||
|
||||
// Hold write locks to verify uploaded parts, also disallows any
|
||||
// parallel PutObjectPart() requests.
|
||||
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
wlkctx, err := uploadIDLock.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return oi, err
|
||||
}
|
||||
ctx = wlkctx.Context()
|
||||
defer uploadIDLock.Unlock(wlkctx)
|
||||
|
||||
fi, partsMetadata, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, true)
|
||||
if err != nil {
|
||||
if errors.Is(err, errVolumeNotFound) {
|
||||
@@ -1162,12 +1136,12 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
// Checksum type set when upload started.
|
||||
var checksumType hash.ChecksumType
|
||||
if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" {
|
||||
checksumType = hash.NewChecksumType(cs)
|
||||
checksumType = hash.NewChecksumType(cs, fi.Metadata[hash.MinIOMultipartChecksumType])
|
||||
if opts.WantChecksum != nil && !opts.WantChecksum.Type.Is(checksumType) {
|
||||
return oi, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Object: fi.Name,
|
||||
Err: fmt.Errorf("checksum type mismatch"),
|
||||
Err: fmt.Errorf("checksum type mismatch. got %q (%s) expected %q (%s)", checksumType.String(), checksumType.ObjType(), opts.WantChecksum.Type.String(), opts.WantChecksum.Type.ObjType()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1247,6 +1221,9 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
// Allocate parts similar to incoming slice.
|
||||
fi.Parts = make([]ObjectPartInfo, len(parts))
|
||||
|
||||
var checksum hash.Checksum
|
||||
checksum.Type = checksumType
|
||||
|
||||
// Validate each part and then commit to disk.
|
||||
for i, part := range parts {
|
||||
partIdx := objectPartIndex(currentFI.Parts, part.PartNumber)
|
||||
@@ -1280,10 +1257,11 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
}
|
||||
}
|
||||
wantCS := map[string]string{
|
||||
hash.ChecksumCRC32.String(): part.ChecksumCRC32,
|
||||
hash.ChecksumCRC32C.String(): part.ChecksumCRC32C,
|
||||
hash.ChecksumSHA1.String(): part.ChecksumSHA1,
|
||||
hash.ChecksumSHA256.String(): part.ChecksumSHA256,
|
||||
hash.ChecksumCRC32.String(): part.ChecksumCRC32,
|
||||
hash.ChecksumCRC32C.String(): part.ChecksumCRC32C,
|
||||
hash.ChecksumSHA1.String(): part.ChecksumSHA1,
|
||||
hash.ChecksumSHA256.String(): part.ChecksumSHA256,
|
||||
hash.ChecksumCRC64NVME.String(): part.ChecksumCRC64NVME,
|
||||
}
|
||||
if wantCS[checksumType.String()] != crc {
|
||||
return oi, InvalidPart{
|
||||
@@ -1298,6 +1276,15 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
PartNumber: part.PartNumber,
|
||||
}
|
||||
}
|
||||
if checksumType.FullObjectRequested() {
|
||||
if err := checksum.AddPart(*cs, expPart.ActualSize); err != nil {
|
||||
return oi, InvalidPart{
|
||||
PartNumber: part.PartNumber,
|
||||
ExpETag: "<nil>",
|
||||
GotETag: err.Error(),
|
||||
}
|
||||
}
|
||||
}
|
||||
checksumCombined = append(checksumCombined, cs.Raw...)
|
||||
}
|
||||
|
||||
@@ -1328,9 +1315,19 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
}
|
||||
|
||||
if opts.WantChecksum != nil {
|
||||
err := opts.WantChecksum.Matches(checksumCombined, len(parts))
|
||||
if err != nil {
|
||||
return oi, err
|
||||
if checksumType.FullObjectRequested() {
|
||||
if opts.WantChecksum.Encoded != checksum.Encoded {
|
||||
err := hash.ChecksumMismatch{
|
||||
Want: opts.WantChecksum.Encoded,
|
||||
Got: checksum.Encoded,
|
||||
}
|
||||
return oi, err
|
||||
}
|
||||
} else {
|
||||
err := opts.WantChecksum.Matches(checksumCombined, len(parts))
|
||||
if err != nil {
|
||||
return oi, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1344,14 +1341,18 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
|
||||
if checksumType.IsSet() {
|
||||
checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
|
||||
var cs *hash.Checksum
|
||||
cs = hash.NewChecksumFromData(checksumType, checksumCombined)
|
||||
fi.Checksum = cs.AppendTo(nil, checksumCombined)
|
||||
checksum.Type = checksumType
|
||||
if !checksumType.FullObjectRequested() {
|
||||
checksum = *hash.NewChecksumFromData(checksumType, checksumCombined)
|
||||
}
|
||||
fi.Checksum = checksum.AppendTo(nil, checksumCombined)
|
||||
if opts.EncryptFn != nil {
|
||||
fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
|
||||
}
|
||||
}
|
||||
delete(fi.Metadata, hash.MinIOMultipartChecksum) // Not needed in final object.
|
||||
// Remove superfluous internal headers.
|
||||
delete(fi.Metadata, hash.MinIOMultipartChecksum)
|
||||
delete(fi.Metadata, hash.MinIOMultipartChecksumType)
|
||||
|
||||
// Save the final object size and modtime.
|
||||
fi.Size = objectSize
|
||||
@@ -1491,14 +1492,6 @@ func (er erasureObjects) AbortMultipartUpload(ctx context.Context, bucket, objec
|
||||
auditObjectErasureSet(ctx, "AbortMultipartUpload", object, &er)
|
||||
}
|
||||
|
||||
lk := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
|
||||
// Validates if upload ID exists.
|
||||
if _, _, err = er.checkUploadIDExists(ctx, bucket, object, uploadID, false); err != nil {
|
||||
if errors.Is(err, errVolumeNotFound) {
|
||||
|
||||
+65
-33
@@ -27,6 +27,8 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -102,7 +104,7 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
||||
|
||||
readQuorum, writeQuorum, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||
if err != nil {
|
||||
if errors.Is(err, errErasureReadQuorum) && !strings.HasPrefix(srcBucket, minioMetaBucket) {
|
||||
if shouldCheckForDangling(err, errs, srcBucket) {
|
||||
_, derr := er.deleteIfDangling(context.Background(), srcBucket, srcObject, metaArr, errs, nil, srcOpts)
|
||||
if derr == nil {
|
||||
if srcOpts.VersionID != "" {
|
||||
@@ -690,13 +692,9 @@ func shouldCheckForDangling(err error, errs []error, bucket string) bool {
|
||||
// Check if we have a read quorum issue
|
||||
case errors.Is(err, errErasureReadQuorum):
|
||||
return true
|
||||
// Check if the object is inexistent in most disks but not all of them
|
||||
case errors.Is(err, errFileNotFound) || errors.Is(err, errFileVersionNotFound):
|
||||
for i := range errs {
|
||||
if errs[i] == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Check if the object is non-existent on most disks but not all of them
|
||||
case (errors.Is(err, errFileNotFound) || errors.Is(err, errFileVersionNotFound)) && (countErrs(errs, nil) > 0):
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -921,6 +919,16 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
||||
}
|
||||
}
|
||||
}
|
||||
// when we have insufficient read quorum and inconsistent metadata return
|
||||
// file not found, since we can't possibly have a way to recover this object
|
||||
// anyway.
|
||||
if v, ok := err.(InsufficientReadQuorum); ok && v.Type == RQInconsistentMeta {
|
||||
if opts.VersionID != "" {
|
||||
err = errFileVersionNotFound
|
||||
} else {
|
||||
err = errFileNotFound
|
||||
}
|
||||
}
|
||||
return fi, nil, nil, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -1700,8 +1708,21 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
||||
}
|
||||
|
||||
dedupVersions := make([]FileInfoVersions, 0, len(versionsMap))
|
||||
for _, version := range versionsMap {
|
||||
dedupVersions = append(dedupVersions, version)
|
||||
for _, fivs := range versionsMap {
|
||||
// Removal of existing versions and adding a delete marker in the same
|
||||
// request is supported. At the same time, we cannot allow adding
|
||||
// two delete markers on top of any object. To avoid this situation,
|
||||
// we will sort deletions to execute existing deletion first,
|
||||
// then add only one delete marker if requested
|
||||
sort.SliceStable(fivs.Versions, func(i, j int) bool {
|
||||
return !fivs.Versions[i].Deleted
|
||||
})
|
||||
if idx := slices.IndexFunc(fivs.Versions, func(fi FileInfo) bool {
|
||||
return fi.Deleted
|
||||
}); idx > -1 {
|
||||
fivs.Versions = fivs.Versions[:idx+1]
|
||||
}
|
||||
dedupVersions = append(dedupVersions, fivs)
|
||||
}
|
||||
|
||||
// Initialize list of errors.
|
||||
@@ -1726,12 +1747,6 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
||||
continue
|
||||
}
|
||||
for _, v := range dedupVersions[i].Versions {
|
||||
if err == errFileNotFound || err == errFileVersionNotFound {
|
||||
if !dobjects[v.Idx].DeleteMarker {
|
||||
// Not delete marker, if not found, ok.
|
||||
continue
|
||||
}
|
||||
}
|
||||
delObjErrs[index][v.Idx] = err
|
||||
}
|
||||
}
|
||||
@@ -1751,6 +1766,13 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
||||
}
|
||||
}
|
||||
err := reduceWriteQuorumErrs(ctx, diskErrs, objectOpIgnoredErrs, writeQuorums[objIndex])
|
||||
if err == nil {
|
||||
dobjects[objIndex].found = true
|
||||
} else if isErrVersionNotFound(err) || isErrObjectNotFound(err) {
|
||||
if !dobjects[objIndex].DeleteMarker {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if objects[objIndex].VersionID != "" {
|
||||
errs[objIndex] = toObjectErr(err, bucket, objects[objIndex].ObjectName, objects[objIndex].VersionID)
|
||||
} else {
|
||||
@@ -2138,7 +2160,7 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
|
||||
|
||||
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||
if err != nil {
|
||||
if errors.Is(err, errErasureReadQuorum) && !strings.HasPrefix(bucket, minioMetaBucket) {
|
||||
if shouldCheckForDangling(err, errs, bucket) {
|
||||
_, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts)
|
||||
if derr == nil {
|
||||
if opts.VersionID != "" {
|
||||
@@ -2192,14 +2214,16 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
|
||||
|
||||
// PutObjectTags - replace or add tags to an existing object
|
||||
func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) (ObjectInfo, error) {
|
||||
// Lock the object before updating tags.
|
||||
lk := er.NewNSLock(bucket, object)
|
||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, err
|
||||
if !opts.NoLock {
|
||||
// Lock the object before updating tags.
|
||||
lk := er.NewNSLock(bucket, object)
|
||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
|
||||
disks := er.getDisks()
|
||||
|
||||
@@ -2215,7 +2239,7 @@ func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object strin
|
||||
|
||||
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||
if err != nil {
|
||||
if errors.Is(err, errErasureReadQuorum) && !strings.HasPrefix(bucket, minioMetaBucket) {
|
||||
if shouldCheckForDangling(err, errs, bucket) {
|
||||
_, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts)
|
||||
if derr == nil {
|
||||
if opts.VersionID != "" {
|
||||
@@ -2310,14 +2334,16 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
|
||||
return err
|
||||
}
|
||||
|
||||
// Acquire write lock before starting to transition the object.
|
||||
lk := er.NewNSLock(bucket, object)
|
||||
lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
if !opts.NoLock {
|
||||
// Acquire write lock before starting to transition the object.
|
||||
lk := er.NewNSLock(bucket, object)
|
||||
lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
|
||||
fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts, true)
|
||||
if err != nil {
|
||||
@@ -2355,6 +2381,7 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
|
||||
|
||||
destObj, err := genTransitionObjName(bucket)
|
||||
if err != nil {
|
||||
traceFn(ILMTransition, nil, err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2365,9 +2392,14 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
|
||||
}()
|
||||
|
||||
var rv remoteVersionID
|
||||
rv, err = tgtClient.Put(ctx, destObj, pr, fi.Size)
|
||||
rv, err = tgtClient.PutWithMeta(ctx, destObj, pr, fi.Size, map[string]string{
|
||||
"name": object, // preserve the original name of the object on the remote tier object metadata.
|
||||
// this is just for future reverse lookup() purposes (applies only for new objects)
|
||||
// does not apply retro-actively on already transitioned objects.
|
||||
})
|
||||
pr.CloseWithError(err)
|
||||
if err != nil {
|
||||
traceFn(ILMTransition, nil, err)
|
||||
return err
|
||||
}
|
||||
fi.TransitionStatus = lifecycle.TransitionComplete
|
||||
|
||||
@@ -131,6 +131,75 @@ func TestErasureDeleteObjectBasic(t *testing.T) {
|
||||
removeRoots(fsDirs)
|
||||
}
|
||||
|
||||
func TestDeleteObjectsVersionedTwoPools(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
obj, fsDirs, err := prepareErasurePools()
|
||||
if err != nil {
|
||||
t.Fatal("Unable to initialize 'Erasure' object layer.", err)
|
||||
}
|
||||
// Remove all dirs.
|
||||
for _, dir := range fsDirs {
|
||||
defer os.RemoveAll(dir)
|
||||
}
|
||||
|
||||
bucketName := "bucket"
|
||||
objectName := "myobject"
|
||||
err = obj.MakeBucket(ctx, bucketName, MakeBucketOptions{
|
||||
VersioningEnabled: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
z, ok := obj.(*erasureServerPools)
|
||||
if !ok {
|
||||
t.Fatal("unexpected object layer type")
|
||||
}
|
||||
|
||||
versions := make([]string, 2)
|
||||
for i := range z.serverPools {
|
||||
objInfo, err := z.serverPools[i].PutObject(ctx, bucketName, objectName,
|
||||
mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{
|
||||
Versioned: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Erasure Object upload failed: <ERROR> %s", err)
|
||||
}
|
||||
versions[i] = objInfo.VersionID
|
||||
}
|
||||
|
||||
// Remove and check the version in the second pool, then
|
||||
// remove and check the version in the first pool
|
||||
for testIdx, vid := range []string{versions[1], versions[0]} {
|
||||
names := []ObjectToDelete{
|
||||
{
|
||||
ObjectV: ObjectV{
|
||||
ObjectName: objectName,
|
||||
VersionID: vid,
|
||||
},
|
||||
},
|
||||
}
|
||||
_, delErrs := obj.DeleteObjects(ctx, bucketName, names, ObjectOptions{
|
||||
Versioned: true,
|
||||
})
|
||||
for i := range delErrs {
|
||||
if delErrs[i] != nil {
|
||||
t.Errorf("Test %d: Failed to remove object `%v` with the error: `%v`", testIdx, names[i], delErrs[i])
|
||||
}
|
||||
_, statErr := obj.GetObjectInfo(ctx, bucketName, objectName, ObjectOptions{
|
||||
VersionID: names[i].ObjectV.VersionID,
|
||||
})
|
||||
switch statErr.(type) {
|
||||
case VersionNotFound:
|
||||
default:
|
||||
t.Errorf("Test %d: Object %s is not removed", testIdx, objectName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteObjectsVersioned(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -845,7 +845,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
// to decommission, just skip it, this also includes
|
||||
// any other versions that have already expired.
|
||||
remainingVersions := len(fivs.Versions) - expired
|
||||
if version.Deleted && remainingVersions == 1 {
|
||||
if version.Deleted && remainingVersions == 1 && rcfg == nil {
|
||||
decommissioned++
|
||||
stopFn(version.Size, errors.New("DELETE marked object with no other non-current versions will be skipped"))
|
||||
continue
|
||||
|
||||
@@ -32,9 +32,9 @@ func prepareErasurePools() (ObjectLayer, []string, error) {
|
||||
pools := mustGetPoolEndpoints(0, fsDirs[:16]...)
|
||||
pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...)
|
||||
|
||||
// Everything is fine, should return nil
|
||||
objLayer, err := newErasureServerPools(context.Background(), pools)
|
||||
objLayer, _, err := initObjectLayer(context.Background(), pools)
|
||||
if err != nil {
|
||||
removeRoots(fsDirs)
|
||||
return nil, nil, err
|
||||
}
|
||||
return objLayer, fsDirs, nil
|
||||
|
||||
+140
-82
@@ -256,7 +256,7 @@ func (z *erasureServerPools) NewNSLock(bucket string, objects ...string) RWLocke
|
||||
|
||||
// GetDisksID will return disks by their ID.
|
||||
func (z *erasureServerPools) GetDisksID(ids ...string) []StorageAPI {
|
||||
idMap := make(map[string]struct{})
|
||||
idMap := make(map[string]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
idMap[id] = struct{}{}
|
||||
}
|
||||
@@ -357,7 +357,7 @@ func (p serverPoolsAvailableSpace) TotalAvailable() uint64 {
|
||||
|
||||
// FilterMaxUsed will filter out any pools that has used percent bigger than max,
|
||||
// unless all have that, in which case all are preserved.
|
||||
func (p serverPoolsAvailableSpace) FilterMaxUsed(max int) {
|
||||
func (p serverPoolsAvailableSpace) FilterMaxUsed(maxUsed int) {
|
||||
// We aren't modifying p, only entries in it, so we don't need to receive a pointer.
|
||||
if len(p) <= 1 {
|
||||
// Nothing to do.
|
||||
@@ -365,7 +365,7 @@ func (p serverPoolsAvailableSpace) FilterMaxUsed(max int) {
|
||||
}
|
||||
var ok bool
|
||||
for _, z := range p {
|
||||
if z.Available > 0 && z.MaxUsedPct < max {
|
||||
if z.Available > 0 && z.MaxUsedPct < maxUsed {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
@@ -378,7 +378,7 @@ func (p serverPoolsAvailableSpace) FilterMaxUsed(max int) {
|
||||
|
||||
// Remove entries that are above.
|
||||
for i, z := range p {
|
||||
if z.Available > 0 && z.MaxUsedPct < max {
|
||||
if z.Available > 0 && z.MaxUsedPct < maxUsed {
|
||||
continue
|
||||
}
|
||||
p[i].Available = 0
|
||||
@@ -1248,89 +1248,42 @@ func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, o
|
||||
ctx = lkctx.Context()
|
||||
defer multiDeleteLock.Unlock(lkctx)
|
||||
|
||||
// Fetch location of up to 10 objects concurrently.
|
||||
poolObjIdxMap := map[int][]ObjectToDelete{}
|
||||
origIndexMap := map[int][]int{}
|
||||
dObjectsByPool := make([][]DeletedObject, len(z.serverPools))
|
||||
dErrsByPool := make([][]error, len(z.serverPools))
|
||||
|
||||
// Always perform 1/10th of the number of objects per delete
|
||||
concurrent := len(objects) / 10
|
||||
if concurrent <= 10 {
|
||||
// if we cannot get 1/10th then choose the number of
|
||||
// objects as concurrent.
|
||||
concurrent = len(objects)
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
eg := errgroup.WithNErrs(len(objects)).WithConcurrency(concurrent)
|
||||
for j, obj := range objects {
|
||||
j := j
|
||||
obj := obj
|
||||
eg := errgroup.WithNErrs(len(z.serverPools)).WithConcurrency(len(z.serverPools))
|
||||
for i, pool := range z.serverPools {
|
||||
i := i
|
||||
pool := pool
|
||||
eg.Go(func() error {
|
||||
pinfo, _, err := z.getPoolInfoExistingWithOpts(ctx, bucket, obj.ObjectName, ObjectOptions{
|
||||
NoLock: true,
|
||||
})
|
||||
if err != nil {
|
||||
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
||||
derrs[j] = err
|
||||
}
|
||||
dobjects[j] = DeletedObject{
|
||||
ObjectName: decodeDirObject(obj.ObjectName),
|
||||
VersionID: obj.VersionID,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete marker already present we are not going to create new delete markers.
|
||||
if pinfo.ObjInfo.DeleteMarker && obj.VersionID == "" {
|
||||
dobjects[j] = DeletedObject{
|
||||
DeleteMarker: pinfo.ObjInfo.DeleteMarker,
|
||||
DeleteMarkerVersionID: pinfo.ObjInfo.VersionID,
|
||||
DeleteMarkerMTime: DeleteMarkerMTime{pinfo.ObjInfo.ModTime},
|
||||
ObjectName: decodeDirObject(pinfo.ObjInfo.Name),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
idx := pinfo.Index
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
poolObjIdxMap[idx] = append(poolObjIdxMap[idx], obj)
|
||||
origIndexMap[idx] = append(origIndexMap[idx], j)
|
||||
dObjectsByPool[i], dErrsByPool[i] = pool.DeleteObjects(ctx, bucket, objects, opts)
|
||||
return nil
|
||||
}, j)
|
||||
}, i)
|
||||
}
|
||||
|
||||
eg.Wait() // wait to check all the pools.
|
||||
|
||||
if len(poolObjIdxMap) > 0 {
|
||||
// Delete concurrently in all server pools.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(z.serverPools))
|
||||
for idx, pool := range z.serverPools {
|
||||
go func(idx int, pool *erasureSets) {
|
||||
defer wg.Done()
|
||||
objs := poolObjIdxMap[idx]
|
||||
if len(objs) == 0 {
|
||||
return
|
||||
}
|
||||
orgIndexes := origIndexMap[idx]
|
||||
deletedObjects, errs := pool.DeleteObjects(ctx, bucket, objs, opts)
|
||||
mu.Lock()
|
||||
for i, derr := range errs {
|
||||
if derr != nil {
|
||||
derrs[orgIndexes[i]] = derr
|
||||
}
|
||||
deletedObjects[i].ObjectName = decodeDirObject(deletedObjects[i].ObjectName)
|
||||
dobjects[orgIndexes[i]] = deletedObjects[i]
|
||||
}
|
||||
mu.Unlock()
|
||||
}(idx, pool)
|
||||
for i := range dobjects {
|
||||
// Iterate over pools
|
||||
for pool := range z.serverPools {
|
||||
if dErrsByPool[pool][i] == nil && dObjectsByPool[pool][i].found {
|
||||
// A fast exit when the object is found and removed
|
||||
dobjects[i] = dObjectsByPool[pool][i]
|
||||
derrs[i] = nil
|
||||
break
|
||||
}
|
||||
if derrs[i] == nil {
|
||||
// No error related to this object is found, assign this pool result
|
||||
// whether it is nil because there is no object found or because of
|
||||
// some other errors such erasure quorum errors.
|
||||
dobjects[i] = dObjectsByPool[pool][i]
|
||||
derrs[i] = dErrsByPool[pool][i]
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
for i := range dobjects {
|
||||
dobjects[i].ObjectName = decodeDirObject(dobjects[i].ObjectName)
|
||||
}
|
||||
return dobjects, derrs
|
||||
}
|
||||
|
||||
@@ -1890,9 +1843,18 @@ func (z *erasureServerPools) GetMultipartInfo(ctx context.Context, bucket, objec
|
||||
return MultipartInfo{}, err
|
||||
}
|
||||
|
||||
uploadIDLock := z.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return MultipartInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer uploadIDLock.RUnlock(lkctx)
|
||||
|
||||
if z.SinglePool() {
|
||||
return z.serverPools[0].GetMultipartInfo(ctx, bucket, object, uploadID, opts)
|
||||
}
|
||||
|
||||
for idx, pool := range z.serverPools {
|
||||
if z.IsSuspended(idx) {
|
||||
continue
|
||||
@@ -1908,6 +1870,7 @@ func (z *erasureServerPools) GetMultipartInfo(ctx context.Context, bucket, objec
|
||||
// any other unhandled error return right here.
|
||||
return MultipartInfo{}, err
|
||||
}
|
||||
|
||||
return MultipartInfo{}, InvalidUploadID{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
@@ -1921,9 +1884,18 @@ func (z *erasureServerPools) ListObjectParts(ctx context.Context, bucket, object
|
||||
return ListPartsInfo{}, err
|
||||
}
|
||||
|
||||
uploadIDLock := z.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return ListPartsInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer uploadIDLock.RUnlock(lkctx)
|
||||
|
||||
if z.SinglePool() {
|
||||
return z.serverPools[0].ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||
}
|
||||
|
||||
for idx, pool := range z.serverPools {
|
||||
if z.IsSuspended(idx) {
|
||||
continue
|
||||
@@ -1937,6 +1909,7 @@ func (z *erasureServerPools) ListObjectParts(ctx context.Context, bucket, object
|
||||
}
|
||||
return ListPartsInfo{}, err
|
||||
}
|
||||
|
||||
return ListPartsInfo{}, InvalidUploadID{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
@@ -1957,6 +1930,14 @@ func (z *erasureServerPools) AbortMultipartUpload(ctx context.Context, bucket, o
|
||||
}
|
||||
}()
|
||||
|
||||
lk := z.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
|
||||
if z.SinglePool() {
|
||||
return z.serverPools[0].AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
|
||||
}
|
||||
@@ -1995,6 +1976,16 @@ func (z *erasureServerPools) CompleteMultipartUpload(ctx context.Context, bucket
|
||||
}
|
||||
}()
|
||||
|
||||
// Hold write locks to verify uploaded parts, also disallows any
|
||||
// parallel PutObjectPart() requests.
|
||||
uploadIDLock := z.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
wlkctx, err := uploadIDLock.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return objInfo, err
|
||||
}
|
||||
ctx = wlkctx.Context()
|
||||
defer uploadIDLock.Unlock(wlkctx)
|
||||
|
||||
if z.SinglePool() {
|
||||
return z.serverPools[0].CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||
}
|
||||
@@ -2430,6 +2421,7 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
|
||||
// HealObjectFn closure function heals the object.
|
||||
type HealObjectFn func(bucket, object, versionID string, scanMode madmin.HealScanMode) error
|
||||
|
||||
// List a prefix or a single object versions and heal
|
||||
func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObjectFn HealObjectFn) error {
|
||||
healEntry := func(bucket string, entry metaCacheEntry, scanMode madmin.HealScanMode) error {
|
||||
if entry.isDir() {
|
||||
@@ -2493,7 +2485,7 @@ func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix str
|
||||
go func(idx int, set *erasureObjects) {
|
||||
defer wg.Done()
|
||||
|
||||
errs[idx] = set.listAndHeal(ctx, bucket, prefix, opts.ScanMode, healEntry)
|
||||
errs[idx] = set.listAndHeal(ctx, bucket, prefix, opts.Recursive, opts.ScanMode, healEntry)
|
||||
}(idx, set)
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -2545,15 +2537,22 @@ func (z *erasureServerPools) HealObject(ctx context.Context, bucket, object, ver
|
||||
}
|
||||
}
|
||||
|
||||
hr := madmin.HealResultItem{
|
||||
Type: madmin.HealItemObject,
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
VersionID: versionID,
|
||||
}
|
||||
|
||||
// At this stage, all errors are 'not found'
|
||||
if versionID != "" {
|
||||
return madmin.HealResultItem{}, VersionNotFound{
|
||||
return hr, VersionNotFound{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
VersionID: versionID,
|
||||
}
|
||||
}
|
||||
return madmin.HealResultItem{}, ObjectNotFound{
|
||||
return hr, ObjectNotFound{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
}
|
||||
@@ -2774,7 +2773,19 @@ func (z *erasureServerPools) PutObjectMetadata(ctx context.Context, bucket, obje
|
||||
return z.serverPools[0].PutObjectMetadata(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
if !opts.NoLock {
|
||||
// Lock the object before updating metadata.
|
||||
lk := z.NewNSLock(bucket, object)
|
||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
}
|
||||
|
||||
opts.MetadataChg = true
|
||||
opts.NoLock = true
|
||||
// We don't know the size here set 1GiB at least.
|
||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
@@ -2791,7 +2802,19 @@ func (z *erasureServerPools) PutObjectTags(ctx context.Context, bucket, object s
|
||||
return z.serverPools[0].PutObjectTags(ctx, bucket, object, tags, opts)
|
||||
}
|
||||
|
||||
if !opts.NoLock {
|
||||
// Lock the object before updating tags.
|
||||
lk := z.NewNSLock(bucket, object)
|
||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
}
|
||||
|
||||
opts.MetadataChg = true
|
||||
opts.NoLock = true
|
||||
|
||||
// We don't know the size here set 1GiB at least.
|
||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||
@@ -2809,8 +2832,19 @@ func (z *erasureServerPools) DeleteObjectTags(ctx context.Context, bucket, objec
|
||||
return z.serverPools[0].DeleteObjectTags(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
opts.MetadataChg = true
|
||||
if !opts.NoLock {
|
||||
// Lock the object before deleting tags.
|
||||
lk := z.NewNSLock(bucket, object)
|
||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
}
|
||||
|
||||
opts.MetadataChg = true
|
||||
opts.NoLock = true
|
||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, err
|
||||
@@ -2841,8 +2875,20 @@ func (z *erasureServerPools) TransitionObject(ctx context.Context, bucket, objec
|
||||
return z.serverPools[0].TransitionObject(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
if !opts.NoLock {
|
||||
// Acquire write lock before starting to transition the object.
|
||||
lk := z.NewNSLock(bucket, object)
|
||||
lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
}
|
||||
|
||||
// Avoid transitioning an object from a pool being decommissioned.
|
||||
opts.SkipDecommissioned = true
|
||||
opts.NoLock = true
|
||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -2858,8 +2904,20 @@ func (z *erasureServerPools) RestoreTransitionedObject(ctx context.Context, buck
|
||||
return z.serverPools[0].RestoreTransitionedObject(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
if !opts.NoLock {
|
||||
// Acquire write lock before restoring transitioned object
|
||||
lk := z.NewNSLock(bucket, object)
|
||||
lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
}
|
||||
|
||||
// Avoid restoring object from a pool being decommissioned.
|
||||
opts.SkipDecommissioned = true
|
||||
opts.NoLock = true
|
||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+4
-11
@@ -39,6 +39,7 @@ import (
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/v3/console"
|
||||
"github.com/minio/pkg/v3/sync/errgroup"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
)
|
||||
|
||||
// setsDsyncLockers is encapsulated type for Close()
|
||||
@@ -570,10 +571,7 @@ func auditObjectErasureSet(ctx context.Context, api, object string, set *erasure
|
||||
|
||||
// NewNSLock - initialize a new namespace RWLocker instance.
|
||||
func (s *erasureSets) NewNSLock(bucket string, objects ...string) RWLocker {
|
||||
if len(objects) == 1 {
|
||||
return s.getHashedSet(objects[0]).NewNSLock(bucket, objects...)
|
||||
}
|
||||
return s.getHashedSet("").NewNSLock(bucket, objects...)
|
||||
return s.sets[0].NewNSLock(bucket, objects...)
|
||||
}
|
||||
|
||||
// SetDriveCount returns the current drives per set.
|
||||
@@ -704,9 +702,8 @@ func (s *erasureSets) getHashedSet(input string) (set *erasureObjects) {
|
||||
}
|
||||
|
||||
// listDeletedBuckets lists deleted buckets from all disks.
|
||||
func listDeletedBuckets(ctx context.Context, storageDisks []StorageAPI, delBuckets map[string]VolInfo, readQuorum int) error {
|
||||
func listDeletedBuckets(ctx context.Context, storageDisks []StorageAPI, delBuckets *xsync.MapOf[string, VolInfo], readQuorum int) error {
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
var mu sync.Mutex
|
||||
for index := range storageDisks {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
@@ -725,11 +722,7 @@ func listDeletedBuckets(ctx context.Context, storageDisks []StorageAPI, delBucke
|
||||
vi, err := storageDisks[index].StatVol(ctx, pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, volName))
|
||||
if err == nil {
|
||||
vi.Name = strings.TrimSuffix(volName, SlashSeparator)
|
||||
mu.Lock()
|
||||
if _, ok := delBuckets[volName]; !ok {
|
||||
delBuckets[volName] = vi
|
||||
}
|
||||
mu.Unlock()
|
||||
delBuckets.Store(volName, vi)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -551,9 +551,6 @@ func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, wa
|
||||
var root dataUsageEntry
|
||||
if r := cache.root(); r != nil {
|
||||
root = cache.flatten(*r)
|
||||
if root.ReplicationStats.empty() {
|
||||
root.ReplicationStats = nil
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -24,6 +24,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -286,6 +288,10 @@ func (driver *ftpDriver) CheckPasswd(c *ftp.Context, username, password string)
|
||||
}
|
||||
|
||||
func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error) {
|
||||
tr := http.RoundTripper(globalRemoteFTPClientTransport)
|
||||
if host, _, err := net.SplitHostPort(ctx.Sess.RemoteAddr().String()); err == nil {
|
||||
tr = forwardForTransport{tr: tr, fwd: host}
|
||||
}
|
||||
ui, ok := globalIAMSys.GetUser(context.Background(), ctx.Sess.LoginUser())
|
||||
if !ok && !globalIAMSys.LDAPConfig.Enabled() {
|
||||
return nil, errNoSuchUser
|
||||
@@ -363,7 +369,7 @@ func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error)
|
||||
return minio.New(driver.endpoint, &minio.Options{
|
||||
Creds: mcreds,
|
||||
Secure: globalIsTLS,
|
||||
Transport: globalRemoteFTPClientTransport,
|
||||
Transport: tr,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -377,7 +383,7 @@ func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error)
|
||||
return minio.New(driver.endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
|
||||
Secure: globalIsTLS,
|
||||
Transport: globalRemoteFTPClientTransport,
|
||||
Transport: tr,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -546,6 +552,7 @@ func (driver *ftpDriver) PutFile(ctx *ftp.Context, objPath string, data io.Reade
|
||||
info, err := clnt.PutObject(context.Background(), bucket, object, data, -1, minio.PutObjectOptions{
|
||||
ContentType: mimedb.TypeByExtension(path.Ext(object)),
|
||||
DisableContentSha256: true,
|
||||
Checksum: minio.ChecksumFullObjectCRC32C,
|
||||
})
|
||||
n = info.Size
|
||||
return n, err
|
||||
|
||||
+32
-20
@@ -22,19 +22,17 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio-go/v7/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/grid"
|
||||
xnet "github.com/minio/pkg/v3/net"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/minio/minio/internal/amztime"
|
||||
"github.com/minio/minio/internal/config/dns"
|
||||
@@ -75,7 +73,7 @@ const (
|
||||
// and must not set by clients
|
||||
func containsReservedMetadata(header http.Header) bool {
|
||||
for key := range header {
|
||||
if slices.Contains(maps.Keys(validSSEReplicationHeaders), key) {
|
||||
if _, ok := validSSEReplicationHeaders[key]; ok {
|
||||
return false
|
||||
}
|
||||
if stringsHasPrefixFold(key, ReservedMetadataPrefix) {
|
||||
@@ -295,12 +293,6 @@ func parseAmzDateHeader(req *http.Request) (time.Time, APIErrorCode) {
|
||||
return time.Time{}, ErrMissingDateHeader
|
||||
}
|
||||
|
||||
// Bad path components to be rejected by the path validity handler.
|
||||
const (
|
||||
dotdotComponent = ".."
|
||||
dotComponent = "."
|
||||
)
|
||||
|
||||
func hasBadHost(host string) error {
|
||||
if globalIsCICD && strings.TrimSpace(host) == "" {
|
||||
// under CI/CD test setups ignore empty hosts as invalid hosts
|
||||
@@ -313,21 +305,41 @@ func hasBadHost(host string) error {
|
||||
// Check if the incoming path has bad path components,
|
||||
// such as ".." and "."
|
||||
func hasBadPathComponent(path string) bool {
|
||||
if len(path) > 4096 {
|
||||
// path cannot be greater than Linux PATH_MAX
|
||||
// this is to avoid a busy loop, that can happen
|
||||
// if the caller sends path of following style
|
||||
// a/a/a/a/a/a/a/a...
|
||||
n := len(path)
|
||||
if n > 32<<10 {
|
||||
// At 32K we are beyond reasonable.
|
||||
return true
|
||||
}
|
||||
path = filepath.ToSlash(strings.TrimSpace(path)) // For windows '\' must be converted to '/'
|
||||
for _, p := range strings.Split(path, SlashSeparator) {
|
||||
switch strings.TrimSpace(p) {
|
||||
case dotdotComponent:
|
||||
i := 0
|
||||
// Skip leading slashes (for sake of Windows \ is included as well)
|
||||
for i < n && (path[i] == SlashSeparatorChar || path[i] == '\\') {
|
||||
i++
|
||||
}
|
||||
|
||||
for i < n {
|
||||
// Find the next segment
|
||||
start := i
|
||||
for i < n && path[i] != SlashSeparatorChar && path[i] != '\\' {
|
||||
i++
|
||||
}
|
||||
|
||||
// Trim whitespace of segment
|
||||
segmentStart, segmentEnd := start, i
|
||||
for segmentStart < segmentEnd && unicode.IsSpace(rune(path[segmentStart])) {
|
||||
segmentStart++
|
||||
}
|
||||
for segmentEnd > segmentStart && unicode.IsSpace(rune(path[segmentEnd-1])) {
|
||||
segmentEnd--
|
||||
}
|
||||
|
||||
// Check for ".." or "."
|
||||
switch {
|
||||
case segmentEnd-segmentStart == 2 && path[segmentStart] == '.' && path[segmentStart+1] == '.':
|
||||
return true
|
||||
case dotComponent:
|
||||
case segmentEnd-segmentStart == 1 && path[segmentStart] == '.':
|
||||
return true
|
||||
}
|
||||
i++
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/internal/crypto"
|
||||
@@ -184,3 +185,27 @@ func TestSSETLSHandler(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_hasBadPathComponent(t *testing.B) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want bool
|
||||
}{
|
||||
{name: "empty", input: "", want: false},
|
||||
{name: "backslashes", input: `\a\a\ \\ \\\\\\\`, want: false},
|
||||
{name: "long", input: strings.Repeat("a/", 2000), want: false},
|
||||
{name: "long-fail", input: strings.Repeat("a/", 2000) + "../..", want: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(b *testing.B) {
|
||||
b.SetBytes(int64(len(tt.input)))
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if got := hasBadPathComponent(tt.input); got != tt.want {
|
||||
t.Fatalf("hasBadPathComponent() = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -459,8 +459,6 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
||||
continue
|
||||
}
|
||||
|
||||
var versionHealed bool
|
||||
|
||||
res, err := er.HealObject(ctx, bucket, encodedEntryName,
|
||||
version.VersionID, madmin.HealOpts{
|
||||
ScanMode: scanMode,
|
||||
@@ -475,21 +473,23 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
||||
}
|
||||
} else {
|
||||
// Look for the healing results
|
||||
if res.After.Drives[tracker.DiskIndex].State == madmin.DriveStateOk {
|
||||
versionHealed = true
|
||||
if res.After.Drives[tracker.DiskIndex].State != madmin.DriveStateOk {
|
||||
err = fmt.Errorf("unexpected after heal state: %s", res.After.Drives[tracker.DiskIndex].State)
|
||||
}
|
||||
}
|
||||
|
||||
if versionHealed {
|
||||
if err == nil {
|
||||
bgSeq.countHealed(madmin.HealItemObject)
|
||||
result = healEntrySuccess(uint64(version.Size))
|
||||
} else {
|
||||
bgSeq.countFailed(madmin.HealItemObject)
|
||||
result = healEntryFailure(uint64(version.Size))
|
||||
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 (version-id=%s): %w",
|
||||
bucket, version.Name, version.VersionID, err))
|
||||
} 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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-5
@@ -28,6 +28,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
|
||||
"github.com/minio/minio/internal/config/api"
|
||||
@@ -61,7 +62,6 @@ type apiConfig struct {
|
||||
const (
|
||||
cgroupV1MemLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes"
|
||||
cgroupV2MemLimitFile = "/sys/fs/cgroup/memory.max"
|
||||
cgroupMemNoLimit = 9223372036854771712
|
||||
)
|
||||
|
||||
func cgroupMemLimit() (limit uint64) {
|
||||
@@ -78,10 +78,8 @@ func cgroupMemLimit() (limit uint64) {
|
||||
// but still, no need to interpret more
|
||||
return 0
|
||||
}
|
||||
if limit == cgroupMemNoLimit {
|
||||
// No limit set, It's the highest positive signed 64-bit
|
||||
// integer (2^63-1), rounded down to multiples of 4096 (2^12),
|
||||
// the most common page size on x86 systems - for cgroup_limits.
|
||||
if limit >= 100*humanize.TiByte {
|
||||
// No limit set, or unreasonably high. Ignore
|
||||
return 0
|
||||
}
|
||||
return limit
|
||||
|
||||
@@ -447,7 +447,7 @@ func getHostName(r *http.Request) (hostName string) {
|
||||
}
|
||||
|
||||
// Proxy any request to an endpoint.
|
||||
func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ep ProxyEndpoint) (success bool) {
|
||||
func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ep ProxyEndpoint, returnErr bool) (success bool) {
|
||||
success = true
|
||||
|
||||
// Make sure we remove any existing headers before
|
||||
@@ -462,7 +462,10 @@ func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, e
|
||||
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
success = false
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
replLogIf(GlobalContext, err)
|
||||
proxyLogIf(GlobalContext, err)
|
||||
}
|
||||
if returnErr {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
+2
-2
@@ -133,7 +133,7 @@ func (bh *bucketHTTPStats) updateHTTPStats(bucket, api string, w *xhttp.Response
|
||||
bucketHTTPRequestsDuration.With(prometheus.Labels{
|
||||
"api": api,
|
||||
"bucket": bucket,
|
||||
}).Observe(w.TimeToFirstByte.Seconds())
|
||||
}).Observe(w.TTFB().Seconds())
|
||||
}
|
||||
|
||||
bh.Lock()
|
||||
@@ -433,7 +433,7 @@ func (st *HTTPStats) updateStats(api string, w *xhttp.ResponseRecorder) {
|
||||
st.totalS3Requests.Inc(api)
|
||||
|
||||
// Increment the prometheus http request response histogram with appropriate label
|
||||
httpRequestsDuration.With(prometheus.Labels{"api": api}).Observe(w.TimeToFirstByte.Seconds())
|
||||
httpRequestsDuration.With(prometheus.Labels{"api": api}).Observe(w.TTFB().Seconds())
|
||||
|
||||
code := w.StatusCode
|
||||
|
||||
|
||||
+1
-1
@@ -164,7 +164,7 @@ func httpTracerMiddleware(h http.Handler) http.Handler {
|
||||
Latency: reqEndTime.Sub(respRecorder.StartTime),
|
||||
InputBytes: inputBytes,
|
||||
OutputBytes: respRecorder.Size(),
|
||||
TimeToFirstByte: respRecorder.TimeToFirstByte,
|
||||
TimeToFirstByte: respRecorder.TTFB(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+25
-7
@@ -481,12 +481,24 @@ var (
|
||||
policyDBGroupsListKey = "policydb/groups/"
|
||||
)
|
||||
|
||||
func findSecondIndex(s string, substr string) int {
|
||||
first := strings.Index(s, substr)
|
||||
if first == -1 {
|
||||
return -1
|
||||
}
|
||||
second := strings.Index(s[first+1:], substr)
|
||||
if second == -1 {
|
||||
return -1
|
||||
}
|
||||
return first + second + 1
|
||||
}
|
||||
|
||||
// splitPath splits a path into a top-level directory and a child item. The
|
||||
// parent directory retains the trailing slash.
|
||||
func splitPath(s string, lastIndex bool) (string, string) {
|
||||
func splitPath(s string, secondIndex bool) (string, string) {
|
||||
var i int
|
||||
if lastIndex {
|
||||
i = strings.LastIndex(s, "/")
|
||||
if secondIndex {
|
||||
i = findSecondIndex(s, "/")
|
||||
} else {
|
||||
i = strings.Index(s, "/")
|
||||
}
|
||||
@@ -506,8 +518,8 @@ func (iamOS *IAMObjectStore) listAllIAMConfigItems(ctx context.Context) (res map
|
||||
return nil, item.Err
|
||||
}
|
||||
|
||||
lastIndex := strings.HasPrefix(item.Item, policyDBPrefix)
|
||||
listKey, trimmedItem := splitPath(item.Item, lastIndex)
|
||||
secondIndex := strings.HasPrefix(item.Item, policyDBPrefix)
|
||||
listKey, trimmedItem := splitPath(item.Item, secondIndex)
|
||||
if listKey == iamFormatFile {
|
||||
continue
|
||||
}
|
||||
@@ -781,7 +793,10 @@ func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iam
|
||||
for _, item := range listedConfigItems[stsListKey] {
|
||||
userName := path.Dir(item)
|
||||
// loadUser() will delete expired user during the load.
|
||||
iamLogIf(ctx, iamOS.loadUser(ctx, userName, stsUser, stsAccountsFromStore))
|
||||
err := iamOS.loadUser(ctx, userName, stsUser, stsAccountsFromStore)
|
||||
if err != nil && !errors.Is(err, errNoSuchUser) {
|
||||
iamLogIf(ctx, err)
|
||||
}
|
||||
// No need to return errors for failed expiration of STS users
|
||||
}
|
||||
|
||||
@@ -789,7 +804,10 @@ func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iam
|
||||
// (removed during loadUser() in the loop above) are removed from memory.
|
||||
for _, item := range listedConfigItems[policyDBSTSUsersListKey] {
|
||||
stsName := strings.TrimSuffix(item, ".json")
|
||||
iamLogIf(ctx, iamOS.loadMappedPolicy(ctx, stsName, stsUser, false, stsAccPoliciesFromStore))
|
||||
err := iamOS.loadMappedPolicy(ctx, stsName, stsUser, false, stsAccPoliciesFromStore)
|
||||
if err != nil && !errors.Is(err, errNoSuchPolicy) {
|
||||
iamLogIf(ctx, err)
|
||||
}
|
||||
// No need to return errors for failed expiration of STS users
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
func TestSplitPath(t *testing.T) {
|
||||
cases := []struct {
|
||||
path string
|
||||
secondIndex bool
|
||||
expectedListKey, expectedItem string
|
||||
}{
|
||||
{"format.json", false, "format.json", ""},
|
||||
{"users/tester.json", false, "users/", "tester.json"},
|
||||
{"groups/test/group.json", false, "groups/", "test/group.json"},
|
||||
{"policydb/groups/testgroup.json", true, "policydb/groups/", "testgroup.json"},
|
||||
{
|
||||
"policydb/sts-users/uid=slash/user,ou=people,ou=swengg,dc=min,dc=io.json", true,
|
||||
"policydb/sts-users/", "uid=slash/user,ou=people,ou=swengg,dc=min,dc=io.json",
|
||||
},
|
||||
{
|
||||
"policydb/sts-users/uid=slash/user/twice,ou=people,ou=swengg,dc=min,dc=io.json", true,
|
||||
"policydb/sts-users/", "uid=slash/user/twice,ou=people,ou=swengg,dc=min,dc=io.json",
|
||||
},
|
||||
{
|
||||
"policydb/groups/cn=project/d,ou=groups,ou=swengg,dc=min,dc=io.json", true,
|
||||
"policydb/groups/", "cn=project/d,ou=groups,ou=swengg,dc=min,dc=io.json",
|
||||
},
|
||||
}
|
||||
for i, test := range cases {
|
||||
listKey, item := splitPath(test.path, test.secondIndex)
|
||||
if listKey != test.expectedListKey || item != test.expectedItem {
|
||||
t.Errorf("unexpected result on test[%v]: expected[%s, %s] but got [%s, %s]", i, test.expectedListKey, test.expectedItem, listKey, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
-14
@@ -26,6 +26,7 @@ import (
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
@@ -37,6 +38,7 @@ import (
|
||||
"github.com/minio/minio/internal/jwt"
|
||||
"github.com/minio/pkg/v3/env"
|
||||
"github.com/minio/pkg/v3/policy"
|
||||
"github.com/minio/pkg/v3/sync/errgroup"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
@@ -357,6 +359,68 @@ func (c *iamCache) removeGroupFromMembershipsMap(group string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *iamCache) policyDBGetGroups(store *IAMStoreSys, userPolicyPresent bool, groups ...string) ([]string, error) {
|
||||
var policies []string
|
||||
for _, group := range groups {
|
||||
if store.getUsersSysType() == MinIOUsersSysType {
|
||||
g, ok := c.iamGroupsMap[group]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Group is disabled, so we return no policy - this
|
||||
// ensures the request is denied.
|
||||
if g.Status == statusDisabled {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
policy, ok := c.iamGroupPolicyMap.Load(group)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
policies = append(policies, policy.toSlice()...)
|
||||
}
|
||||
|
||||
found := len(policies) > 0
|
||||
if found {
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
if userPolicyPresent {
|
||||
// if user mapping present and no group policies found
|
||||
// rely on user policy for access, instead of fallback.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
// no mappings found, fallback for all groups.
|
||||
g := errgroup.WithNErrs(len(groups)).WithConcurrency(10) // load like 10 groups at a time.
|
||||
|
||||
for index := range groups {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
err := store.loadMappedPolicy(context.TODO(), groups[index], regUser, true, c.iamGroupPolicyMap)
|
||||
if err != nil && !errors.Is(err, errNoSuchPolicy) {
|
||||
return err
|
||||
}
|
||||
if errors.Is(err, errNoSuchPolicy) {
|
||||
return nil
|
||||
}
|
||||
policy, _ := c.iamGroupPolicyMap.Load(groups[index])
|
||||
mu.Lock()
|
||||
policies = append(policies, policy.toSlice()...)
|
||||
mu.Unlock()
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
|
||||
err := errors.Join(g.Wait()...)
|
||||
return policies, err
|
||||
}
|
||||
|
||||
// policyDBGet - lower-level helper; does not take locks.
|
||||
//
|
||||
// If a group is passed, it returns policies associated with the group.
|
||||
@@ -676,7 +740,8 @@ func (store *IAMStoreSys) LoadIAMCache(ctx context.Context, firstTime bool) erro
|
||||
type IAMStoreSys struct {
|
||||
IAMStorageAPI
|
||||
|
||||
group *singleflight.Group
|
||||
group *singleflight.Group
|
||||
policy *singleflight.Group
|
||||
}
|
||||
|
||||
// HasWatcher - returns if the storage system has a watcher.
|
||||
@@ -757,21 +822,32 @@ func (store *IAMStoreSys) PolicyDBGet(name string, groups ...string) ([]string,
|
||||
cache := store.rlock()
|
||||
defer store.runlock()
|
||||
|
||||
policies, _, err := cache.policyDBGet(store, name, false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userPolicyPresent := len(policies) > 0
|
||||
for _, group := range groups {
|
||||
ps, _, err := cache.policyDBGet(store, group, true, userPolicyPresent)
|
||||
getPolicies := func() ([]string, error) {
|
||||
policies, _, err := cache.policyDBGet(store, name, false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policies = append(policies, ps...)
|
||||
}
|
||||
|
||||
return policies, nil
|
||||
userPolicyPresent := len(policies) > 0
|
||||
|
||||
groupPolicies, err := cache.policyDBGetGroups(store, userPolicyPresent, groups...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policies = append(policies, groupPolicies...)
|
||||
return policies, nil
|
||||
}
|
||||
if store.policy != nil {
|
||||
val, err, _ := store.policy.Do(name, func() (interface{}, error) {
|
||||
return getPolicies()
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return val.([]string), nil
|
||||
}
|
||||
return getPolicies()
|
||||
}
|
||||
|
||||
// AddUsersToGroup - adds users to group, creating the group if needed.
|
||||
@@ -2009,7 +2085,8 @@ type ParentUserInfo struct {
|
||||
// GetAllParentUsers - returns all distinct "parent-users" associated with STS
|
||||
// or service credentials, mapped to all distinct roleARNs associated with the
|
||||
// parent user. The dummy role ARN is associated with parent users from
|
||||
// policy-claim based OpenID providers.
|
||||
// policy-claim based OpenID providers. The root credential as a parent
|
||||
// user is not included in the result.
|
||||
func (store *IAMStoreSys) GetAllParentUsers() map[string]ParentUserInfo {
|
||||
cache := store.rlock()
|
||||
defer store.runlock()
|
||||
@@ -2048,7 +2125,7 @@ func (store *IAMStoreSys) getParentUsers(cache *iamCache) map[string]ParentUserI
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if cred.ParentUser == "" {
|
||||
if cred.ParentUser == "" || cred.ParentUser == globalActiveCred.AccessKey {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
+10
-4
@@ -178,13 +178,18 @@ func (sys *IAMSys) initStore(objAPI ObjectLayer, etcdClient *etcd.Client) {
|
||||
}
|
||||
|
||||
if etcdClient == nil {
|
||||
var group *singleflight.Group
|
||||
var (
|
||||
group *singleflight.Group
|
||||
policy *singleflight.Group
|
||||
)
|
||||
if env.Get("_MINIO_IAM_SINGLE_FLIGHT", config.EnableOn) == config.EnableOn {
|
||||
group = &singleflight.Group{}
|
||||
policy = &singleflight.Group{}
|
||||
}
|
||||
sys.store = &IAMStoreSys{
|
||||
IAMStorageAPI: newIAMObjectStore(objAPI, sys.usersSysType),
|
||||
group: group,
|
||||
policy: policy,
|
||||
}
|
||||
} else {
|
||||
sys.store = &IAMStoreSys{IAMStorageAPI: newIAMEtcdStore(etcdClient, sys.usersSysType)}
|
||||
@@ -406,6 +411,7 @@ func (sys *IAMSys) periodicRoutines(ctx context.Context, baseInterval time.Durat
|
||||
timer := time.NewTimer(waitInterval())
|
||||
defer timer.Stop()
|
||||
|
||||
lastPurgeHour := -1
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
@@ -421,9 +427,9 @@ func (sys *IAMSys) periodicRoutines(ctx context.Context, baseInterval time.Durat
|
||||
}
|
||||
}
|
||||
|
||||
// The following actions are performed about once in 4 times that
|
||||
// IAM is refreshed:
|
||||
if r.Intn(4) == 0 {
|
||||
// Run purge routines once in each hour.
|
||||
if refreshStart.Hour() != lastPurgeHour {
|
||||
lastPurgeHour = refreshStart.Hour()
|
||||
// Poll and remove accounts for those users who were removed
|
||||
// from LDAP/OpenID.
|
||||
if sys.LDAPConfig.Enabled() {
|
||||
|
||||
+152
-111
@@ -24,11 +24,20 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/dsync"
|
||||
)
|
||||
|
||||
// Reject new lock requests immediately when this many are queued
|
||||
// for the local lock mutex.
|
||||
// We do not block unlocking or maintenance, but they add to the count.
|
||||
// The limit is set to allow for bursty behavior,
|
||||
// but prevent requests to overload the server completely.
|
||||
// Rejected clients are expected to retry.
|
||||
const lockMutexWaitLimit = 1000
|
||||
|
||||
// lockRequesterInfo stores various info from the client for each lock that is requested.
|
||||
type lockRequesterInfo struct {
|
||||
Name string // name of the resource lock was requested for
|
||||
@@ -52,9 +61,25 @@ func isWriteLock(lri []lockRequesterInfo) bool {
|
||||
//
|
||||
//msgp:ignore localLocker
|
||||
type localLocker struct {
|
||||
mutex sync.Mutex
|
||||
lockMap map[string][]lockRequesterInfo
|
||||
lockUID map[string]string // UUID -> resource map.
|
||||
mutex sync.Mutex
|
||||
waitMutex atomic.Int32
|
||||
lockMap map[string][]lockRequesterInfo
|
||||
lockUID map[string]string // UUID -> resource map.
|
||||
|
||||
// the following are updated on every cleanup defined in lockValidityDuration
|
||||
readers atomic.Int32
|
||||
writers atomic.Int32
|
||||
lastCleanup atomic.Pointer[time.Time]
|
||||
locksOverloaded atomic.Int64
|
||||
}
|
||||
|
||||
// getMutex will lock the mutex.
|
||||
// Call the returned function to unlock.
|
||||
func (l *localLocker) getMutex() func() {
|
||||
l.waitMutex.Add(1)
|
||||
l.mutex.Lock()
|
||||
l.waitMutex.Add(-1)
|
||||
return l.mutex.Unlock
|
||||
}
|
||||
|
||||
func (l *localLocker) String() string {
|
||||
@@ -76,9 +101,16 @@ func (l *localLocker) Lock(ctx context.Context, args dsync.LockArgs) (reply bool
|
||||
return false, fmt.Errorf("internal error: localLocker.Lock called with more than %d resources", maxDeleteList)
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
// If we have too many waiting, reject this at once.
|
||||
if l.waitMutex.Load() > lockMutexWaitLimit {
|
||||
l.locksOverloaded.Add(1)
|
||||
return false, nil
|
||||
}
|
||||
// Wait for mutex
|
||||
defer l.getMutex()()
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
if !l.canTakeLock(args.Resources...) {
|
||||
// Not all locks can be taken on resources,
|
||||
// reject it completely.
|
||||
@@ -117,9 +149,7 @@ func (l *localLocker) Unlock(_ context.Context, args dsync.LockArgs) (reply bool
|
||||
return false, fmt.Errorf("internal error: localLocker.Unlock called with more than %d resources", maxDeleteList)
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
err = nil
|
||||
defer l.getMutex()()
|
||||
|
||||
for _, resource := range args.Resources {
|
||||
lri, ok := l.lockMap[resource]
|
||||
@@ -164,9 +194,17 @@ func (l *localLocker) RLock(ctx context.Context, args dsync.LockArgs) (reply boo
|
||||
if len(args.Resources) != 1 {
|
||||
return false, fmt.Errorf("internal error: localLocker.RLock called with more than one resource")
|
||||
}
|
||||
// If we have too many waiting, reject this at once.
|
||||
if l.waitMutex.Load() > lockMutexWaitLimit {
|
||||
l.locksOverloaded.Add(1)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
// Wait for mutex
|
||||
defer l.getMutex()()
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
resource := args.Resources[0]
|
||||
now := UTCNow()
|
||||
lrInfo := lockRequesterInfo{
|
||||
@@ -200,8 +238,7 @@ func (l *localLocker) RUnlock(_ context.Context, args dsync.LockArgs) (reply boo
|
||||
return false, fmt.Errorf("internal error: localLocker.RUnlock called with more than one resource")
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
defer l.getMutex()()
|
||||
var lri []lockRequesterInfo
|
||||
|
||||
resource := args.Resources[0]
|
||||
@@ -218,35 +255,28 @@ func (l *localLocker) RUnlock(_ context.Context, args dsync.LockArgs) (reply boo
|
||||
}
|
||||
|
||||
type lockStats struct {
|
||||
Total int
|
||||
Writes int
|
||||
Reads int
|
||||
Total int
|
||||
Writes int
|
||||
Reads int
|
||||
LockQueue int
|
||||
LocksAbandoned int
|
||||
LastCleanup *time.Time
|
||||
}
|
||||
|
||||
func (l *localLocker) stats() lockStats {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
st := lockStats{Total: len(l.lockMap)}
|
||||
for _, v := range l.lockMap {
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
}
|
||||
entry := v[0]
|
||||
if entry.Writer {
|
||||
st.Writes++
|
||||
} else {
|
||||
st.Reads += len(v)
|
||||
}
|
||||
return lockStats{
|
||||
Total: len(l.lockMap),
|
||||
Reads: int(l.readers.Load()),
|
||||
Writes: int(l.writers.Load()),
|
||||
LockQueue: int(l.waitMutex.Load()),
|
||||
LastCleanup: l.lastCleanup.Load(),
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
type localLockMap map[string][]lockRequesterInfo
|
||||
|
||||
func (l *localLocker) DupLockMap() localLockMap {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
defer l.getMutex()()
|
||||
|
||||
lockCopy := make(map[string][]lockRequesterInfo, len(l.lockMap))
|
||||
for k, v := range l.lockMap {
|
||||
@@ -274,108 +304,110 @@ func (l *localLocker) IsLocal() bool {
|
||||
}
|
||||
|
||||
func (l *localLocker) ForceUnlock(ctx context.Context, args dsync.LockArgs) (reply bool, err error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
default:
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
if len(args.UID) == 0 {
|
||||
for _, resource := range args.Resources {
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Collect uids, so we don't mutate while we delete
|
||||
uids := make([]string, 0, len(lris))
|
||||
for _, lri := range lris {
|
||||
uids = append(uids, lri.UID)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete collected uids:
|
||||
for _, uid := range uids {
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
// Just to be safe, delete uuids.
|
||||
for idx := 0; idx < maxDeleteList; idx++ {
|
||||
mapID := formatUUID(uid, idx)
|
||||
if _, ok := l.lockUID[mapID]; !ok {
|
||||
break
|
||||
}
|
||||
delete(l.lockUID, mapID)
|
||||
}
|
||||
continue
|
||||
}
|
||||
l.removeEntry(resource, dsync.LockArgs{UID: uid}, &lris)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
idx := 0
|
||||
for {
|
||||
mapID := formatUUID(args.UID, idx)
|
||||
resource, ok := l.lockUID[mapID]
|
||||
if !ok {
|
||||
return idx > 0, nil
|
||||
}
|
||||
defer l.getMutex()()
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
if len(args.UID) == 0 {
|
||||
for _, resource := range args.Resources {
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
// Unexpected inconsistency, delete.
|
||||
delete(l.lockUID, mapID)
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
reply = true
|
||||
l.removeEntry(resource, dsync.LockArgs{UID: args.UID}, &lris)
|
||||
idx++
|
||||
// Collect uids, so we don't mutate while we delete
|
||||
uids := make([]string, 0, len(lris))
|
||||
for _, lri := range lris {
|
||||
uids = append(uids, lri.UID)
|
||||
}
|
||||
|
||||
// Delete collected uids:
|
||||
for _, uid := range uids {
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
// Just to be safe, delete uuids.
|
||||
for idx := 0; idx < maxDeleteList; idx++ {
|
||||
mapID := formatUUID(uid, idx)
|
||||
if _, ok := l.lockUID[mapID]; !ok {
|
||||
break
|
||||
}
|
||||
delete(l.lockUID, mapID)
|
||||
}
|
||||
continue
|
||||
}
|
||||
l.removeEntry(resource, dsync.LockArgs{UID: uid}, &lris)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
idx := 0
|
||||
for {
|
||||
mapID := formatUUID(args.UID, idx)
|
||||
resource, ok := l.lockUID[mapID]
|
||||
if !ok {
|
||||
return idx > 0, nil
|
||||
}
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
// Unexpected inconsistency, delete.
|
||||
delete(l.lockUID, mapID)
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
reply = true
|
||||
l.removeEntry(resource, dsync.LockArgs{UID: args.UID}, &lris)
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
func (l *localLocker) Refresh(ctx context.Context, args dsync.LockArgs) (refreshed bool, err error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
default:
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Check whether uid is still active.
|
||||
resource, ok := l.lockUID[formatUUID(args.UID, 0)]
|
||||
defer l.getMutex()()
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
}
|
||||
|
||||
// Check whether uid is still active.
|
||||
resource, ok := l.lockUID[formatUUID(args.UID, 0)]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
idx := 0
|
||||
for {
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
return false, nil
|
||||
// Inconsistent. Delete UID.
|
||||
delete(l.lockUID, formatUUID(args.UID, idx))
|
||||
return idx > 0, nil
|
||||
}
|
||||
idx := 0
|
||||
for {
|
||||
lris, ok := l.lockMap[resource]
|
||||
if !ok {
|
||||
// Inconsistent. Delete UID.
|
||||
delete(l.lockUID, formatUUID(args.UID, idx))
|
||||
return idx > 0, nil
|
||||
}
|
||||
now := UTCNow()
|
||||
for i := range lris {
|
||||
if lris[i].UID == args.UID {
|
||||
lris[i].TimeLastRefresh = now.UnixNano()
|
||||
}
|
||||
}
|
||||
idx++
|
||||
resource, ok = l.lockUID[formatUUID(args.UID, idx)]
|
||||
if !ok {
|
||||
// No more resources for UID, but we did update at least one.
|
||||
return true, nil
|
||||
now := UTCNow()
|
||||
for i := range lris {
|
||||
if lris[i].UID == args.UID {
|
||||
lris[i].TimeLastRefresh = now.UnixNano()
|
||||
}
|
||||
}
|
||||
idx++
|
||||
resource, ok = l.lockUID[formatUUID(args.UID, idx)]
|
||||
if !ok {
|
||||
// No more resources for UID, but we did update at least one.
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Similar to removeEntry but only removes an entry only if the lock entry exists in map.
|
||||
// Caller must hold 'l.mutex' lock.
|
||||
func (l *localLocker) expireOldLocks(interval time.Duration) {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
defer l.getMutex()()
|
||||
|
||||
var readers, writers int32
|
||||
for k, lris := range l.lockMap {
|
||||
modified := false
|
||||
for i := 0; i < len(lris); {
|
||||
@@ -393,6 +425,11 @@ func (l *localLocker) expireOldLocks(interval time.Duration) {
|
||||
lris = append(lris[:i], lris[i+1:]...)
|
||||
// Check same i
|
||||
} else {
|
||||
if lri.Writer {
|
||||
writers++
|
||||
} else {
|
||||
readers++
|
||||
}
|
||||
// Move to next
|
||||
i++
|
||||
}
|
||||
@@ -401,6 +438,10 @@ func (l *localLocker) expireOldLocks(interval time.Duration) {
|
||||
l.lockMap[k] = lris
|
||||
}
|
||||
}
|
||||
t := time.Now()
|
||||
l.lastCleanup.Store(&t)
|
||||
l.readers.Store(readers)
|
||||
l.writers.Store(writers)
|
||||
}
|
||||
|
||||
func newLocker() *localLocker {
|
||||
|
||||
+124
-8
@@ -3,6 +3,8 @@ package cmd
|
||||
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
)
|
||||
|
||||
@@ -506,6 +508,36 @@ func (z *lockStats) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err, "Reads")
|
||||
return
|
||||
}
|
||||
case "LockQueue":
|
||||
z.LockQueue, err = dc.ReadInt()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LockQueue")
|
||||
return
|
||||
}
|
||||
case "LocksAbandoned":
|
||||
z.LocksAbandoned, err = dc.ReadInt()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LocksAbandoned")
|
||||
return
|
||||
}
|
||||
case "LastCleanup":
|
||||
if dc.IsNil() {
|
||||
err = dc.ReadNil()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastCleanup")
|
||||
return
|
||||
}
|
||||
z.LastCleanup = nil
|
||||
} else {
|
||||
if z.LastCleanup == nil {
|
||||
z.LastCleanup = new(time.Time)
|
||||
}
|
||||
*z.LastCleanup, err = dc.ReadTime()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastCleanup")
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
err = dc.Skip()
|
||||
if err != nil {
|
||||
@@ -518,10 +550,10 @@ func (z *lockStats) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
}
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z lockStats) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// map header, size 3
|
||||
func (z *lockStats) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// map header, size 6
|
||||
// write "Total"
|
||||
err = en.Append(0x83, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
|
||||
err = en.Append(0x86, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -550,15 +582,52 @@ func (z lockStats) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = msgp.WrapError(err, "Reads")
|
||||
return
|
||||
}
|
||||
// write "LockQueue"
|
||||
err = en.Append(0xa9, 0x4c, 0x6f, 0x63, 0x6b, 0x51, 0x75, 0x65, 0x75, 0x65)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteInt(z.LockQueue)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LockQueue")
|
||||
return
|
||||
}
|
||||
// write "LocksAbandoned"
|
||||
err = en.Append(0xae, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x65, 0x64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteInt(z.LocksAbandoned)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LocksAbandoned")
|
||||
return
|
||||
}
|
||||
// write "LastCleanup"
|
||||
err = en.Append(0xab, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if z.LastCleanup == nil {
|
||||
err = en.WriteNil()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = en.WriteTime(*z.LastCleanup)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastCleanup")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z lockStats) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
func (z *lockStats) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// map header, size 3
|
||||
// map header, size 6
|
||||
// string "Total"
|
||||
o = append(o, 0x83, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
|
||||
o = append(o, 0x86, 0xa5, 0x54, 0x6f, 0x74, 0x61, 0x6c)
|
||||
o = msgp.AppendInt(o, z.Total)
|
||||
// string "Writes"
|
||||
o = append(o, 0xa6, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73)
|
||||
@@ -566,6 +635,19 @@ func (z lockStats) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
// string "Reads"
|
||||
o = append(o, 0xa5, 0x52, 0x65, 0x61, 0x64, 0x73)
|
||||
o = msgp.AppendInt(o, z.Reads)
|
||||
// string "LockQueue"
|
||||
o = append(o, 0xa9, 0x4c, 0x6f, 0x63, 0x6b, 0x51, 0x75, 0x65, 0x75, 0x65)
|
||||
o = msgp.AppendInt(o, z.LockQueue)
|
||||
// string "LocksAbandoned"
|
||||
o = append(o, 0xae, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x41, 0x62, 0x61, 0x6e, 0x64, 0x6f, 0x6e, 0x65, 0x64)
|
||||
o = msgp.AppendInt(o, z.LocksAbandoned)
|
||||
// string "LastCleanup"
|
||||
o = append(o, 0xab, 0x4c, 0x61, 0x73, 0x74, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70)
|
||||
if z.LastCleanup == nil {
|
||||
o = msgp.AppendNil(o)
|
||||
} else {
|
||||
o = msgp.AppendTime(o, *z.LastCleanup)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -605,6 +687,35 @@ func (z *lockStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "Reads")
|
||||
return
|
||||
}
|
||||
case "LockQueue":
|
||||
z.LockQueue, bts, err = msgp.ReadIntBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LockQueue")
|
||||
return
|
||||
}
|
||||
case "LocksAbandoned":
|
||||
z.LocksAbandoned, bts, err = msgp.ReadIntBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LocksAbandoned")
|
||||
return
|
||||
}
|
||||
case "LastCleanup":
|
||||
if msgp.IsNil(bts) {
|
||||
bts, err = msgp.ReadNilBytes(bts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
z.LastCleanup = nil
|
||||
} else {
|
||||
if z.LastCleanup == nil {
|
||||
z.LastCleanup = new(time.Time)
|
||||
}
|
||||
*z.LastCleanup, bts, err = msgp.ReadTimeBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastCleanup")
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
@@ -618,7 +729,12 @@ func (z *lockStats) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
}
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z lockStats) Msgsize() (s int) {
|
||||
s = 1 + 6 + msgp.IntSize + 7 + msgp.IntSize + 6 + msgp.IntSize
|
||||
func (z *lockStats) Msgsize() (s int) {
|
||||
s = 1 + 6 + msgp.IntSize + 7 + msgp.IntSize + 6 + msgp.IntSize + 10 + msgp.IntSize + 15 + msgp.IntSize + 12
|
||||
if z.LastCleanup == nil {
|
||||
s += msgp.NilSize
|
||||
} else {
|
||||
s += msgp.TimeSize
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
+13
-3
@@ -33,8 +33,12 @@ type lockRESTServer struct {
|
||||
|
||||
// RefreshHandler - refresh the current lock
|
||||
func (l *lockRESTServer) RefreshHandler(args *dsync.LockArgs) (*dsync.LockResp, *grid.RemoteErr) {
|
||||
// Add a timeout similar to what we expect upstream.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dsync.DefaultTimeouts.RefreshCall)
|
||||
defer cancel()
|
||||
|
||||
resp := lockRPCRefresh.NewResponse()
|
||||
refreshed, err := l.ll.Refresh(context.Background(), *args)
|
||||
refreshed, err := l.ll.Refresh(ctx, *args)
|
||||
if err != nil {
|
||||
return l.makeResp(resp, err)
|
||||
}
|
||||
@@ -46,8 +50,11 @@ func (l *lockRESTServer) RefreshHandler(args *dsync.LockArgs) (*dsync.LockResp,
|
||||
|
||||
// LockHandler - Acquires a lock.
|
||||
func (l *lockRESTServer) LockHandler(args *dsync.LockArgs) (*dsync.LockResp, *grid.RemoteErr) {
|
||||
// Add a timeout similar to what we expect upstream.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dsync.DefaultTimeouts.Acquire)
|
||||
defer cancel()
|
||||
resp := lockRPCLock.NewResponse()
|
||||
success, err := l.ll.Lock(context.Background(), *args)
|
||||
success, err := l.ll.Lock(ctx, *args)
|
||||
if err == nil && !success {
|
||||
return l.makeResp(resp, errLockConflict)
|
||||
}
|
||||
@@ -65,8 +72,11 @@ func (l *lockRESTServer) UnlockHandler(args *dsync.LockArgs) (*dsync.LockResp, *
|
||||
|
||||
// RLockHandler - Acquires an RLock.
|
||||
func (l *lockRESTServer) RLockHandler(args *dsync.LockArgs) (*dsync.LockResp, *grid.RemoteErr) {
|
||||
// Add a timeout similar to what we expect upstream.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dsync.DefaultTimeouts.Acquire)
|
||||
defer cancel()
|
||||
resp := lockRPCRLock.NewResponse()
|
||||
success, err := l.ll.RLock(context.Background(), *args)
|
||||
success, err := l.ll.RLock(ctx, *args)
|
||||
if err == nil && !success {
|
||||
err = errLockConflict
|
||||
}
|
||||
|
||||
+12
-16
@@ -29,6 +29,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
@@ -182,8 +183,7 @@ func (o *listPathOptions) gatherResults(ctx context.Context, in <-chan metaCache
|
||||
resultsDone := make(chan metaCacheEntriesSorted)
|
||||
// Copy so we can mutate
|
||||
resCh := resultsDone
|
||||
var done bool
|
||||
var mu sync.Mutex
|
||||
var done atomic.Bool
|
||||
resErr := io.EOF
|
||||
|
||||
go func() {
|
||||
@@ -194,9 +194,7 @@ func (o *listPathOptions) gatherResults(ctx context.Context, in <-chan metaCache
|
||||
// past limit
|
||||
continue
|
||||
}
|
||||
mu.Lock()
|
||||
returned = done
|
||||
mu.Unlock()
|
||||
returned = done.Load()
|
||||
if returned {
|
||||
resCh = nil
|
||||
continue
|
||||
@@ -250,9 +248,7 @@ func (o *listPathOptions) gatherResults(ctx context.Context, in <-chan metaCache
|
||||
return func() (metaCacheEntriesSorted, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
mu.Lock()
|
||||
done = true
|
||||
mu.Unlock()
|
||||
done.Store(true)
|
||||
return metaCacheEntriesSorted{}, ctx.Err()
|
||||
case r := <-resultsDone:
|
||||
return r, resErr
|
||||
@@ -626,18 +622,18 @@ func calcCommonWritesDeletes(infos []DiskInfo, readQuorum int) (commonWrite, com
|
||||
}
|
||||
|
||||
filter := func(list []uint64) (commonCount uint64) {
|
||||
max := 0
|
||||
maxCnt := 0
|
||||
signatureMap := map[uint64]int{}
|
||||
for _, v := range list {
|
||||
signatureMap[v]++
|
||||
}
|
||||
for ops, count := range signatureMap {
|
||||
if max < count && commonCount < ops {
|
||||
max = count
|
||||
if maxCnt < count && commonCount < ops {
|
||||
maxCnt = count
|
||||
commonCount = ops
|
||||
}
|
||||
}
|
||||
if max < readQuorum {
|
||||
if maxCnt < readQuorum {
|
||||
return 0
|
||||
}
|
||||
return commonCount
|
||||
@@ -650,7 +646,7 @@ func calcCommonWritesDeletes(infos []DiskInfo, readQuorum int) (commonWrite, com
|
||||
|
||||
func calcCommonCounter(infos []DiskInfo, readQuorum int) (commonCount uint64) {
|
||||
filter := func() (commonCount uint64) {
|
||||
max := 0
|
||||
maxCnt := 0
|
||||
signatureMap := map[uint64]int{}
|
||||
for _, info := range infos {
|
||||
if info.Error != "" {
|
||||
@@ -660,12 +656,12 @@ func calcCommonCounter(infos []DiskInfo, readQuorum int) (commonCount uint64) {
|
||||
signatureMap[mutations]++
|
||||
}
|
||||
for ops, count := range signatureMap {
|
||||
if max < count && commonCount < ops {
|
||||
max = count
|
||||
if maxCnt < count && commonCount < ops {
|
||||
maxCnt = count
|
||||
commonCount = ops
|
||||
}
|
||||
}
|
||||
if max < readQuorum {
|
||||
if maxCnt < readQuorum {
|
||||
return 0
|
||||
}
|
||||
return commonCount
|
||||
|
||||
+17
-3
@@ -685,6 +685,16 @@ func getUsageLastScanActivityMD() MetricDescription {
|
||||
}
|
||||
}
|
||||
|
||||
func getBucketUsageLastScanActivityMD() MetricDescription {
|
||||
return MetricDescription{
|
||||
Namespace: bucketMetricNamespace,
|
||||
Subsystem: usageSubsystem,
|
||||
Name: lastActivityTime,
|
||||
Help: "Time elapsed (in nano seconds) since last scan activity",
|
||||
Type: gaugeMetric,
|
||||
}
|
||||
}
|
||||
|
||||
func getBucketUsageQuotaTotalBytesMD() MetricDescription {
|
||||
return MetricDescription{
|
||||
Namespace: bucketMetricNamespace,
|
||||
@@ -1875,13 +1885,17 @@ func getHistogramMetrics(hist *prometheus.HistogramVec, desc MetricDescription,
|
||||
// add metrics with +Inf label
|
||||
labels1 := make(map[string]string)
|
||||
for _, lp := range dtoMetric.GetLabel() {
|
||||
labels1[*lp.Name] = *lp.Value
|
||||
if *lp.Name == "api" && toLowerAPILabels {
|
||||
labels1[*lp.Name] = strings.ToLower(*lp.Value)
|
||||
} else {
|
||||
labels1[*lp.Name] = *lp.Value
|
||||
}
|
||||
}
|
||||
labels1["le"] = fmt.Sprintf("%.3f", math.Inf(+1))
|
||||
metrics = append(metrics, MetricV2{
|
||||
Description: desc,
|
||||
VariableLabels: labels1,
|
||||
Value: dtoMetric.Counter.GetValue(),
|
||||
Value: float64(dtoMetric.Histogram.GetSampleCount()),
|
||||
})
|
||||
}
|
||||
return metrics
|
||||
@@ -3239,7 +3253,7 @@ func getBucketUsageMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
|
||||
}
|
||||
|
||||
metrics = append(metrics, MetricV2{
|
||||
Description: getUsageLastScanActivityMD(),
|
||||
Description: getBucketUsageLastScanActivityMD(),
|
||||
Value: float64(time.Since(dataUsageInfo.LastUpdate)),
|
||||
})
|
||||
|
||||
|
||||
+133
-2
@@ -18,13 +18,15 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func TestGetHistogramMetrics(t *testing.T) {
|
||||
func TestGetHistogramMetrics_BucketCount(t *testing.T) {
|
||||
histBuckets := []float64{0.05, 0.1, 0.25, 0.5, 0.75}
|
||||
labels := []string{"GetObject", "PutObject", "CopyObject", "CompleteMultipartUpload"}
|
||||
ttfbHist := prometheus.NewHistogramVec(
|
||||
@@ -83,6 +85,135 @@ func TestGetHistogramMetrics(t *testing.T) {
|
||||
metrics := getHistogramMetrics(ttfbHist, getBucketTTFBDistributionMD(), false)
|
||||
// additional labels for +Inf for all histogram metrics
|
||||
if expPoints := len(labels) * (len(histBuckets) + 1); expPoints != len(metrics) {
|
||||
t.Fatalf("Expected %v data points but got %v", expPoints, len(metrics))
|
||||
t.Fatalf("Expected %v data points when toLowerAPILabels=false but got %v", expPoints, len(metrics))
|
||||
}
|
||||
|
||||
metrics = getHistogramMetrics(ttfbHist, getBucketTTFBDistributionMD(), true)
|
||||
// additional labels for +Inf for all histogram metrics
|
||||
if expPoints := len(labels) * (len(histBuckets) + 1); expPoints != len(metrics) {
|
||||
t.Fatalf("Expected %v data points when toLowerAPILabels=true but got %v", expPoints, len(metrics))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHistogramMetrics_Values(t *testing.T) {
|
||||
histBuckets := []float64{0.50, 5.00}
|
||||
labels := []string{"PutObject", "CopyObject"}
|
||||
ttfbHist := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "s3_ttfb_seconds",
|
||||
Help: "Time taken by requests served by current MinIO server instance",
|
||||
Buckets: histBuckets,
|
||||
},
|
||||
[]string{"api"},
|
||||
)
|
||||
observations := []struct {
|
||||
val float64
|
||||
label string
|
||||
}{
|
||||
{
|
||||
val: 0.02,
|
||||
label: labels[0],
|
||||
},
|
||||
{
|
||||
val: 0.19,
|
||||
label: labels[1],
|
||||
},
|
||||
{
|
||||
val: 0.31,
|
||||
label: labels[1],
|
||||
},
|
||||
{
|
||||
val: 0.61,
|
||||
label: labels[0],
|
||||
},
|
||||
{
|
||||
val: 6.79,
|
||||
label: labels[1],
|
||||
},
|
||||
}
|
||||
ticker := time.NewTicker(1 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for _, obs := range observations {
|
||||
// Send observations once every 1ms, to simulate delay between
|
||||
// observations. This is to test the channel based
|
||||
// synchronization used internally.
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ttfbHist.With(prometheus.Labels{"api": obs.label}).Observe(obs.val)
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate regular-cased API label metrics for 'PutObject' for deeper verification
|
||||
metrics := getHistogramMetrics(ttfbHist, getBucketTTFBDistributionMD(), false)
|
||||
capitalPutObjects := make([]MetricV2, 0, len(histBuckets)+1)
|
||||
for _, metric := range metrics {
|
||||
if value := metric.VariableLabels["api"]; value == "PutObject" {
|
||||
capitalPutObjects = append(capitalPutObjects, metric)
|
||||
}
|
||||
}
|
||||
if expMetricsPerAPI := len(histBuckets) + 1; expMetricsPerAPI != len(capitalPutObjects) {
|
||||
t.Fatalf("Expected %d api=PutObject metrics but got %d", expMetricsPerAPI, len(capitalPutObjects))
|
||||
}
|
||||
|
||||
// Deterministic ordering
|
||||
slices.SortFunc(capitalPutObjects, func(a MetricV2, b MetricV2) int {
|
||||
le1 := a.VariableLabels["le"]
|
||||
le2 := a.VariableLabels["le"]
|
||||
return strings.Compare(le1, le2)
|
||||
})
|
||||
if le := capitalPutObjects[0].VariableLabels["le"]; le != "0.500" {
|
||||
t.Errorf("Expected le='0.050' api=PutObject metrics but got '%v'", le)
|
||||
}
|
||||
if value := capitalPutObjects[0].Value; value != 1 {
|
||||
t.Errorf("Expected le='0.050' api=PutObject value to be 1 but got '%v'", value)
|
||||
}
|
||||
if le := capitalPutObjects[1].VariableLabels["le"]; le != "5.000" {
|
||||
t.Errorf("Expected le='5.000' api=PutObject metrics but got '%v'", le)
|
||||
}
|
||||
if value := capitalPutObjects[1].Value; value != 2 {
|
||||
t.Errorf("Expected le='5.000' api=PutObject value to be 2 but got '%v'", value)
|
||||
}
|
||||
if le := capitalPutObjects[2].VariableLabels["le"]; le != "+Inf" {
|
||||
t.Errorf("Expected le='+Inf' api=PutObject metrics but got '%v'", le)
|
||||
}
|
||||
if value := capitalPutObjects[2].Value; value != 2 {
|
||||
t.Errorf("Expected le='+Inf' api=PutObject value to be 2 but got '%v'", value)
|
||||
}
|
||||
|
||||
// Accumulate lower-cased API label metrics for 'copyobject' for deeper verification
|
||||
metrics = getHistogramMetrics(ttfbHist, getBucketTTFBDistributionMD(), true)
|
||||
lowerCopyObjects := make([]MetricV2, 0, len(histBuckets)+1)
|
||||
for _, metric := range metrics {
|
||||
if value := metric.VariableLabels["api"]; value == "copyobject" {
|
||||
lowerCopyObjects = append(lowerCopyObjects, metric)
|
||||
}
|
||||
}
|
||||
if expMetricsPerAPI := len(histBuckets) + 1; expMetricsPerAPI != len(lowerCopyObjects) {
|
||||
t.Fatalf("Expected %d api=copyobject metrics but got %d", expMetricsPerAPI, len(lowerCopyObjects))
|
||||
}
|
||||
|
||||
// Deterministic ordering
|
||||
slices.SortFunc(lowerCopyObjects, func(a MetricV2, b MetricV2) int {
|
||||
le1 := a.VariableLabels["le"]
|
||||
le2 := a.VariableLabels["le"]
|
||||
return strings.Compare(le1, le2)
|
||||
})
|
||||
if le := lowerCopyObjects[0].VariableLabels["le"]; le != "0.500" {
|
||||
t.Errorf("Expected le='0.050' api=copyobject metrics but got '%v'", le)
|
||||
}
|
||||
if value := lowerCopyObjects[0].Value; value != 2 {
|
||||
t.Errorf("Expected le='0.050' api=copyobject value to be 2 but got '%v'", value)
|
||||
}
|
||||
if le := lowerCopyObjects[1].VariableLabels["le"]; le != "5.000" {
|
||||
t.Errorf("Expected le='5.000' api=copyobject metrics but got '%v'", le)
|
||||
}
|
||||
if value := lowerCopyObjects[1].Value; value != 2 {
|
||||
t.Errorf("Expected le='5.000' api=copyobject value to be 2 but got '%v'", value)
|
||||
}
|
||||
if le := lowerCopyObjects[2].VariableLabels["le"]; le != "+Inf" {
|
||||
t.Errorf("Expected le='+Inf' api=copyobject metrics but got '%v'", le)
|
||||
}
|
||||
if value := lowerCopyObjects[2].Value; value != 3 {
|
||||
t.Errorf("Expected le='+Inf' api=copyobject value to be 3 but got '%v'", value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func loadAPIRequestsHTTPMetrics(ctx context.Context, m MetricValues, _ *metricsC
|
||||
// This is a `MetricsLoaderFn`.
|
||||
func loadAPIRequestsTTFBMetrics(ctx context.Context, m MetricValues, _ *metricsCache) error {
|
||||
renameLabels := map[string]string{"api": "name"}
|
||||
labelsFilter := map[string]set.StringSet{"api": set.CreateStringSet("GetObject")}
|
||||
labelsFilter := map[string]set.StringSet{}
|
||||
m.SetHistogram(apiRequestsTTFBSecondsDistribution, httpRequestsDuration, labelsFilter, renameLabels, nil,
|
||||
"type", "s3")
|
||||
return nil
|
||||
@@ -217,7 +217,7 @@ func loadBucketAPIHTTPMetrics(ctx context.Context, m MetricValues, _ *metricsCac
|
||||
// This is a `MetricsLoaderFn`.
|
||||
func loadBucketAPITTFBMetrics(ctx context.Context, m MetricValues, _ *metricsCache, buckets []string) error {
|
||||
renameLabels := map[string]string{"api": "name"}
|
||||
labelsFilter := map[string]set.StringSet{"api": set.CreateStringSet("GetObject")}
|
||||
labelsFilter := map[string]set.StringSet{}
|
||||
m.SetHistogram(apiRequestsTTFBSecondsDistribution, bucketHTTPRequestsDuration, labelsFilter, renameLabels,
|
||||
buckets, "type", "s3")
|
||||
return nil
|
||||
|
||||
@@ -20,6 +20,7 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -27,7 +28,6 @@ import (
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type collectorPath string
|
||||
|
||||
@@ -229,6 +229,7 @@ type localLockInstance struct {
|
||||
// path. The returned lockInstance object encapsulates the nsLockMap,
|
||||
// volume, path and operation ID.
|
||||
func (n *nsLockMap) NewNSLock(lockers func() ([]dsync.NetLocker, string), volume string, paths ...string) RWLocker {
|
||||
sort.Strings(paths)
|
||||
opsID := mustGetUUID()
|
||||
if n.isDistErasure {
|
||||
drwmutex := dsync.NewDRWMutex(&dsync.Dsync{
|
||||
@@ -237,7 +238,6 @@ func (n *nsLockMap) NewNSLock(lockers func() ([]dsync.NetLocker, string), volume
|
||||
}, pathsJoinPrefix(volume, paths...)...)
|
||||
return &distLockInstance{drwmutex, opsID}
|
||||
}
|
||||
sort.Strings(paths)
|
||||
return &localLockInstance{n, volume, paths, opsID}
|
||||
}
|
||||
|
||||
|
||||
+28
-18
@@ -419,6 +419,9 @@ type ListPartsInfo struct {
|
||||
|
||||
// ChecksumAlgorithm if set
|
||||
ChecksumAlgorithm string
|
||||
|
||||
// ChecksumType if set
|
||||
ChecksumType string
|
||||
}
|
||||
|
||||
// Lookup - returns if uploadID is valid
|
||||
@@ -597,10 +600,11 @@ type PartInfo struct {
|
||||
ActualSize int64
|
||||
|
||||
// Checksum values
|
||||
ChecksumCRC32 string
|
||||
ChecksumCRC32C string
|
||||
ChecksumSHA1 string
|
||||
ChecksumSHA256 string
|
||||
ChecksumCRC32 string
|
||||
ChecksumCRC32C string
|
||||
ChecksumSHA1 string
|
||||
ChecksumSHA256 string
|
||||
ChecksumCRC64NVME string
|
||||
}
|
||||
|
||||
// CompletePart - represents the part that was completed, this is sent by the client
|
||||
@@ -613,11 +617,14 @@ type CompletePart struct {
|
||||
// Entity tag returned when the part was uploaded.
|
||||
ETag string
|
||||
|
||||
Size int64
|
||||
|
||||
// Checksum values. Optional.
|
||||
ChecksumCRC32 string
|
||||
ChecksumCRC32C string
|
||||
ChecksumSHA1 string
|
||||
ChecksumSHA256 string
|
||||
ChecksumCRC32 string
|
||||
ChecksumCRC32C string
|
||||
ChecksumSHA1 string
|
||||
ChecksumSHA256 string
|
||||
ChecksumCRC64NVME string
|
||||
}
|
||||
|
||||
// CompleteMultipartUpload - represents list of parts which are completed, this is sent by the
|
||||
@@ -630,6 +637,7 @@ type CompleteMultipartUpload struct {
|
||||
type NewMultipartUploadResult struct {
|
||||
UploadID string
|
||||
ChecksumAlgo string
|
||||
ChecksumType string
|
||||
}
|
||||
|
||||
type getObjectAttributesResponse struct {
|
||||
@@ -641,10 +649,11 @@ type getObjectAttributesResponse struct {
|
||||
}
|
||||
|
||||
type objectAttributesChecksum struct {
|
||||
ChecksumCRC32 string `xml:",omitempty"`
|
||||
ChecksumCRC32C string `xml:",omitempty"`
|
||||
ChecksumSHA1 string `xml:",omitempty"`
|
||||
ChecksumSHA256 string `xml:",omitempty"`
|
||||
ChecksumCRC32 string `xml:",omitempty"`
|
||||
ChecksumCRC32C string `xml:",omitempty"`
|
||||
ChecksumSHA1 string `xml:",omitempty"`
|
||||
ChecksumSHA256 string `xml:",omitempty"`
|
||||
ChecksumCRC64NVME string `xml:",omitempty"`
|
||||
}
|
||||
|
||||
type objectAttributesParts struct {
|
||||
@@ -657,12 +666,13 @@ type objectAttributesParts struct {
|
||||
}
|
||||
|
||||
type objectAttributesPart struct {
|
||||
PartNumber int
|
||||
Size int64
|
||||
ChecksumCRC32 string `xml:",omitempty"`
|
||||
ChecksumCRC32C string `xml:",omitempty"`
|
||||
ChecksumSHA1 string `xml:",omitempty"`
|
||||
ChecksumSHA256 string `xml:",omitempty"`
|
||||
PartNumber int
|
||||
Size int64
|
||||
ChecksumCRC32 string `xml:",omitempty"`
|
||||
ChecksumCRC32C string `xml:",omitempty"`
|
||||
ChecksumSHA1 string `xml:",omitempty"`
|
||||
ChecksumSHA256 string `xml:",omitempty"`
|
||||
ChecksumCRC64NVME string `xml:",omitempty"`
|
||||
}
|
||||
|
||||
type objectAttributesErrorResponse struct {
|
||||
|
||||
@@ -201,13 +201,16 @@ func (z *CompleteMultipartUpload) Msgsize() (s int) {
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z *CompletePart) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// map header, size 6
|
||||
// map header, size 8
|
||||
// string "PartNumber"
|
||||
o = append(o, 0x86, 0xaa, 0x50, 0x61, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72)
|
||||
o = append(o, 0x88, 0xaa, 0x50, 0x61, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72)
|
||||
o = msgp.AppendInt(o, z.PartNumber)
|
||||
// string "ETag"
|
||||
o = append(o, 0xa4, 0x45, 0x54, 0x61, 0x67)
|
||||
o = msgp.AppendString(o, z.ETag)
|
||||
// string "Size"
|
||||
o = append(o, 0xa4, 0x53, 0x69, 0x7a, 0x65)
|
||||
o = msgp.AppendInt64(o, z.Size)
|
||||
// string "ChecksumCRC32"
|
||||
o = append(o, 0xad, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x43, 0x52, 0x43, 0x33, 0x32)
|
||||
o = msgp.AppendString(o, z.ChecksumCRC32)
|
||||
@@ -220,6 +223,9 @@ func (z *CompletePart) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
// string "ChecksumSHA256"
|
||||
o = append(o, 0xae, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36)
|
||||
o = msgp.AppendString(o, z.ChecksumSHA256)
|
||||
// string "ChecksumCRC64NVME"
|
||||
o = append(o, 0xb1, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x43, 0x52, 0x43, 0x36, 0x34, 0x4e, 0x56, 0x4d, 0x45)
|
||||
o = msgp.AppendString(o, z.ChecksumCRC64NVME)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -253,6 +259,12 @@ func (z *CompletePart) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "ETag")
|
||||
return
|
||||
}
|
||||
case "Size":
|
||||
z.Size, bts, err = msgp.ReadInt64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Size")
|
||||
return
|
||||
}
|
||||
case "ChecksumCRC32":
|
||||
z.ChecksumCRC32, bts, err = msgp.ReadStringBytes(bts)
|
||||
if err != nil {
|
||||
@@ -277,6 +289,12 @@ func (z *CompletePart) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "ChecksumSHA256")
|
||||
return
|
||||
}
|
||||
case "ChecksumCRC64NVME":
|
||||
z.ChecksumCRC64NVME, bts, err = msgp.ReadStringBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ChecksumCRC64NVME")
|
||||
return
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
@@ -291,7 +309,7 @@ func (z *CompletePart) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z *CompletePart) Msgsize() (s int) {
|
||||
s = 1 + 11 + msgp.IntSize + 5 + msgp.StringPrefixSize + len(z.ETag) + 14 + msgp.StringPrefixSize + len(z.ChecksumCRC32) + 15 + msgp.StringPrefixSize + len(z.ChecksumCRC32C) + 13 + msgp.StringPrefixSize + len(z.ChecksumSHA1) + 15 + msgp.StringPrefixSize + len(z.ChecksumSHA256)
|
||||
s = 1 + 11 + msgp.IntSize + 5 + msgp.StringPrefixSize + len(z.ETag) + 5 + msgp.Int64Size + 14 + msgp.StringPrefixSize + len(z.ChecksumCRC32) + 15 + msgp.StringPrefixSize + len(z.ChecksumCRC32C) + 13 + msgp.StringPrefixSize + len(z.ChecksumSHA1) + 15 + msgp.StringPrefixSize + len(z.ChecksumSHA256) + 18 + msgp.StringPrefixSize + len(z.ChecksumCRC64NVME)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -956,9 +974,9 @@ func (z *ListObjectsV2Info) Msgsize() (s int) {
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z *ListPartsInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// map header, size 11
|
||||
// map header, size 12
|
||||
// string "Bucket"
|
||||
o = append(o, 0x8b, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
|
||||
o = append(o, 0x8c, 0xa6, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74)
|
||||
o = msgp.AppendString(o, z.Bucket)
|
||||
// string "Object"
|
||||
o = append(o, 0xa6, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74)
|
||||
@@ -1001,6 +1019,9 @@ func (z *ListPartsInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
// string "ChecksumAlgorithm"
|
||||
o = append(o, 0xb1, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d)
|
||||
o = msgp.AppendString(o, z.ChecksumAlgorithm)
|
||||
// string "ChecksumType"
|
||||
o = append(o, 0xac, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x54, 0x79, 0x70, 0x65)
|
||||
o = msgp.AppendString(o, z.ChecksumType)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1125,6 +1146,12 @@ func (z *ListPartsInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "ChecksumAlgorithm")
|
||||
return
|
||||
}
|
||||
case "ChecksumType":
|
||||
z.ChecksumType, bts, err = msgp.ReadStringBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ChecksumType")
|
||||
return
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
@@ -1150,7 +1177,7 @@ func (z *ListPartsInfo) Msgsize() (s int) {
|
||||
s += msgp.StringPrefixSize + len(za0002) + msgp.StringPrefixSize + len(za0003)
|
||||
}
|
||||
}
|
||||
s += 18 + msgp.StringPrefixSize + len(z.ChecksumAlgorithm)
|
||||
s += 18 + msgp.StringPrefixSize + len(z.ChecksumAlgorithm) + 13 + msgp.StringPrefixSize + len(z.ChecksumType)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1279,13 +1306,16 @@ func (z *MultipartInfo) Msgsize() (s int) {
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z NewMultipartUploadResult) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// map header, size 2
|
||||
// map header, size 3
|
||||
// string "UploadID"
|
||||
o = append(o, 0x82, 0xa8, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x44)
|
||||
o = append(o, 0x83, 0xa8, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x49, 0x44)
|
||||
o = msgp.AppendString(o, z.UploadID)
|
||||
// string "ChecksumAlgo"
|
||||
o = append(o, 0xac, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x41, 0x6c, 0x67, 0x6f)
|
||||
o = msgp.AppendString(o, z.ChecksumAlgo)
|
||||
// string "ChecksumType"
|
||||
o = append(o, 0xac, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x54, 0x79, 0x70, 0x65)
|
||||
o = msgp.AppendString(o, z.ChecksumType)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1319,6 +1349,12 @@ func (z *NewMultipartUploadResult) UnmarshalMsg(bts []byte) (o []byte, err error
|
||||
err = msgp.WrapError(err, "ChecksumAlgo")
|
||||
return
|
||||
}
|
||||
case "ChecksumType":
|
||||
z.ChecksumType, bts, err = msgp.ReadStringBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ChecksumType")
|
||||
return
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
@@ -1333,7 +1369,7 @@ func (z *NewMultipartUploadResult) UnmarshalMsg(bts []byte) (o []byte, err error
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z NewMultipartUploadResult) Msgsize() (s int) {
|
||||
s = 1 + 9 + msgp.StringPrefixSize + len(z.UploadID) + 13 + msgp.StringPrefixSize + len(z.ChecksumAlgo)
|
||||
s = 1 + 9 + msgp.StringPrefixSize + len(z.UploadID) + 13 + msgp.StringPrefixSize + len(z.ChecksumAlgo) + 13 + msgp.StringPrefixSize + len(z.ChecksumType)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1772,9 +1808,9 @@ func (z *ObjectInfo) Msgsize() (s int) {
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z *PartInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// map header, size 9
|
||||
// map header, size 10
|
||||
// string "PartNumber"
|
||||
o = append(o, 0x89, 0xaa, 0x50, 0x61, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72)
|
||||
o = append(o, 0x8a, 0xaa, 0x50, 0x61, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72)
|
||||
o = msgp.AppendInt(o, z.PartNumber)
|
||||
// string "LastModified"
|
||||
o = append(o, 0xac, 0x4c, 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64)
|
||||
@@ -1800,6 +1836,9 @@ func (z *PartInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
// string "ChecksumSHA256"
|
||||
o = append(o, 0xae, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36)
|
||||
o = msgp.AppendString(o, z.ChecksumSHA256)
|
||||
// string "ChecksumCRC64NVME"
|
||||
o = append(o, 0xb1, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x75, 0x6d, 0x43, 0x52, 0x43, 0x36, 0x34, 0x4e, 0x56, 0x4d, 0x45)
|
||||
o = msgp.AppendString(o, z.ChecksumCRC64NVME)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1875,6 +1914,12 @@ func (z *PartInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "ChecksumSHA256")
|
||||
return
|
||||
}
|
||||
case "ChecksumCRC64NVME":
|
||||
z.ChecksumCRC64NVME, bts, err = msgp.ReadStringBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ChecksumCRC64NVME")
|
||||
return
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
@@ -1889,7 +1934,7 @@ func (z *PartInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z *PartInfo) Msgsize() (s int) {
|
||||
s = 1 + 11 + msgp.IntSize + 13 + msgp.TimeSize + 5 + msgp.StringPrefixSize + len(z.ETag) + 5 + msgp.Int64Size + 11 + msgp.Int64Size + 14 + msgp.StringPrefixSize + len(z.ChecksumCRC32) + 15 + msgp.StringPrefixSize + len(z.ChecksumCRC32C) + 13 + msgp.StringPrefixSize + len(z.ChecksumSHA1) + 15 + msgp.StringPrefixSize + len(z.ChecksumSHA256)
|
||||
s = 1 + 11 + msgp.IntSize + 13 + msgp.TimeSize + 5 + msgp.StringPrefixSize + len(z.ETag) + 5 + msgp.Int64Size + 11 + msgp.Int64Size + 14 + msgp.StringPrefixSize + len(z.ChecksumCRC32) + 15 + msgp.StringPrefixSize + len(z.ChecksumCRC32C) + 13 + msgp.StringPrefixSize + len(z.ChecksumSHA1) + 15 + msgp.StringPrefixSize + len(z.ChecksumSHA256) + 18 + msgp.StringPrefixSize + len(z.ChecksumCRC64NVME)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -404,12 +404,7 @@ func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[strin
|
||||
metadata = make(map[string]string)
|
||||
}
|
||||
|
||||
wantCRC, err := hash.GetContentChecksum(hdr)
|
||||
if err != nil {
|
||||
return opts, fmt.Errorf("invalid/unknown checksum sent: %v", err)
|
||||
}
|
||||
etag := strings.TrimSpace(hdr.Get(xhttp.MinIOSourceETag))
|
||||
|
||||
if crypto.S3KMS.IsRequested(hdr) {
|
||||
keyID, context, err := crypto.S3KMS.ParseHTTP(hdr)
|
||||
if err != nil {
|
||||
@@ -423,7 +418,6 @@ func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[strin
|
||||
ServerSideEncryption: sseKms,
|
||||
UserDefined: metadata,
|
||||
MTime: mtime,
|
||||
WantChecksum: wantCRC,
|
||||
PreserveETag: etag,
|
||||
}, nil
|
||||
}
|
||||
@@ -438,7 +432,6 @@ func putOptsFromHeaders(ctx context.Context, hdr http.Header, metadata map[strin
|
||||
opts.ReplicationSourceRetentionTimestamp = retaintimestmp
|
||||
opts.ReplicationSourceTaggingTimestamp = taggingtimestmp
|
||||
opts.PreserveETag = etag
|
||||
opts.WantChecksum = wantCRC
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -52,7 +53,6 @@ import (
|
||||
"github.com/minio/pkg/v3/trie"
|
||||
"github.com/minio/pkg/v3/wildcard"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -352,11 +352,11 @@ func isETagEqual(left, right string) bool {
|
||||
// setPutObjHeaders sets all the necessary headers returned back
|
||||
// upon a success Put/Copy/CompleteMultipart/Delete requests
|
||||
// to activate delete only headers set delete as true
|
||||
func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, delete bool, h http.Header) {
|
||||
func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, del bool, h http.Header) {
|
||||
// We must not use the http.Header().Set method here because some (broken)
|
||||
// clients expect the ETag header key to be literally "ETag" - not "Etag" (case-sensitive).
|
||||
// Therefore, we have to set the ETag directly as map entry.
|
||||
if objInfo.ETag != "" && !delete {
|
||||
if objInfo.ETag != "" && !del {
|
||||
w.Header()[xhttp.ETag] = []string{`"` + objInfo.ETag + `"`}
|
||||
}
|
||||
|
||||
@@ -364,17 +364,18 @@ func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, delete bool, h
|
||||
if objInfo.VersionID != "" && objInfo.VersionID != nullVersionID {
|
||||
w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
|
||||
// If version is a deleted marker, set this header as well
|
||||
if objInfo.DeleteMarker && delete { // only returned during delete object
|
||||
if objInfo.DeleteMarker && del { // only returned during delete object
|
||||
w.Header()[xhttp.AmzDeleteMarker] = []string{strconv.FormatBool(objInfo.DeleteMarker)}
|
||||
}
|
||||
}
|
||||
|
||||
if objInfo.Bucket != "" && objInfo.Name != "" {
|
||||
if lc, err := globalLifecycleSys.Get(objInfo.Bucket); err == nil && !delete {
|
||||
if lc, err := globalLifecycleSys.Get(objInfo.Bucket); err == nil && !del {
|
||||
lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts())
|
||||
}
|
||||
}
|
||||
hash.AddChecksumHeader(w, objInfo.decryptChecksums(0, h))
|
||||
cs, _ := objInfo.decryptChecksums(0, h)
|
||||
hash.AddChecksumHeader(w, cs)
|
||||
}
|
||||
|
||||
func deleteObjectVersions(ctx context.Context, o ObjectLayer, bucket string, toDel []ObjectToDelete, lcEvent lifecycle.Event) {
|
||||
|
||||
+41
-28
@@ -521,7 +521,8 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj
|
||||
|
||||
if r.Header.Get(xhttp.AmzChecksumMode) == "ENABLED" && rs == nil {
|
||||
// AWS S3 silently drops checksums on range requests.
|
||||
hash.AddChecksumHeader(w, objInfo.decryptChecksums(opts.PartNumber, r.Header))
|
||||
cs, _ := objInfo.decryptChecksums(opts.PartNumber, r.Header)
|
||||
hash.AddChecksumHeader(w, cs)
|
||||
}
|
||||
|
||||
if err = setObjectHeaders(ctx, w, objInfo, rs, opts); err != nil {
|
||||
@@ -632,14 +633,15 @@ func (api objectAPIHandlers) getObjectAttributesHandler(ctx context.Context, obj
|
||||
w.Header().Del(xhttp.ContentType)
|
||||
|
||||
if _, ok := opts.ObjectAttributes[xhttp.Checksum]; ok {
|
||||
chkSums := objInfo.decryptChecksums(0, r.Header)
|
||||
chkSums, _ := objInfo.decryptChecksums(0, r.Header)
|
||||
// AWS does not appear to append part number on this API call.
|
||||
if len(chkSums) > 0 {
|
||||
OA.Checksum = &objectAttributesChecksum{
|
||||
ChecksumCRC32: strings.Split(chkSums["CRC32"], "-")[0],
|
||||
ChecksumCRC32C: strings.Split(chkSums["CRC32C"], "-")[0],
|
||||
ChecksumSHA1: strings.Split(chkSums["SHA1"], "-")[0],
|
||||
ChecksumSHA256: strings.Split(chkSums["SHA256"], "-")[0],
|
||||
ChecksumCRC32: strings.Split(chkSums["CRC32"], "-")[0],
|
||||
ChecksumCRC32C: strings.Split(chkSums["CRC32C"], "-")[0],
|
||||
ChecksumSHA1: strings.Split(chkSums["SHA1"], "-")[0],
|
||||
ChecksumSHA256: strings.Split(chkSums["SHA256"], "-")[0],
|
||||
ChecksumCRC64NVME: strings.Split(chkSums["CRC64NVME"], "-")[0],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -678,12 +680,13 @@ func (api objectAPIHandlers) getObjectAttributesHandler(ctx context.Context, obj
|
||||
|
||||
OA.ObjectParts.NextPartNumberMarker = v.Number
|
||||
OA.ObjectParts.Parts = append(OA.ObjectParts.Parts, &objectAttributesPart{
|
||||
ChecksumSHA1: objInfo.Parts[i].Checksums["SHA1"],
|
||||
ChecksumSHA256: objInfo.Parts[i].Checksums["SHA256"],
|
||||
ChecksumCRC32: objInfo.Parts[i].Checksums["CRC32"],
|
||||
ChecksumCRC32C: objInfo.Parts[i].Checksums["CRC32C"],
|
||||
PartNumber: objInfo.Parts[i].Number,
|
||||
Size: objInfo.Parts[i].Size,
|
||||
ChecksumSHA1: objInfo.Parts[i].Checksums["SHA1"],
|
||||
ChecksumSHA256: objInfo.Parts[i].Checksums["SHA256"],
|
||||
ChecksumCRC32: objInfo.Parts[i].Checksums["CRC32"],
|
||||
ChecksumCRC32C: objInfo.Parts[i].Checksums["CRC32C"],
|
||||
ChecksumCRC64NVME: objInfo.Parts[i].Checksums["CRC64NVME"],
|
||||
PartNumber: objInfo.Parts[i].Number,
|
||||
Size: objInfo.Parts[i].Size,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -943,7 +946,8 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob
|
||||
|
||||
if r.Header.Get(xhttp.AmzChecksumMode) == "ENABLED" && rs == nil {
|
||||
// AWS S3 silently drops checksums on range requests.
|
||||
hash.AddChecksumHeader(w, objInfo.decryptChecksums(opts.PartNumber, r.Header))
|
||||
cs, _ := objInfo.decryptChecksums(opts.PartNumber, r.Header)
|
||||
hash.AddChecksumHeader(w, cs)
|
||||
}
|
||||
|
||||
// Set standard object headers.
|
||||
@@ -1191,6 +1195,9 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
// Sanitize the source object name similar to NewMultipart and PutObject API
|
||||
srcObject = trimLeadingSlash(srcObject)
|
||||
|
||||
if vid != "" && vid != nullVersionID {
|
||||
_, err := uuid.Parse(vid)
|
||||
if err != nil {
|
||||
@@ -1522,7 +1529,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
if !srcTimestamp.IsZero() {
|
||||
ondiskTimestamp, err := time.Parse(time.RFC3339Nano, lastTaggingTimestamp)
|
||||
// update tagging metadata only if replica timestamp is newer than what's on disk
|
||||
if err != nil || (err == nil && ondiskTimestamp.Before(srcTimestamp)) {
|
||||
if err != nil || (err == nil && !ondiskTimestamp.After(srcTimestamp)) {
|
||||
srcInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp] = srcTimestamp.UTC().Format(time.RFC3339Nano)
|
||||
srcInfo.UserDefined[xhttp.AmzObjectTagging] = objTags
|
||||
}
|
||||
@@ -1899,6 +1906,13 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
var reader io.Reader
|
||||
reader = rd
|
||||
|
||||
var opts ObjectOptions
|
||||
opts, err = putOptsFromReq(ctx, r, bucket, object, metadata)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
actualSize := size
|
||||
var idxCb func() []byte
|
||||
if isCompressible(r.Header, object) && size > minCompressibleSize {
|
||||
@@ -1915,6 +1929,8 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidChecksum), r.URL)
|
||||
return
|
||||
}
|
||||
opts.WantChecksum = actualReader.Checksum()
|
||||
|
||||
// Set compression metrics.
|
||||
var s2c io.ReadCloser
|
||||
wantEncryption := crypto.Requested(r.Header)
|
||||
@@ -1945,22 +1961,17 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if err := hashReader.AddChecksum(r, size < 0); err != nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidChecksum), r.URL)
|
||||
return
|
||||
if size >= 0 {
|
||||
if err := hashReader.AddChecksum(r, false); err != nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidChecksum), r.URL)
|
||||
return
|
||||
}
|
||||
opts.WantChecksum = hashReader.Checksum()
|
||||
}
|
||||
|
||||
rawReader := hashReader
|
||||
pReader := NewPutObjReader(rawReader)
|
||||
|
||||
var opts ObjectOptions
|
||||
opts, err = putOptsFromReq(ctx, r, bucket, object, metadata)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
opts.IndexCB = idxCb
|
||||
opts.WantChecksum = hashReader.Checksum()
|
||||
|
||||
if opts.PreserveETag != "" ||
|
||||
r.Header.Get(xhttp.IfMatch) != "" ||
|
||||
@@ -2724,7 +2735,7 @@ func (api objectAPIHandlers) PutObjectLegalHoldHandler(w http.ResponseWriter, r
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if !hasContentMD5(r.Header) {
|
||||
if !validateLengthAndChecksum(r) {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -2734,7 +2745,7 @@ func (api objectAPIHandlers) PutObjectLegalHoldHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
legalHold, err := objectlock.ParseObjectLegalHold(io.LimitReader(r.Body, r.ContentLength))
|
||||
legalHold, err := objectlock.ParseObjectLegalHold(r.Body)
|
||||
if err != nil {
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||
apiErr.Description = err.Error()
|
||||
@@ -2882,7 +2893,7 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
if !hasContentMD5(r.Header) {
|
||||
if !validateLengthAndChecksum(r) {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -2899,6 +2910,8 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
reqInfo := logger.GetReqInfo(ctx)
|
||||
reqInfo.SetTags("retention", objRetention.String())
|
||||
|
||||
opts, err := getOpts(ctx, r, bucket, object)
|
||||
if err != nil {
|
||||
|
||||
@@ -93,8 +93,8 @@ func getLambdaEventData(bucket, object string, cred auth.Credentials, r *http.Re
|
||||
},
|
||||
UserIdentity: levent.Identity{
|
||||
Type: "IAMUser",
|
||||
PrincipalID: cred.AccessKey,
|
||||
AccessKeyID: cred.SecretKey,
|
||||
PrincipalID: cred.ParentUser,
|
||||
AccessKeyID: cred.AccessKey,
|
||||
},
|
||||
}
|
||||
return eventData, nil
|
||||
|
||||
@@ -214,7 +214,7 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
}
|
||||
}
|
||||
|
||||
checksumType := hash.NewChecksumType(r.Header.Get(xhttp.AmzChecksumAlgo))
|
||||
checksumType := hash.NewChecksumHeader(r.Header)
|
||||
if checksumType.Is(hash.ChecksumInvalid) {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequestParameter), r.URL)
|
||||
return
|
||||
@@ -233,6 +233,9 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
response := generateInitiateMultipartUploadResponse(bucket, object, res.UploadID)
|
||||
if res.ChecksumAlgo != "" {
|
||||
w.Header().Set(xhttp.AmzChecksumAlgo, res.ChecksumAlgo)
|
||||
if res.ChecksumType != "" {
|
||||
w.Header().Set(xhttp.AmzChecksumType, res.ChecksumType)
|
||||
}
|
||||
}
|
||||
encodedSuccessResponse := encodeResponse(response)
|
||||
|
||||
@@ -1204,6 +1207,10 @@ func (api objectAPIHandlers) ListObjectPartsHandler(w http.ResponseWriter, r *ht
|
||||
listPartsInfo.Parts[i].ETag = tryDecryptETag(objectEncryptionKey, p.ETag, kind == crypto.S3)
|
||||
listPartsInfo.Parts[i].Size = p.ActualSize
|
||||
}
|
||||
} else if _, ok := listPartsInfo.UserDefined[ReservedMetadataPrefix+"compression"]; ok {
|
||||
for i, p := range listPartsInfo.Parts {
|
||||
listPartsInfo.Parts[i].Size = p.ActualSize
|
||||
}
|
||||
}
|
||||
|
||||
response := generateListPartsResponse(listPartsInfo, encodingType)
|
||||
|
||||
+17
-19
@@ -60,20 +60,19 @@ func readDirFn(dirPath string, filter func(name string, typ os.FileMode) error)
|
||||
if err != nil {
|
||||
if err == syscall.ERROR_NO_MORE_FILES {
|
||||
break
|
||||
} else {
|
||||
if isSysErrPathNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
err = osErrToFileErr(&os.PathError{
|
||||
Op: "FindNextFile",
|
||||
Path: dirPath,
|
||||
Err: err,
|
||||
})
|
||||
if err == errFileNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if isSysErrPathNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
err = osErrToFileErr(&os.PathError{
|
||||
Op: "FindNextFile",
|
||||
Path: dirPath,
|
||||
Err: err,
|
||||
})
|
||||
if err == errFileNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
name := syscall.UTF16ToString(data.FileName[0:])
|
||||
if name == "" || name == "." || name == ".." { // Useless names
|
||||
@@ -136,13 +135,12 @@ func readDirWithOpts(dirPath string, opts readDirOpts) (entries []string, err er
|
||||
if err != nil {
|
||||
if err == syscall.ERROR_NO_MORE_FILES {
|
||||
break
|
||||
} else {
|
||||
return nil, osErrToFileErr(&os.PathError{
|
||||
Op: "FindNextFile",
|
||||
Path: dirPath,
|
||||
Err: err,
|
||||
})
|
||||
}
|
||||
return nil, osErrToFileErr(&os.PathError{
|
||||
Op: "FindNextFile",
|
||||
Path: dirPath,
|
||||
Err: err,
|
||||
})
|
||||
}
|
||||
|
||||
name := syscall.UTF16ToString(data.FileName[0:])
|
||||
|
||||
@@ -56,6 +56,7 @@ var (
|
||||
aoBucketInfo = grid.NewArrayOf[*BucketInfo](func() *BucketInfo { return &BucketInfo{} })
|
||||
aoMetricsGroup = grid.NewArrayOf[*MetricV2](func() *MetricV2 { return &MetricV2{} })
|
||||
madminBgHealState = grid.NewJSONPool[madmin.BgHealState]()
|
||||
madminHealResultItem = grid.NewJSONPool[madmin.HealResultItem]()
|
||||
madminCPUs = grid.NewJSONPool[madmin.CPUs]()
|
||||
madminMemInfo = grid.NewJSONPool[madmin.MemInfo]()
|
||||
madminNetInfo = grid.NewJSONPool[madmin.NetInfo]()
|
||||
@@ -97,7 +98,7 @@ var (
|
||||
getSysErrorsRPC = grid.NewSingleHandler[*grid.MSS, *grid.JSON[madmin.SysErrors]](grid.HandlerGetSysErrors, grid.NewMSS, madminSysErrors.NewJSON)
|
||||
getSysServicesRPC = grid.NewSingleHandler[*grid.MSS, *grid.JSON[madmin.SysServices]](grid.HandlerGetSysServices, grid.NewMSS, madminSysServices.NewJSON)
|
||||
headBucketRPC = grid.NewSingleHandler[*grid.MSS, *VolInfo](grid.HandlerHeadBucket, grid.NewMSS, func() *VolInfo { return &VolInfo{} })
|
||||
healBucketRPC = grid.NewSingleHandler[*grid.MSS, grid.NoPayload](grid.HandlerHealBucket, grid.NewMSS, grid.NewNoPayload)
|
||||
healBucketRPC = grid.NewSingleHandler[*grid.MSS, *grid.JSON[madmin.HealResultItem]](grid.HandlerHealBucket, grid.NewMSS, madminHealResultItem.NewJSON)
|
||||
listBucketsRPC = grid.NewSingleHandler[*BucketOptions, *grid.Array[*BucketInfo]](grid.HandlerListBuckets, func() *BucketOptions { return &BucketOptions{} }, aoBucketInfo.New)
|
||||
loadBucketMetadataRPC = grid.NewSingleHandler[*grid.MSS, grid.NoPayload](grid.HandlerLoadBucketMetadata, grid.NewMSS, grid.NewNoPayload).IgnoreNilConn()
|
||||
loadGroupRPC = grid.NewSingleHandler[*grid.MSS, grid.NoPayload](grid.HandlerLoadGroup, grid.NewMSS, grid.NewNoPayload)
|
||||
@@ -636,7 +637,7 @@ func (s *peerRESTServer) VerifyBinaryHandler(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
if lrTime.Sub(currentReleaseTime) <= 0 {
|
||||
s.writeErrorResponse(w, fmt.Errorf("server is already running the latest version: %s", Version))
|
||||
s.writeErrorResponse(w, fmt.Errorf("server is running the latest version: %s", Version))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1258,21 +1259,21 @@ func (s *peerRESTServer) NetSpeedTestHandler(w http.ResponseWriter, r *http.Requ
|
||||
peersLogIf(r.Context(), gob.NewEncoder(w).Encode(result))
|
||||
}
|
||||
|
||||
func (s *peerRESTServer) HealBucketHandler(mss *grid.MSS) (np grid.NoPayload, nerr *grid.RemoteErr) {
|
||||
func (s *peerRESTServer) HealBucketHandler(mss *grid.MSS) (np *grid.JSON[madmin.HealResultItem], nerr *grid.RemoteErr) {
|
||||
bucket := mss.Get(peerS3Bucket)
|
||||
if isMinioMetaBucket(bucket) {
|
||||
return np, grid.NewRemoteErr(errInvalidArgument)
|
||||
}
|
||||
|
||||
bucketDeleted := mss.Get(peerS3BucketDeleted) == "true"
|
||||
_, err := healBucketLocal(context.Background(), bucket, madmin.HealOpts{
|
||||
res, err := healBucketLocal(context.Background(), bucket, madmin.HealOpts{
|
||||
Remove: bucketDeleted,
|
||||
})
|
||||
if err != nil {
|
||||
return np, grid.NewRemoteErr(err)
|
||||
}
|
||||
|
||||
return np, nil
|
||||
return madminHealResultItem.NewJSONWith(&res), nil
|
||||
}
|
||||
|
||||
func (s *peerRESTServer) ListBucketsHandler(opts *BucketOptions) (*grid.Array[*BucketInfo], *grid.RemoteErr) {
|
||||
@@ -1307,6 +1308,7 @@ func (s *peerRESTServer) HeadBucketHandler(mss *grid.MSS) (info *VolInfo, nerr *
|
||||
return &VolInfo{
|
||||
Name: bucketInfo.Name,
|
||||
Created: bucketInfo.Created,
|
||||
Deleted: bucketInfo.Deleted, // needed for site replication
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
+17
-11
@@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
@@ -29,7 +30,6 @@ import (
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/grid"
|
||||
"github.com/minio/pkg/v3/sync/errgroup"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var errPeerOffline = errors.New("peer is offline")
|
||||
@@ -178,13 +178,24 @@ func (sys *S3PeerSys) HealBucket(ctx context.Context, bucket string, opts madmin
|
||||
}
|
||||
}
|
||||
|
||||
if healBucketErr := reduceWriteQuorumErrs(ctx, errs, bucketOpIgnoredErrs, len(errs)/2+1); healBucketErr != nil {
|
||||
return madmin.HealResultItem{}, toObjectErr(healBucketErr, bucket)
|
||||
}
|
||||
|
||||
res := madmin.HealResultItem{
|
||||
Type: madmin.HealItemBucket,
|
||||
Bucket: bucket,
|
||||
SetCount: -1, // explicitly set an invalid value -1, for bucket heal scenario
|
||||
}
|
||||
|
||||
for i, err := range errs {
|
||||
if err == nil {
|
||||
return healBucketResults[i], nil
|
||||
res.Before.Drives = append(res.Before.Drives, healBucketResults[i].Before.Drives...)
|
||||
res.After.Drives = append(res.After.Drives, healBucketResults[i].After.Drives...)
|
||||
}
|
||||
}
|
||||
|
||||
return madmin.HealResultItem{}, toObjectErr(errVolumeNotFound, bucket)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ListBuckets lists buckets across all nodes and returns a consistent view:
|
||||
@@ -355,14 +366,8 @@ func (client *remotePeerS3Client) HealBucket(ctx context.Context, bucket string,
|
||||
ctx, cancel := context.WithTimeout(ctx, globalDriveConfig.GetMaxTimeout())
|
||||
defer cancel()
|
||||
|
||||
_, err := healBucketRPC.Call(ctx, conn, mss)
|
||||
|
||||
// Initialize heal result info
|
||||
return madmin.HealResultItem{
|
||||
Type: madmin.HealItemBucket,
|
||||
Bucket: bucket,
|
||||
SetCount: -1, // explicitly set an invalid value -1, for bucket heal scenario
|
||||
}, toStorageErr(err)
|
||||
resp, err := healBucketRPC.Call(ctx, conn, mss)
|
||||
return resp.ValueOrZero(), toStorageErr(err)
|
||||
}
|
||||
|
||||
// GetBucketInfo returns bucket stat info from a peer
|
||||
@@ -388,6 +393,7 @@ func (client *remotePeerS3Client) GetBucketInfo(ctx context.Context, bucket stri
|
||||
return BucketInfo{
|
||||
Name: volInfo.Name,
|
||||
Created: volInfo.Created,
|
||||
Deleted: volInfo.Deleted,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
+15
-12
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/pkg/v3/sync/errgroup"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -101,7 +102,7 @@ func healBucketLocal(ctx context.Context, bucket string, opts madmin.HealOpts) (
|
||||
for i := range beforeState {
|
||||
res.Before.Drives = append(res.Before.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: localDrives[i].String(),
|
||||
Endpoint: localDrives[i].Endpoint().String(),
|
||||
State: beforeState[i],
|
||||
})
|
||||
}
|
||||
@@ -149,7 +150,7 @@ func healBucketLocal(ctx context.Context, bucket string, opts madmin.HealOpts) (
|
||||
for i := range afterState {
|
||||
res.After.Drives = append(res.After.Drives, madmin.HealDriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: localDrives[i].String(),
|
||||
Endpoint: localDrives[i].Endpoint().String(),
|
||||
State: afterState[i],
|
||||
})
|
||||
}
|
||||
@@ -164,7 +165,7 @@ func listBucketsLocal(ctx context.Context, opts BucketOptions) (buckets []Bucket
|
||||
quorum := (len(localDrives) / 2)
|
||||
|
||||
buckets = make([]BucketInfo, 0, 32)
|
||||
healBuckets := map[string]VolInfo{}
|
||||
healBuckets := xsync.NewMapOf[string, VolInfo]()
|
||||
|
||||
// lists all unique buckets across drives.
|
||||
if err := listAllBuckets(ctx, localDrives, healBuckets, quorum); err != nil {
|
||||
@@ -172,7 +173,7 @@ func listBucketsLocal(ctx context.Context, opts BucketOptions) (buckets []Bucket
|
||||
}
|
||||
|
||||
// include deleted buckets in listBuckets output
|
||||
deletedBuckets := map[string]VolInfo{}
|
||||
deletedBuckets := xsync.NewMapOf[string, VolInfo]()
|
||||
|
||||
if opts.Deleted {
|
||||
// lists all deleted buckets across drives.
|
||||
@@ -181,25 +182,27 @@ func listBucketsLocal(ctx context.Context, opts BucketOptions) (buckets []Bucket
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range healBuckets {
|
||||
healBuckets.Range(func(_ string, volInfo VolInfo) bool {
|
||||
bi := BucketInfo{
|
||||
Name: v.Name,
|
||||
Created: v.Created,
|
||||
Name: volInfo.Name,
|
||||
Created: volInfo.Created,
|
||||
}
|
||||
if vi, ok := deletedBuckets[v.Name]; ok {
|
||||
if vi, ok := deletedBuckets.Load(volInfo.Name); ok {
|
||||
bi.Deleted = vi.Created
|
||||
}
|
||||
buckets = append(buckets, bi)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for _, v := range deletedBuckets {
|
||||
if _, ok := healBuckets[v.Name]; !ok {
|
||||
deletedBuckets.Range(func(_ string, v VolInfo) bool {
|
||||
if _, ok := healBuckets.Load(v.Name); !ok {
|
||||
buckets = append(buckets, BucketInfo{
|
||||
Name: v.Name,
|
||||
Deleted: v.Created,
|
||||
})
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
+12
-8
@@ -56,10 +56,12 @@ func newPostPolicyBytesV4WithContentRange(credential, bucketName, objectKey stri
|
||||
credentialConditionStr := fmt.Sprintf(`["eq", "$x-amz-credential", "%s"]`, credential)
|
||||
// Add the meta-uuid string, set to 1234
|
||||
uuidConditionStr := fmt.Sprintf(`["eq", "$x-amz-meta-uuid", "%s"]`, "1234")
|
||||
// Add the content-encoding string, set to gzip.
|
||||
contentEncodingConditionStr := fmt.Sprintf(`["eq", "$content-encoding", "%s"]`, "gzip")
|
||||
|
||||
// Combine all conditions into one string.
|
||||
conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s, %s]`, bucketConditionStr,
|
||||
keyConditionStr, contentLengthCondStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr)
|
||||
conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s, %s, %s]`, bucketConditionStr,
|
||||
keyConditionStr, contentLengthCondStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr, contentEncodingConditionStr)
|
||||
retStr := "{"
|
||||
retStr = retStr + expirationStr + ","
|
||||
retStr += conditionStr
|
||||
@@ -85,9 +87,11 @@ func newPostPolicyBytesV4(credential, bucketName, objectKey string, expiration t
|
||||
credentialConditionStr := fmt.Sprintf(`["eq", "$x-amz-credential", "%s"]`, credential)
|
||||
// Add the meta-uuid string, set to 1234
|
||||
uuidConditionStr := fmt.Sprintf(`["eq", "$x-amz-meta-uuid", "%s"]`, "1234")
|
||||
// Add the content-encoding string, set to gzip
|
||||
contentEncodingConditionStr := fmt.Sprintf(`["eq", "$content-encoding", "%s"]`, "gzip")
|
||||
|
||||
// Combine all conditions into one string.
|
||||
conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s]`, bucketConditionStr, keyConditionStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr)
|
||||
conditionStr := fmt.Sprintf(`"conditions":[%s, %s, %s, %s, %s, %s, %s]`, bucketConditionStr, keyConditionStr, algorithmConditionStr, dateConditionStr, credentialConditionStr, uuidConditionStr, contentEncodingConditionStr)
|
||||
retStr := "{"
|
||||
retStr = retStr + expirationStr + ","
|
||||
retStr += conditionStr
|
||||
@@ -331,7 +335,7 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
dates: []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)},
|
||||
policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]}`,
|
||||
policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"],["eq", "$content-encoding", "gzip"]]}`,
|
||||
},
|
||||
// Success case, no multipart filename.
|
||||
{
|
||||
@@ -341,7 +345,7 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
dates: []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)},
|
||||
policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]}`,
|
||||
policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"],["eq", "$content-encoding", "gzip"]]}`,
|
||||
noFilename: true,
|
||||
},
|
||||
// Success case, big body.
|
||||
@@ -352,7 +356,7 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
dates: []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)},
|
||||
policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]}`,
|
||||
policy: `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"],["eq", "$content-encoding", "gzip"]]}`,
|
||||
},
|
||||
// Corrupted Base 64 result
|
||||
{
|
||||
@@ -447,7 +451,7 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr
|
||||
malformedBody: false,
|
||||
ignoreContentLength: false,
|
||||
},
|
||||
// Failed with Content-Length not specified.
|
||||
// Success with Content-Length not specified.
|
||||
{
|
||||
objectName: "test",
|
||||
data: bytes.Repeat([]byte("a"), 1025),
|
||||
@@ -547,7 +551,7 @@ func testPostPolicyBucketHandlerRedirect(obj ObjectLayer, instanceType string, t
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
dates := []interface{}{curTimePlus5Min.Format(iso8601TimeFormat), curTime.Format(iso8601DateFormat), curTime.Format(yyyymmdd)}
|
||||
policy := `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], {"success_action_redirect":"` + redirectURL.String() + `"},["starts-with", "$key", "test/"], ["eq", "$x-amz-meta-uuid", "1234"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"]]}`
|
||||
policy := `{"expiration": "%s","conditions":[["eq", "$bucket", "` + bucketName + `"], {"success_action_redirect":"` + redirectURL.String() + `"},["starts-with", "$key", "test/"], ["eq", "$x-amz-meta-uuid", "1234"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", "` + credentials.AccessKey + `/%s/us-east-1/s3/aws4_request"],["eq", "$content-encoding", "gzip"]]}`
|
||||
|
||||
// Generate the final policy document
|
||||
policy = fmt.Sprintf(policy, dates...)
|
||||
|
||||
+53
-68
@@ -29,10 +29,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/jstream"
|
||||
"github.com/minio/minio-go/v7/pkg/encrypt"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/s3select/jstream"
|
||||
)
|
||||
|
||||
// startWithConds - map which indicates if a given condition supports starts-with policy operator
|
||||
@@ -53,18 +53,6 @@ var startsWithConds = map[string]bool{
|
||||
"$x-amz-date": false,
|
||||
}
|
||||
|
||||
var postPolicyIgnoreKeys = map[string]bool{
|
||||
"Policy": true,
|
||||
xhttp.AmzSignature: true,
|
||||
xhttp.ContentEncoding: true,
|
||||
http.CanonicalHeaderKey(xhttp.AmzChecksumAlgo): true,
|
||||
http.CanonicalHeaderKey(xhttp.AmzChecksumCRC32): true,
|
||||
http.CanonicalHeaderKey(xhttp.AmzChecksumCRC32C): true,
|
||||
http.CanonicalHeaderKey(xhttp.AmzChecksumSHA1): true,
|
||||
http.CanonicalHeaderKey(xhttp.AmzChecksumSHA256): true,
|
||||
http.CanonicalHeaderKey(xhttp.AmzChecksumMode): true,
|
||||
}
|
||||
|
||||
// Add policy conditionals.
|
||||
const (
|
||||
policyCondEqual = "eq"
|
||||
@@ -140,7 +128,7 @@ type PostPolicyForm struct {
|
||||
func sanitizePolicy(r io.Reader) (io.Reader, error) {
|
||||
var buf bytes.Buffer
|
||||
e := json.NewEncoder(&buf)
|
||||
d := jstream.NewDecoder(r, 0).ObjectAsKVS()
|
||||
d := jstream.NewDecoder(r, 0).ObjectAsKVS().MaxDepth(10)
|
||||
sset := set.NewStringSet()
|
||||
for mv := range d.Stream() {
|
||||
var kvs jstream.KVS
|
||||
@@ -232,19 +220,19 @@ func parsePostPolicyForm(r io.Reader) (PostPolicyForm, error) {
|
||||
operator, matchType, value,
|
||||
})
|
||||
case policyCondContentLength:
|
||||
min, err := toInteger(condt[1])
|
||||
minLen, err := toInteger(condt[1])
|
||||
if err != nil {
|
||||
return parsedPolicy, err
|
||||
}
|
||||
|
||||
max, err := toInteger(condt[2])
|
||||
maxLen, err := toInteger(condt[2])
|
||||
if err != nil {
|
||||
return parsedPolicy, err
|
||||
}
|
||||
|
||||
parsedPolicy.Conditions.ContentLengthRange = contentLengthRange{
|
||||
Min: min,
|
||||
Max: max,
|
||||
Min: minLen,
|
||||
Max: maxLen,
|
||||
Valid: true,
|
||||
}
|
||||
default:
|
||||
@@ -272,60 +260,57 @@ func checkPolicyCond(op string, input1, input2 string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// S3 docs: "Each form field that you specify in a form (except x-amz-signature, file, policy, and field names
|
||||
// that have an x-ignore- prefix) must appear in the list of conditions."
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
|
||||
// keyInPolicyExceptions - list of keys that, when present in the form, can be missing in the conditions of the policy.
|
||||
var keyInPolicyExceptions = map[string]bool{
|
||||
xhttp.AmzSignature: true,
|
||||
"File": true,
|
||||
"Policy": true,
|
||||
|
||||
// MinIO specific exceptions to the general S3 rule above.
|
||||
encrypt.SseKmsKeyID: true,
|
||||
encrypt.SseEncryptionContext: true,
|
||||
encrypt.SseCustomerAlgorithm: true,
|
||||
encrypt.SseCustomerKey: true,
|
||||
encrypt.SseCustomerKeyMD5: true,
|
||||
}
|
||||
|
||||
// checkPostPolicy - apply policy conditions and validate input values.
|
||||
// (http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html)
|
||||
// Note that content-length-range is checked in the API handler function PostPolicyBucketHandler.
|
||||
// formValues is the already-canonicalized form values from the POST request.
|
||||
func checkPostPolicy(formValues http.Header, postPolicyForm PostPolicyForm) error {
|
||||
// Check if policy document expiry date is still not reached
|
||||
if !postPolicyForm.Expiration.After(UTCNow()) {
|
||||
return fmt.Errorf("Invalid according to Policy: Policy expired")
|
||||
}
|
||||
// check all formValues appear in postPolicyForm or return error. #https://github.com/minio/minio/issues/17391
|
||||
checkHeader := map[string][]string{}
|
||||
ignoreKeys := map[string]bool{}
|
||||
for key, value := range formValues {
|
||||
switch {
|
||||
case ignoreKeys[key], postPolicyIgnoreKeys[key], strings.HasPrefix(key, encrypt.SseGenericHeader):
|
||||
continue
|
||||
case strings.HasPrefix(key, "X-Amz-Ignore-"):
|
||||
ignoreKey := strings.Replace(key, "X-Amz-Ignore-", "", 1)
|
||||
ignoreKeys[ignoreKey] = true
|
||||
// if it have already
|
||||
delete(checkHeader, ignoreKey)
|
||||
default:
|
||||
checkHeader[key] = value
|
||||
}
|
||||
}
|
||||
// map to store the metadata
|
||||
metaMap := make(map[string]string)
|
||||
for _, policy := range postPolicyForm.Conditions.Policies {
|
||||
if strings.HasPrefix(policy.Key, "$x-amz-meta-") {
|
||||
formCanonicalName := http.CanonicalHeaderKey(strings.TrimPrefix(policy.Key, "$"))
|
||||
metaMap[formCanonicalName] = policy.Value
|
||||
}
|
||||
}
|
||||
// Check if any extra metadata field is passed as input
|
||||
for key := range formValues {
|
||||
if strings.HasPrefix(key, "X-Amz-Meta-") {
|
||||
if _, ok := metaMap[key]; !ok {
|
||||
return fmt.Errorf("Invalid according to Policy: Extra input fields: %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flag to indicate if all policies conditions are satisfied
|
||||
var condPassed bool
|
||||
// mustFindInPolicy is a map to list all the keys that we must find in the policy as
|
||||
// we process it below. At the end of checkPostPolicy function, if any key is left in
|
||||
// this map, that's an error.
|
||||
mustFindInPolicy := make(map[string][]string, len(formValues))
|
||||
for key, values := range formValues {
|
||||
if keyInPolicyExceptions[key] || strings.HasPrefix(key, "X-Ignore-") {
|
||||
continue
|
||||
}
|
||||
mustFindInPolicy[key] = values
|
||||
}
|
||||
|
||||
// Iterate over policy conditions and check them against received form fields
|
||||
for _, policy := range postPolicyForm.Conditions.Policies {
|
||||
// Form fields names are in canonical format, convert conditions names
|
||||
// to canonical for simplification purpose, so `$key` will become `Key`
|
||||
formCanonicalName := http.CanonicalHeaderKey(strings.TrimPrefix(policy.Key, "$"))
|
||||
|
||||
// Operator for the current policy condition
|
||||
op := policy.Operator
|
||||
// Multiple values should not occur
|
||||
if len(checkHeader[formCanonicalName]) >= 2 {
|
||||
return fmt.Errorf("Invalid according to Policy: Policy Condition failed: [%s, %s, %s]. FormValues have multiple values: [%s]", op, policy.Key, policy.Value, strings.Join(checkHeader[formCanonicalName], ", "))
|
||||
|
||||
// Multiple values are not allowed for a single form field
|
||||
if len(mustFindInPolicy[formCanonicalName]) >= 2 {
|
||||
return fmt.Errorf("Invalid according to Policy: Policy Condition failed: [%s, %s, %s]. FormValues have multiple values: [%s]", op, policy.Key, policy.Value, strings.Join(mustFindInPolicy[formCanonicalName], ", "))
|
||||
}
|
||||
|
||||
// If the current policy condition is known
|
||||
if startsWithSupported, condFound := startsWithConds[policy.Key]; condFound {
|
||||
// Check if the current condition supports starts-with operator
|
||||
@@ -333,35 +318,35 @@ func checkPostPolicy(formValues http.Header, postPolicyForm PostPolicyForm) erro
|
||||
return fmt.Errorf("Invalid according to Policy: Policy Condition failed")
|
||||
}
|
||||
// Check if current policy condition is satisfied
|
||||
condPassed = checkPolicyCond(op, formValues.Get(formCanonicalName), policy.Value)
|
||||
if !condPassed {
|
||||
if !checkPolicyCond(op, formValues.Get(formCanonicalName), policy.Value) {
|
||||
return fmt.Errorf("Invalid according to Policy: Policy Condition failed")
|
||||
}
|
||||
} else if strings.HasPrefix(policy.Key, "$x-amz-meta-") || strings.HasPrefix(policy.Key, "$x-amz-") {
|
||||
// This covers all conditions X-Amz-Meta-* and X-Amz-*
|
||||
// Check if policy condition is satisfied
|
||||
condPassed = checkPolicyCond(op, formValues.Get(formCanonicalName), policy.Value)
|
||||
if !condPassed {
|
||||
if !checkPolicyCond(op, formValues.Get(formCanonicalName), policy.Value) {
|
||||
return fmt.Errorf("Invalid according to Policy: Policy Condition failed: [%s, %s, %s]", op, policy.Key, policy.Value)
|
||||
}
|
||||
}
|
||||
delete(checkHeader, formCanonicalName)
|
||||
delete(mustFindInPolicy, formCanonicalName)
|
||||
}
|
||||
// For SignV2 - Signature/AWSAccessKeyId field will be ignored.
|
||||
|
||||
// For SignV2 - Signature/AWSAccessKeyId fields do not need to be in the policy
|
||||
if _, ok := formValues[xhttp.AmzSignatureV2]; ok {
|
||||
delete(checkHeader, xhttp.AmzSignatureV2)
|
||||
for k := range checkHeader {
|
||||
delete(mustFindInPolicy, xhttp.AmzSignatureV2)
|
||||
for k := range mustFindInPolicy {
|
||||
// case-insensitivity for AWSAccessKeyId
|
||||
if strings.EqualFold(k, xhttp.AmzAccessKeyID) {
|
||||
delete(checkHeader, k)
|
||||
delete(mustFindInPolicy, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(checkHeader) != 0 {
|
||||
logKeys := make([]string, 0, len(checkHeader))
|
||||
for key := range checkHeader {
|
||||
// Check mustFindInPolicy to see if any key is left, if so, it was not found in policy and we return an error.
|
||||
if len(mustFindInPolicy) != 0 {
|
||||
logKeys := make([]string, 0, len(mustFindInPolicy))
|
||||
for key := range mustFindInPolicy {
|
||||
logKeys = append(logKeys, key)
|
||||
}
|
||||
return fmt.Errorf("Each form field that you specify in a form must appear in the list of policy conditions. %q not specified in the policy.", strings.Join(logKeys, ", "))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user