mirror of
https://github.com/pgsty/minio.git
synced 2026-10-17 15:16:00 +03:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 035791669e | |||
| 6017b63a06 | |||
| 11d04279c8 | |||
| 0448728228 | |||
| 12047702f5 | |||
| fb1492f531 | |||
| d14ead7bec | |||
| 05444a0f6a | |||
| b3c54ec81e | |||
| 6c11dbffd5 | |||
| 3b5dbf9046 | |||
| 09c733677a | |||
| 8d6558b236 | |||
| 67f4ba154a | |||
| 440ad20c1d | |||
| 31fba6f434 | |||
| 280442e533 | |||
| 850a945a18 | |||
| 46f9049fb4 | |||
| 58266c9e2c | |||
| d1e775313d | |||
| a65df1e67b | |||
| e700be8cd6 | |||
| 3fdd574f54 | |||
| e0f4dd6027 | |||
| de02eca467 | |||
| 50dbd2cacc | |||
| c2f9cc5824 | |||
| c7f7e67a10 | |||
| 628042e65e | |||
| cde7eeb660 | |||
| 6305b206e1 | |||
| d85da9236e | |||
| 9800760cb3 | |||
| 5c087bdcad | |||
| a547bf517d | |||
| b984bf8d1a | |||
| 18f9cccfa7 | |||
| fb6ab1cca2 | |||
| 56c57e2c53 | |||
| a6057c35cc | |||
| 901887e6bf | |||
| ee54643004 | |||
| 0a17acdb34 | |||
| 72e5212842 | |||
| 714283fae2 | |||
| 3423028713 | |||
| 9d062b37d7 | |||
| 4636d3a9c3 | |||
| c95ede35c1 | |||
| 7415e1aa56 | |||
| f350953a19 | |||
| 958bba5b42 | |||
| 0f2b95b497 | |||
| d07089ceac | |||
| 47dfa62384 | |||
| 3a3265cf88 |
@@ -20,11 +20,11 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x]
|
||||
go-version: [1.20.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
||||
- uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 # v2
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||
- uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
|
||||
@@ -20,21 +20,28 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x]
|
||||
go-version: [1.20.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Setup dockerfile for build test
|
||||
run: |
|
||||
echo "FROM golang:1.19.4" >> Dockerfile.fips.test
|
||||
echo "COPY . /minio" >> Dockerfile.fips.test
|
||||
echo "WORKDIR /minio" >> Dockerfile.fips.test
|
||||
echo "ENV GOEXPERIMENT=boringcrypto" >> Dockerfile.fips.test
|
||||
echo "RUN make" >> Dockerfile.fips.test
|
||||
GO_VERSION=$(go version | cut -d ' ' -f 3 | sed 's/go//')
|
||||
echo Detected go version $GO_VERSION
|
||||
cat > Dockerfile.fips.test <<EOF
|
||||
FROM golang:${GO_VERSION}
|
||||
COPY . /minio
|
||||
WORKDIR /minio
|
||||
ENV GOEXPERIMENT=boringcrypto
|
||||
RUN make
|
||||
EOF
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v3
|
||||
@@ -49,4 +56,4 @@ jobs:
|
||||
- name: Test binary
|
||||
run: |
|
||||
docker run --rm minio/fips-test:latest ./minio --version
|
||||
docker run --rm -i minio/fips-test:latest /bin/bash -c 'go tool nm ./minio' | grep -q FIPS
|
||||
docker run --rm -i minio/fips-test:latest /bin/bash -c 'go tool nm ./minio | grep FIPS | grep -q FIPS'
|
||||
|
||||
@@ -20,10 +20,10 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x]
|
||||
go-version: [1.20.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
@@ -20,10 +20,15 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x]
|
||||
go-version: [1.20.x, 1.19.x]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
exclude:
|
||||
- os: ubuntu-latest
|
||||
go-version: 1.19.x
|
||||
- os: windows-latest
|
||||
go-version: 1.20.x
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
@@ -20,10 +20,10 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x]
|
||||
go-version: [1.20.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
@@ -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.19.x]
|
||||
go-version: [1.20.x]
|
||||
ldap: ["", "localhost:389"]
|
||||
etcd: ["", "http://localhost:2379"]
|
||||
openid: ["", "http://127.0.0.1:5556/dex"]
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
openid: "http://127.0.0.1:5556/dex"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Lint all docs
|
||||
run: |
|
||||
|
||||
@@ -21,10 +21,10 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x]
|
||||
go-version: [1.20.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
@@ -20,11 +20,11 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x]
|
||||
go-version: [1.20.x]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version: 1.20.x
|
||||
check-latest: true
|
||||
- name: Get official govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
||||
@@ -38,3 +38,4 @@ docs/debugging/s3-check-md5/s3-check-md5
|
||||
docs/debugging/hash-set/hash-set
|
||||
docs/debugging/healing-bin/healing-bin
|
||||
docs/debugging/inspect/inspect
|
||||
.bin/
|
||||
|
||||
+18
-23
@@ -1,39 +1,34 @@
|
||||
linters-settings:
|
||||
gofumpt:
|
||||
lang-version: "1.18"
|
||||
simplify: true
|
||||
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
staticcheck:
|
||||
checks: ['all', '-ST1005', '-ST1000', '-SA4000', '-SA9004', '-SA1019', '-SA1008', '-U1000', '-ST1016']
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- typecheck
|
||||
- goimports
|
||||
- misspell
|
||||
- govet
|
||||
- revive
|
||||
- ineffassign
|
||||
- gomodguard
|
||||
- durationcheck
|
||||
- gocritic
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
- gomodguard
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- revive
|
||||
- staticcheck
|
||||
- tenv
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- gocritic
|
||||
- gofumpt
|
||||
- tenv
|
||||
- durationcheck
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
- should have a package comment
|
||||
- error strings should not be capitalized or end with punctuation or a newline
|
||||
# todo fix these when we get enough time.
|
||||
- "singleCaseSwitch: should rewrite switch statement to if statement"
|
||||
- "unlambda: replace"
|
||||
- "captLocal:"
|
||||
- "ifElseChain:"
|
||||
- "elseif:"
|
||||
|
||||
service:
|
||||
golangci-lint-version: 1.43.0 # use the fixed version to not introduce new linters unexpectedly
|
||||
- should have a package comment
|
||||
- error strings should not be capitalized or end with punctuation or a newline
|
||||
|
||||
@@ -3037,8 +3037,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
================================================================
|
||||
|
||||
github.com/aymanbagabas/go-osc52
|
||||
https://github.com/aymanbagabas/go-osc52
|
||||
github.com/aymanbagabas/go-osc52/v2
|
||||
https://github.com/aymanbagabas/go-osc52/v2
|
||||
----------------------------------------------------------------
|
||||
MIT License
|
||||
|
||||
@@ -3175,69 +3175,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/bits-and-blooms/bitset
|
||||
https://github.com/bits-and-blooms/bitset
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2014 Will Fitzgerald. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/bits-and-blooms/bloom/v3
|
||||
https://github.com/bits-and-blooms/bloom/v3
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2014 Will Fitzgerald. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/buger/jsonparser
|
||||
https://github.com/buger/jsonparser
|
||||
----------------------------------------------------------------
|
||||
@@ -15500,37 +15437,36 @@ https://github.com/matttproud/golang_protobuf_extensions
|
||||
github.com/miekg/dns
|
||||
https://github.com/miekg/dns
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2009, The Go Authors. Extensions copyright (c) 2011, Miek Gieben.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
As this is fork of the official Go code the same license applies.
|
||||
Extensions of the original work are copyright (c) 2011 Miek Gieben
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/minio/cli
|
||||
@@ -25887,61 +25823,6 @@ https://github.com/tklauser/numcpus
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/twmb/murmur3
|
||||
https://github.com/twmb/murmur3
|
||||
----------------------------------------------------------------
|
||||
Copyright 2013, Sébastien Paolacci.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the library nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Copyright 2018, Travis Bischel.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the library nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/unrolled/secure
|
||||
https://github.com/unrolled/secure
|
||||
----------------------------------------------------------------
|
||||
|
||||
@@ -8,6 +8,10 @@ GOOS := $(shell go env GOOS)
|
||||
VERSION ?= $(shell git describe --tags)
|
||||
TAG ?= "minio/minio:$(VERSION)"
|
||||
|
||||
GOLANGCI_VERSION = v1.51.2
|
||||
GOLANGCI_DIR = .bin/golangci/$(GOLANGCI_VERSION)
|
||||
GOLANGCI = $(GOLANGCI_DIR)/golangci-lint
|
||||
|
||||
all: build
|
||||
|
||||
checks: ## check dependencies
|
||||
@@ -19,24 +23,22 @@ 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 $(GOPATH)/bin
|
||||
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOLANGCI_DIR) $(GOLANGCI_VERSION)
|
||||
@echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.1.7
|
||||
@echo "Installing stringer" && go install -v golang.org/x/tools/cmd/stringer@latest
|
||||
@echo "Installing staticcheck" && go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
|
||||
crosscompile: ## cross compile minio
|
||||
@(env bash $(PWD)/buildscripts/cross-compile.sh)
|
||||
|
||||
verifiers: getdeps lint check-gen
|
||||
verifiers: lint check-gen
|
||||
|
||||
check-gen: ## check for updated autogenerated files
|
||||
@go generate ./... >/dev/null
|
||||
@(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false)
|
||||
|
||||
lint: ## runs golangci-lint suite of linters
|
||||
lint: getdeps ## runs golangci-lint suite of linters
|
||||
@echo "Running $@ check"
|
||||
@${GOPATH}/bin/golangci-lint run --build-tags kqueue --timeout=10m --config ./.golangci.yml
|
||||
@${GOPATH}/bin/staticcheck --tests=false ./...
|
||||
@$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml --fix
|
||||
|
||||
check: test
|
||||
test: verifiers build ## builds minio, runs linters, tests
|
||||
|
||||
@@ -5,7 +5,6 @@ set -e
|
||||
export GORACE="history_size=7"
|
||||
export MINIO_API_REQUESTS_MAX=10000
|
||||
|
||||
## TODO remove `dsync` from race detector once this is merged and released https://go-review.googlesource.com/c/go/+/333529/
|
||||
for d in $(go list ./... | grep -v dsync); do
|
||||
for d in $(go list ./...); do
|
||||
CGO_ENABLED=1 go test -v -race --timeout 100m "$d"
|
||||
done
|
||||
|
||||
@@ -686,8 +686,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
continue
|
||||
}
|
||||
bucket, fileName := slc[0], slc[1]
|
||||
switch fileName {
|
||||
case objectLockConfig:
|
||||
if fileName == objectLockConfig {
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
rpt.SetStatus(bucket, fileName, err)
|
||||
@@ -756,8 +755,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
continue
|
||||
}
|
||||
bucket, fileName := slc[0], slc[1]
|
||||
switch fileName {
|
||||
case bucketVersioningConfig:
|
||||
if fileName == bucketVersioningConfig {
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
rpt.SetStatus(bucket, fileName, err)
|
||||
|
||||
@@ -86,7 +86,7 @@ func (a adminAPIHandlers) addOrUpdateIDPHandler(ctx context.Context, w http.Resp
|
||||
if idpCfgType == madmin.LDAPIDPCfg && cfgName != madmin.Default {
|
||||
// LDAP does not support multiple configurations. So cfgName must be
|
||||
// empty or `madmin.Default`.
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPNonDefaultConfigName), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
minio "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio/internal/sync/errgroup"
|
||||
)
|
||||
|
||||
func runAllIAMConcurrencyTests(suite *TestSuiteIAM, c *check) {
|
||||
@@ -129,18 +129,21 @@ func (s *TestSuiteIAM) TestDeleteUserRace(c *check) {
|
||||
secretKeys[i] = secretKey
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
g := errgroup.Group{}
|
||||
for i := 0; i < userCount; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
uClient := s.getUserClient(c, accessKeys[i], secretKeys[i], "")
|
||||
err := s.adm.RemoveUser(ctx, accessKeys[i])
|
||||
if err != nil {
|
||||
c.Fatalf("unable to remove user: %v", err)
|
||||
g.Go(func(i int) func() error {
|
||||
return func() error {
|
||||
uClient := s.getUserClient(c, accessKeys[i], secretKeys[i], "")
|
||||
err := s.adm.RemoveUser(ctx, accessKeys[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.mustNotListObjects(ctx, uClient, bucket)
|
||||
return nil
|
||||
}
|
||||
c.mustNotListObjects(ctx, uClient, bucket)
|
||||
}(i)
|
||||
}(i), i)
|
||||
}
|
||||
if errs := g.Wait(); len(errs) > 0 {
|
||||
c.Fatalf("unable to remove users: %v", errs)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
+12
-10
@@ -431,7 +431,7 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Not allowed to add a user with same access key as root credential
|
||||
if owner && accessKey == cred.AccessKey {
|
||||
if accessKey == globalActiveCred.AccessKey {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1332,11 +1332,12 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
||||
// Fetch the data usage of the current bucket
|
||||
var size uint64
|
||||
var objectsCount uint64
|
||||
var objectsHist map[string]uint64
|
||||
var objectsHist, versionsHist map[string]uint64
|
||||
if !dataUsageInfo.LastUpdate.IsZero() {
|
||||
size = dataUsageInfo.BucketsUsage[bucket.Name].Size
|
||||
objectsCount = dataUsageInfo.BucketsUsage[bucket.Name].ObjectsCount
|
||||
objectsHist = dataUsageInfo.BucketsUsage[bucket.Name].ObjectSizesHistogram
|
||||
versionsHist = dataUsageInfo.BucketsUsage[bucket.Name].ObjectVersionsHistogram
|
||||
}
|
||||
// Fetch the prefix usage of the current bucket
|
||||
var prefixUsage map[string]uint64
|
||||
@@ -1350,12 +1351,13 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
||||
tcfg, _, _ := globalBucketMetadataSys.GetTaggingConfig(bucket.Name)
|
||||
|
||||
acctInfo.Buckets = append(acctInfo.Buckets, madmin.BucketAccessInfo{
|
||||
Name: bucket.Name,
|
||||
Created: bucket.Created,
|
||||
Size: size,
|
||||
Objects: objectsCount,
|
||||
ObjectSizesHistogram: objectsHist,
|
||||
PrefixUsage: prefixUsage,
|
||||
Name: bucket.Name,
|
||||
Created: bucket.Created,
|
||||
Size: size,
|
||||
Objects: objectsCount,
|
||||
ObjectSizesHistogram: objectsHist,
|
||||
ObjectVersionsHistogram: versionsHist,
|
||||
PrefixUsage: prefixUsage,
|
||||
Details: &madmin.BucketDetails{
|
||||
Versioning: globalBucketVersioningSys.Enabled(bucket.Name),
|
||||
VersioningSuspended: globalBucketVersioningSys.Suspended(bucket.Name),
|
||||
@@ -1954,7 +1956,7 @@ func (a adminAPIHandlers) DetachPolicyBuiltin(w http.ResponseWriter, r *http.Req
|
||||
UserOrGroup: userOrGroup,
|
||||
UserType: int(userType),
|
||||
IsGroup: isGroup,
|
||||
Policy: strings.Join(policiesToDetach, ","),
|
||||
Policy: newPolicies,
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
}))
|
||||
@@ -2295,7 +2297,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
for accessKey, ureq := range userAccts {
|
||||
// Not allowed to add a user with same access key as root credential
|
||||
if owner && accessKey == cred.AccessKey {
|
||||
if accessKey == globalActiveCred.AccessKey {
|
||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -825,7 +825,7 @@ func (s *TestSuiteIAM) TestGroupAddRemove(c *check) {
|
||||
if set.CreateStringSet(groups...).Contains(group) {
|
||||
c.Fatalf("created group still present!")
|
||||
}
|
||||
groupInfo, err = s.adm.GetGroupDescription(ctx, group)
|
||||
_, err = s.adm.GetGroupDescription(ctx, group)
|
||||
if err == nil {
|
||||
c.Fatalf("group appears to exist")
|
||||
}
|
||||
|
||||
+11
-3
@@ -533,16 +533,19 @@ func lriToLockEntry(l lockRequesterInfo, now time.Time, resource, server string)
|
||||
func topLockEntries(peerLocks []*PeerLocks, stale bool) madmin.LockEntries {
|
||||
now := time.Now().UTC()
|
||||
entryMap := make(map[string]*madmin.LockEntry)
|
||||
toEntry := func(lri lockRequesterInfo) string {
|
||||
return fmt.Sprintf("%s/%s", lri.Name, lri.UID)
|
||||
}
|
||||
for _, peerLock := range peerLocks {
|
||||
if peerLock == nil {
|
||||
continue
|
||||
}
|
||||
for k, v := range peerLock.Locks {
|
||||
for _, lockReqInfo := range v {
|
||||
if val, ok := entryMap[lockReqInfo.Name]; ok {
|
||||
if val, ok := entryMap[toEntry(lockReqInfo)]; ok {
|
||||
val.ServerList = append(val.ServerList, peerLock.Addr)
|
||||
} else {
|
||||
entryMap[lockReqInfo.Name] = lriToLockEntry(lockReqInfo, now, k, peerLock.Addr)
|
||||
entryMap[toEntry(lockReqInfo)] = lriToLockEntry(lockReqInfo, now, k, peerLock.Addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1526,6 +1529,11 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Publish bootstrap events that have already occurred before client could subscribe.
|
||||
if traceOpts.TraceTypes().Contains(madmin.TraceBootstrap) {
|
||||
go globalBootstrapTracer.Publish(ctx, globalTrace)
|
||||
}
|
||||
|
||||
for _, peer := range peers {
|
||||
if peer == nil {
|
||||
continue
|
||||
@@ -2429,7 +2437,7 @@ func assignPoolNumbers(servers []madmin.ServerProperties) {
|
||||
func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus {
|
||||
lambdaMap := make(map[string][]madmin.TargetIDStatus)
|
||||
|
||||
for _, tgt := range globalConfigTargetList.Targets() {
|
||||
for _, tgt := range globalNotifyTargetList.Targets() {
|
||||
targetIDStatus := make(map[string]madmin.Status)
|
||||
active, _ := tgt.IsActive()
|
||||
targetID := tgt.ID()
|
||||
|
||||
@@ -21,10 +21,12 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -380,3 +382,146 @@ func TestExtractHealInitParams(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type byResourceUID struct{ madmin.LockEntries }
|
||||
|
||||
func (b byResourceUID) Less(i, j int) bool {
|
||||
toUniqLock := func(entry madmin.LockEntry) string {
|
||||
return fmt.Sprintf("%s/%s", entry.Resource, entry.ID)
|
||||
}
|
||||
return toUniqLock(b.LockEntries[i]) < toUniqLock(b.LockEntries[j])
|
||||
}
|
||||
|
||||
func TestTopLockEntries(t *testing.T) {
|
||||
locksHeld := make(map[string][]lockRequesterInfo)
|
||||
var owners []string
|
||||
for i := 0; i < 4; i++ {
|
||||
owners = append(owners, fmt.Sprintf("node-%d", i))
|
||||
}
|
||||
|
||||
// Simulate DeleteObjects of 10 objects in a single request. i.e same lock
|
||||
// request UID, but 10 different resource names associated with it.
|
||||
var lris []lockRequesterInfo
|
||||
uuid := mustGetUUID()
|
||||
for i := 0; i < 10; i++ {
|
||||
resource := fmt.Sprintf("bucket/delete-object-%d", i)
|
||||
lri := lockRequesterInfo{
|
||||
Name: resource,
|
||||
Writer: true,
|
||||
UID: uuid,
|
||||
Owner: owners[i%len(owners)],
|
||||
Group: true,
|
||||
Quorum: 3,
|
||||
}
|
||||
lris = append(lris, lri)
|
||||
locksHeld[resource] = []lockRequesterInfo{lri}
|
||||
}
|
||||
|
||||
// Add a few concurrent read locks to the mix
|
||||
for i := 0; i < 50; i++ {
|
||||
resource := fmt.Sprintf("bucket/get-object-%d", i)
|
||||
lri := lockRequesterInfo{
|
||||
Name: resource,
|
||||
UID: mustGetUUID(),
|
||||
Owner: owners[i%len(owners)],
|
||||
Quorum: 2,
|
||||
}
|
||||
lris = append(lris, lri)
|
||||
locksHeld[resource] = append(locksHeld[resource], lri)
|
||||
// concurrent read lock, same resource different uid
|
||||
lri.UID = mustGetUUID()
|
||||
lris = append(lris, lri)
|
||||
locksHeld[resource] = append(locksHeld[resource], lri)
|
||||
}
|
||||
|
||||
var peerLocks []*PeerLocks
|
||||
for _, owner := range owners {
|
||||
peerLocks = append(peerLocks, &PeerLocks{
|
||||
Addr: owner,
|
||||
Locks: locksHeld,
|
||||
})
|
||||
}
|
||||
var exp madmin.LockEntries
|
||||
for _, lri := range lris {
|
||||
lockType := func(lri lockRequesterInfo) string {
|
||||
if lri.Writer {
|
||||
return "WRITE"
|
||||
}
|
||||
return "READ"
|
||||
}
|
||||
exp = append(exp, madmin.LockEntry{
|
||||
Resource: lri.Name,
|
||||
Type: lockType(lri),
|
||||
ServerList: owners,
|
||||
Owner: lri.Owner,
|
||||
ID: lri.UID,
|
||||
Quorum: lri.Quorum,
|
||||
})
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
peerLocks []*PeerLocks
|
||||
expected madmin.LockEntries
|
||||
}{
|
||||
{
|
||||
peerLocks: peerLocks,
|
||||
expected: exp,
|
||||
},
|
||||
}
|
||||
|
||||
// printEntries := func(entries madmin.LockEntries) {
|
||||
// for i, entry := range entries {
|
||||
// fmt.Printf("%d: %s %s %s %s %v %d\n", i, entry.Resource, entry.ID, entry.Owner, entry.Type, entry.ServerList, entry.Elapsed)
|
||||
// }
|
||||
// }
|
||||
|
||||
check := func(exp, got madmin.LockEntries) (int, bool) {
|
||||
if len(exp) != len(got) {
|
||||
return 0, false
|
||||
}
|
||||
sort.Sort(byResourceUID{exp})
|
||||
sort.Sort(byResourceUID{got})
|
||||
// printEntries(exp)
|
||||
// printEntries(got)
|
||||
for i, e := range exp {
|
||||
if !e.Timestamp.Equal(got[i].Timestamp) {
|
||||
return i, false
|
||||
}
|
||||
// Skip checking elapsed since it's time sensitive.
|
||||
// if e.Elapsed != got[i].Elapsed {
|
||||
// return false
|
||||
// }
|
||||
if e.Resource != got[i].Resource {
|
||||
return i, false
|
||||
}
|
||||
if e.Type != got[i].Type {
|
||||
return i, false
|
||||
}
|
||||
if e.Source != got[i].Source {
|
||||
return i, false
|
||||
}
|
||||
if e.Owner != got[i].Owner {
|
||||
return i, false
|
||||
}
|
||||
if e.ID != got[i].ID {
|
||||
return i, false
|
||||
}
|
||||
if len(e.ServerList) != len(got[i].ServerList) {
|
||||
return i, false
|
||||
}
|
||||
for j := range e.ServerList {
|
||||
if e.ServerList[j] != got[i].ServerList[j] {
|
||||
return i, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, true
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
got := topLockEntries(tc.peerLocks, false)
|
||||
if idx, ok := check(tc.expected, got); !ok {
|
||||
t.Fatalf("%d: mismatch at %d \n expected %#v but got %#v", i, idx, tc.expected[idx], got[idx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +237,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/describe-job").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.DescribeBatchJob)))
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/cancel-job").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.CancelBatchJob)))
|
||||
|
||||
// Bucket migration operations
|
||||
// ExportBucketMetaHandler
|
||||
|
||||
+64
-60
@@ -38,10 +38,12 @@ import (
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
"github.com/minio/minio/internal/config/dns"
|
||||
"github.com/minio/minio/internal/crypto"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
|
||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||
"github.com/minio/minio/internal/bucket/versioning"
|
||||
levent "github.com/minio/minio/internal/config/lambda/event"
|
||||
"github.com/minio/minio/internal/event"
|
||||
"github.com/minio/minio/internal/hash"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
@@ -266,6 +268,7 @@ const (
|
||||
ErrAdminNoSuchUser
|
||||
ErrAdminNoSuchGroup
|
||||
ErrAdminGroupNotEmpty
|
||||
ErrAdminGroupDisabled
|
||||
ErrAdminNoSuchJob
|
||||
ErrAdminNoSuchPolicy
|
||||
ErrAdminPolicyChangeAlreadyApplied
|
||||
@@ -279,6 +282,7 @@ const (
|
||||
ErrAdminConfigEnvOverridden
|
||||
ErrAdminConfigDuplicateKeys
|
||||
ErrAdminConfigInvalidIDPType
|
||||
ErrAdminConfigLDAPNonDefaultConfigName
|
||||
ErrAdminConfigLDAPValidation
|
||||
ErrAdminConfigIDPCfgNameAlreadyExists
|
||||
ErrAdminConfigIDPCfgNameDoesNotExist
|
||||
@@ -410,6 +414,10 @@ const (
|
||||
|
||||
ErrInvalidChecksum
|
||||
|
||||
// Lambda functions
|
||||
ErrLambdaARNInvalid
|
||||
ErrLambdaARNNotFound
|
||||
|
||||
apiErrCodeEnd // This is used only for the testing code
|
||||
)
|
||||
|
||||
@@ -424,8 +432,7 @@ func (e errorCodeMap) ToAPIErrWithErr(errCode APIErrorCode, err error) APIError
|
||||
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
|
||||
}
|
||||
if globalSite.Region != "" {
|
||||
switch errCode {
|
||||
case ErrAuthorizationHeaderMalformed:
|
||||
if errCode == ErrAuthorizationHeaderMalformed {
|
||||
apiErr.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalSite.Region)
|
||||
return apiErr
|
||||
}
|
||||
@@ -1261,6 +1268,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "The specified group is not empty - cannot remove it.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminGroupDisabled: {
|
||||
Code: "XMinioAdminGroupDisabled",
|
||||
Description: "The specified group is disabled.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminNoSuchPolicy: {
|
||||
Code: "XMinioAdminNoSuchPolicy",
|
||||
Description: "The canned policy does not exist.",
|
||||
@@ -1323,6 +1335,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: fmt.Sprintf("Invalid IDP configuration type - must be one of %v", madmin.ValidIDPConfigTypes),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminConfigLDAPNonDefaultConfigName: {
|
||||
Code: "XMinioAdminConfigLDAPNonDefaultConfigName",
|
||||
Description: "Only a single LDAP configuration is supported - config name must be empty or `_`",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminConfigLDAPValidation: {
|
||||
Code: "XMinioAdminConfigLDAPValidation",
|
||||
Description: "LDAP Configuration validation failed",
|
||||
@@ -1959,6 +1976,16 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Invalid checksum provided.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrLambdaARNInvalid: {
|
||||
Code: "LambdaARNInvalid",
|
||||
Description: "The specified lambda ARN is invalid",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrLambdaARNNotFound: {
|
||||
Code: "LambdaARNNotFound",
|
||||
Description: "The specified lambda ARN does not exist",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrPolicyAlreadyAttached: {
|
||||
Code: "XMinioPolicyAlreadyAttached",
|
||||
Description: "The specified policy is already attached.",
|
||||
@@ -1982,15 +2009,15 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
|
||||
// Only return ErrClientDisconnected if the provided context is actually canceled.
|
||||
// This way downstream context.Canceled will still report ErrOperationTimedOut
|
||||
if contextCanceled(ctx) {
|
||||
if ctx.Err() == context.Canceled {
|
||||
return ErrClientDisconnected
|
||||
}
|
||||
if contextCanceled(ctx) && errors.Is(ctx.Err(), context.Canceled) {
|
||||
return ErrClientDisconnected
|
||||
}
|
||||
|
||||
switch err {
|
||||
case errInvalidArgument:
|
||||
apiErr = ErrAdminInvalidArgument
|
||||
case errNoSuchPolicy:
|
||||
apiErr = ErrAdminNoSuchPolicy
|
||||
case errNoSuchUser:
|
||||
apiErr = ErrAdminNoSuchUser
|
||||
case errNoSuchServiceAccount:
|
||||
@@ -2019,6 +2046,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrAdminInvalidSecretKey
|
||||
case errInvalidStorageClass:
|
||||
apiErr = ErrInvalidStorageClass
|
||||
case errErasureReadQuorum:
|
||||
apiErr = ErrSlowDown
|
||||
case errErasureWriteQuorum:
|
||||
apiErr = ErrSlowDown
|
||||
// SSE errors
|
||||
case errInvalidEncryptionParameters:
|
||||
apiErr = ErrInvalidEncryptionParameters
|
||||
@@ -2066,16 +2097,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrObjectLockInvalidHeaders
|
||||
case objectlock.ErrMalformedXML:
|
||||
apiErr = ErrMalformedXML
|
||||
default:
|
||||
switch {
|
||||
case errors.Is(err, errNoSuchPolicy):
|
||||
apiErr = ErrAdminNoSuchPolicy
|
||||
}
|
||||
}
|
||||
|
||||
// Compression errors
|
||||
switch err {
|
||||
case errInvalidDecompressedSize:
|
||||
if err == errInvalidDecompressedSize {
|
||||
apiErr = ErrInvalidDecompressedSize
|
||||
}
|
||||
|
||||
@@ -2086,7 +2111,7 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
|
||||
// etcd specific errors, a key is always a bucket for us return
|
||||
// ErrNoSuchBucket in such a case.
|
||||
if err == dns.ErrNoEntriesFound {
|
||||
if errors.Is(err, dns.ErrNoEntriesFound) {
|
||||
return ErrNoSuchBucket
|
||||
}
|
||||
|
||||
@@ -2211,6 +2236,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrARNNotification
|
||||
case *event.ErrARNNotFound:
|
||||
apiErr = ErrARNNotification
|
||||
case *levent.ErrInvalidARN:
|
||||
apiErr = ErrLambdaARNInvalid
|
||||
case *levent.ErrARNNotFound:
|
||||
apiErr = ErrLambdaARNNotFound
|
||||
case *event.ErrUnknownRegion:
|
||||
apiErr = ErrRegionNotification
|
||||
case *event.ErrInvalidFilterName:
|
||||
@@ -2274,38 +2303,27 @@ func toAPIError(ctx context.Context, err error) APIError {
|
||||
}
|
||||
|
||||
apiErr := errorCodes.ToAPIErr(toAPIErrorCode(ctx, err))
|
||||
e, ok := err.(dns.ErrInvalidBucketName)
|
||||
if ok {
|
||||
code := toAPIErrorCode(ctx, e)
|
||||
apiErr = errorCodes.ToAPIErrWithErr(code, e)
|
||||
}
|
||||
|
||||
if apiErr.Code == "NotImplemented" {
|
||||
switch e := err.(type) {
|
||||
case NotImplemented:
|
||||
desc := e.Error()
|
||||
if desc == "" {
|
||||
desc = apiErr.Description
|
||||
}
|
||||
apiErr = APIError{
|
||||
Code: apiErr.Code,
|
||||
Description: desc,
|
||||
HTTPStatusCode: apiErr.HTTPStatusCode,
|
||||
}
|
||||
return apiErr
|
||||
switch apiErr.Code {
|
||||
case "NotImplemented":
|
||||
desc := fmt.Sprintf("%s (%v)", apiErr.Description, err)
|
||||
apiErr = APIError{
|
||||
Code: apiErr.Code,
|
||||
Description: desc,
|
||||
HTTPStatusCode: apiErr.HTTPStatusCode,
|
||||
}
|
||||
}
|
||||
|
||||
if apiErr.Code == "XMinioBackendDown" {
|
||||
case "XMinioBackendDown":
|
||||
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
|
||||
return apiErr
|
||||
}
|
||||
|
||||
if apiErr.Code == "InternalError" {
|
||||
case "InternalError":
|
||||
// If we see an internal error try to interpret
|
||||
// any underlying errors if possible depending on
|
||||
// their internal error types.
|
||||
switch e := err.(type) {
|
||||
case kms.Error:
|
||||
apiErr = APIError{
|
||||
Description: e.Err.Error(),
|
||||
Code: e.APICode,
|
||||
HTTPStatusCode: e.HTTPStatusCode,
|
||||
}
|
||||
case batchReplicationJobError:
|
||||
apiErr = APIError(e)
|
||||
case InvalidArgument:
|
||||
@@ -2316,22 +2334,20 @@ func toAPIError(ctx context.Context, err error) APIError {
|
||||
}
|
||||
case *xml.SyntaxError:
|
||||
apiErr = APIError{
|
||||
Code: "MalformedXML",
|
||||
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrMalformedXML].Description,
|
||||
e.Error()),
|
||||
Code: "MalformedXML",
|
||||
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrMalformedXML].Description, e),
|
||||
HTTPStatusCode: errorCodes[ErrMalformedXML].HTTPStatusCode,
|
||||
}
|
||||
case url.EscapeError:
|
||||
apiErr = APIError{
|
||||
Code: "XMinioInvalidObjectName",
|
||||
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrInvalidObjectName].Description,
|
||||
e.Error()),
|
||||
Code: "XMinioInvalidObjectName",
|
||||
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrInvalidObjectName].Description, e),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case versioning.Error:
|
||||
apiErr = APIError{
|
||||
Code: "IllegalVersioningConfigurationException",
|
||||
Description: fmt.Sprintf("Versioning configuration specified in the request is invalid. (%s)", e.Error()),
|
||||
Description: fmt.Sprintf("Versioning configuration specified in the request is invalid. (%s)", e),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case lifecycle.Error:
|
||||
@@ -2397,19 +2413,7 @@ func toAPIError(ctx context.Context, err error) APIError {
|
||||
// Add more other SDK related errors here if any in future.
|
||||
default:
|
||||
//nolint:gocritic
|
||||
if errors.Is(err, errMalformedEncoding) {
|
||||
apiErr = APIError{
|
||||
Code: "BadRequest",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
} else if errors.Is(err, errChunkTooBig) {
|
||||
apiErr = APIError{
|
||||
Code: "BadRequest",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
} else if errors.Is(err, strconv.ErrRange) {
|
||||
if errors.Is(err, errMalformedEncoding) || errors.Is(err, errChunkTooBig) || errors.Is(err, strconv.ErrRange) {
|
||||
apiErr = APIError{
|
||||
Code: "BadRequest",
|
||||
Description: err.Error(),
|
||||
|
||||
+7
-3
@@ -285,7 +285,10 @@ func registerAPIRouter(router *mux.Router) {
|
||||
// GetObjectLegalHold
|
||||
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||
collectAPIStats("getobjectlegalhold", maxClients(gz(httpTraceAll(api.GetObjectLegalHoldHandler))))).Queries("legal-hold", "")
|
||||
// GetObject - note gzip compression is *not* added due to Range requests.
|
||||
// GetObject with lambda ARNs
|
||||
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||
collectAPIStats("getobject", maxClients(gz(httpTraceHdrs(api.GetObjectLambdaHandler))))).Queries("lambdaArn", "{lambdaArn:.*}")
|
||||
// GetObject
|
||||
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||
collectAPIStats("getobject", maxClients(gz(httpTraceHdrs(api.GetObjectHandler)))))
|
||||
// CopyObject
|
||||
@@ -431,8 +434,9 @@ func registerAPIRouter(router *mux.Router) {
|
||||
router.Methods(http.MethodHead).HandlerFunc(
|
||||
collectAPIStats("headbucket", maxClients(gz(httpTraceAll(api.HeadBucketHandler)))))
|
||||
// PostPolicy
|
||||
router.Methods(http.MethodPost).HeadersRegexp(xhttp.ContentType, "multipart/form-data*").HandlerFunc(
|
||||
collectAPIStats("postpolicybucket", maxClients(gz(httpTraceHdrs(api.PostPolicyBucketHandler)))))
|
||||
router.Methods(http.MethodPost).MatcherFunc(func(r *http.Request, _ *mux.RouteMatch) bool {
|
||||
return isRequestPostPolicySignatureV4(r)
|
||||
}).HandlerFunc(collectAPIStats("postpolicybucket", maxClients(gz(httpTraceHdrs(api.PostPolicyBucketHandler)))))
|
||||
// DeleteMultipleObjects
|
||||
router.Methods(http.MethodPost).HandlerFunc(
|
||||
collectAPIStats("deletemultipleobjects", maxClients(gz(httpTraceAll(api.DeleteMultipleObjectsHandler))))).Queries("delete", "")
|
||||
|
||||
+2
-8
@@ -94,14 +94,8 @@ func s3URLEncode(s string) string {
|
||||
}
|
||||
|
||||
// s3EncodeName encodes string in response when encodingType is specified in AWS S3 requests.
|
||||
func s3EncodeName(name string, encodingType string) (result string) {
|
||||
// Quick path to exit
|
||||
if encodingType == "" {
|
||||
return name
|
||||
}
|
||||
encodingType = strings.ToLower(encodingType)
|
||||
switch encodingType {
|
||||
case "url":
|
||||
func s3EncodeName(name, encodingType string) string {
|
||||
if strings.ToLower(encodingType) == "url" {
|
||||
return s3URLEncode(name)
|
||||
}
|
||||
return name
|
||||
|
||||
+139
-135
File diff suppressed because one or more lines are too long
+12
-3
@@ -25,6 +25,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@@ -74,8 +75,11 @@ func isRequestPresignedSignatureV2(r *http.Request) bool {
|
||||
|
||||
// Verify if request has AWS Post policy Signature Version '4'.
|
||||
func isRequestPostPolicySignatureV4(r *http.Request) bool {
|
||||
return strings.Contains(r.Header.Get(xhttp.ContentType), "multipart/form-data") &&
|
||||
r.Method == http.MethodPost
|
||||
mediaType, _, err := mime.ParseMediaType(r.Header.Get(xhttp.ContentType))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return mediaType == "multipart/form-data" && r.Method == http.MethodPost
|
||||
}
|
||||
|
||||
// Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation.
|
||||
@@ -154,6 +158,10 @@ func validateAdminSignature(ctx context.Context, r *http.Request, region string)
|
||||
return cred, owner, s3Err
|
||||
}
|
||||
|
||||
logger.GetReqInfo(ctx).Cred = cred
|
||||
logger.GetReqInfo(ctx).Owner = owner
|
||||
logger.GetReqInfo(ctx).Region = globalSite.Region
|
||||
|
||||
return cred, owner, ErrNone
|
||||
}
|
||||
|
||||
@@ -203,7 +211,7 @@ func getClaimsFromTokenWithSecret(token, secret string) (map[string]interface{},
|
||||
// that clients cannot decode the token using the temp
|
||||
// secret keys and generate an entirely new claim by essentially
|
||||
// hijacking the policies. We need to make sure that this is
|
||||
// based an admin credential such that token cannot be decoded
|
||||
// based on admin credential such that token cannot be decoded
|
||||
// on the client side and is treated like an opaque value.
|
||||
claims, err := auth.ExtractClaims(token, secret)
|
||||
if err != nil {
|
||||
@@ -344,6 +352,7 @@ func authenticateRequest(ctx context.Context, r *http.Request, action policy.Act
|
||||
|
||||
logger.GetReqInfo(ctx).Cred = cred
|
||||
logger.GetReqInfo(ctx).Owner = owner
|
||||
logger.GetReqInfo(ctx).Region = globalSite.Region
|
||||
|
||||
// region is valid only for CreateBucketAction.
|
||||
var region string
|
||||
|
||||
@@ -271,6 +271,9 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
|
||||
}
|
||||
|
||||
func getLocalDisksToHeal() (disksToHeal Endpoints) {
|
||||
globalLocalDrivesMu.RLock()
|
||||
globalLocalDrives := globalLocalDrives
|
||||
globalLocalDrivesMu.RUnlock()
|
||||
for _, disk := range globalLocalDrives {
|
||||
_, err := disk.GetDiskID()
|
||||
if errors.Is(err, errUnformattedDisk) {
|
||||
|
||||
+58
-4
@@ -234,6 +234,7 @@ type BatchJobRequest struct {
|
||||
Started time.Time `yaml:"-" json:"started"`
|
||||
Location string `yaml:"-" json:"location"`
|
||||
Replicate *BatchJobReplicateV1 `yaml:"replicate" json:"replicate"`
|
||||
ctx context.Context `msg:"-"`
|
||||
}
|
||||
|
||||
// Notify notifies notification endpoint if configured regarding job failure or success.
|
||||
@@ -859,9 +860,7 @@ func (j BatchJobRequest) Validate(ctx context.Context, o ObjectLayer) error {
|
||||
}
|
||||
|
||||
func (j BatchJobRequest) delete(ctx context.Context, api ObjectLayer) {
|
||||
if j.Replicate != nil {
|
||||
deleteConfig(ctx, api, pathJoin(j.Location, batchReplName))
|
||||
}
|
||||
deleteConfig(ctx, api, pathJoin(j.Location, batchReplName))
|
||||
globalBatchJobsMetrics.delete(j.ID)
|
||||
deleteConfig(ctx, api, j.Location)
|
||||
}
|
||||
@@ -1061,6 +1060,34 @@ func (a adminAPIHandlers) StartBatchJob(w http.ResponseWriter, r *http.Request)
|
||||
writeSuccessResponseJSON(w, buf)
|
||||
}
|
||||
|
||||
// CancelBatchJob cancels a job in progress
|
||||
func (a adminAPIHandlers) CancelBatchJob(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "CancelBatchJob")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.CancelBatchJobAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
jobID := r.Form.Get("id")
|
||||
if jobID == "" {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
if err := globalBatchJobPool.canceler(jobID, true); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, err), r.URL)
|
||||
return
|
||||
}
|
||||
j := BatchJobRequest{
|
||||
ID: jobID,
|
||||
Location: pathJoin(batchJobPrefix, jobID),
|
||||
}
|
||||
j.delete(ctx, objectAPI)
|
||||
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
//msgp:ignore BatchJobPool
|
||||
|
||||
// BatchJobPool batch job pool
|
||||
@@ -1070,6 +1097,8 @@ type BatchJobPool struct {
|
||||
once sync.Once
|
||||
mu sync.Mutex
|
||||
jobCh chan *BatchJobRequest
|
||||
jmu sync.Mutex // protects jobCancelers
|
||||
jobCancelers map[string]context.CancelFunc
|
||||
workerKillCh chan struct{}
|
||||
workerSize int
|
||||
}
|
||||
@@ -1083,6 +1112,7 @@ func newBatchJobPool(ctx context.Context, o ObjectLayer, workers int) *BatchJobP
|
||||
objLayer: o,
|
||||
jobCh: make(chan *BatchJobRequest, 10000),
|
||||
workerKillCh: make(chan struct{}, workers),
|
||||
jobCancelers: make(map[string]context.CancelFunc),
|
||||
}
|
||||
jpool.ResizeWorkers(workers)
|
||||
jpool.resume()
|
||||
@@ -1124,15 +1154,17 @@ func (j *BatchJobPool) AddWorker() {
|
||||
return
|
||||
}
|
||||
if job.Replicate != nil {
|
||||
if err := job.Replicate.Start(j.ctx, j.objLayer, *job); err != nil {
|
||||
if err := job.Replicate.Start(job.ctx, j.objLayer, *job); err != nil {
|
||||
if !isErrBucketNotFound(err) {
|
||||
logger.LogIf(j.ctx, err)
|
||||
j.canceler(job.ID, false)
|
||||
continue
|
||||
}
|
||||
// Bucket not found proceed to delete such a job.
|
||||
}
|
||||
}
|
||||
job.delete(j.ctx, j.objLayer)
|
||||
j.canceler(job.ID, false)
|
||||
case <-j.workerKillCh:
|
||||
return
|
||||
}
|
||||
@@ -1162,6 +1194,12 @@ func (j *BatchJobPool) queueJob(req *BatchJobRequest) error {
|
||||
if j == nil {
|
||||
return errInvalidArgument
|
||||
}
|
||||
jctx, jcancel := context.WithCancel(j.ctx)
|
||||
j.jmu.Lock()
|
||||
j.jobCancelers[req.ID] = jcancel
|
||||
j.jmu.Unlock()
|
||||
req.ctx = jctx
|
||||
|
||||
select {
|
||||
case <-j.ctx.Done():
|
||||
j.once.Do(func() {
|
||||
@@ -1174,6 +1212,22 @@ func (j *BatchJobPool) queueJob(req *BatchJobRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// delete canceler from the map, cancel job if requested
|
||||
func (j *BatchJobPool) canceler(jobID string, cancel bool) error {
|
||||
if j == nil {
|
||||
return errInvalidArgument
|
||||
}
|
||||
j.jmu.Lock()
|
||||
defer j.jmu.Unlock()
|
||||
if canceler, ok := j.jobCancelers[jobID]; ok {
|
||||
if cancel {
|
||||
canceler()
|
||||
}
|
||||
}
|
||||
delete(j.jobCancelers, jobID)
|
||||
return nil
|
||||
}
|
||||
|
||||
//msgp:ignore batchJobMetrics
|
||||
type batchJobMetrics struct {
|
||||
sync.RWMutex
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/pubsub"
|
||||
)
|
||||
|
||||
const bootstrapMsgsLimit = 4 << 10
|
||||
|
||||
type bootstrapInfo struct {
|
||||
msg string
|
||||
ts time.Time
|
||||
source string
|
||||
}
|
||||
type bootstrapTracer struct {
|
||||
mu sync.RWMutex
|
||||
idx int
|
||||
info [bootstrapMsgsLimit]bootstrapInfo
|
||||
lastUpdate time.Time
|
||||
}
|
||||
|
||||
var globalBootstrapTracer = &bootstrapTracer{}
|
||||
|
||||
func (bs *bootstrapTracer) DropEvents() {
|
||||
bs.mu.Lock()
|
||||
defer bs.mu.Unlock()
|
||||
|
||||
if time.Now().UTC().Sub(bs.lastUpdate) > 24*time.Hour {
|
||||
bs.info = [4096]bootstrapInfo{}
|
||||
bs.idx = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *bootstrapTracer) Empty() bool {
|
||||
var empty bool
|
||||
bs.mu.RLock()
|
||||
empty = bs.info[0].msg == ""
|
||||
bs.mu.RUnlock()
|
||||
|
||||
return empty
|
||||
}
|
||||
|
||||
func (bs *bootstrapTracer) Record(msg string) {
|
||||
source := getSource(2)
|
||||
bs.mu.Lock()
|
||||
now := time.Now().UTC()
|
||||
bs.info[bs.idx] = bootstrapInfo{
|
||||
msg: msg,
|
||||
ts: now,
|
||||
source: source,
|
||||
}
|
||||
bs.lastUpdate = now
|
||||
bs.idx = (bs.idx + 1) % bootstrapMsgsLimit
|
||||
bs.mu.Unlock()
|
||||
}
|
||||
|
||||
func (bs *bootstrapTracer) Events() []madmin.TraceInfo {
|
||||
traceInfo := make([]madmin.TraceInfo, 0, bootstrapMsgsLimit)
|
||||
|
||||
// Add all messages in order
|
||||
addAll := func(info []bootstrapInfo) {
|
||||
for _, msg := range info {
|
||||
if msg.ts.IsZero() {
|
||||
continue // skip empty events
|
||||
}
|
||||
traceInfo = append(traceInfo, madmin.TraceInfo{
|
||||
TraceType: madmin.TraceBootstrap,
|
||||
Time: msg.ts,
|
||||
NodeName: globalLocalNodeName,
|
||||
FuncName: "BOOTSTRAP",
|
||||
Message: fmt.Sprintf("%s %s", msg.source, msg.msg),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
bs.mu.RLock()
|
||||
addAll(bs.info[bs.idx:])
|
||||
addAll(bs.info[:bs.idx])
|
||||
bs.mu.RUnlock()
|
||||
return traceInfo
|
||||
}
|
||||
|
||||
func (bs *bootstrapTracer) Publish(ctx context.Context, trace *pubsub.PubSub[madmin.TraceInfo, madmin.TraceType]) {
|
||||
if bs.Empty() {
|
||||
return
|
||||
}
|
||||
for _, bsEvent := range bs.Events() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
default:
|
||||
trace.Publish(bsEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
// Bootstrap events exceed bootstrap messages limit
|
||||
bsTracer := &bootstrapTracer{}
|
||||
for i := 0; i < bootstrapMsgsLimit+10; i++ {
|
||||
bsTracer.Record(fmt.Sprintf("msg-%d", i))
|
||||
}
|
||||
|
||||
traceInfos := bsTracer.Events()
|
||||
if len(traceInfos) != bootstrapMsgsLimit {
|
||||
t.Fatalf("Expected length of events %d but got %d", bootstrapMsgsLimit, len(traceInfos))
|
||||
}
|
||||
|
||||
// Simulate the case where bootstrap events were updated a day ago
|
||||
bsTracer.lastUpdate = time.Now().UTC().Add(-25 * time.Hour)
|
||||
bsTracer.DropEvents()
|
||||
if !bsTracer.Empty() {
|
||||
t.Fatalf("Expected all bootstrap events to have been dropped, but found %d events", len(bsTracer.Events()))
|
||||
}
|
||||
|
||||
// Fewer than 4K bootstrap events
|
||||
for i := 0; i < 10; i++ {
|
||||
bsTracer.Record(fmt.Sprintf("msg-%d", i))
|
||||
}
|
||||
events := bsTracer.Events()
|
||||
if len(events) != 10 {
|
||||
t.Fatalf("Expected length of events %d but got %d", 10, len(events))
|
||||
}
|
||||
for i, traceInfo := range bsTracer.Events() {
|
||||
msg := fmt.Sprintf("msg-%d", i)
|
||||
if !strings.HasSuffix(traceInfo.Message, msg) {
|
||||
t.Fatalf("Expected %s but got %s", msg, traceInfo.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,10 +101,14 @@ func (s1 ServerSystemConfig) Diff(s2 ServerSystemConfig) error {
|
||||
}
|
||||
|
||||
var skipEnvs = map[string]struct{}{
|
||||
"MINIO_OPTS": {},
|
||||
"MINIO_CERT_PASSWD": {},
|
||||
"MINIO_SERVER_DEBUG": {},
|
||||
"MINIO_DSYNC_TRACE": {},
|
||||
"MINIO_OPTS": {},
|
||||
"MINIO_CERT_PASSWD": {},
|
||||
"MINIO_SERVER_DEBUG": {},
|
||||
"MINIO_DSYNC_TRACE": {},
|
||||
"MINIO_ROOT_USER": {},
|
||||
"MINIO_ROOT_PASSWORD": {},
|
||||
"MINIO_ACCESS_KEY": {},
|
||||
"MINIO_SECRET_KEY": {},
|
||||
}
|
||||
|
||||
func getServerSystemCfg() ServerSystemConfig {
|
||||
@@ -118,7 +122,7 @@ func getServerSystemCfg() ServerSystemConfig {
|
||||
if _, ok := skipEnvs[envK]; ok {
|
||||
continue
|
||||
}
|
||||
envValues[envK] = env.Get(envK, "")
|
||||
envValues[envK] = logger.HashString(env.Get(envK, ""))
|
||||
}
|
||||
return ServerSystemConfig{
|
||||
MinioEndpoints: globalEndpoints,
|
||||
@@ -126,11 +130,22 @@ func getServerSystemCfg() ServerSystemConfig {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bootstrapRESTServer) writeErrorResponse(w http.ResponseWriter, err error) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte(err.Error()))
|
||||
}
|
||||
|
||||
// HealthHandler returns success if request is valid
|
||||
func (b *bootstrapRESTServer) HealthHandler(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func (b *bootstrapRESTServer) VerifyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "VerifyHandler")
|
||||
|
||||
if err := storageServerRequestValidate(r); err != nil {
|
||||
b.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := getServerSystemCfg()
|
||||
logger.LogIf(ctx, json.NewEncoder(w).Encode(&cfg))
|
||||
}
|
||||
|
||||
@@ -796,8 +796,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
||||
|
||||
// Make sure to add Location information here only for bucket
|
||||
w.Header().Set(xhttp.Location,
|
||||
getObjectLocation(r, globalDomainNames, bucket, ""))
|
||||
w.Header().Set(xhttp.Location, pathJoin(SlashSeparator, bucket))
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
|
||||
@@ -848,9 +847,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.MakeBucketHook(ctx, bucket, opts))
|
||||
|
||||
// Make sure to add Location information here only for bucket
|
||||
if cp := pathClean(r.URL.Path); cp != "" {
|
||||
w.Header().Set(xhttp.Location, cp) // Clean any trailing slashes.
|
||||
}
|
||||
w.Header().Set(xhttp.Location, pathJoin(SlashSeparator, bucket))
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
|
||||
@@ -1123,7 +1120,9 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
|
||||
}
|
||||
|
||||
w.Header().Set(xhttp.Location, getObjectLocation(r, globalDomainNames, bucket, object))
|
||||
if obj := getObjectLocation(r, globalDomainNames, bucket, object); obj != "" {
|
||||
w.Header().Set(xhttp.Location, obj)
|
||||
}
|
||||
|
||||
// Notify object created event.
|
||||
defer sendEvent(eventArgs{
|
||||
|
||||
@@ -323,8 +323,7 @@ func (b *BucketMetadata) getAllLegacyConfigs(ctx context.Context, objectAPI Obje
|
||||
|
||||
configData, err := readConfig(ctx, objectAPI, configFile)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case ObjectExistsAsDirectory:
|
||||
if _, ok := err.(ObjectExistsAsDirectory); ok {
|
||||
// in FS mode it possible that we have actual
|
||||
// files in this folder with `.minio.sys/buckets/bucket/configFile`
|
||||
continue
|
||||
|
||||
@@ -90,8 +90,8 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
|
||||
|
||||
opts.VersionID = object.VersionID
|
||||
if gerr != nil { // error from GetObjectInfo
|
||||
switch gerr.(type) {
|
||||
case MethodNotAllowed: // This happens usually for a delete marker
|
||||
if _, ok := gerr.(MethodNotAllowed); ok {
|
||||
// This happens usually for a delete marker
|
||||
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
|
||||
// Delete marker should be present and valid.
|
||||
return ErrNone
|
||||
@@ -192,8 +192,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi Objec
|
||||
days, objRetention.RetainUntilDate.Time,
|
||||
objRetention.Mode, byPassSet, r, cred,
|
||||
owner)
|
||||
switch apiErr {
|
||||
case ErrAccessDenied:
|
||||
if apiErr == ErrAccessDenied {
|
||||
return errAuthentication
|
||||
}
|
||||
return nil
|
||||
@@ -210,8 +209,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi Objec
|
||||
return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID}
|
||||
}
|
||||
}
|
||||
switch govPerm {
|
||||
case ErrAccessDenied:
|
||||
if govPerm == ErrAccessDenied {
|
||||
return errAuthentication
|
||||
}
|
||||
return nil
|
||||
@@ -224,8 +222,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi Objec
|
||||
apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
|
||||
days, objRetention.RetainUntilDate.Time, objRetention.Mode,
|
||||
false, r, cred, owner)
|
||||
switch apiErr {
|
||||
case ErrAccessDenied:
|
||||
if apiErr == ErrAccessDenied {
|
||||
return errAuthentication
|
||||
}
|
||||
return nil
|
||||
@@ -236,8 +233,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi Objec
|
||||
apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
|
||||
days, objRetention.RetainUntilDate.Time,
|
||||
objRetention.Mode, byPassSet, r, cred, owner)
|
||||
switch apiErr {
|
||||
case ErrAccessDenied:
|
||||
if apiErr == ErrAccessDenied {
|
||||
return errAuthentication
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -496,8 +496,7 @@ func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) Repl
|
||||
oi.VersionPurgeStatusInternal = fmt.Sprintf("%s=%s;", rcfg.Config.RoleArn, oi.VersionPurgeStatus)
|
||||
}
|
||||
for k, v := range oi.UserDefined {
|
||||
switch {
|
||||
case strings.EqualFold(k, ReservedMetadataPrefixLower+ReplicationReset):
|
||||
if strings.EqualFold(k, ReservedMetadataPrefixLower+ReplicationReset) {
|
||||
delete(oi.UserDefined, k)
|
||||
oi.UserDefined[targetResetHeader(rcfg.Config.RoleArn)] = v
|
||||
}
|
||||
|
||||
+22
-17
@@ -586,11 +586,13 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
|
||||
return
|
||||
}
|
||||
}
|
||||
// mark delete marker replication as failed if target cluster not ready to receive
|
||||
// this request yet (object version not replicated yet)
|
||||
if err != nil && !toi.ReplicationReady {
|
||||
rinfo.ReplicationStatus = replication.Failed
|
||||
return
|
||||
if !isErrObjectNotFound(ErrorRespToObjectError(err, dobj.Bucket, dobj.ObjectName)) {
|
||||
// mark delete marker replication as failed if target cluster not ready to receive
|
||||
// this request yet (object version not replicated yet)
|
||||
if err != nil && !toi.ReplicationReady {
|
||||
rinfo.ReplicationStatus = replication.Failed
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,10 +804,12 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati
|
||||
oi1.ModTime.Unix() > oi2.LastModified.Unix() && oi1.VersionID == nullVersionID {
|
||||
return replicateNone
|
||||
}
|
||||
sz, _ := oi1.GetActualSize()
|
||||
|
||||
// needs full replication
|
||||
if oi1.ETag != oi2.ETag ||
|
||||
oi1.VersionID != oi2.VersionID ||
|
||||
oi1.Size != oi2.Size ||
|
||||
sz != oi2.Size ||
|
||||
oi1.DeleteMarker != oi2.IsDeleteMarker ||
|
||||
oi1.ModTime.Unix() != oi2.LastModified.Unix() {
|
||||
return replicateAll
|
||||
@@ -1202,12 +1206,6 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
||||
ReplicationAction: rAction,
|
||||
}
|
||||
|
||||
if ri.ObjectInfo.TargetReplicationStatus(tgt.ARN) == replication.Completed && !ri.ExistingObjResync.Empty() && !ri.ExistingObjResync.mustResyncTarget(tgt.ARN) {
|
||||
rinfo.ReplicationStatus = replication.Completed
|
||||
rinfo.ReplicationResynced = true
|
||||
return
|
||||
}
|
||||
|
||||
if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
|
||||
logger.LogIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", bucket, tgt.ARN))
|
||||
sendEvent(eventArgs{
|
||||
@@ -1243,6 +1241,13 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
||||
|
||||
objInfo = gr.ObjInfo
|
||||
|
||||
// use latest ObjectInfo to check if previous replication attempt succeeded
|
||||
if objInfo.TargetReplicationStatus(tgt.ARN) == replication.Completed && !ri.ExistingObjResync.Empty() && !ri.ExistingObjResync.mustResyncTarget(tgt.ARN) {
|
||||
rinfo.ReplicationStatus = replication.Completed
|
||||
rinfo.ReplicationResynced = true
|
||||
return
|
||||
}
|
||||
|
||||
size, err := objInfo.GetActualSize()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -1256,7 +1261,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
||||
}
|
||||
|
||||
if tgt.Bucket == "" {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to replicate object %s(%s), bucket is empty", objInfo.Name, objInfo.VersionID))
|
||||
logger.LogIf(ctx, fmt.Errorf("unable to replicate object %s(%s), bucket is empty", objInfo.Name, objInfo.VersionID))
|
||||
sendEvent(eventArgs{
|
||||
EventName: event.ObjectReplicationNotTracked,
|
||||
BucketName: bucket,
|
||||
@@ -1286,7 +1291,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
||||
if rAction == replicateNone {
|
||||
if ri.OpType == replication.ExistingObjectReplicationType &&
|
||||
objInfo.ModTime.Unix() > oi.LastModified.Unix() && objInfo.VersionID == nullVersionID {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to replicate %s/%s (null). Newer version exists on target", bucket, object))
|
||||
logger.LogIf(ctx, fmt.Errorf("unable to replicate %s/%s (null). Newer version exists on target", bucket, object))
|
||||
sendEvent(eventArgs{
|
||||
EventName: event.ObjectReplicationNotTracked,
|
||||
BucketName: bucket,
|
||||
@@ -1328,7 +1333,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
||||
}
|
||||
if _, err = c.CopyObject(ctx, tgt.Bucket, object, tgt.Bucket, object, getCopyObjMetadata(objInfo, tgt.StorageClass), srcOpts, dstOpts); err != nil {
|
||||
rinfo.ReplicationStatus = replication.Failed
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to replicate metadata for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
||||
logger.LogIf(ctx, fmt.Errorf("unable to replicate metadata for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
||||
}
|
||||
} else {
|
||||
var putOpts minio.PutObjectOptions
|
||||
@@ -1365,7 +1370,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
||||
r, objInfo, putOpts); err != nil {
|
||||
if minio.ToErrorResponse(err).Code != "PreconditionFailed" {
|
||||
rinfo.ReplicationStatus = replication.Failed
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
||||
logger.LogIf(ctx, fmt.Errorf("unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
||||
} else {
|
||||
rinfo.ReplicationStatus = replication.Completed
|
||||
}
|
||||
@@ -1374,7 +1379,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
||||
if _, err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts); err != nil {
|
||||
if minio.ToErrorResponse(err).Code != "PreconditionFailed" {
|
||||
rinfo.ReplicationStatus = replication.Failed
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
||||
logger.LogIf(ctx, fmt.Errorf("unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
||||
} else {
|
||||
rinfo.ReplicationStatus = replication.Completed
|
||||
}
|
||||
|
||||
+1
-1
@@ -168,7 +168,7 @@ func sendHealthInfo(ctx context.Context, healthInfo madmin.HealthInfo) error {
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("health_%s.json.gz", UTCNow().Format("20060102150405"))
|
||||
url += "?filename=" + filename
|
||||
url += "?filename=" + filename + "&src=callhome"
|
||||
|
||||
_, err := globalSubnetConfig.Upload(url, filename, createHealthJSONGzip(ctx, healthInfo))
|
||||
return err
|
||||
|
||||
+2
-2
@@ -661,6 +661,7 @@ func handleCommonEnvVars() {
|
||||
}
|
||||
u.Path = "" // remove any path component such as `/`
|
||||
globalMinioEndpoint = u.String()
|
||||
globalMinioEndpointURL = u
|
||||
}
|
||||
|
||||
globalFSOSync, err = config.ParseBool(env.Get(config.EnvFSOSync, config.EnableOff))
|
||||
@@ -783,8 +784,7 @@ func handleCommonEnvVars() {
|
||||
// Initialize KMS global variable after valiadating and loading the configuration.
|
||||
// It depends on KMS env variables and global cli flags.
|
||||
func handleKMSConfig() {
|
||||
switch {
|
||||
case env.IsSet(kms.EnvKMSSecretKey) && env.IsSet(kms.EnvKESEndpoint):
|
||||
if env.IsSet(kms.EnvKMSSecretKey) && env.IsSet(kms.EnvKESEndpoint) {
|
||||
logger.Fatal(errors.New("ambigious KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKMSSecretKey, kms.EnvKESEndpoint))
|
||||
}
|
||||
|
||||
|
||||
+27
-2
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/minio/minio/internal/config/identity/openid"
|
||||
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
||||
xtls "github.com/minio/minio/internal/config/identity/tls"
|
||||
"github.com/minio/minio/internal/config/lambda"
|
||||
"github.com/minio/minio/internal/config/notify"
|
||||
"github.com/minio/minio/internal/config/policy/opa"
|
||||
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||
@@ -74,6 +75,9 @@ func initHelp() {
|
||||
for k, v := range notify.DefaultNotificationKVS {
|
||||
kvs[k] = v
|
||||
}
|
||||
for k, v := range lambda.DefaultLambdaKVS {
|
||||
kvs[k] = v
|
||||
}
|
||||
if globalIsErasure {
|
||||
kvs[config.StorageClassSubSys] = storageclass.DefaultKVS
|
||||
kvs[config.HealSubSys] = heal.DefaultKVS
|
||||
@@ -196,6 +200,11 @@ func initHelp() {
|
||||
Description: "publish bucket notifications to Redis datastores",
|
||||
MultipleTargets: true,
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.LambdaWebhookSubSys,
|
||||
Description: "manage remote lambda functions",
|
||||
MultipleTargets: true,
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.EtcdSubSys,
|
||||
Description: "persist IAM assets externally to etcd",
|
||||
@@ -246,6 +255,7 @@ func initHelp() {
|
||||
config.NotifyRedisSubSys: notify.HelpRedis,
|
||||
config.NotifyWebhookSubSys: notify.HelpWebhook,
|
||||
config.NotifyESSubSys: notify.HelpES,
|
||||
config.LambdaWebhookSubSys: lambda.HelpWebhook,
|
||||
config.SubnetSubSys: subnet.HelpSubnet,
|
||||
config.CallhomeSubSys: callhome.HelpCallhome,
|
||||
}
|
||||
@@ -387,6 +397,13 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if config.LambdaSubSystems.Contains(subSys) {
|
||||
if err := lambda.TestSubSysLambdaTargets(GlobalContext, s, subSys, NewHTTPTransport()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -423,6 +440,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize remote webhook DNS config %w", err))
|
||||
}
|
||||
if err == nil && dnsURL != "" {
|
||||
bootstrapTrace("initialize remote bucket DNS store")
|
||||
globalDNSConfig, err = dns.NewOperatorDNS(dnsURL,
|
||||
dns.Authentication(dnsUser, dnsPass),
|
||||
dns.RootCAs(globalRootCAs))
|
||||
@@ -437,6 +455,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
||||
}
|
||||
|
||||
if etcdCfg.Enabled {
|
||||
bootstrapTrace("initialize etcd store")
|
||||
globalEtcdClient, err = etcd.New(etcdCfg)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize etcd config: %w", err))
|
||||
@@ -495,12 +514,18 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
||||
|
||||
transport := NewHTTPTransport()
|
||||
|
||||
bootstrapTrace("lookup the event notification targets")
|
||||
globalConfigTargetList, err = notify.FetchEnabledTargets(GlobalContext, s, transport)
|
||||
bootstrapTrace("initialize the event notification targets")
|
||||
globalNotifyTargetList, err = notify.FetchEnabledTargets(GlobalContext, s, transport)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
|
||||
}
|
||||
|
||||
bootstrapTrace("initialize the lambda targets")
|
||||
globalLambdaTargetList, err = lambda.FetchEnabledTargets(GlobalContext, s, transport)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize lambda target(s): %w", err))
|
||||
}
|
||||
|
||||
bootstrapTrace("applying the dynamic configuration")
|
||||
// Apply dynamic config values
|
||||
if err := applyDynamicConfig(ctx, objAPI, s); err != nil {
|
||||
|
||||
@@ -33,8 +33,7 @@ const crossDomainXMLEntity = "/crossdomain.xml"
|
||||
func setCrossDomainPolicy(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Look for 'crossdomain.xml' in the incoming request.
|
||||
switch r.URL.Path {
|
||||
case crossDomainXMLEntity:
|
||||
if r.URL.Path == crossDomainXMLEntity {
|
||||
// Write the standard cross domain policy xml.
|
||||
w.Write([]byte(crossDomainXML))
|
||||
// Request completed, no need to serve to other handlers.
|
||||
|
||||
+1
-2
@@ -1273,8 +1273,7 @@ func (i *scannerItem) healReplication(ctx context.Context, o ObjectLayer, oi Obj
|
||||
sizeS.replTargetStats[arn] = tgtSizeS
|
||||
}
|
||||
|
||||
switch oi.ReplicationStatus {
|
||||
case replication.Replica:
|
||||
if oi.ReplicationStatus == replication.Replica {
|
||||
sizeS.replicaSize += oi.Size
|
||||
}
|
||||
}
|
||||
|
||||
+71
-22
@@ -23,6 +23,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -47,6 +48,9 @@ type dataUsageHash string
|
||||
// sizeHistogram is a size histogram.
|
||||
type sizeHistogram [dataUsageBucketLen]uint64
|
||||
|
||||
// versionsHistogram is a histogram of number of versions in an object.
|
||||
type versionsHistogram [dataUsageVersionLen]uint64
|
||||
|
||||
type dataUsageEntry struct {
|
||||
Children dataUsageHashMap `msg:"ch"`
|
||||
// These fields do no include any children.
|
||||
@@ -54,6 +58,7 @@ type dataUsageEntry struct {
|
||||
Objects uint64 `msg:"os"`
|
||||
Versions uint64 `msg:"vs"` // Versions that are not delete markers.
|
||||
ObjSizes sizeHistogram `msg:"szs"`
|
||||
ObjVersions versionsHistogram `msg:"vh"`
|
||||
ReplicationStats *replicationAllStats `msg:"rs,omitempty"`
|
||||
AllTierStats *allTierStats `msg:"ats,omitempty"`
|
||||
Compacted bool `msg:"c"`
|
||||
@@ -291,6 +296,7 @@ func (e *dataUsageEntry) addSizes(summary sizeSummary) {
|
||||
e.Size += summary.totalSize
|
||||
e.Versions += summary.versions
|
||||
e.ObjSizes.add(summary.totalSize)
|
||||
e.ObjVersions.add(summary.versions)
|
||||
|
||||
if e.ReplicationStats == nil {
|
||||
e.ReplicationStats = &replicationAllStats{
|
||||
@@ -351,6 +357,10 @@ func (e *dataUsageEntry) merge(other dataUsageEntry) {
|
||||
e.ObjSizes[i] += v
|
||||
}
|
||||
|
||||
for i, v := range other.ObjVersions[:] {
|
||||
e.ObjVersions[i] += v
|
||||
}
|
||||
|
||||
if other.AllTierStats != nil {
|
||||
if e.AllTierStats == nil {
|
||||
e.AllTierStats = newAllTierStats()
|
||||
@@ -694,7 +704,7 @@ func (d *dataUsageCache) flatten(root dataUsageEntry) dataUsageEntry {
|
||||
func (h *sizeHistogram) add(size int64) {
|
||||
// Fetch the histogram interval corresponding
|
||||
// to the passed object size.
|
||||
for i, interval := range ObjectsHistogramIntervals {
|
||||
for i, interval := range ObjectsHistogramIntervals[:] {
|
||||
if size >= interval.start && size <= interval.end {
|
||||
h[i]++
|
||||
break
|
||||
@@ -711,6 +721,27 @@ func (h *sizeHistogram) toMap() map[string]uint64 {
|
||||
return res
|
||||
}
|
||||
|
||||
// add a version count to the histogram.
|
||||
func (h *versionsHistogram) add(versions uint64) {
|
||||
// Fetch the histogram interval corresponding
|
||||
// to the passed object size.
|
||||
for i, interval := range ObjectsVersionCountIntervals[:] {
|
||||
if versions >= uint64(interval.start) && versions <= uint64(interval.end) {
|
||||
h[i]++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// toMap returns the map to a map[string]uint64.
|
||||
func (h *versionsHistogram) toMap() map[string]uint64 {
|
||||
res := make(map[string]uint64, dataUsageVersionLen)
|
||||
for i, count := range h {
|
||||
res[ObjectsVersionCountIntervals[i].name] = count
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (d *dataUsageCache) tiersUsageInfo(buckets []BucketInfo) *allTierStats {
|
||||
dst := newAllTierStats()
|
||||
for _, bucket := range buckets {
|
||||
@@ -741,10 +772,11 @@ func (d *dataUsageCache) bucketsUsageInfo(buckets []BucketInfo) map[string]Bucke
|
||||
}
|
||||
flat := d.flatten(*e)
|
||||
bui := BucketUsageInfo{
|
||||
Size: uint64(flat.Size),
|
||||
VersionsCount: flat.Versions,
|
||||
ObjectsCount: flat.Objects,
|
||||
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
||||
Size: uint64(flat.Size),
|
||||
VersionsCount: flat.Versions,
|
||||
ObjectsCount: flat.Objects,
|
||||
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
||||
ObjectVersionsHistogram: flat.ObjVersions.toMap(),
|
||||
}
|
||||
if flat.ReplicationStats != nil {
|
||||
bui.ReplicaSize = flat.ReplicationStats.ReplicaSize
|
||||
@@ -847,37 +879,54 @@ type objectIO interface {
|
||||
|
||||
// load the cache content with name from minioMetaBackgroundOpsBucket.
|
||||
// Only backend errors are returned as errors.
|
||||
// The loader is optimistic and has no locking, but tries 5 times before giving up.
|
||||
// If the object is not found or unable to deserialize d is cleared and nil error is returned.
|
||||
func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string) error {
|
||||
// Abandon if more than 5 minutes, so we don't hold up scanner.
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
r, err := store.GetObjectNInfo(ctx, dataUsageBucket, name, nil, http.Header{}, readLock, ObjectOptions{})
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case ObjectNotFound:
|
||||
case BucketNotFound:
|
||||
case InsufficientReadQuorum:
|
||||
case StorageErr:
|
||||
default:
|
||||
return toObjectErr(err, dataUsageBucket, name)
|
||||
// Caches are read+written without locks,
|
||||
retries := 0
|
||||
for retries < 5 {
|
||||
r, err := store.GetObjectNInfo(ctx, dataUsageBucket, name, nil, http.Header{}, noLock, ObjectOptions{NoLock: true})
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case ObjectNotFound, BucketNotFound:
|
||||
case InsufficientReadQuorum, StorageErr:
|
||||
retries++
|
||||
time.Sleep(time.Duration(rand.Int63n(int64(time.Second))))
|
||||
continue
|
||||
default:
|
||||
return toObjectErr(err, dataUsageBucket, name)
|
||||
}
|
||||
*d = dataUsageCache{}
|
||||
return nil
|
||||
}
|
||||
*d = dataUsageCache{}
|
||||
if err := d.deserialize(r); err != nil {
|
||||
r.Close()
|
||||
retries++
|
||||
time.Sleep(time.Duration(rand.Int63n(int64(time.Second))))
|
||||
continue
|
||||
}
|
||||
r.Close()
|
||||
return nil
|
||||
}
|
||||
defer r.Close()
|
||||
if err := d.deserialize(r); err != nil {
|
||||
*d = dataUsageCache{}
|
||||
logger.LogOnceIf(ctx, err, err.Error())
|
||||
}
|
||||
*d = dataUsageCache{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Maximum running concurrent saves on server.
|
||||
var maxConcurrentScannerSaves = make(chan struct{}, 4)
|
||||
|
||||
// save the content of the cache to minioMetaBackgroundOpsBucket with the provided name.
|
||||
// Note that no locking is done when saving.
|
||||
func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string) error {
|
||||
var r io.Reader
|
||||
|
||||
maxConcurrentScannerSaves <- struct{}{}
|
||||
defer func() {
|
||||
<-maxConcurrentScannerSaves
|
||||
}()
|
||||
// If big, do streaming...
|
||||
size := int64(-1)
|
||||
if len(d.Cache) > 10000 {
|
||||
@@ -909,7 +958,7 @@ func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string)
|
||||
dataUsageBucket,
|
||||
name,
|
||||
NewPutObjReader(hr),
|
||||
ObjectOptions{})
|
||||
ObjectOptions{NoLock: true})
|
||||
if isErrBucketNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
+150
-13
@@ -1540,6 +1540,24 @@ func (z *dataUsageEntry) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
case "vh":
|
||||
var zb0003 uint32
|
||||
zb0003, err = dc.ReadArrayHeader()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjVersions")
|
||||
return
|
||||
}
|
||||
if zb0003 != uint32(dataUsageVersionLen) {
|
||||
err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0003}
|
||||
return
|
||||
}
|
||||
for za0002 := range z.ObjVersions {
|
||||
z.ObjVersions[za0002], err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjVersions", za0002)
|
||||
return
|
||||
}
|
||||
}
|
||||
case "rs":
|
||||
if dc.IsNil() {
|
||||
err = dc.ReadNil()
|
||||
@@ -1596,16 +1614,16 @@ func (z *dataUsageEntry) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z *dataUsageEntry) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// omitempty: check for empty values
|
||||
zb0001Len := uint32(8)
|
||||
var zb0001Mask uint8 /* 8 bits */
|
||||
zb0001Len := uint32(9)
|
||||
var zb0001Mask uint16 /* 9 bits */
|
||||
_ = zb0001Mask
|
||||
if z.ReplicationStats == nil {
|
||||
zb0001Len--
|
||||
zb0001Mask |= 0x20
|
||||
zb0001Mask |= 0x40
|
||||
}
|
||||
if z.AllTierStats == nil {
|
||||
zb0001Len--
|
||||
zb0001Mask |= 0x40
|
||||
zb0001Mask |= 0x80
|
||||
}
|
||||
// variable map header, size zb0001Len
|
||||
err = en.Append(0x80 | uint8(zb0001Len))
|
||||
@@ -1672,7 +1690,24 @@ func (z *dataUsageEntry) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (zb0001Mask & 0x20) == 0 { // if not empty
|
||||
// write "vh"
|
||||
err = en.Append(0xa2, 0x76, 0x68)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteArrayHeader(uint32(dataUsageVersionLen))
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjVersions")
|
||||
return
|
||||
}
|
||||
for za0002 := range z.ObjVersions {
|
||||
err = en.WriteUint64(z.ObjVersions[za0002])
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjVersions", za0002)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (zb0001Mask & 0x40) == 0 { // if not empty
|
||||
// write "rs"
|
||||
err = en.Append(0xa2, 0x72, 0x73)
|
||||
if err != nil {
|
||||
@@ -1691,7 +1726,7 @@ func (z *dataUsageEntry) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (zb0001Mask & 0x40) == 0 { // if not empty
|
||||
if (zb0001Mask & 0x80) == 0 { // if not empty
|
||||
// write "ats"
|
||||
err = en.Append(0xa3, 0x61, 0x74, 0x73)
|
||||
if err != nil {
|
||||
@@ -1727,16 +1762,16 @@ func (z *dataUsageEntry) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
func (z *dataUsageEntry) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// omitempty: check for empty values
|
||||
zb0001Len := uint32(8)
|
||||
var zb0001Mask uint8 /* 8 bits */
|
||||
zb0001Len := uint32(9)
|
||||
var zb0001Mask uint16 /* 9 bits */
|
||||
_ = zb0001Mask
|
||||
if z.ReplicationStats == nil {
|
||||
zb0001Len--
|
||||
zb0001Mask |= 0x20
|
||||
zb0001Mask |= 0x40
|
||||
}
|
||||
if z.AllTierStats == nil {
|
||||
zb0001Len--
|
||||
zb0001Mask |= 0x40
|
||||
zb0001Mask |= 0x80
|
||||
}
|
||||
// variable map header, size zb0001Len
|
||||
o = append(o, 0x80|uint8(zb0001Len))
|
||||
@@ -1765,7 +1800,13 @@ func (z *dataUsageEntry) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
for za0001 := range z.ObjSizes {
|
||||
o = msgp.AppendUint64(o, z.ObjSizes[za0001])
|
||||
}
|
||||
if (zb0001Mask & 0x20) == 0 { // if not empty
|
||||
// string "vh"
|
||||
o = append(o, 0xa2, 0x76, 0x68)
|
||||
o = msgp.AppendArrayHeader(o, uint32(dataUsageVersionLen))
|
||||
for za0002 := range z.ObjVersions {
|
||||
o = msgp.AppendUint64(o, z.ObjVersions[za0002])
|
||||
}
|
||||
if (zb0001Mask & 0x40) == 0 { // if not empty
|
||||
// string "rs"
|
||||
o = append(o, 0xa2, 0x72, 0x73)
|
||||
if z.ReplicationStats == nil {
|
||||
@@ -1778,7 +1819,7 @@ func (z *dataUsageEntry) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (zb0001Mask & 0x40) == 0 { // if not empty
|
||||
if (zb0001Mask & 0x80) == 0 { // if not empty
|
||||
// string "ats"
|
||||
o = append(o, 0xa3, 0x61, 0x74, 0x73)
|
||||
if z.AllTierStats == nil {
|
||||
@@ -1857,6 +1898,24 @@ func (z *dataUsageEntry) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
case "vh":
|
||||
var zb0003 uint32
|
||||
zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjVersions")
|
||||
return
|
||||
}
|
||||
if zb0003 != uint32(dataUsageVersionLen) {
|
||||
err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0003}
|
||||
return
|
||||
}
|
||||
for za0002 := range z.ObjVersions {
|
||||
z.ObjVersions[za0002], bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjVersions", za0002)
|
||||
return
|
||||
}
|
||||
}
|
||||
case "rs":
|
||||
if msgp.IsNil(bts) {
|
||||
bts, err = msgp.ReadNilBytes(bts)
|
||||
@@ -1911,7 +1970,7 @@ func (z *dataUsageEntry) 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 *dataUsageEntry) Msgsize() (s int) {
|
||||
s = 1 + 3 + z.Children.Msgsize() + 3 + msgp.Int64Size + 3 + msgp.Uint64Size + 3 + msgp.Uint64Size + 4 + msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size)) + 3
|
||||
s = 1 + 3 + z.Children.Msgsize() + 3 + msgp.Int64Size + 3 + msgp.Uint64Size + 3 + msgp.Uint64Size + 4 + msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size)) + 3 + msgp.ArrayHeaderSize + (dataUsageVersionLen * (msgp.Uint64Size)) + 3
|
||||
if z.ReplicationStats == nil {
|
||||
s += msgp.NilSize
|
||||
} else {
|
||||
@@ -3704,3 +3763,81 @@ func (z tierStats) Msgsize() (s int) {
|
||||
s = 1 + 3 + msgp.Uint64Size + 3 + msgp.IntSize + 3 + msgp.IntSize
|
||||
return
|
||||
}
|
||||
|
||||
// DecodeMsg implements msgp.Decodable
|
||||
func (z *versionsHistogram) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
var zb0001 uint32
|
||||
zb0001, err = dc.ReadArrayHeader()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if zb0001 != uint32(dataUsageVersionLen) {
|
||||
err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0001}
|
||||
return
|
||||
}
|
||||
for za0001 := range z {
|
||||
z[za0001], err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, za0001)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z *versionsHistogram) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = en.WriteArrayHeader(uint32(dataUsageVersionLen))
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
for za0001 := range z {
|
||||
err = en.WriteUint64(z[za0001])
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, za0001)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z *versionsHistogram) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
o = msgp.AppendArrayHeader(o, uint32(dataUsageVersionLen))
|
||||
for za0001 := range z {
|
||||
o = msgp.AppendUint64(o, z[za0001])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalMsg implements msgp.Unmarshaler
|
||||
func (z *versionsHistogram) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
var zb0001 uint32
|
||||
zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if zb0001 != uint32(dataUsageVersionLen) {
|
||||
err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0001}
|
||||
return
|
||||
}
|
||||
for za0001 := range z {
|
||||
z[za0001], bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, za0001)
|
||||
return
|
||||
}
|
||||
}
|
||||
o = bts
|
||||
return
|
||||
}
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z *versionsHistogram) Msgsize() (s int) {
|
||||
s = msgp.ArrayHeaderSize + (dataUsageVersionLen * (msgp.Uint64Size))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1196,3 +1196,116 @@ func BenchmarkDecodetierStats(b *testing.B) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshalversionsHistogram(t *testing.T) {
|
||||
v := versionsHistogram{}
|
||||
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 BenchmarkMarshalMsgversionsHistogram(b *testing.B) {
|
||||
v := versionsHistogram{}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.MarshalMsg(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendMsgversionsHistogram(b *testing.B) {
|
||||
v := versionsHistogram{}
|
||||
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 BenchmarkUnmarshalversionsHistogram(b *testing.B) {
|
||||
v := versionsHistogram{}
|
||||
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 TestEncodeDecodeversionsHistogram(t *testing.T) {
|
||||
v := versionsHistogram{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
|
||||
m := v.Msgsize()
|
||||
if buf.Len() > m {
|
||||
t.Log("WARNING: TestEncodeDecodeversionsHistogram Msgsize() is inaccurate")
|
||||
}
|
||||
|
||||
vn := versionsHistogram{}
|
||||
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 BenchmarkEncodeversionsHistogram(b *testing.B) {
|
||||
v := versionsHistogram{}
|
||||
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 BenchmarkDecodeversionsHistogram(b *testing.B) {
|
||||
v := versionsHistogram{}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,11 +58,12 @@ type BucketUsageInfo struct {
|
||||
// Total number of objects that failed replication
|
||||
ReplicationFailedCountV1 uint64 `json:"objectsFailedReplicationCount"`
|
||||
|
||||
ObjectsCount uint64 `json:"objectsCount"`
|
||||
ObjectSizesHistogram map[string]uint64 `json:"objectsSizesHistogram"`
|
||||
VersionsCount uint64 `json:"versionsCount"`
|
||||
ReplicaSize uint64 `json:"objectReplicaTotalSize"`
|
||||
ReplicationInfo map[string]BucketTargetUsageInfo `json:"objectsReplicationInfo"`
|
||||
ObjectsCount uint64 `json:"objectsCount"`
|
||||
ObjectSizesHistogram map[string]uint64 `json:"objectsSizesHistogram"`
|
||||
ObjectVersionsHistogram map[string]uint64 `json:"objectsVersionsHistogram"`
|
||||
VersionsCount uint64 `json:"versionsCount"`
|
||||
ReplicaSize uint64 `json:"objectReplicaTotalSize"`
|
||||
ReplicationInfo map[string]BucketTargetUsageInfo `json:"objectsReplicationInfo"`
|
||||
}
|
||||
|
||||
// DataUsageInfo represents data usage stats of the underlying Object API
|
||||
|
||||
+15
-4
@@ -25,7 +25,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||
@@ -35,6 +34,7 @@ import (
|
||||
"github.com/minio/minio/internal/hash"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/sync/errgroup"
|
||||
xnet "github.com/minio/pkg/net"
|
||||
"github.com/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
@@ -448,7 +448,10 @@ func (c *cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string,
|
||||
return cachedObjInfo, nil
|
||||
}
|
||||
c.cacheStats.incMiss()
|
||||
return ObjectInfo{}, BackendDown{}
|
||||
if xnet.IsNetworkOrHostDown(err, false) {
|
||||
return ObjectInfo{}, BackendDown{Err: err.Error()}
|
||||
}
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
// Reaching here implies cache miss
|
||||
c.cacheStats.incMiss()
|
||||
@@ -893,8 +896,16 @@ func newServerCacheObjects(ctx context.Context, config cache.Config) (CacheObjec
|
||||
cacheDiskStats[i].UsageSize = info.Used
|
||||
cacheDiskStats[i].TotalCapacity = info.Total
|
||||
cacheDiskStats[i].Dir = dcache.stats.Dir
|
||||
atomic.StoreInt32(&cacheDiskStats[i].UsageState, atomic.LoadInt32(&dcache.stats.UsageState))
|
||||
atomic.StoreUint64(&cacheDiskStats[i].UsagePercent, atomic.LoadUint64(&dcache.stats.UsagePercent))
|
||||
if info.Total != 0 {
|
||||
// UsageState
|
||||
gcTriggerPct := dcache.quotaPct * dcache.highWatermark / 100
|
||||
usedPercent := float64(info.Used) * 100 / float64(info.Total)
|
||||
if usedPercent >= float64(gcTriggerPct) {
|
||||
cacheDiskStats[i].UsageState = 1
|
||||
}
|
||||
// UsagePercent
|
||||
cacheDiskStats[i].UsagePercent = uint64(usedPercent)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cacheDiskStats
|
||||
|
||||
@@ -110,7 +110,7 @@ func kmsKeyIDFromMetadata(metadata map[string]string) string {
|
||||
//
|
||||
// DecryptETags uses a KMS bulk decryption API, if available, which
|
||||
// is more efficient than decrypting ETags sequentually.
|
||||
func DecryptETags(ctx context.Context, KMS kms.KMS, objects []ObjectInfo) error {
|
||||
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 (
|
||||
metadata = make([]map[string]string, 0, BatchSize)
|
||||
@@ -170,7 +170,7 @@ func DecryptETags(ctx context.Context, KMS kms.KMS, objects []ObjectInfo) error
|
||||
// For all SSE-S3 single-part objects we have to
|
||||
// fetch their decryption keys. We do this using
|
||||
// a Bulk-Decryption API call, if available.
|
||||
keys, err := crypto.S3.UnsealObjectKeys(ctx, KMS, metadata, buckets, names)
|
||||
keys, err := crypto.S3.UnsealObjectKeys(ctx, k, metadata, buckets, names)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -62,13 +62,14 @@ func TestNewEndpoint(t *testing.T) {
|
||||
err = endpoint.UpdateIsLocal()
|
||||
}
|
||||
|
||||
if test.expectedErr == nil {
|
||||
switch {
|
||||
case test.expectedErr == nil:
|
||||
if err != nil {
|
||||
t.Errorf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
case err == nil:
|
||||
t.Errorf("error: expected = %v, got = <nil>", test.expectedErr)
|
||||
} else if test.expectedErr.Error() != err.Error() {
|
||||
case test.expectedErr.Error() != err.Error():
|
||||
t.Errorf("error: expected = %v, got = %v", test.expectedErr, err)
|
||||
}
|
||||
|
||||
@@ -115,13 +116,14 @@ func TestNewEndpoints(t *testing.T) {
|
||||
|
||||
for _, testCase := range testCases {
|
||||
_, err := NewEndpoints(testCase.args...)
|
||||
if testCase.expectedErr == nil {
|
||||
switch {
|
||||
case testCase.expectedErr == nil:
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
case err == nil:
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
} else if testCase.expectedErr.Error() != err.Error() {
|
||||
case testCase.expectedErr.Error() != err.Error():
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
+35
-7
@@ -505,8 +505,34 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
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)
|
||||
logger.LogIf(ctx, err)
|
||||
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
|
||||
bucket, object, versionID), err
|
||||
}
|
||||
|
||||
latestDisks := shuffleDisks(availableDisks, 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)",
|
||||
latestMeta.Erasure.Distribution, outDatedDisks, bucket, object, versionID)
|
||||
logger.LogIf(ctx, err)
|
||||
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
|
||||
bucket, object, versionID), err
|
||||
}
|
||||
|
||||
outDatedDisks = shuffleDisks(outDatedDisks, latestMeta.Erasure.Distribution)
|
||||
|
||||
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(partsMetadata) {
|
||||
err := fmt.Errorf("unexpected file distribution (%v) from metadata entries (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
|
||||
latestMeta.Erasure.Distribution, len(partsMetadata), bucket, object, versionID)
|
||||
logger.LogIf(ctx, err)
|
||||
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
|
||||
bucket, object, versionID), err
|
||||
}
|
||||
|
||||
partsMetadata = shufflePartsMetadata(partsMetadata, latestMeta.Erasure.Distribution)
|
||||
|
||||
copyPartsMetadata := make([]FileInfo, len(partsMetadata))
|
||||
@@ -910,13 +936,14 @@ func isObjectDirDangling(errs []error) (ok bool) {
|
||||
var foundNotEmpty int
|
||||
var otherFound int
|
||||
for _, readErr := range errs {
|
||||
if readErr == nil {
|
||||
switch {
|
||||
case readErr == nil:
|
||||
found++
|
||||
} else if readErr == errFileNotFound || readErr == errVolumeNotFound {
|
||||
case readErr == errFileNotFound || readErr == errVolumeNotFound:
|
||||
notFound++
|
||||
} else if readErr == errVolumeNotEmpty {
|
||||
case readErr == errVolumeNotEmpty:
|
||||
foundNotEmpty++
|
||||
} else {
|
||||
default:
|
||||
otherFound++
|
||||
}
|
||||
}
|
||||
@@ -938,11 +965,12 @@ func isObjectDangling(metaArr []FileInfo, errs []error, dataErrs []error) (valid
|
||||
diskNotFoundCount int
|
||||
)
|
||||
for _, readErr := range cerrs {
|
||||
if errors.Is(readErr, errFileNotFound) || errors.Is(readErr, errFileVersionNotFound) {
|
||||
switch {
|
||||
case errors.Is(readErr, errFileNotFound) || errors.Is(readErr, errFileVersionNotFound):
|
||||
notFoundCount++
|
||||
} else if errors.Is(readErr, errFileCorrupt) {
|
||||
case errors.Is(readErr, errFileCorrupt):
|
||||
corruptedCount++
|
||||
} else if errors.Is(readErr, errDiskNotFound) {
|
||||
case errors.Is(readErr, errDiskNotFound):
|
||||
diskNotFoundCount++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1621,10 +1621,10 @@ func TestHealLastDataShard(t *testing.T) {
|
||||
}
|
||||
|
||||
firstGr, err := obj.GetObjectNInfo(ctx, bucket, object, nil, nil, noLock, ObjectOptions{})
|
||||
defer firstGr.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer firstGr.Close()
|
||||
|
||||
firstHealedH := sha256.New()
|
||||
_, err = io.Copy(firstHealedH, firstGr)
|
||||
@@ -1651,10 +1651,10 @@ func TestHealLastDataShard(t *testing.T) {
|
||||
}
|
||||
|
||||
secondGr, err := obj.GetObjectNInfo(ctx, bucket, object, nil, nil, noLock, ObjectOptions{})
|
||||
defer secondGr.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer secondGr.Close()
|
||||
|
||||
secondHealedH := sha256.New()
|
||||
_, err = io.Copy(secondHealedH, secondGr)
|
||||
|
||||
+56
-10
@@ -658,6 +658,13 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
||||
return fi, nil, nil, err
|
||||
}
|
||||
|
||||
if !fi.Deleted && len(fi.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)",
|
||||
fi.Erasure.Distribution, onlineDisks, bucket, object, opts.VersionID)
|
||||
logger.LogIf(ctx, err)
|
||||
return fi, nil, nil, toObjectErr(err, bucket, object, opts.VersionID)
|
||||
}
|
||||
|
||||
filterOnlineDisksInplace(fi, metaArr, onlineDisks)
|
||||
|
||||
// if one of the disk is offline, return right here no need
|
||||
@@ -972,17 +979,14 @@ func healObjectVersionsDisparity(bucket string, entry metaCacheEntry) error {
|
||||
|
||||
fivs, err := entry.fileInfoVersions(bucket)
|
||||
if err != nil {
|
||||
go healObject(bucket, entry.name, "", madmin.HealNormalScan)
|
||||
go healObject(bucket, entry.name, "", madmin.HealDeepScan)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(fivs.Versions) > 2 {
|
||||
return fmt.Errorf("bucket(%s)/object(%s) object with %d versions needs healing, allowing lazy healing via scanner instead here for versions greater than 2",
|
||||
bucket, entry.name, len(fivs.Versions))
|
||||
}
|
||||
|
||||
for _, version := range fivs.Versions {
|
||||
go healObject(bucket, entry.name, version.VersionID, madmin.HealNormalScan)
|
||||
if len(fivs.Versions) <= 2 {
|
||||
for _, version := range fivs.Versions {
|
||||
go healObject(bucket, entry.name, version.VersionID, madmin.HealNormalScan)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1566,8 +1570,7 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
||||
objInfo = ObjectInfo{VersionID: opts.VersionID} // version id needed in Delete API response.
|
||||
goi, _, gerr := er.getObjectInfoAndQuorum(ctx, bucket, object, opts)
|
||||
if gerr != nil && goi.Name == "" {
|
||||
switch gerr.(type) {
|
||||
case InsufficientReadQuorum:
|
||||
if _, ok := gerr.(InsufficientReadQuorum); ok {
|
||||
return objInfo, InsufficientWriteQuorum{}
|
||||
}
|
||||
// For delete marker replication, versionID being replicated will not exist on disk
|
||||
@@ -2093,3 +2096,46 @@ func (er erasureObjects) restoreTransitionedObject(ctx context.Context, bucket s
|
||||
})
|
||||
return setRestoreHeaderFn(oi, err)
|
||||
}
|
||||
|
||||
// DecomTieredObject - moves tiered object to another pool during decommissioning.
|
||||
func (er erasureObjects) DecomTieredObject(ctx context.Context, bucket, object string, fi FileInfo, opts ObjectOptions) error {
|
||||
if opts.UserDefined == nil {
|
||||
opts.UserDefined = make(map[string]string)
|
||||
}
|
||||
// overlay Erasure info for this set of disks
|
||||
storageDisks := er.getDisks()
|
||||
// Get parity and data drive count based on storage class metadata
|
||||
parityDrives := globalStorageClass.GetParityForSC(opts.UserDefined[xhttp.AmzStorageClass])
|
||||
if parityDrives < 0 {
|
||||
parityDrives = er.defaultParityCount
|
||||
}
|
||||
dataDrives := len(storageDisks) - parityDrives
|
||||
|
||||
// we now know the number of blocks this object needs for data and parity.
|
||||
// writeQuorum is dataBlocks + 1
|
||||
writeQuorum := dataDrives
|
||||
if dataDrives == parityDrives {
|
||||
writeQuorum++
|
||||
}
|
||||
|
||||
// Initialize parts metadata
|
||||
partsMetadata := make([]FileInfo, len(storageDisks))
|
||||
|
||||
fi2 := newFileInfo(pathJoin(bucket, object), dataDrives, parityDrives)
|
||||
fi.Erasure = fi2.Erasure
|
||||
// Initialize erasure metadata.
|
||||
for index := range partsMetadata {
|
||||
partsMetadata[index] = fi
|
||||
partsMetadata[index].Erasure.Index = index + 1
|
||||
}
|
||||
|
||||
// Order disks according to erasure distribution
|
||||
var onlineDisks []StorageAPI
|
||||
onlineDisks, partsMetadata = shuffleDisksAndPartsMetadata(storageDisks, partsMetadata, fi)
|
||||
|
||||
if _, err := writeUniqueFileInfo(ctx, onlineDisks, bucket, object, partsMetadata, writeQuorum); err != nil {
|
||||
return toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import (
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/hash"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/console"
|
||||
@@ -735,18 +734,18 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
}
|
||||
versioned := vc != nil && vc.Versioned(object)
|
||||
objInfo := fi.ToObjectInfo(bucket, object, versioned)
|
||||
|
||||
evt := evalActionFromLifecycle(ctx, *lc, lr, objInfo)
|
||||
switch evt.Action {
|
||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
||||
globalExpiryState.enqueueByDays(objInfo, false, evt.Action == lifecycle.DeleteVersionAction)
|
||||
// Skip this entry.
|
||||
return true
|
||||
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||
globalExpiryState.enqueueByDays(objInfo, true, evt.Action == lifecycle.DeleteRestoredVersionAction)
|
||||
// Skip this entry.
|
||||
switch {
|
||||
case evt.Action.DeleteRestored(): // if restored copy has expired,delete it synchronously
|
||||
applyExpiryOnTransitionedObject(ctx, z, objInfo, evt.Action.DeleteRestored())
|
||||
return false
|
||||
case evt.Action.Delete():
|
||||
globalExpiryState.enqueueByDays(objInfo, evt.Action.DeleteRestored(), evt.Action.DeleteVersioned())
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
decommissionEntry := func(entry metaCacheEntry) {
|
||||
@@ -770,12 +769,6 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
|
||||
var decommissionedCount int
|
||||
for _, version := range fivs.Versions {
|
||||
// TODO: Skip transitioned objects for now.
|
||||
if version.IsRemote() {
|
||||
logger.LogIf(ctx, fmt.Errorf("found %s/%s transitioned object, transitioned object won't be decommissioned", bi.Name, version.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply lifecycle rules on the objects that are expired.
|
||||
if filterLifecycle(bi.Name, version.Name, version) {
|
||||
logger.LogIf(ctx, fmt.Errorf("found %s/%s (%s) expired object based on ILM rules, skipping and scheduled for deletion", bi.Name, version.Name, version.VersionID))
|
||||
@@ -824,6 +817,22 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
var failure, ignore bool
|
||||
// gr.Close() is ensured by decommissionObject().
|
||||
for try := 0; try < 3; try++ {
|
||||
if version.IsRemote() {
|
||||
stopFn := globalDecommissionMetrics.log(decomMetricDecommissionObject, idx, bi.Name, version.Name, version.VersionID)
|
||||
if err := z.DecomTieredObject(ctx, bi.Name, version.Name, version, ObjectOptions{
|
||||
VersionID: version.VersionID,
|
||||
MTime: version.ModTime,
|
||||
UserDefined: version.Metadata,
|
||||
}); err != nil {
|
||||
stopFn(err)
|
||||
failure = true
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
stopFn(nil)
|
||||
failure = false
|
||||
break
|
||||
}
|
||||
gr, err := set.GetObjectNInfo(ctx,
|
||||
bi.Name,
|
||||
encodeDirObject(version.Name),
|
||||
@@ -1271,17 +1280,6 @@ func (z *erasureServerPools) StartDecommission(ctx context.Context, indices ...i
|
||||
z.HealBucket(ctx, bucket.Name, madmin.HealOpts{})
|
||||
}
|
||||
|
||||
// TODO: Support decommissioning transition tiers.
|
||||
for _, bucket := range decomBuckets {
|
||||
if lc, err := globalLifecycleSys.Get(bucket.Name); err == nil {
|
||||
if lc.HasTransition() {
|
||||
return decomError{
|
||||
Err: fmt.Sprintf("Bucket is part of transitioned tier %s: decommission is not allowed in Tier'd setups", bucket.Name),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create .minio.sys/conifg, .minio.sys/buckets paths if missing,
|
||||
// this code is present to avoid any missing meta buckets on other
|
||||
// pools.
|
||||
|
||||
@@ -32,7 +32,6 @@ import (
|
||||
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/hash"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/env"
|
||||
@@ -463,17 +462,13 @@ func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string,
|
||||
}
|
||||
versioned := vc != nil && vc.Versioned(object)
|
||||
objInfo := fi.ToObjectInfo(bucket, object, versioned)
|
||||
event := evalActionFromLifecycle(ctx, *lc, lr, objInfo)
|
||||
switch action := event.Action; action {
|
||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
||||
globalExpiryState.enqueueByDays(objInfo, false, action == lifecycle.DeleteVersionAction)
|
||||
// Skip this entry.
|
||||
return true
|
||||
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||
globalExpiryState.enqueueByDays(objInfo, true, action == lifecycle.DeleteRestoredVersionAction)
|
||||
// Skip this entry.
|
||||
|
||||
evt := evalActionFromLifecycle(ctx, *lc, lr, objInfo)
|
||||
if evt.Action.Delete() {
|
||||
globalExpiryState.enqueueByDays(objInfo, evt.Action.DeleteRestored(), evt.Action.DeleteVersioned())
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
+57
-26
@@ -37,7 +37,6 @@ import (
|
||||
"github.com/minio/minio-go/v7/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/config/storageclass"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/sync/errgroup"
|
||||
@@ -157,7 +156,9 @@ func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServ
|
||||
break
|
||||
}
|
||||
|
||||
globalLocalDrivesMu.Lock()
|
||||
globalLocalDrives = localDrives
|
||||
defer globalLocalDrivesMu.Unlock()
|
||||
return z, nil
|
||||
}
|
||||
|
||||
@@ -421,6 +422,7 @@ func (z *erasureServerPools) getPoolInfoExistingWithOpts(ctx context.Context, bu
|
||||
return mtime1.After(mtime2)
|
||||
})
|
||||
|
||||
defPool := PoolObjInfo{Index: -1}
|
||||
for _, pinfo := range poolObjInfos {
|
||||
// skip all objects from suspended pools if asked by the
|
||||
// caller.
|
||||
@@ -435,14 +437,13 @@ func (z *erasureServerPools) getPoolInfoExistingWithOpts(ctx context.Context, bu
|
||||
if pinfo.Err != nil && !isErrObjectNotFound(pinfo.Err) {
|
||||
return pinfo, pinfo.Err
|
||||
}
|
||||
|
||||
defPool = pinfo
|
||||
if isErrObjectNotFound(pinfo.Err) {
|
||||
// No object exists or its a delete marker,
|
||||
// check objInfo to confirm.
|
||||
if pinfo.ObjInfo.DeleteMarker && pinfo.ObjInfo.Name != "" {
|
||||
return pinfo, nil
|
||||
}
|
||||
|
||||
// objInfo is not valid, truly the object doesn't
|
||||
// exist proceed to next pool.
|
||||
continue
|
||||
@@ -450,7 +451,12 @@ func (z *erasureServerPools) getPoolInfoExistingWithOpts(ctx context.Context, bu
|
||||
|
||||
return pinfo, nil
|
||||
}
|
||||
|
||||
if opts.ReplicationRequest && opts.DeleteMarker && defPool.Index >= 0 {
|
||||
// If the request is a delete marker replication request, return a default pool
|
||||
// in cases where the object does not exist.
|
||||
// This is to ensure that the delete marker is replicated to the destination.
|
||||
return defPool, nil
|
||||
}
|
||||
return PoolObjInfo{}, toObjectErr(errFileNotFound, bucket, object)
|
||||
}
|
||||
|
||||
@@ -689,9 +695,11 @@ func (z *erasureServerPools) NSScanner(ctx context.Context, updates chan<- DataU
|
||||
case updateCloser <- ch:
|
||||
<-ch
|
||||
case <-ctx.Done():
|
||||
mu.Lock()
|
||||
if firstErr == nil {
|
||||
firstErr = ctx.Err()
|
||||
}
|
||||
defer mu.Unlock()
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
@@ -985,8 +993,7 @@ func (z *erasureServerPools) DeleteObject(ctx context.Context, bucket string, ob
|
||||
gopts.NoLock = true
|
||||
pinfo, err := z.getPoolInfoExistingWithOpts(ctx, bucket, object, gopts)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case InsufficientReadQuorum:
|
||||
if _, ok := err.(InsufficientReadQuorum); ok {
|
||||
return objInfo, InsufficientWriteQuorum{}
|
||||
}
|
||||
return objInfo, err
|
||||
@@ -1299,11 +1306,12 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
|
||||
if err == nil {
|
||||
if opts.Lifecycle != nil {
|
||||
evt := evalActionFromLifecycle(ctx, *opts.Lifecycle, opts.Retention, objInfo)
|
||||
switch evt.Action {
|
||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
||||
fallthrough
|
||||
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||
return loi, nil
|
||||
if evt.Action.Delete() {
|
||||
globalExpiryState.enqueueByDays(objInfo, evt.Action.DeleteRestored(), evt.Action.DeleteVersioned())
|
||||
if !evt.Action.DeleteRestored() {
|
||||
// Skip entry if ILM action was DeleteVersionAction or DeleteAction
|
||||
return loi, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
loi.Objects = append(loi.Objects, objInfo)
|
||||
@@ -1452,8 +1460,7 @@ func (z *erasureServerPools) PutObjectPart(ctx context.Context, bucket, object,
|
||||
if err == nil {
|
||||
return pi, nil
|
||||
}
|
||||
switch err.(type) {
|
||||
case InvalidUploadID:
|
||||
if _, ok := err.(InvalidUploadID); ok {
|
||||
// Look for information on the next pool
|
||||
continue
|
||||
}
|
||||
@@ -1484,8 +1491,7 @@ func (z *erasureServerPools) GetMultipartInfo(ctx context.Context, bucket, objec
|
||||
if err == nil {
|
||||
return mi, nil
|
||||
}
|
||||
switch err.(type) {
|
||||
case InvalidUploadID:
|
||||
if _, ok := err.(InvalidUploadID); ok {
|
||||
// upload id not found, continue to the next pool.
|
||||
continue
|
||||
}
|
||||
@@ -1516,8 +1522,7 @@ func (z *erasureServerPools) ListObjectParts(ctx context.Context, bucket, object
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
switch err.(type) {
|
||||
case InvalidUploadID:
|
||||
if _, ok := err.(InvalidUploadID); ok {
|
||||
continue
|
||||
}
|
||||
return ListPartsInfo{}, err
|
||||
@@ -1547,8 +1552,7 @@ func (z *erasureServerPools) AbortMultipartUpload(ctx context.Context, bucket, o
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
switch err.(type) {
|
||||
case InvalidUploadID:
|
||||
if _, ok := err.(InvalidUploadID); ok {
|
||||
// upload id not found move to next pool
|
||||
continue
|
||||
}
|
||||
@@ -1579,8 +1583,7 @@ func (z *erasureServerPools) CompleteMultipartUpload(ctx context.Context, bucket
|
||||
if err == nil {
|
||||
return objInfo, nil
|
||||
}
|
||||
switch err.(type) {
|
||||
case InvalidUploadID:
|
||||
if _, ok := err.(InvalidUploadID); ok {
|
||||
// upload id not found move to next pool
|
||||
continue
|
||||
}
|
||||
@@ -1637,8 +1640,7 @@ func (z *erasureServerPools) DeleteBucket(ctx context.Context, bucket string, op
|
||||
err := z.s3Peer.DeleteBucket(ctx, bucket, opts)
|
||||
if err == nil || errors.Is(err, errVolumeNotFound) {
|
||||
// If site replication is configured, hold on to deleted bucket state until sites sync
|
||||
switch opts.SRDeleteOp {
|
||||
case MarkDelete:
|
||||
if opts.SRDeleteOp == MarkDelete {
|
||||
z.s3Peer.MakeBucket(context.Background(), pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, bucket), MakeBucketOptions{})
|
||||
}
|
||||
}
|
||||
@@ -1742,8 +1744,7 @@ func (z *erasureServerPools) HealBucket(ctx context.Context, bucket string, opts
|
||||
for _, pool := range z.serverPools {
|
||||
result, err := pool.HealBucket(ctx, bucket, opts)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case BucketNotFound:
|
||||
if _, ok := err.(BucketNotFound); ok {
|
||||
continue
|
||||
}
|
||||
return result, err
|
||||
@@ -2076,9 +2077,14 @@ func (z *erasureServerPools) getPoolAndSet(id string) (poolIdx, setIdx, diskIdx
|
||||
return -1, -1, -1, fmt.Errorf("DriveID(%s) %w", id, errDiskNotFound)
|
||||
}
|
||||
|
||||
const (
|
||||
vmware = "VMWare"
|
||||
)
|
||||
|
||||
// HealthOptions takes input options to return sepcific information
|
||||
type HealthOptions struct {
|
||||
Maintenance bool
|
||||
DeploymentType string
|
||||
}
|
||||
|
||||
// HealthResult returns the current state of the system, also
|
||||
@@ -2164,7 +2170,8 @@ func (z *erasureServerPools) Health(ctx context.Context, opts HealthOptions) Hea
|
||||
}
|
||||
|
||||
var aggHealStateResult madmin.BgHealState
|
||||
if opts.Maintenance {
|
||||
// Check if disks are healing on in-case of VMware vsphere deployments.
|
||||
if opts.Maintenance && opts.DeploymentType == vmware {
|
||||
// check if local disks are being healed, if they are being healed
|
||||
// we need to tell healthy status as 'false' so that this server
|
||||
// is not taken down for maintenance
|
||||
@@ -2347,3 +2354,27 @@ func (z *erasureServerPools) CheckAbandonedParts(ctx context.Context, bucket, ob
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecomTieredObject - moves tiered object to another pool during decommissioning.
|
||||
func (z *erasureServerPools) DecomTieredObject(ctx context.Context, bucket, object string, fi FileInfo, opts ObjectOptions) error {
|
||||
object = encodeDirObject(object)
|
||||
if z.SinglePool() {
|
||||
return fmt.Errorf("error decommissioning %s/%s", bucket, object)
|
||||
}
|
||||
if !opts.NoLock {
|
||||
ns := z.NewNSLock(bucket, object)
|
||||
lkctx, err := ns.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer ns.Unlock(lkctx)
|
||||
opts.NoLock = true
|
||||
}
|
||||
idx, err := z.getPoolIdxNoLock(ctx, bucket, object, fi.Size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return z.serverPools[idx].DecomTieredObject(ctx, bucket, object, fi, opts)
|
||||
}
|
||||
|
||||
@@ -1215,6 +1215,12 @@ func (s *erasureSets) PutObjectMetadata(ctx context.Context, bucket, object stri
|
||||
return er.PutObjectMetadata(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
// DecomTieredObject - moves tiered object to another pool during decommissioning.
|
||||
func (s *erasureSets) DecomTieredObject(ctx context.Context, bucket, object string, fi FileInfo, opts ObjectOptions) error {
|
||||
er := s.getHashedSet(object)
|
||||
return er.DecomTieredObject(ctx, bucket, object, fi, opts)
|
||||
}
|
||||
|
||||
// PutObjectTags - replace or add tags to an existing object
|
||||
func (s *erasureSets) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) (ObjectInfo, error) {
|
||||
er := s.getHashedSet(object)
|
||||
|
||||
+11
-9
@@ -412,9 +412,6 @@ func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, wa
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Return without saving.
|
||||
return
|
||||
case <-t.C:
|
||||
if cache.Info.LastUpdate.Equal(lastSave) {
|
||||
continue
|
||||
@@ -452,6 +449,7 @@ func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, wa
|
||||
// Start one scanner per disk
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(disks))
|
||||
|
||||
for i := range disks {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
@@ -488,10 +486,13 @@ func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, wa
|
||||
go func(name string) {
|
||||
defer wg.Done()
|
||||
for update := range updates {
|
||||
bucketResults <- dataUsageEntryInfo{
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case bucketResults <- dataUsageEntryInfo{
|
||||
Name: name,
|
||||
Parent: dataUsageRoot,
|
||||
Entry: update,
|
||||
}:
|
||||
}
|
||||
}
|
||||
}(cache.Info.Name)
|
||||
@@ -518,15 +519,16 @@ func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, wa
|
||||
if r := cache.root(); r != nil {
|
||||
root = cache.flatten(*r)
|
||||
}
|
||||
t := time.Now()
|
||||
bucketResults <- dataUsageEntryInfo{
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case bucketResults <- dataUsageEntryInfo{
|
||||
Name: cache.Info.Name,
|
||||
Parent: dataUsageRoot,
|
||||
Entry: root,
|
||||
}:
|
||||
}
|
||||
// We want to avoid synchronizing up all writes in case
|
||||
// the results are piled up.
|
||||
time.Sleep(time.Duration(float64(time.Since(t)) * rand.Float64()))
|
||||
|
||||
// Save cache
|
||||
logger.LogIf(ctx, cache.save(ctx, er, cacheName))
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func (evnot *EventNotifier) InitBucketTargets(ctx context.Context, objAPI Object
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
if err := evnot.targetList.Add(globalConfigTargetList.Targets()...); err != nil {
|
||||
if err := evnot.targetList.Add(globalNotifyTargetList.Targets()...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -349,7 +350,7 @@ func hasBadHost(host string) error {
|
||||
// Check if the incoming path has bad path components,
|
||||
// such as ".." and "."
|
||||
func hasBadPathComponent(path string) bool {
|
||||
path = strings.TrimSpace(path)
|
||||
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:
|
||||
|
||||
+9
-4
@@ -49,6 +49,7 @@ import (
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
etcd "go.etcd.io/etcd/client/v3"
|
||||
|
||||
levent "github.com/minio/minio/internal/config/lambda/event"
|
||||
"github.com/minio/minio/internal/event"
|
||||
"github.com/minio/minio/internal/pubsub"
|
||||
"github.com/minio/pkg/certs"
|
||||
@@ -174,7 +175,8 @@ var (
|
||||
globalMinioConsoleHost = ""
|
||||
|
||||
// Holds the possible host endpoint.
|
||||
globalMinioEndpoint = ""
|
||||
globalMinioEndpoint = ""
|
||||
globalMinioEndpointURL *xnet.URL
|
||||
|
||||
// globalConfigSys server config system.
|
||||
globalConfigSys *ConfigSys
|
||||
@@ -182,7 +184,8 @@ var (
|
||||
globalNotificationSys *NotificationSys
|
||||
|
||||
globalEventNotifier *EventNotifier
|
||||
globalConfigTargetList *event.TargetList
|
||||
globalNotifyTargetList *event.TargetList
|
||||
globalLambdaTargetList *levent.TargetList
|
||||
|
||||
globalBucketMetadataSys *BucketMetadataSys
|
||||
globalBucketMonitor *bandwidth.Monitor
|
||||
@@ -350,8 +353,10 @@ var (
|
||||
globalServiceFreezeCnt int32
|
||||
globalServiceFreezeMu sync.Mutex // Updates.
|
||||
|
||||
// List of local drives to this node, this is only set during server startup.
|
||||
globalLocalDrives []StorageAPI
|
||||
// List of local drives to this node, this is only set during server startup,
|
||||
// and should never be mutated. Hold globalLocalDrivesMu to access.
|
||||
globalLocalDrives []StorageAPI
|
||||
globalLocalDrivesMu sync.RWMutex
|
||||
|
||||
// Is MINIO_CI_CD set?
|
||||
globalIsCICD bool
|
||||
|
||||
@@ -47,7 +47,10 @@ func ClusterCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(ctx, globalAPIConfig.getClusterDeadline())
|
||||
defer cancel()
|
||||
|
||||
opts := HealthOptions{Maintenance: r.Form.Get("maintenance") == "true"}
|
||||
opts := HealthOptions{
|
||||
Maintenance: r.Form.Get("maintenance") == "true",
|
||||
DeploymentType: r.Form.Get("deployment-type"),
|
||||
}
|
||||
result := objLayer.Health(ctx, opts)
|
||||
if result.WriteQuorum > 0 {
|
||||
w.Header().Set(xhttp.MinIOWriteQuorum, strconv.Itoa(result.WriteQuorum))
|
||||
|
||||
@@ -248,6 +248,13 @@ func (ies *IAMEtcdStore) addUser(ctx context.Context, user string, userType IAMU
|
||||
if u.Credentials.SessionToken != "" {
|
||||
jwtClaims, err := extractJWTClaims(u)
|
||||
if err != nil {
|
||||
if u.Credentials.IsTemp() {
|
||||
// We should delete such that the client can re-request
|
||||
// for the expiring credentials.
|
||||
deleteKeyEtcd(ctx, ies.client, getUserIdentityPath(user, userType))
|
||||
deleteKeyEtcd(ctx, ies.client, getMappedPolicyPath(user, userType, false))
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
u.Credentials.Claims = jwtClaims.Map()
|
||||
|
||||
+11
-1
@@ -187,7 +187,15 @@ func (iamOS *IAMObjectStore) loadUser(ctx context.Context, user string, userType
|
||||
if u.Credentials.SessionToken != "" {
|
||||
jwtClaims, err := extractJWTClaims(u)
|
||||
if err != nil {
|
||||
if u.Credentials.IsTemp() {
|
||||
// We should delete such that the client can re-request
|
||||
// for the expiring credentials.
|
||||
iamOS.deleteIAMConfig(ctx, getUserIdentityPath(user, userType))
|
||||
iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(user, userType, false))
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
|
||||
}
|
||||
u.Credentials.Claims = jwtClaims.Map()
|
||||
}
|
||||
@@ -349,7 +357,9 @@ func (iamOS *IAMObjectStore) listAllIAMConfigItems(ctx context.Context) (map[str
|
||||
// Assumes cache is locked by caller.
|
||||
func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iamCache) error {
|
||||
bootstrapTrace("loading all IAM items")
|
||||
|
||||
if iamOS.objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
listedConfigItems, err := iamOS.listAllIAMConfigItems(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+1
-2
@@ -937,8 +937,7 @@ func (store *IAMStoreSys) PolicyDBUpdate(ctx context.Context, name string, isGro
|
||||
}
|
||||
|
||||
if g.Status == statusDisabled {
|
||||
// TODO: return an error?
|
||||
return updatedAt, nil, nil
|
||||
return updatedAt, nil, errGroupDisabled
|
||||
}
|
||||
}
|
||||
mp = cache.iamGroupPolicyMap[name]
|
||||
|
||||
@@ -419,7 +419,7 @@ func Test_localLocker_RUnlock(t *testing.T) {
|
||||
}
|
||||
start = time.Now()
|
||||
for _, lock := range toUnLock {
|
||||
ok, err := l.RUnlock(nil, lock)
|
||||
ok, err := l.RUnlock(context.TODO(), lock)
|
||||
if err != nil || !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -816,15 +816,16 @@ func (m *metaCacheEntriesSorted) merge(other metaCacheEntriesSorted, limit int)
|
||||
a := m.entries()
|
||||
b := other.entries()
|
||||
for len(a) > 0 && len(b) > 0 {
|
||||
if a[0].name == b[0].name && bytes.Equal(a[0].metadata, b[0].metadata) {
|
||||
switch {
|
||||
case a[0].name == b[0].name && bytes.Equal(a[0].metadata, b[0].metadata):
|
||||
// Same, discard one.
|
||||
merged = append(merged, a[0])
|
||||
a = a[1:]
|
||||
b = b[1:]
|
||||
} else if a[0].name < b[0].name {
|
||||
case a[0].name < b[0].name:
|
||||
merged = append(merged, a[0])
|
||||
a = a[1:]
|
||||
} else {
|
||||
default:
|
||||
merged = append(merged, b[0])
|
||||
b = b[1:]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
@@ -341,7 +340,6 @@ func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions,
|
||||
func applyBucketActions(ctx context.Context, o listPathOptions, in <-chan metaCacheEntry, out chan<- metaCacheEntry) {
|
||||
defer close(out)
|
||||
|
||||
vcfg, _ := globalBucketVersioningSys.Get(o.Bucket)
|
||||
for {
|
||||
var obj metaCacheEntry
|
||||
var ok bool
|
||||
@@ -354,37 +352,61 @@ func applyBucketActions(ctx context.Context, o listPathOptions, in <-chan metaCa
|
||||
}
|
||||
}
|
||||
|
||||
fi, err := obj.fileInfo(o.Bucket)
|
||||
var skip bool
|
||||
|
||||
versioned := o.Versioning != nil && o.Versioning.Versioned(obj.name)
|
||||
|
||||
// skip latest object from listing only for regular
|
||||
// listObjects calls, versioned based listing cannot
|
||||
// filter out between versions 'obj' cannot be truncated
|
||||
// in such a manner, so look for skipping an object only
|
||||
// for regular ListObjects() call only.
|
||||
if !o.Versioned {
|
||||
fi, err := obj.fileInfo(o.Bucket)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
objInfo := fi.ToObjectInfo(o.Bucket, obj.name, versioned)
|
||||
if o.Lifecycle != nil {
|
||||
act := evalActionFromLifecycle(ctx, *o.Lifecycle, o.Retention, objInfo).Action
|
||||
skip = act.Delete()
|
||||
if act.DeleteRestored() {
|
||||
// do not skip DeleteRestored* actions
|
||||
skip = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip entry only if needed via ILM, skipping is never true for versioned listing.
|
||||
if !skip {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case out <- obj:
|
||||
}
|
||||
}
|
||||
|
||||
fiv, err := obj.fileInfoVersions(o.Bucket)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
versioned := vcfg != nil && vcfg.Versioned(obj.name)
|
||||
// Expire all versions if needed, if not attempt to queue for replication.
|
||||
for _, version := range fiv.Versions {
|
||||
objInfo := version.ToObjectInfo(o.Bucket, obj.name, versioned)
|
||||
|
||||
objInfo := fi.ToObjectInfo(o.Bucket, obj.name, versioned)
|
||||
if o.Lifecycle != nil {
|
||||
evt := evalActionFromLifecycle(ctx, *o.Lifecycle, o.Retention, objInfo)
|
||||
switch evt.Action {
|
||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
||||
globalExpiryState.enqueueByDays(objInfo, false, evt.Action == lifecycle.DeleteVersionAction)
|
||||
// Skip this entry.
|
||||
continue
|
||||
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||
globalExpiryState.enqueueByDays(objInfo, true, evt.Action == lifecycle.DeleteRestoredVersionAction)
|
||||
// Skip this entry.
|
||||
continue
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case out <- obj:
|
||||
if fiv, err := obj.fileInfoVersions(o.Bucket); err == nil {
|
||||
for _, version := range fiv.Versions {
|
||||
objInfo := version.ToObjectInfo(o.Bucket, obj.name, versioned)
|
||||
queueReplicationHeal(ctx, o.Bucket, objInfo, o.Replication)
|
||||
if o.Lifecycle != nil {
|
||||
evt := evalActionFromLifecycle(ctx, *o.Lifecycle, o.Retention, objInfo)
|
||||
if evt.Action.Delete() {
|
||||
globalExpiryState.enqueueByDays(objInfo, evt.Action.DeleteRestored(), evt.Action.DeleteVersioned())
|
||||
if !evt.Action.DeleteRestored() {
|
||||
continue
|
||||
} // queue version for replication upon expired restored copies if needed.
|
||||
}
|
||||
}
|
||||
|
||||
queueReplicationHeal(ctx, o.Bucket, objInfo, o.Replication)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-12
@@ -34,6 +34,7 @@ import (
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/bucket/object/lock"
|
||||
"github.com/minio/minio/internal/bucket/versioning"
|
||||
"github.com/minio/minio/internal/color"
|
||||
"github.com/minio/minio/internal/hash"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
@@ -95,6 +96,10 @@ type listPathOptions struct {
|
||||
// Versioned is this a ListObjectVersions call.
|
||||
Versioned bool
|
||||
|
||||
// Versioning config is used for if the path
|
||||
// has versioning enabled.
|
||||
Versioning *versioning.Versioning
|
||||
|
||||
// Lifecycle performs filtering based on lifecycle.
|
||||
// This will filter out objects if the most recent version should be deleted by lifecycle.
|
||||
// Is not transferred across request calls.
|
||||
@@ -119,12 +124,14 @@ func init() {
|
||||
|
||||
func (o *listPathOptions) setBucketMeta(ctx context.Context) {
|
||||
lc, _ := globalLifecycleSys.Get(o.Bucket)
|
||||
vc, _ := globalBucketVersioningSys.Get(o.Bucket)
|
||||
|
||||
// Check if bucket is object locked.
|
||||
rcfg, _ := globalBucketObjectLockSys.Get(o.Bucket)
|
||||
replCfg, _, _ := globalBucketMetadataSys.GetReplicationConfig(ctx, o.Bucket)
|
||||
tgts, _ := globalBucketTargetSys.ListBucketTargets(ctx, o.Bucket)
|
||||
o.Lifecycle = lc
|
||||
o.Versioning = vc
|
||||
o.Replication = replicationConfig{
|
||||
Config: replCfg,
|
||||
remotes: tgts,
|
||||
@@ -390,6 +397,11 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
retries := 0
|
||||
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
|
||||
|
||||
const (
|
||||
retryDelay = 50 * time.Millisecond
|
||||
retryDelay250 = 250 * time.Millisecond
|
||||
)
|
||||
|
||||
for {
|
||||
if contextCanceled(ctx) {
|
||||
return entries, ctx.Err()
|
||||
@@ -404,7 +416,6 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
retries = 1
|
||||
}
|
||||
|
||||
const retryDelay = 250 * time.Millisecond
|
||||
// All operations are performed without locks, so we must be careful and allow for failures.
|
||||
// Read metadata associated with the object from a disk.
|
||||
if retries > 0 {
|
||||
@@ -418,7 +429,7 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
_, err := disk.ReadVersion(ctx, minioMetaBucket,
|
||||
o.objectPath(0), "", false)
|
||||
if err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
time.Sleep(retryDelay250)
|
||||
retries++
|
||||
continue
|
||||
}
|
||||
@@ -433,11 +444,19 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
switch toObjectErr(err, minioMetaBucket, o.objectPath(0)).(type) {
|
||||
case ObjectNotFound:
|
||||
retries++
|
||||
time.Sleep(retryDelay)
|
||||
if retries == 1 {
|
||||
time.Sleep(retryDelay)
|
||||
} else {
|
||||
time.Sleep(retryDelay250)
|
||||
}
|
||||
continue
|
||||
case InsufficientReadQuorum:
|
||||
retries++
|
||||
time.Sleep(retryDelay)
|
||||
if retries == 1 {
|
||||
time.Sleep(retryDelay)
|
||||
} else {
|
||||
time.Sleep(retryDelay250)
|
||||
}
|
||||
continue
|
||||
default:
|
||||
return entries, fmt.Errorf("reading first part metadata: %w", err)
|
||||
@@ -456,7 +475,7 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
retries = -1
|
||||
}
|
||||
retries++
|
||||
time.Sleep(retryDelay)
|
||||
time.Sleep(retryDelay250)
|
||||
continue
|
||||
case errors.Is(err, io.EOF):
|
||||
return entries, io.EOF
|
||||
@@ -490,7 +509,7 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
_, err := disk.ReadVersion(ctx, minioMetaBucket,
|
||||
o.objectPath(partN), "", false)
|
||||
if err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
time.Sleep(retryDelay250)
|
||||
retries++
|
||||
continue
|
||||
}
|
||||
@@ -501,7 +520,7 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
// Load partN metadata...
|
||||
fi, metaArr, onlineDisks, err = er.getObjectFileInfo(ctx, minioMetaBucket, o.objectPath(partN), ObjectOptions{}, true)
|
||||
if err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
time.Sleep(retryDelay250)
|
||||
retries++
|
||||
continue
|
||||
}
|
||||
@@ -523,9 +542,9 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
}()
|
||||
|
||||
tmp := newMetacacheReader(pr)
|
||||
defer tmp.Close()
|
||||
e, err := tmp.filter(o)
|
||||
pr.CloseWithError(err)
|
||||
tmp.Close()
|
||||
entries.o = append(entries.o, e.o...)
|
||||
if o.Limit > 0 && entries.len() > o.Limit {
|
||||
entries.truncate(o.Limit)
|
||||
@@ -539,11 +558,11 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
switch toObjectErr(err, minioMetaBucket, o.objectPath(partN)).(type) {
|
||||
case ObjectNotFound:
|
||||
retries++
|
||||
time.Sleep(retryDelay)
|
||||
time.Sleep(retryDelay250)
|
||||
continue
|
||||
case InsufficientReadQuorum:
|
||||
retries++
|
||||
time.Sleep(retryDelay)
|
||||
time.Sleep(retryDelay250)
|
||||
continue
|
||||
default:
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -870,8 +889,7 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
|
||||
var fdMu sync.Mutex
|
||||
fds := opts.fallbackDisks
|
||||
fallback := func(err error) StorageAPI {
|
||||
switch err.(type) {
|
||||
case StorageErr:
|
||||
if _, ok := err.(StorageErr); ok {
|
||||
// Attempt to grab a fallback disk
|
||||
fdMu.Lock()
|
||||
defer fdMu.Unlock()
|
||||
|
||||
@@ -562,8 +562,7 @@ func (r *metacacheReader) readAll(ctx context.Context, dst chan<- metaCacheEntry
|
||||
}
|
||||
for {
|
||||
if more, err := r.mr.ReadBool(); !more {
|
||||
switch err {
|
||||
case io.EOF:
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
r.err = err
|
||||
|
||||
@@ -323,13 +323,6 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
|
||||
}
|
||||
case isSysErrNotDir(err):
|
||||
// skip
|
||||
default:
|
||||
// It is totally possible that xl.meta was overwritten
|
||||
// while being concurrently listed at the same time in
|
||||
// such scenarios the 'xl.meta' might get truncated
|
||||
if !IsErrIgnored(err, io.EOF, io.ErrUnexpectedEOF) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+63
-10
@@ -131,6 +131,7 @@ const (
|
||||
iamSubsystem MetricSubsystem = "iam"
|
||||
kmsSubsystem MetricSubsystem = "kms"
|
||||
notifySubsystem MetricSubsystem = "notify"
|
||||
lambdaSubsystem MetricSubsystem = "lambda"
|
||||
auditSubsystem MetricSubsystem = "audit"
|
||||
)
|
||||
|
||||
@@ -182,8 +183,9 @@ const (
|
||||
usageInfo MetricName = "usage_info"
|
||||
versionInfo MetricName = "version_info"
|
||||
|
||||
sizeDistribution = "size_distribution"
|
||||
ttfbDistribution = "ttfb_seconds_distribution"
|
||||
sizeDistribution = "size_distribution"
|
||||
versionDistribution = "version_distribution"
|
||||
ttfbDistribution = "ttfb_seconds_distribution"
|
||||
|
||||
lastActivityTime = "last_activity_nano_seconds"
|
||||
startTime = "starttime_seconds"
|
||||
@@ -564,6 +566,16 @@ func getBucketObjectDistributionMD() MetricDescription {
|
||||
}
|
||||
}
|
||||
|
||||
func getBucketObjectVersionsMD() MetricDescription {
|
||||
return MetricDescription{
|
||||
Namespace: bucketMetricNamespace,
|
||||
Subsystem: objectsSubsystem,
|
||||
Name: versionDistribution,
|
||||
Help: "Distribution of object sizes in the bucket, includes label for the bucket name",
|
||||
Type: histogramMetric,
|
||||
}
|
||||
}
|
||||
|
||||
func getInternodeFailedRequests() MetricDescription {
|
||||
return MetricDescription{
|
||||
Namespace: interNodeMetricNamespace,
|
||||
@@ -1656,8 +1668,8 @@ func getNotificationMetrics() *MetricsGroup {
|
||||
cacheInterval: 10 * time.Second,
|
||||
}
|
||||
mg.RegisterRead(func(ctx context.Context) []Metric {
|
||||
stats := globalConfigTargetList.Stats()
|
||||
metrics := make([]Metric, 0, 1+len(stats.TargetStats))
|
||||
nstats := globalNotifyTargetList.Stats()
|
||||
metrics := make([]Metric, 0, 1+len(nstats.TargetStats))
|
||||
metrics = append(metrics, Metric{
|
||||
Description: MetricDescription{
|
||||
Namespace: minioNamespace,
|
||||
@@ -1666,9 +1678,9 @@ func getNotificationMetrics() *MetricsGroup {
|
||||
Help: "Number of concurrent async Send calls active to all targets",
|
||||
Type: gaugeMetric,
|
||||
},
|
||||
Value: float64(stats.CurrentSendCalls),
|
||||
Value: float64(nstats.CurrentSendCalls),
|
||||
})
|
||||
for _, st := range stats.TargetStats {
|
||||
for _, st := range nstats.TargetStats {
|
||||
metrics = append(metrics, Metric{
|
||||
Description: MetricDescription{
|
||||
Namespace: minioNamespace,
|
||||
@@ -1681,6 +1693,43 @@ func getNotificationMetrics() *MetricsGroup {
|
||||
Value: float64(st.CurrentQueue),
|
||||
})
|
||||
}
|
||||
|
||||
lstats := globalLambdaTargetList.Stats()
|
||||
for _, st := range lstats.TargetStats {
|
||||
metrics = append(metrics, Metric{
|
||||
Description: MetricDescription{
|
||||
Namespace: minioNamespace,
|
||||
Subsystem: lambdaSubsystem,
|
||||
Name: "active_requests",
|
||||
Help: "Number of in progress requests",
|
||||
},
|
||||
VariableLabels: map[string]string{"target_id": st.ID.ID, "target_name": st.ID.Name},
|
||||
Value: float64(st.ActiveRequests),
|
||||
})
|
||||
metrics = append(metrics, Metric{
|
||||
Description: MetricDescription{
|
||||
Namespace: minioNamespace,
|
||||
Subsystem: lambdaSubsystem,
|
||||
Name: "total_requests",
|
||||
Help: "Total number of requests sent since start",
|
||||
Type: counterMetric,
|
||||
},
|
||||
VariableLabels: map[string]string{"target_id": st.ID.ID, "target_name": st.ID.Name},
|
||||
Value: float64(st.TotalRequests),
|
||||
})
|
||||
metrics = append(metrics, Metric{
|
||||
Description: MetricDescription{
|
||||
Namespace: minioNamespace,
|
||||
Subsystem: lambdaSubsystem,
|
||||
Name: "failed_requests",
|
||||
Help: "Total number of requests that failed to send since start",
|
||||
Type: counterMetric,
|
||||
},
|
||||
VariableLabels: map[string]string{"target_id": st.ID.ID, "target_name": st.ID.Name},
|
||||
Value: float64(st.FailedRequests),
|
||||
})
|
||||
}
|
||||
|
||||
// Audit and system:
|
||||
audit := logger.CurrentStats()
|
||||
for id, st := range audit {
|
||||
@@ -1961,6 +2010,12 @@ func getBucketUsageMetrics() *MetricsGroup {
|
||||
HistogramBucketLabel: "range",
|
||||
VariableLabels: map[string]string{"bucket": bucket},
|
||||
})
|
||||
metrics = append(metrics, Metric{
|
||||
Description: getBucketObjectVersionsMD(),
|
||||
Histogram: usage.ObjectVersionsHistogram,
|
||||
HistogramBucketLabel: "range",
|
||||
VariableLabels: map[string]string{"bucket": bucket},
|
||||
})
|
||||
}
|
||||
return
|
||||
})
|
||||
@@ -2322,8 +2377,7 @@ func (c *minioClusterCollector) Collect(out chan<- prometheus.Metric) {
|
||||
continue
|
||||
}
|
||||
metricType := prometheus.GaugeValue
|
||||
switch metric.Description.Type {
|
||||
case counterMetric:
|
||||
if metric.Description.Type == counterMetric {
|
||||
metricType = prometheus.CounterValue
|
||||
}
|
||||
toPost := prometheus.MustNewConstMetric(
|
||||
@@ -2431,8 +2485,7 @@ func (c *minioNodeCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
}
|
||||
|
||||
metricType := prometheus.GaugeValue
|
||||
switch metric.Description.Type {
|
||||
case counterMetric:
|
||||
if metric.Description.Type == counterMetric {
|
||||
metricType = prometheus.CounterValue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
|
||||
@@ -441,6 +441,18 @@ func bucketUsageMetricsPrometheus(ch chan<- prometheus.Metric) {
|
||||
k,
|
||||
)
|
||||
}
|
||||
for k, v := range usageInfo.ObjectVersionsHistogram {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
prometheus.NewDesc(
|
||||
prometheus.BuildFQName(bucketNamespace, "objects", "histogram"),
|
||||
"Total number of versions of objects in a bucket",
|
||||
[]string{"bucket", "object_versions"}, nil),
|
||||
prometheus.GaugeValue,
|
||||
float64(v),
|
||||
bucket,
|
||||
k,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-10
@@ -170,9 +170,8 @@ func (di *distLockInstance) GetLock(ctx context.Context, timeout *dynamicTimeout
|
||||
RetryInterval: timeout.RetryInterval(),
|
||||
}) {
|
||||
timeout.LogFailure()
|
||||
cancel()
|
||||
switch err := newCtx.Err(); err {
|
||||
case context.Canceled:
|
||||
defer cancel()
|
||||
if err := newCtx.Err(); err == context.Canceled {
|
||||
return LockContext{ctx: ctx, cancel: func() {}}, err
|
||||
}
|
||||
return LockContext{ctx: ctx, cancel: func() {}}, OperationTimedOut{}
|
||||
@@ -200,9 +199,8 @@ func (di *distLockInstance) GetRLock(ctx context.Context, timeout *dynamicTimeou
|
||||
RetryInterval: timeout.RetryInterval(),
|
||||
}) {
|
||||
timeout.LogFailure()
|
||||
cancel()
|
||||
switch err := newCtx.Err(); err {
|
||||
case context.Canceled:
|
||||
defer cancel()
|
||||
if err := newCtx.Err(); err == context.Canceled {
|
||||
return LockContext{ctx: ctx, cancel: func() {}}, err
|
||||
}
|
||||
return LockContext{ctx: ctx, cancel: func() {}}, OperationTimedOut{}
|
||||
@@ -257,8 +255,7 @@ func (li *localLockInstance) GetLock(ctx context.Context, timeout *dynamicTimeou
|
||||
li.ns.unlock(li.volume, li.paths[si], readLock)
|
||||
}
|
||||
}
|
||||
switch err := ctx.Err(); err {
|
||||
case context.Canceled:
|
||||
if err := ctx.Err(); err == context.Canceled {
|
||||
return LockContext{}, err
|
||||
}
|
||||
return LockContext{}, OperationTimedOut{}
|
||||
@@ -294,8 +291,7 @@ func (li *localLockInstance) GetRLock(ctx context.Context, timeout *dynamicTimeo
|
||||
li.ns.unlock(li.volume, li.paths[si], readLock)
|
||||
}
|
||||
}
|
||||
switch err := ctx.Err(); err {
|
||||
case context.Canceled:
|
||||
if err := ctx.Err(); err == context.Canceled {
|
||||
return LockContext{}, err
|
||||
}
|
||||
return LockContext{}, OperationTimedOut{}
|
||||
|
||||
+12
-9
@@ -138,13 +138,14 @@ func TestGetHostIP(t *testing.T) {
|
||||
|
||||
for _, testCase := range testCases {
|
||||
ipList, err := getHostIP(testCase.host)
|
||||
if testCase.expectedErr == nil {
|
||||
switch {
|
||||
case testCase.expectedErr == nil:
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
case err == nil:
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
} else if testCase.expectedErr.Error() != err.Error() {
|
||||
case testCase.expectedErr.Error() != err.Error():
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
@@ -221,13 +222,14 @@ func TestCheckPortAvailability(t *testing.T) {
|
||||
}
|
||||
|
||||
err := checkPortAvailability(testCase.host, testCase.port)
|
||||
if testCase.expectedErr == nil {
|
||||
switch {
|
||||
case testCase.expectedErr == nil:
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
case err == nil:
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
} else if testCase.expectedErr.Error() != err.Error() {
|
||||
case testCase.expectedErr.Error() != err.Error():
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
}
|
||||
@@ -252,13 +254,14 @@ func TestCheckLocalServerAddr(t *testing.T) {
|
||||
testCase := testCase
|
||||
t.Run("", func(t *testing.T) {
|
||||
err := CheckLocalServerAddr(testCase.serverAddr)
|
||||
if testCase.expectedErr == nil {
|
||||
switch {
|
||||
case testCase.expectedErr == nil:
|
||||
if err != nil {
|
||||
t.Errorf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
case err == nil:
|
||||
t.Errorf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
} else if testCase.expectedErr.Error() != err.Error() {
|
||||
case testCase.expectedErr.Error() != err.Error():
|
||||
t.Errorf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
})
|
||||
|
||||
+26
-17
@@ -56,14 +56,23 @@ type NotificationPeerErr struct {
|
||||
//
|
||||
// A zero NotificationGroup is valid and does not cancel on error.
|
||||
type NotificationGroup struct {
|
||||
wg sync.WaitGroup
|
||||
errs []NotificationPeerErr
|
||||
wg sync.WaitGroup
|
||||
errs []NotificationPeerErr
|
||||
retryCount int
|
||||
}
|
||||
|
||||
// WithNPeers returns a new NotificationGroup with length of errs slice upto nerrs,
|
||||
// upon Wait() errors are returned collected from all tasks.
|
||||
func WithNPeers(nerrs int) *NotificationGroup {
|
||||
return &NotificationGroup{errs: make([]NotificationPeerErr, nerrs)}
|
||||
return &NotificationGroup{errs: make([]NotificationPeerErr, nerrs), retryCount: 3}
|
||||
}
|
||||
|
||||
// WithRetries sets the retry count for all function calls from the Go method.
|
||||
func (g *NotificationGroup) WithRetries(retryCount int) *NotificationGroup {
|
||||
if g != nil {
|
||||
g.retryCount = retryCount
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
// Wait blocks until all function calls from the Go method have returned, then
|
||||
@@ -85,17 +94,17 @@ func (g *NotificationGroup) Go(ctx context.Context, f func() error, index int, a
|
||||
g.errs[index] = NotificationPeerErr{
|
||||
Host: addr,
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
for i := 0; i < g.retryCount; i++ {
|
||||
if err := f(); err != nil {
|
||||
g.errs[index].Err = err
|
||||
// Last iteration log the error.
|
||||
if i == 2 {
|
||||
if i == g.retryCount-1 {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
// Wait for one second and no need wait after last attempt.
|
||||
if i < 2 {
|
||||
if i < g.retryCount-1 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
continue
|
||||
@@ -107,7 +116,7 @@ func (g *NotificationGroup) Go(ctx context.Context, f func() error, index int, a
|
||||
|
||||
// DeletePolicy - deletes policy across all peers.
|
||||
func (sys *NotificationSys) DeletePolicy(policyName string) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
@@ -122,7 +131,7 @@ func (sys *NotificationSys) DeletePolicy(policyName string) []NotificationPeerEr
|
||||
|
||||
// LoadPolicy - reloads a specific modified policy across all peers
|
||||
func (sys *NotificationSys) LoadPolicy(policyName string) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
@@ -137,7 +146,7 @@ func (sys *NotificationSys) LoadPolicy(policyName string) []NotificationPeerErr
|
||||
|
||||
// LoadPolicyMapping - reloads a policy mapping across all peers
|
||||
func (sys *NotificationSys) LoadPolicyMapping(userOrGroup string, userType IAMUserType, isGroup bool) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
@@ -152,7 +161,7 @@ func (sys *NotificationSys) LoadPolicyMapping(userOrGroup string, userType IAMUs
|
||||
|
||||
// DeleteUser - deletes a specific user across all peers
|
||||
func (sys *NotificationSys) DeleteUser(accessKey string) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
@@ -167,7 +176,7 @@ func (sys *NotificationSys) DeleteUser(accessKey string) []NotificationPeerErr {
|
||||
|
||||
// LoadUser - reloads a specific user across all peers
|
||||
func (sys *NotificationSys) LoadUser(accessKey string, temp bool) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
@@ -182,7 +191,7 @@ func (sys *NotificationSys) LoadUser(accessKey string, temp bool) []Notification
|
||||
|
||||
// LoadGroup - loads a specific group on all peers.
|
||||
func (sys *NotificationSys) LoadGroup(group string) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
@@ -195,7 +204,7 @@ func (sys *NotificationSys) LoadGroup(group string) []NotificationPeerErr {
|
||||
|
||||
// DeleteServiceAccount - deletes a specific service account across all peers
|
||||
func (sys *NotificationSys) DeleteServiceAccount(accessKey string) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
@@ -210,7 +219,7 @@ func (sys *NotificationSys) DeleteServiceAccount(accessKey string) []Notificatio
|
||||
|
||||
// LoadServiceAccount - reloads a specific service account across all peers
|
||||
func (sys *NotificationSys) LoadServiceAccount(accessKey string) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
@@ -468,7 +477,7 @@ func (sys *NotificationSys) DeleteBucketMetadata(ctx context.Context, bucketName
|
||||
|
||||
// GetClusterAllBucketStats - returns bucket stats for all buckets from all remote peers.
|
||||
func (sys *NotificationSys) GetClusterAllBucketStats(ctx context.Context) []BucketStatsMap {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
|
||||
replicationStats := make([]BucketStatsMap, len(sys.peerClients))
|
||||
for index, client := range sys.peerClients {
|
||||
index := index
|
||||
@@ -509,7 +518,7 @@ func (sys *NotificationSys) GetClusterAllBucketStats(ctx context.Context) []Buck
|
||||
|
||||
// GetClusterBucketStats - calls GetClusterBucketStats call on all peers for a cluster statistics view.
|
||||
func (sys *NotificationSys) GetClusterBucketStats(ctx context.Context, bucketName string) []BucketStats {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
ng := WithNPeers(len(sys.peerClients)).WithRetries(1)
|
||||
bucketStats := make([]BucketStats, len(sys.peerClients))
|
||||
for index, client := range sys.peerClients {
|
||||
index := index
|
||||
@@ -1368,7 +1377,7 @@ func (sys *NotificationSys) GetLastDayTierStats(ctx context.Context) DailyAllTie
|
||||
merged := globalTransitionState.getDailyAllTierStats()
|
||||
for i, stat := range lastDayStats {
|
||||
if errs[i] != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("failed to fetch last day tier stats: %w", errs[i]))
|
||||
logger.LogOnceIf(ctx, fmt.Errorf("failed to fetch last day tier stats: %w", errs[i]), sys.peerClients[i].host.String())
|
||||
continue
|
||||
}
|
||||
merged.merge(stat)
|
||||
|
||||
@@ -54,12 +54,13 @@ type objectHistogramInterval struct {
|
||||
|
||||
const (
|
||||
// dataUsageBucketLen must be length of ObjectsHistogramIntervals
|
||||
dataUsageBucketLen = 7
|
||||
dataUsageBucketLen = 7
|
||||
dataUsageVersionLen = 7
|
||||
)
|
||||
|
||||
// ObjectsHistogramIntervals is the list of all intervals
|
||||
// of object sizes to be included in objects histogram.
|
||||
var ObjectsHistogramIntervals = []objectHistogramInterval{
|
||||
var ObjectsHistogramIntervals = [dataUsageBucketLen]objectHistogramInterval{
|
||||
{"LESS_THAN_1024_B", 0, humanize.KiByte - 1},
|
||||
{"BETWEEN_1024_B_AND_1_MB", humanize.KiByte, humanize.MiByte - 1},
|
||||
{"BETWEEN_1_MB_AND_10_MB", humanize.MiByte, humanize.MiByte*10 - 1},
|
||||
@@ -69,6 +70,18 @@ var ObjectsHistogramIntervals = []objectHistogramInterval{
|
||||
{"GREATER_THAN_512_MB", humanize.MiByte * 512, math.MaxInt64},
|
||||
}
|
||||
|
||||
// ObjectsVersionCountIntervals is the list of all intervals
|
||||
// of object version count to be included in objects histogram.
|
||||
var ObjectsVersionCountIntervals = [dataUsageVersionLen]objectHistogramInterval{
|
||||
{"UNVERSIONED", 0, 0},
|
||||
{"SINGLE_VERSION", 1, 1},
|
||||
{"BETWEEN_2_AND_10", 2, 9},
|
||||
{"BETWEEN_10_AND_100", 10, 99},
|
||||
{"BETWEEN_100_AND_1000", 100, 999},
|
||||
{"BETWEEN_1000_AND_10000", 1000, 9999},
|
||||
{"GREATER_THAN_10000", 10000, math.MaxInt64},
|
||||
}
|
||||
|
||||
// BucketInfo - represents bucket metadata.
|
||||
type BucketInfo struct {
|
||||
// Name of the bucket.
|
||||
|
||||
@@ -254,6 +254,7 @@ type ObjectLayer interface {
|
||||
|
||||
// Metadata operations
|
||||
PutObjectMetadata(context.Context, string, string, ObjectOptions) (ObjectInfo, error)
|
||||
DecomTieredObject(context.Context, string, string, FileInfo, ObjectOptions) error
|
||||
|
||||
// ObjectTagging operations
|
||||
PutObjectTags(context.Context, string, string, string, ObjectOptions) (ObjectInfo, error)
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -32,6 +33,9 @@ import (
|
||||
|
||||
// Wrapper for calling NewMultipartUpload tests for both Erasure multiple disks and single node setup.
|
||||
func TestObjectNewMultipartUpload(t *testing.T) {
|
||||
if runtime.GOOS == globalWindowsOSName {
|
||||
t.Skip()
|
||||
}
|
||||
ExecObjectLayerTest(t, testObjectNewMultipartUpload)
|
||||
}
|
||||
|
||||
@@ -110,9 +114,18 @@ func testObjectAbortMultipartUpload(obj ObjectLayer, instanceType string, t Test
|
||||
{"--", object, uploadID, BucketNotFound{}},
|
||||
{"foo", object, uploadID, BucketNotFound{}},
|
||||
{bucket, object, "foo-foo", InvalidUploadID{}},
|
||||
{bucket, "\\", uploadID, InvalidUploadID{}},
|
||||
{bucket, object, uploadID, nil},
|
||||
}
|
||||
|
||||
if runtime.GOOS != globalWindowsOSName {
|
||||
abortTestCases = append(abortTestCases, struct {
|
||||
bucketName string
|
||||
objName string
|
||||
uploadID string
|
||||
expectedErrType error
|
||||
}{bucket, "\\", uploadID, InvalidUploadID{}})
|
||||
}
|
||||
|
||||
// Iterating over creatPartCases to generate multipart chunks.
|
||||
for i, testCase := range abortTestCases {
|
||||
err = obj.AbortMultipartUpload(context.Background(), testCase.bucketName, testCase.objName, testCase.uploadID, opts)
|
||||
|
||||
@@ -174,10 +174,7 @@ func IsValidObjectPrefix(object string) bool {
|
||||
// work with file systems, we will reject here
|
||||
// to return object name invalid rather than
|
||||
// a cryptic error from the file system.
|
||||
if strings.ContainsRune(object, 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return !strings.ContainsRune(object, 0)
|
||||
}
|
||||
|
||||
// checkObjectNameForLengthAndSlash -check for the validity of object name length and prefis as slash
|
||||
@@ -199,7 +196,7 @@ func checkObjectNameForLengthAndSlash(bucket, object string) error {
|
||||
if runtime.GOOS == globalWindowsOSName {
|
||||
// Explicitly disallowed characters on windows.
|
||||
// Avoids most problematic names.
|
||||
if strings.ContainsAny(object, `:*?"|<>`) {
|
||||
if strings.ContainsAny(object, `\:*?"|<>`) {
|
||||
return ObjectNameInvalid{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
|
||||
@@ -19,19 +19,75 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/klauspost/compress/s2"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config/compress"
|
||||
"github.com/minio/minio/internal/crypto"
|
||||
"github.com/minio/pkg/trie"
|
||||
)
|
||||
|
||||
// Wrapper
|
||||
func TestPathTraversalExploit(t *testing.T) {
|
||||
if runtime.GOOS != globalWindowsOSName {
|
||||
t.Skip()
|
||||
}
|
||||
defer DetectTestLeak(t)()
|
||||
ExecExtendedObjectLayerAPITest(t, testPathTraversalExploit, []string{"PutObject"})
|
||||
}
|
||||
|
||||
// testPathTraversal exploit test, exploits path traversal on windows
|
||||
// with following object names "\\../.minio.sys/config/iam/${username}/identity.json"
|
||||
// #16852
|
||||
func testPathTraversalExploit(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T,
|
||||
) {
|
||||
if err := newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
||||
t.Fatalf("Initializing config.json failed")
|
||||
}
|
||||
|
||||
objectName := `\../.minio.sys/config/hello.txt`
|
||||
|
||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||
rec := httptest.NewRecorder()
|
||||
// construct HTTP request for Get Object end point.
|
||||
req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, objectName),
|
||||
int64(5), bytes.NewReader([]byte("hello")), credentials.AccessKey, credentials.SecretKey, map[string]string{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create HTTP request for Put Object: <ERROR> %v", err)
|
||||
}
|
||||
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler.
|
||||
// Call the ServeHTTP to execute the handler.
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
|
||||
ctx, cancel := context.WithCancel(GlobalContext)
|
||||
defer cancel()
|
||||
|
||||
// Now check if we actually wrote to backend (regardless of the response
|
||||
// returned by the server).
|
||||
z := obj.(*erasureServerPools)
|
||||
xl := z.serverPools[0].sets[0]
|
||||
erasureDisks := xl.getDisks()
|
||||
parts, errs := readAllFileInfo(ctx, erasureDisks, bucketName, objectName, "", false)
|
||||
for i := range parts {
|
||||
if errs[i] == nil {
|
||||
if parts[i].Name == objectName {
|
||||
t.Errorf("path traversal allowed to allow writing to minioMetaBucket: %s", instanceType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests validate bucket name.
|
||||
func TestIsValidBucketName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
|
||||
+21
-30
@@ -472,22 +472,18 @@ func (api objectAPIHandlers) getObjectHandler(ctx context.Context, objectAPI Obj
|
||||
}
|
||||
|
||||
if !proxy.Proxy { // apply lifecycle rules only for local requests
|
||||
// Automatically remove the object/version is an expiry lifecycle rule can be applied
|
||||
// Automatically remove the object/version if an expiry lifecycle rule can be applied
|
||||
if lc, err := globalLifecycleSys.Get(bucket); err == nil {
|
||||
rcfg, _ := globalBucketObjectLockSys.Get(bucket)
|
||||
evt := evalActionFromLifecycle(ctx, *lc, rcfg, objInfo)
|
||||
var success bool
|
||||
switch evt.Action {
|
||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
||||
success = applyExpiryRule(objInfo, false, evt.Action == lifecycle.DeleteVersionAction)
|
||||
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||
// Restored object delete would be still allowed to proceed as success
|
||||
// since transition behavior is slightly different.
|
||||
applyExpiryRule(objInfo, true, evt.Action == lifecycle.DeleteRestoredVersionAction)
|
||||
}
|
||||
if success {
|
||||
writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrNoSuchKey))
|
||||
return
|
||||
act := evalActionFromLifecycle(ctx, *lc, rcfg, objInfo).Action
|
||||
if act.Delete() {
|
||||
// apply whatever the expiry rule is.
|
||||
applyExpiryRule(objInfo, act.DeleteRestored(), act.DeleteVersioned())
|
||||
if !act.DeleteRestored() {
|
||||
// If the ILM action is not on restored object return error.
|
||||
writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrNoSuchKey))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -729,22 +725,18 @@ func (api objectAPIHandlers) headObjectHandler(ctx context.Context, objectAPI Ob
|
||||
}
|
||||
|
||||
if !proxy.Proxy { // apply lifecycle rules only locally not for proxied requests
|
||||
// Automatically remove the object/version is an expiry lifecycle rule can be applied
|
||||
// Automatically remove the object/version if an expiry lifecycle rule can be applied
|
||||
if lc, err := globalLifecycleSys.Get(bucket); err == nil {
|
||||
rcfg, _ := globalBucketObjectLockSys.Get(bucket)
|
||||
evt := evalActionFromLifecycle(ctx, *lc, rcfg, objInfo)
|
||||
var success bool
|
||||
switch evt.Action {
|
||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
||||
success = applyExpiryRule(objInfo, false, evt.Action == lifecycle.DeleteVersionAction)
|
||||
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||
// Restored object delete would be still allowed to proceed as success
|
||||
// since transition behavior is slightly different.
|
||||
applyExpiryRule(objInfo, true, evt.Action == lifecycle.DeleteRestoredVersionAction)
|
||||
}
|
||||
if success {
|
||||
writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrNoSuchKey))
|
||||
return
|
||||
act := evalActionFromLifecycle(ctx, *lc, rcfg, objInfo).Action
|
||||
if act.Delete() {
|
||||
// apply whatever the expiry rule is.
|
||||
applyExpiryRule(objInfo, act.DeleteRestored(), act.DeleteVersioned())
|
||||
if !act.DeleteRestored() {
|
||||
// If the ILM action is not on restored object return error.
|
||||
writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(ErrNoSuchKey))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
QueueReplicationHeal(ctx, bucket, objInfo)
|
||||
@@ -2368,8 +2360,7 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectDELETE.html
|
||||
objInfo, err := deleteObject(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case BucketNotFound:
|
||||
if _, ok := err.(BucketNotFound); ok {
|
||||
// When bucket doesn't exist specially handle it.
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
|
||||
+13
-15
@@ -1197,17 +1197,18 @@ func testAPIPutObjectStreamSigV4Handler(obj ObjectLayer, instanceType, bucketNam
|
||||
rec := httptest.NewRecorder()
|
||||
// construct HTTP request for Put Object end point.
|
||||
var req *http.Request
|
||||
if testCase.fault == chunkDateMismatch {
|
||||
switch {
|
||||
case testCase.fault == chunkDateMismatch:
|
||||
req, err = newTestStreamingSignedBadChunkDateRequest(http.MethodPut,
|
||||
getPutObjectURL("", testCase.bucketName, testCase.objectName),
|
||||
int64(testCase.dataLen), testCase.chunkSize, bytes.NewReader(testCase.data),
|
||||
testCase.accessKey, testCase.secretKey)
|
||||
} else if testCase.contentEncoding == "" {
|
||||
case testCase.contentEncoding == "":
|
||||
req, err = newTestStreamingSignedRequest(http.MethodPut,
|
||||
getPutObjectURL("", testCase.bucketName, testCase.objectName),
|
||||
int64(testCase.dataLen), testCase.chunkSize, bytes.NewReader(testCase.data),
|
||||
testCase.accessKey, testCase.secretKey)
|
||||
} else if testCase.contentEncoding != "" {
|
||||
case testCase.contentEncoding != "":
|
||||
req, err = newTestStreamingSignedCustomEncodingRequest(http.MethodPut,
|
||||
getPutObjectURL("", testCase.bucketName, testCase.objectName),
|
||||
int64(testCase.dataLen), testCase.chunkSize, bytes.NewReader(testCase.data),
|
||||
@@ -2045,12 +2046,13 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
var req *http.Request
|
||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||
rec := httptest.NewRecorder()
|
||||
if !testCase.invalidPartNumber || !testCase.maximumPartNumber {
|
||||
switch {
|
||||
case !testCase.invalidPartNumber || !testCase.maximumPartNumber:
|
||||
// construct HTTP request for copy object.
|
||||
req, err = newTestSignedRequestV4(http.MethodPut, getCopyObjectPartURL("", testCase.bucketName, testObject, testCase.uploadID, "1"), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||
} else if testCase.invalidPartNumber {
|
||||
case testCase.invalidPartNumber:
|
||||
req, err = newTestSignedRequestV4(http.MethodPut, getCopyObjectPartURL("", testCase.bucketName, testObject, testCase.uploadID, "abc"), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||
} else if testCase.maximumPartNumber {
|
||||
case testCase.maximumPartNumber:
|
||||
req, err = newTestSignedRequestV4(http.MethodPut, getCopyObjectPartURL("", testCase.bucketName, testObject, testCase.uploadID, "99999"), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -3545,11 +3547,9 @@ func testAPIPutObjectPartHandlerStreaming(obj ObjectLayer, instanceType, bucketN
|
||||
t.Errorf("Test %d %s expected to fail with error %s, but received %s", i+1, instanceType,
|
||||
test.expectedErr.Code, errXML.Code)
|
||||
}
|
||||
} else {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Test %d %s expected to succeed, but failed with HTTP status code %d",
|
||||
i+1, instanceType, rec.Code)
|
||||
}
|
||||
} else if rec.Code != http.StatusOK {
|
||||
t.Errorf("Test %d %s expected to succeed, but failed with HTTP status code %d",
|
||||
i+1, instanceType, rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4119,10 +4119,8 @@ func testAPIListObjectPartsHandler(obj ObjectLayer, instanceType, bucketName str
|
||||
reqType, test.expectedErr.Code, errXML.Code)
|
||||
}
|
||||
// in case error is not expected response status should be 200OK.
|
||||
} else {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("%s, Expected to succeed with response HTTP status 200OK, but failed with HTTP status code %d.", reqType, rec.Code)
|
||||
}
|
||||
} else if rec.Code != http.StatusOK {
|
||||
t.Errorf("%s, Expected to succeed with response HTTP status 200OK, but failed with HTTP status code %d.", reqType, rec.Code)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/gzhttp"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
miniogo "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
|
||||
"github.com/minio/minio/internal/auth"
|
||||
levent "github.com/minio/minio/internal/config/lambda/event"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
func getLambdaEventData(bucket, object string, cred auth.Credentials, r *http.Request) (levent.Event, error) {
|
||||
host := globalLocalNodeName
|
||||
secure := globalIsTLS
|
||||
if globalMinioEndpointURL != nil {
|
||||
host = globalMinioEndpointURL.Host
|
||||
secure = globalMinioEndpointURL.Scheme == "https"
|
||||
}
|
||||
|
||||
duration := time.Since(cred.Expiration)
|
||||
if cred.Expiration.IsZero() {
|
||||
duration = time.Hour
|
||||
}
|
||||
|
||||
clnt, err := miniogo.New(host, &miniogo.Options{
|
||||
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
|
||||
Secure: secure,
|
||||
Transport: globalRemoteTargetTransport,
|
||||
Region: globalSite.Region,
|
||||
})
|
||||
if err != nil {
|
||||
return levent.Event{}, err
|
||||
}
|
||||
|
||||
reqParams := url.Values{}
|
||||
if partNumberStr := r.Form.Get("partNumber"); partNumberStr != "" {
|
||||
reqParams.Set("partNumber", partNumberStr)
|
||||
}
|
||||
for k := range supportedHeadGetReqParams {
|
||||
if v := r.Form.Get(k); v != "" {
|
||||
reqParams.Set(k, v)
|
||||
}
|
||||
}
|
||||
extraHeaders := http.Header{}
|
||||
if rng := r.Header.Get(xhttp.Range); rng != "" {
|
||||
extraHeaders.Set(xhttp.Range, r.Header.Get(xhttp.Range))
|
||||
}
|
||||
|
||||
u, err := clnt.PresignHeader(r.Context(), http.MethodGet, bucket, object, duration, reqParams, extraHeaders)
|
||||
if err != nil {
|
||||
return levent.Event{}, err
|
||||
}
|
||||
|
||||
token, err := authenticateNode(cred.AccessKey, cred.SecretKey, u.RawQuery)
|
||||
if err != nil {
|
||||
return levent.Event{}, err
|
||||
}
|
||||
|
||||
eventData := levent.Event{
|
||||
GetObjectContext: &levent.GetObjectContext{
|
||||
InputS3URL: u.String(),
|
||||
OutputRoute: shortuuid.New(),
|
||||
OutputToken: token,
|
||||
},
|
||||
UserRequest: levent.UserRequest{
|
||||
URL: r.URL.String(),
|
||||
Headers: r.Header.Clone(),
|
||||
},
|
||||
UserIdentity: levent.Identity{
|
||||
Type: "IAMUser",
|
||||
PrincipalID: cred.AccessKey,
|
||||
AccessKeyID: cred.SecretKey,
|
||||
},
|
||||
}
|
||||
return eventData, nil
|
||||
}
|
||||
|
||||
var statusTextToCode = map[string]int{
|
||||
"Continue": http.StatusContinue,
|
||||
"Switching Protocols": http.StatusSwitchingProtocols,
|
||||
"Processing": http.StatusProcessing,
|
||||
"Early Hints": http.StatusEarlyHints,
|
||||
"OK": http.StatusOK,
|
||||
"Created": http.StatusCreated,
|
||||
"Accepted": http.StatusAccepted,
|
||||
"Non-Authoritative Information": http.StatusNonAuthoritativeInfo,
|
||||
"No Content": http.StatusNoContent,
|
||||
"Reset Content": http.StatusResetContent,
|
||||
"Partial Content": http.StatusPartialContent,
|
||||
"Multi-Status": http.StatusMultiStatus,
|
||||
"Already Reported": http.StatusAlreadyReported,
|
||||
"IM Used": http.StatusIMUsed,
|
||||
"Multiple Choices": http.StatusMultipleChoices,
|
||||
"Moved Permanently": http.StatusMovedPermanently,
|
||||
"Found": http.StatusFound,
|
||||
"See Other": http.StatusSeeOther,
|
||||
"Not Modified": http.StatusNotModified,
|
||||
"Use Proxy": http.StatusUseProxy,
|
||||
"Temporary Redirect": http.StatusTemporaryRedirect,
|
||||
"Permanent Redirect": http.StatusPermanentRedirect,
|
||||
"Bad Request": http.StatusBadRequest,
|
||||
"Unauthorized": http.StatusUnauthorized,
|
||||
"Payment Required": http.StatusPaymentRequired,
|
||||
"Forbidden": http.StatusForbidden,
|
||||
"Not Found": http.StatusNotFound,
|
||||
"Method Not Allowed": http.StatusMethodNotAllowed,
|
||||
"Not Acceptable": http.StatusNotAcceptable,
|
||||
"Proxy Authentication Required": http.StatusProxyAuthRequired,
|
||||
"Request Timeout": http.StatusRequestTimeout,
|
||||
"Conflict": http.StatusConflict,
|
||||
"Gone": http.StatusGone,
|
||||
"Length Required": http.StatusLengthRequired,
|
||||
"Precondition Failed": http.StatusPreconditionFailed,
|
||||
"Request Entity Too Large": http.StatusRequestEntityTooLarge,
|
||||
"Request URI Too Long": http.StatusRequestURITooLong,
|
||||
"Unsupported Media Type": http.StatusUnsupportedMediaType,
|
||||
"Requested Range Not Satisfiable": http.StatusRequestedRangeNotSatisfiable,
|
||||
"Expectation Failed": http.StatusExpectationFailed,
|
||||
"I'm a teapot": http.StatusTeapot,
|
||||
"Misdirected Request": http.StatusMisdirectedRequest,
|
||||
"Unprocessable Entity": http.StatusUnprocessableEntity,
|
||||
"Locked": http.StatusLocked,
|
||||
"Failed Dependency": http.StatusFailedDependency,
|
||||
"Too Early": http.StatusTooEarly,
|
||||
"Upgrade Required": http.StatusUpgradeRequired,
|
||||
"Precondition Required": http.StatusPreconditionRequired,
|
||||
"Too Many Requests": http.StatusTooManyRequests,
|
||||
"Request Header Fields Too Large": http.StatusRequestHeaderFieldsTooLarge,
|
||||
"Unavailable For Legal Reasons": http.StatusUnavailableForLegalReasons,
|
||||
"Internal Server Error": http.StatusInternalServerError,
|
||||
"Not Implemented": http.StatusNotImplemented,
|
||||
"Bad Gateway": http.StatusBadGateway,
|
||||
"Service Unavailable": http.StatusServiceUnavailable,
|
||||
"Gateway Timeout": http.StatusGatewayTimeout,
|
||||
"HTTP Version Not Supported": http.StatusHTTPVersionNotSupported,
|
||||
"Variant Also Negotiates": http.StatusVariantAlsoNegotiates,
|
||||
"Insufficient Storage": http.StatusInsufficientStorage,
|
||||
"Loop Detected": http.StatusLoopDetected,
|
||||
"Not Extended": http.StatusNotExtended,
|
||||
"Network Authentication Required": http.StatusNetworkAuthenticationRequired,
|
||||
}
|
||||
|
||||
// StatusCode returns a HTTP Status code for the HTTP text. It returns -1
|
||||
// if the text is unknown.
|
||||
func StatusCode(text string) int {
|
||||
if code, ok := statusTextToCode[text]; ok {
|
||||
return code
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func fwdHeadersToS3(h http.Header, w http.ResponseWriter) {
|
||||
const trim = "x-amz-fwd-header-"
|
||||
for k, v := range h {
|
||||
if strings.HasPrefix(strings.ToLower(k), trim) {
|
||||
w.Header()[k[len(trim):]] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fwdStatusToAPIError(resp *http.Response) *APIError {
|
||||
if status := resp.Header.Get(xhttp.AmzFwdStatus); status != "" && StatusCode(status) > -1 {
|
||||
apiErr := &APIError{
|
||||
HTTPStatusCode: StatusCode(status),
|
||||
Description: resp.Header.Get(xhttp.AmzFwdErrorMessage),
|
||||
Code: resp.Header.Get(xhttp.AmzFwdErrorCode),
|
||||
}
|
||||
if apiErr.HTTPStatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
return apiErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetObjectLamdbaHandler - GET Object with transformed data via lambda functions
|
||||
// ----------
|
||||
// This implementation of the GET operation applies lambda functions and returns the
|
||||
// response generated via the lambda functions. To use this API, you must have READ access
|
||||
// to the object.
|
||||
func (api objectAPIHandlers) GetObjectLambdaHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetObjectLambda")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
object, err := unescapePath(vars["object"])
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for auth type to return S3 compatible error.
|
||||
cred, _, s3Error := checkRequestAuthTypeCredential(ctx, r, policy.GetObjectAction)
|
||||
if s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
target, err := globalLambdaTargetList.Lookup(r.Form.Get("lambdaArn"))
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
eventData, err := getLambdaEventData(bucket, object, cred, r)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := target.Send(eventData)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if eventData.GetObjectContext.OutputRoute != resp.Header.Get(xhttp.AmzRequestRoute) {
|
||||
tokenErr := errorCodes.ToAPIErr(ErrInvalidRequest)
|
||||
tokenErr.Description = "The request route included in the request is invalid"
|
||||
writeErrorResponse(ctx, w, tokenErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(resp.Header.Get(xhttp.AmzRequestToken)), []byte(eventData.GetObjectContext.OutputToken)) != 1 {
|
||||
tokenErr := errorCodes.ToAPIErr(ErrInvalidToken)
|
||||
tokenErr.Description = "The request token included in the request is invalid"
|
||||
writeErrorResponse(ctx, w, tokenErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Set all the relevant lambda forward headers if found.
|
||||
fwdHeadersToS3(resp.Header, w)
|
||||
|
||||
if apiErr := fwdStatusToAPIError(resp); apiErr != nil {
|
||||
writeErrorResponse(ctx, w, *apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
writeErrorResponse(ctx, w, APIError{
|
||||
Code: "LambdaFunctionError",
|
||||
HTTPStatusCode: resp.StatusCode,
|
||||
Description: "unexpected failure reported from lambda function",
|
||||
}, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if !globalAPIConfig.shouldGzipObjects() {
|
||||
w.Header().Set(gzhttp.HeaderNoCompression, "true")
|
||||
}
|
||||
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
@@ -57,6 +57,10 @@ func (client *peerRESTClient) call(method string, values url.Values, body io.Rea
|
||||
// permanently. The only way to restore the connection is at the xl-sets layer by xlsets.monitorAndConnectEndpoints()
|
||||
// after verifying format.json
|
||||
func (client *peerRESTClient) callWithContext(ctx context.Context, method string, values url.Values, body io.Reader, length int64) (respBody io.ReadCloser, err error) {
|
||||
if client == nil || !client.IsOnline() {
|
||||
return nil, errPeerNotReachable
|
||||
}
|
||||
|
||||
if values == nil {
|
||||
values = make(url.Values)
|
||||
}
|
||||
|
||||
@@ -977,6 +977,12 @@ func (s *peerRESTServer) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Publish bootstrap events that have already occurred before client could subscribe.
|
||||
if traceOpts.TraceTypes().Contains(madmin.TraceBootstrap) {
|
||||
go globalBootstrapTracer.Publish(r.Context(), globalTrace)
|
||||
}
|
||||
|
||||
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
|
||||
defer keepAliveTicker.Stop()
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ func (sys *S3PeerSys) GetBucketInfo(ctx context.Context, bucket string, opts Buc
|
||||
|
||||
quorum := (len(sys.allPeerClients) / 2)
|
||||
if err = reduceReadQuorumErrs(ctx, errs, bucketOpIgnoredErrs, quorum); err != nil {
|
||||
return BucketInfo{}, err
|
||||
return BucketInfo{}, toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
for i, err := range errs {
|
||||
@@ -266,12 +266,9 @@ func (sys *S3PeerSys) DeleteBucket(ctx context.Context, bucket string, opts Dele
|
||||
errs := g.Wait()
|
||||
errs = append(errs, deleteBucketLocal(ctx, bucket, opts))
|
||||
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
quorum := (len(sys.allPeerClients) / 2) + 1
|
||||
err := reduceWriteQuorumErrs(ctx, errs, bucketOpIgnoredErrs, quorum)
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// DeleteBucket deletes bucket on a peer
|
||||
|
||||
@@ -80,6 +80,10 @@ func (s *peerS3Server) HealthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func listBucketsLocal(ctx context.Context, opts BucketOptions) (buckets []BucketInfo, err error) {
|
||||
globalLocalDrivesMu.RLock()
|
||||
globalLocalDrives := globalLocalDrives
|
||||
globalLocalDrivesMu.RUnlock()
|
||||
|
||||
quorum := (len(globalLocalDrives) / 2)
|
||||
|
||||
buckets = make([]BucketInfo, 0, 32)
|
||||
@@ -128,6 +132,10 @@ func listBucketsLocal(ctx context.Context, opts BucketOptions) (buckets []Bucket
|
||||
}
|
||||
|
||||
func getBucketInfoLocal(ctx context.Context, bucket string, opts BucketOptions) (BucketInfo, error) {
|
||||
globalLocalDrivesMu.RLock()
|
||||
globalLocalDrives := globalLocalDrives
|
||||
globalLocalDrivesMu.RUnlock()
|
||||
|
||||
g := errgroup.WithNErrs(len(globalLocalDrives)).WithConcurrency(32)
|
||||
bucketsInfo := make([]BucketInfo, len(globalLocalDrives))
|
||||
|
||||
@@ -173,6 +181,10 @@ func getBucketInfoLocal(ctx context.Context, bucket string, opts BucketOptions)
|
||||
}
|
||||
|
||||
func deleteBucketLocal(ctx context.Context, bucket string, opts DeleteBucketOptions) error {
|
||||
globalLocalDrivesMu.RLock()
|
||||
globalLocalDrives := globalLocalDrives
|
||||
globalLocalDrivesMu.RUnlock()
|
||||
|
||||
g := errgroup.WithNErrs(len(globalLocalDrives)).WithConcurrency(32)
|
||||
|
||||
// Make a volume entry on all underlying storage disks.
|
||||
@@ -208,6 +220,10 @@ func deleteBucketLocal(ctx context.Context, bucket string, opts DeleteBucketOpti
|
||||
}
|
||||
|
||||
func makeBucketLocal(ctx context.Context, bucket string, opts MakeBucketOptions) error {
|
||||
globalLocalDrivesMu.RLock()
|
||||
globalLocalDrives := globalLocalDrives
|
||||
globalLocalDrivesMu.RUnlock()
|
||||
|
||||
g := errgroup.WithNErrs(len(globalLocalDrives)).WithConcurrency(32)
|
||||
|
||||
// Make a volume entry on all underlying storage disks.
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -114,6 +115,65 @@ func newPostPolicyBytesV2(bucketName, objectKey string, expiration time.Time) []
|
||||
return []byte(retStr)
|
||||
}
|
||||
|
||||
// Wrapper
|
||||
func TestPostPolicyReservedBucketExploit(t *testing.T) {
|
||||
ExecObjectLayerTestWithDirs(t, testPostPolicyReservedBucketExploit)
|
||||
}
|
||||
|
||||
// testPostPolicyReservedBucketExploit is a test for the exploit fixed in PR
|
||||
// #16849
|
||||
func testPostPolicyReservedBucketExploit(obj ObjectLayer, instanceType string, dirs []string, t TestErrHandler) {
|
||||
if err := newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
||||
t.Fatalf("Initializing config.json failed")
|
||||
}
|
||||
|
||||
// Register the API end points with Erasure/FS object layer.
|
||||
apiRouter := initTestAPIEndPoints(obj, []string{"PostPolicy"})
|
||||
|
||||
credentials := globalActiveCred
|
||||
bucketName := minioMetaBucket
|
||||
objectName := "config/x"
|
||||
|
||||
// This exploit needs browser to be enabled.
|
||||
if !globalBrowserEnabled {
|
||||
globalBrowserEnabled = true
|
||||
defer func() { globalBrowserEnabled = false }()
|
||||
}
|
||||
|
||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||
rec := httptest.NewRecorder()
|
||||
req, perr := newPostRequestV4("", bucketName, objectName, []byte("pwned"), credentials.AccessKey, credentials.SecretKey)
|
||||
if perr != nil {
|
||||
t.Fatalf("Test %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", instanceType, perr)
|
||||
}
|
||||
|
||||
contentTypeHdr := req.Header.Get("Content-Type")
|
||||
contentTypeHdr = strings.Replace(contentTypeHdr, "multipart/form-data", "multipart/form-datA", 1)
|
||||
req.Header.Set("Content-Type", contentTypeHdr)
|
||||
req.Header.Set("User-Agent", "Mozilla")
|
||||
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler.
|
||||
// Call the ServeHTTP to execute the handler.
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
|
||||
ctx, cancel := context.WithCancel(GlobalContext)
|
||||
defer cancel()
|
||||
|
||||
// Now check if we actually wrote to backend (regardless of the response
|
||||
// returned by the server).
|
||||
z := obj.(*erasureServerPools)
|
||||
xl := z.serverPools[0].sets[0]
|
||||
erasureDisks := xl.getDisks()
|
||||
parts, errs := readAllFileInfo(ctx, erasureDisks, bucketName, objectName+"/upload.txt", "", false)
|
||||
for i := range parts {
|
||||
if errs[i] == nil {
|
||||
if parts[i].Name == objectName+"/upload.txt" {
|
||||
t.Errorf("Test %s: Failed to stop post policy handler from writing to minioMetaBucket", instanceType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for calling TestPostPolicyBucketHandler tests for both Erasure multiple disks and single node setup.
|
||||
func TestPostPolicyBucketHandler(t *testing.T) {
|
||||
ExecObjectLayerTest(t, testPostPolicyBucketHandler)
|
||||
|
||||
@@ -352,6 +352,8 @@ func configRetriableErrors(err error) bool {
|
||||
}
|
||||
|
||||
func bootstrapTrace(msg string) {
|
||||
globalBootstrapTracer.Record(msg)
|
||||
|
||||
if globalTrace.NumSubscribers(madmin.TraceBootstrap) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ func printServerCommonMsg(apiEndpoints []string) {
|
||||
}
|
||||
}
|
||||
printEventNotifiers()
|
||||
printLambdaTargets()
|
||||
|
||||
if globalBrowserEnabled {
|
||||
consoleEndpointStr := strings.Join(stripStandardPorts(getConsoleEndpoints(), globalMinioConsoleHost), " ")
|
||||
@@ -152,6 +153,18 @@ func printObjectAPIMsg() {
|
||||
logger.Info(color.Blue("\nDocumentation: ") + "https://min.io/docs/minio/linux/index.html")
|
||||
}
|
||||
|
||||
func printLambdaTargets() {
|
||||
if globalLambdaTargetList == nil || globalLambdaTargetList.Empty() {
|
||||
return
|
||||
}
|
||||
|
||||
arnMsg := color.Blue("Object Lambda ARNs: ")
|
||||
for _, arn := range globalLambdaTargetList.List(globalSite.Region) {
|
||||
arnMsg += color.Bold(fmt.Sprintf("%s ", arn))
|
||||
}
|
||||
logger.Info(arnMsg + "\n")
|
||||
}
|
||||
|
||||
// Prints bucket notification configurations.
|
||||
func printEventNotifiers() {
|
||||
if globalNotificationSys == nil {
|
||||
@@ -168,7 +181,7 @@ func printEventNotifiers() {
|
||||
arnMsg += color.Bold(fmt.Sprintf("%s ", arn))
|
||||
}
|
||||
|
||||
logger.Info(arnMsg)
|
||||
logger.Info(arnMsg + "\n")
|
||||
}
|
||||
|
||||
// Prints startup message for command line access. Prints link to our documentation
|
||||
|
||||
@@ -109,8 +109,7 @@ func parseCredentialHeader(credElement string, region string, stype serviceType)
|
||||
return ch, ErrAuthorizationHeaderMalformed
|
||||
}
|
||||
if credElements[2] != string(stype) {
|
||||
switch stype {
|
||||
case serviceSTS:
|
||||
if stype == serviceSTS {
|
||||
return ch, ErrInvalidServiceSTS
|
||||
}
|
||||
return ch, ErrInvalidServiceS3
|
||||
|
||||
@@ -630,9 +630,8 @@ func TestDoesV4PresignParamsExist(t *testing.T) {
|
||||
// TestParsePreSignV4 - Validates the parsing logic of Presignied v4 request from its url query values.
|
||||
func TestParsePreSignV4(t *testing.T) {
|
||||
// converts the duration in seconds into string format.
|
||||
getDurationStr := func(expires int) string {
|
||||
return strconv.Itoa(expires)
|
||||
}
|
||||
getDurationStr := strconv.Itoa
|
||||
|
||||
// used in expected preSignValues, preSignValues.Date is of type time.Time .
|
||||
queryTime := UTCNow()
|
||||
|
||||
|
||||
@@ -150,10 +150,8 @@ func TestSkipContentSha256Cksum(t *testing.T) {
|
||||
q.Add(testCase.inputHeaderKey, testCase.inputHeaderValue)
|
||||
}
|
||||
inputReq.URL.RawQuery = q.Encode()
|
||||
} else {
|
||||
if testCase.inputHeaderKey != "" {
|
||||
inputReq.Header.Set(testCase.inputHeaderKey, testCase.inputHeaderValue)
|
||||
}
|
||||
} else if testCase.inputHeaderKey != "" {
|
||||
inputReq.Header.Set(testCase.inputHeaderKey, testCase.inputHeaderValue)
|
||||
}
|
||||
inputReq.ParseForm()
|
||||
|
||||
|
||||
+13
-5
@@ -1327,8 +1327,7 @@ func (c *SiteReplicationSys) PeerSTSAccHandler(ctx context.Context, stsCred *mad
|
||||
|
||||
// Extract the username and lookup DN and groups in LDAP.
|
||||
ldapUser, isLDAPSTS := claims.Lookup(ldapUserN)
|
||||
switch {
|
||||
case isLDAPSTS:
|
||||
if isLDAPSTS {
|
||||
// Need to lookup the groups from LDAP.
|
||||
_, ldapGroups, err := globalIAMSys.LDAPConfig.LookupUserDN(ldapUser)
|
||||
if err != nil {
|
||||
@@ -4729,10 +4728,19 @@ func (c *SiteReplicationSys) healUsers(ctx context.Context, objAPI ObjectLayer,
|
||||
continue
|
||||
}
|
||||
if creds.IsTemp() && !creds.IsExpired() {
|
||||
var parentPolicy string
|
||||
u, err := globalIAMSys.GetUserInfo(ctx, creds.ParentUser)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to heal temporary credentials %s from peer site %s -> %s : %w", user, latestPeerName, peerName, err))
|
||||
continue
|
||||
// Parent may be "virtual" (for ldap, oidc, client tls auth,
|
||||
// custom auth plugin), so in such cases we apply no parent
|
||||
// policy. The session token will contain info about policy to
|
||||
// be applied.
|
||||
if !errors.Is(err, errNoSuchUser) {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to heal temporary credentials %s from peer site %s -> %s : %w", user, latestPeerName, peerName, err))
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
parentPolicy = u.PolicyName
|
||||
}
|
||||
// Call hook for site replication.
|
||||
if err := c.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
@@ -4742,7 +4750,7 @@ func (c *SiteReplicationSys) healUsers(ctx context.Context, objAPI ObjectLayer,
|
||||
SecretKey: creds.SecretKey,
|
||||
SessionToken: creds.SessionToken,
|
||||
ParentUser: creds.ParentUser,
|
||||
ParentPolicyMapping: u.PolicyName,
|
||||
ParentPolicyMapping: parentPolicy,
|
||||
},
|
||||
UpdatedAt: lastUpdate,
|
||||
}); err != nil {
|
||||
|
||||
@@ -627,6 +627,11 @@ func (s *TestSuiteIAM) TestSTSForRoot(c *check) {
|
||||
if !gotBuckets.Equals(shouldHaveBuckets) {
|
||||
c.Fatalf("root user should have access to all buckets")
|
||||
}
|
||||
|
||||
// This must fail.
|
||||
if err := userAdmClient.AddUser(ctx, globalActiveCred.AccessKey, globalActiveCred.SecretKey); err == nil {
|
||||
c.Fatal("AddUser() for root credential must fail via root STS creds")
|
||||
}
|
||||
}
|
||||
|
||||
// SetUpLDAP - expects to setup an LDAP test server using the test LDAP
|
||||
|
||||
+18
-10
@@ -1477,9 +1477,9 @@ func getListenNotificationURL(endPoint, bucketName string, prefixes, suffixes, e
|
||||
}
|
||||
|
||||
// getRandomDisks - Creates a slice of N random disks, each of the form - minio-XXX
|
||||
func getRandomDisks(N int) ([]string, error) {
|
||||
func getRandomDisks(n int) ([]string, error) {
|
||||
var erasureDisks []string
|
||||
for i := 0; i < N; i++ {
|
||||
for i := 0; i < n; i++ {
|
||||
path, err := os.MkdirTemp(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
// Remove directories created so far.
|
||||
@@ -1885,21 +1885,29 @@ func ExecObjectLayerTestWithDirs(t TestErrHandler, objTest objTestTypeWithDirs)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if localMetacacheMgr != nil {
|
||||
localMetacacheMgr.deleteAll()
|
||||
}
|
||||
|
||||
initAllSubsystems(ctx)
|
||||
objLayer, fsDirs, err := prepareErasure16(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Initialization of object layer failed for Erasure setup: %s", err)
|
||||
}
|
||||
defer objLayer.Shutdown(ctx)
|
||||
|
||||
// initialize the server and obtain the credentials and root.
|
||||
// credentials are necessary to sign the HTTP request.
|
||||
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||
t.Fatal("Unexpected error", err)
|
||||
}
|
||||
setObjectLayer(objLayer)
|
||||
initConfigSubsystem(ctx, objLayer)
|
||||
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||
|
||||
// Executing the object layer tests for Erasure.
|
||||
objTest(objLayer, ErasureTestStr, fsDirs, t)
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
objLayer.Shutdown(context.Background())
|
||||
if localMetacacheMgr != nil {
|
||||
localMetacacheMgr.deleteAll()
|
||||
}
|
||||
setObjectLayer(newObjectLayerFn())
|
||||
cancel()
|
||||
removeRoots(fsDirs)
|
||||
}
|
||||
|
||||
// ExecObjectLayerDiskAlteredTest - executes object layer tests while altering
|
||||
|
||||
@@ -88,6 +88,9 @@ var errNoPolicyToAttachOrDetach = errors.New("Specified policy update has no net
|
||||
// deleted.
|
||||
var errGroupNotEmpty = errors.New("Specified group is not empty - cannot remove it")
|
||||
|
||||
// error returned in IAM subsystem when a group is disabled
|
||||
var errGroupDisabled = errors.New("Specified group is disabled")
|
||||
|
||||
// error returned in IAM subsystem when policy doesn't exist.
|
||||
var errNoSuchPolicy = errors.New("Specified canned policy does not exist")
|
||||
|
||||
|
||||
+1
-1
@@ -243,7 +243,7 @@ func untar(ctx context.Context, r io.Reader, putObject func(reader io.Reader, in
|
||||
rc.Close()
|
||||
<-asyncWriters
|
||||
wg.Done()
|
||||
//lint:ignore SA6002 we are fine with the tiny alloc
|
||||
//nolint:staticcheck // SA6002 we are fine with the tiny alloc
|
||||
poolBuf128k.Put(b)
|
||||
}()
|
||||
if err := putObject(&rc, fi, name); err != nil {
|
||||
|
||||
+6
-1
@@ -518,6 +518,11 @@ func downloadBinary(u *url.URL, mode string) (readerReturn []byte, err error) {
|
||||
return binaryFile, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// Update this whenever the official minisign pubkey is rotated.
|
||||
defaultMinisignPubkey = "RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav"
|
||||
)
|
||||
|
||||
func verifyBinary(u *url.URL, sha256Sum []byte, releaseInfo string, mode string, reader []byte) (err error) {
|
||||
if !atomic.CompareAndSwapUint32(&updateInProgress, 0, 1) {
|
||||
return errors.New("update already in progress")
|
||||
@@ -538,7 +543,7 @@ func verifyBinary(u *url.URL, sha256Sum []byte, releaseInfo string, mode string,
|
||||
}
|
||||
}
|
||||
|
||||
minisignPubkey := env.Get(envMinisignPubKey, "")
|
||||
minisignPubkey := env.Get(envMinisignPubKey, defaultMinisignPubkey)
|
||||
if minisignPubkey != "" {
|
||||
v := selfupdate.NewVerifier()
|
||||
u.Path = path.Dir(u.Path) + slashSeparator + releaseInfo + ".minisig"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user