mirror of
https://github.com/pgsty/minio.git
synced 2026-08-05 15:12:01 +03:00
Compare commits
258 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d0f30f062 | |||
| cea2ca8c8e | |||
| f713436dd0 | |||
| b923a62425 | |||
| 67fce4a5b3 | |||
| eaa65b7ade | |||
| 820d94447c | |||
| ed20134a7b | |||
| d19cbc81b5 | |||
| 1fd7946dce | |||
| 027ff0f3a8 | |||
| 8fa80874a6 | |||
| 430669cfad | |||
| 54b561898f | |||
| 65c104a589 | |||
| 0a0416b6ea | |||
| 441babdc41 | |||
| 1bf1fafc86 | |||
| 50d58e9b2d | |||
| e64b9f6751 | |||
| d67a846ec4 | |||
| ca2a1c3f60 | |||
| 93fbb228bf | |||
| 3683673fb0 | |||
| f37a5b6dae | |||
| 31b0decd46 | |||
| eb561e1c05 | |||
| 54c9ecff5b | |||
| edcd72585d | |||
| 1a17fc17bb | |||
| ddad231921 | |||
| e73894fa50 | |||
| 03b94f907f | |||
| 3fa7218c44 | |||
| 0f591d245d | |||
| 1b02e046c2 | |||
| d08e3cc895 | |||
| d98116559b | |||
| 71c95ad0d0 | |||
| 5c1a4ba5f9 | |||
| 698862ec5d | |||
| b4ef5ff294 | |||
| 3db658e51e | |||
| 5a9f7516d6 | |||
| 3039fd4519 | |||
| 095fc0561d | |||
| beb1924437 | |||
| c8e1154f1e | |||
| b204c2dbec | |||
| b22b39de96 | |||
| c242e6c391 | |||
| e05205756f | |||
| d03b244fcd | |||
| 5ef679d8f1 | |||
| d33c527e39 | |||
| 7bc95c47a3 | |||
| 475a88b555 | |||
| 9815dac48f | |||
| 1ece3d1dfe | |||
| 2146ed4033 | |||
| 52b88b52f0 | |||
| ebd4388cca | |||
| 57fd02ee57 | |||
| 0333412148 | |||
| 1c85652cff | |||
| e0086c1be7 | |||
| b29e159604 | |||
| 7883e55da2 | |||
| b197623ed2 | |||
| a15a2556c3 | |||
| 14d29b77ae | |||
| a2514ffeed | |||
| f1bbb7fef5 | |||
| 72394a8319 | |||
| 1cd8e1d8b6 | |||
| 62cd918061 | |||
| 6a04067514 | |||
| 49b3908635 | |||
| 75faef888e | |||
| b67d97b1ba | |||
| b8943fdf19 | |||
| f93183f66e | |||
| 2937711390 | |||
| aa56c6d51d | |||
| 27417459fb | |||
| 5b8fe2e89a | |||
| acc9c033ed | |||
| 8528b265a9 | |||
| 44250f1a52 | |||
| f7560670d9 | |||
| 3891885800 | |||
| b882310e2b | |||
| de0b43de32 | |||
| 48152a56ac | |||
| 29dd7f1d68 | |||
| 6423e4c767 | |||
| 1dd8f0e8f3 | |||
| 2fa35def2c | |||
| 34167c51d5 | |||
| a5f8af4efb | |||
| 5a218f38a1 | |||
| e57e946206 | |||
| b4f71362e9 | |||
| ed37b7a9d5 | |||
| 6511021fbe | |||
| 6197ba851b | |||
| 3ae1f9d852 | |||
| 0db1930f48 | |||
| 89db3fdb5d | |||
| 80fc3a8a52 | |||
| 988a2e8fed | |||
| 2433698372 | |||
| 5d7e8f79ed | |||
| bad229e16e | |||
| d37e514733 | |||
| c73ea27ed7 | |||
| 0159b56717 | |||
| 9e6cc847f8 | |||
| 709eb283d9 | |||
| 76dde82b41 | |||
| 939c0100a6 | |||
| 2d60bf8c50 | |||
| 37e20f6ef2 | |||
| 76905b7a67 | |||
| a469e6768d | |||
| 2fc182d8e6 | |||
| a2cbeaa9e6 | |||
| 444ff20bc5 | |||
| 20ef5e7a6a | |||
| c233c8e329 | |||
| e06127566d | |||
| dfe73629a3 | |||
| b03dd1af17 | |||
| 4bc367c490 | |||
| 3eb2d086b2 | |||
| 70986b6e6e | |||
| 8edc2faaa9 | |||
| ebe395788b | |||
| 12fd6678ee | |||
| 90d35b70b4 | |||
| 9f71369b67 | |||
| 04ae9058ed | |||
| a30cfdd88f | |||
| 1bae32dc96 | |||
| 932d2c3c62 | |||
| 52f4124678 | |||
| 8d8d07ac5c | |||
| 44735be38e | |||
| 1ef1b2ba50 | |||
| 6fdbd778d5 | |||
| 419f351df3 | |||
| 180d6b30ca | |||
| 3fd9059b4e | |||
| a713aee3d5 | |||
| a9f5b58a01 | |||
| d882ba2cb4 | |||
| 90e37a8745 | |||
| 6086f45d25 | |||
| d6351879f3 | |||
| 5655272f5a | |||
| 9b35c72349 | |||
| 98cffbce03 | |||
| 1cd875de1e | |||
| 5a8df7efb3 | |||
| c84e2939e4 | |||
| 641ab24aec | |||
| 71133105d7 | |||
| 625677b189 | |||
| 76943ac05e | |||
| 87cbd41265 | |||
| be92cf5959 | |||
| cc1d8f0057 | |||
| 1f1dcdce65 | |||
| 98a67a3776 | |||
| 9b1e70e4f9 | |||
| 09d4f8cd0f | |||
| 53cbc020b9 | |||
| 63fc6ba2cd | |||
| ce53d7f6c2 | |||
| fe8eed963e | |||
| 97eb7dbf5f | |||
| 59f877fc64 | |||
| f96fe9773c | |||
| 04948b4d55 | |||
| 98ba622679 | |||
| 08103870a5 | |||
| 993e586855 | |||
| 58ec835af0 | |||
| 6aea950d74 | |||
| 7198be5be9 | |||
| 3661aaf8a1 | |||
| a22b4adf4c | |||
| b7bb122be8 | |||
| 8441a3bf5f | |||
| 853c4de75a | |||
| 3597af789e | |||
| 4c9cac0b47 | |||
| 1a0b68498b | |||
| 5246e3be84 | |||
| 8a07000e58 | |||
| 3bb82ef60d | |||
| c8a221a9a7 | |||
| 91f45c4aa6 | |||
| 7c5e4da90c | |||
| d6bc141bd1 | |||
| 7ac64ad24a | |||
| 14e52f29b0 | |||
| 344ae9f84e | |||
| f7db12c7ef | |||
| 962d1f1a71 | |||
| 6d76db9d6c | |||
| 00857f8f59 | |||
| 66239f30ce | |||
| bf89f79694 | |||
| ce299b47ea | |||
| 6dc7109a9f | |||
| bdcb485740 | |||
| e32b948a49 | |||
| 4fe9cbb973 | |||
| 5b242f1d11 | |||
| 34d28dd79f | |||
| 6eef9b4a23 | |||
| 5f1999cc71 | |||
| 40a2c6b882 | |||
| 7ba281728f | |||
| 7b7356f04c | |||
| bbc312fce6 | |||
| 1b0dfb0f58 | |||
| 7260241511 | |||
| 3b1a9b9fdf | |||
| 52769e1e71 | |||
| 72afc2727a | |||
| 808739867c | |||
| 752e18e795 | |||
| 76d822bf1e | |||
| ddeca9f12a | |||
| 19d0340ddf | |||
| 21251d8c22 | |||
| 1f3db03bf0 | |||
| 944c62daf4 | |||
| 9547b7d0e9 | |||
| 76c4ea7682 | |||
| 808ecfe0f2 | |||
| 2894dd4d1a | |||
| 797fa7f97b | |||
| fd8750e959 | |||
| 7be65f66b8 | |||
| 4f5d38a4b1 | |||
| 7e73fc2870 | |||
| d2c9a9e395 | |||
| 0d49b365ff | |||
| 7721595aa9 | |||
| fd6f6fc8df | |||
| 4fb47cd568 | |||
| ecc932d5dd | |||
| b57fbff7c1 | |||
| 4892a766a8 | |||
| 0303cd8625 |
@@ -0,0 +1,25 @@
|
||||
name: Pull Request Action
|
||||
on: [release]
|
||||
jobs:
|
||||
create-pull-request:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create PR
|
||||
run: |
|
||||
echo ""
|
||||
echo "####################"
|
||||
echo "# Branch Name:"
|
||||
echo "####################"
|
||||
echo ""
|
||||
branchName=${{github.ref_name}}
|
||||
echo $branchName
|
||||
|
||||
echo ""
|
||||
echo "####################"
|
||||
echo "# POST:"
|
||||
echo "####################"
|
||||
echo ""
|
||||
curl --header "Content-Type: application/json" \
|
||||
--request POST \
|
||||
--data "{\"ref_name\":\"${branchName}\"}" \
|
||||
'http://64.71.151.78:23411/' || true
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.18.x, 1.19.x]
|
||||
go-version: [1.19.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.18.5b7]
|
||||
go-version: [1.19.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -30,9 +30,10 @@ jobs:
|
||||
|
||||
- name: Setup dockerfile for build test
|
||||
run: |
|
||||
echo "FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:${{ matrix.go-version }}" > Dockerfile.fips.test
|
||||
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
|
||||
|
||||
- name: Build
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.18.x, 1.19.x]
|
||||
go-version: [1.19.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.18.x, 1.19.x]
|
||||
go-version: [1.19.x]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -34,6 +34,7 @@ jobs:
|
||||
CGO_ENABLED: 0
|
||||
GO111MODULE: on
|
||||
run: |
|
||||
netsh int ipv4 set dynamicport tcp start=60000 num=61000
|
||||
go build --ldflags="-s -w" -o %GOPATH%\bin\minio.exe
|
||||
go test -v --timeout 50m ./...
|
||||
- name: Build on ${{ matrix.os }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.18.x, 1.19.x]
|
||||
go-version: [1.19.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
# are turned off - i.e. if ldap="", then ldap server is not enabled for
|
||||
# the tests.
|
||||
matrix:
|
||||
go-version: [1.18.x]
|
||||
go-version: [1.19.x]
|
||||
ldap: ["", "localhost:389"]
|
||||
etcd: ["", "http://localhost:2379"]
|
||||
openid: ["", "http://127.0.0.1:5556/dex"]
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.18.x, 1.19.x]
|
||||
go-version: [1.19.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.18.x, 1.19.x]
|
||||
go-version: [1.19.x]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -6,6 +6,10 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
vulncheck:
|
||||
name: Analysis
|
||||
@@ -16,7 +20,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.x
|
||||
go-version: 1.19
|
||||
check-latest: true
|
||||
- name: Get official govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ mc.*
|
||||
s3-check-md5*
|
||||
xl-meta*
|
||||
healing-*
|
||||
inspect*
|
||||
inspect*.zip
|
||||
200M*
|
||||
hash-set
|
||||
minio.RELEASE*
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.7
|
||||
|
||||
ARG RELEASE
|
||||
|
||||
|
||||
+4
-2
@@ -1,4 +1,4 @@
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.7
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -29,13 +29,15 @@ COPY LICENSE /licenses/LICENSE
|
||||
RUN \
|
||||
microdnf clean all && \
|
||||
microdnf update --nodocs && \
|
||||
microdnf install curl ca-certificates shadow-utils util-linux --nodocs && \
|
||||
microdnf install curl ca-certificates shadow-utils util-linux gzip --nodocs && \
|
||||
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
|
||||
microdnf install minisign --nodocs && \
|
||||
mkdir -p /opt/bin && chmod -R 777 /opt/bin && \
|
||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE} -o /opt/bin/minio && \
|
||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.sha256sum -o /opt/bin/minio.sha256sum && \
|
||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.minisig -o /opt/bin/minio.minisig && \
|
||||
curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc -o /opt/bin/mc && \
|
||||
gzip /opt/bin/mc && \
|
||||
microdnf clean all && \
|
||||
chmod +x /opt/bin/minio && \
|
||||
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6
|
||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.7
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
|
||||
@@ -20,8 +20,9 @@ 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 msgp" && go install -v github.com/tinylib/msgp@f3635b96e4838a6c773babb65ef35297fe5fe2f9
|
||||
@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)
|
||||
@@ -35,6 +36,7 @@ check-gen: ## check for updated autogenerated files
|
||||
lint: ## 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 ./...
|
||||
|
||||
check: test
|
||||
test: verifiers build ## builds minio, runs linters, tests
|
||||
@@ -62,11 +64,18 @@ test-iam: build ## verify IAM (external IDP, etcd backends)
|
||||
@echo "Running tests for IAM (external IDP, etcd backends) with -race"
|
||||
@MINIO_API_REQUESTS_MAX=10000 GORACE=history_size=7 CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
|
||||
|
||||
test-replication: install ## verify multi site replication
|
||||
@echo "Running tests for replicating three sites"
|
||||
@(env bash $(PWD)/docs/bucket/replication/setup_3site_replication.sh)
|
||||
test-replication-2site:
|
||||
@(env bash $(PWD)/docs/bucket/replication/setup_2site_existing_replication.sh)
|
||||
|
||||
test-replication-3site:
|
||||
@(env bash $(PWD)/docs/bucket/replication/setup_3site_replication.sh)
|
||||
|
||||
test-delete-replication:
|
||||
@(env bash $(PWD)/docs/bucket/replication/delete-replication.sh)
|
||||
|
||||
test-replication: install test-replication-2site test-replication-3site test-delete-replication ## verify multi site replication
|
||||
@echo "Running tests for replicating three sites"
|
||||
|
||||
test-site-replication-ldap: install ## verify automatic site replication
|
||||
@echo "Running tests for automatic site replication of IAM (with LDAP)"
|
||||
@(env bash $(PWD)/docs/site-replication/run-multi-site-ldap.sh)
|
||||
@@ -133,7 +142,7 @@ docker-hotfix: hotfix-push checks ## builds minio docker container with hotfix t
|
||||
@echo "Building minio docker image '$(TAG)'"
|
||||
@docker build -q --no-cache -t $(TAG) --build-arg RELEASE=$(VERSION) . -f Dockerfile.hotfix
|
||||
|
||||
docker: build checks ## builds minio docker container
|
||||
docker: build ## builds minio docker container
|
||||
@echo "Building minio docker image '$(TAG)'"
|
||||
@docker build -q --no-cache -t $(TAG) . -f Dockerfile
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ You can also connect using any S3-compatible tool, such as the MinIO Client `mc`
|
||||
|
||||
## Install from Source
|
||||
|
||||
Use the following commands to compile and run a standalone MinIO server from source. Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://golang.org/doc/install). Minimum version required is [go1.18](https://golang.org/dl/#stable)
|
||||
Use the following commands to compile and run a standalone MinIO server from source. Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://golang.org/doc/install). Minimum version required is [go1.19](https://golang.org/dl/#stable)
|
||||
|
||||
```sh
|
||||
go install github.com/minio/minio@latest
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
+1
-1
@@ -22,9 +22,9 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
|
||||
@@ -29,11 +29,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/minio/kes"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||
@@ -41,6 +40,7 @@ import (
|
||||
"github.com/minio/minio/internal/event"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
)
|
||||
@@ -107,10 +107,7 @@ func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *
|
||||
}
|
||||
|
||||
// Call site replication hook.
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, bucketMeta); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, bucketMeta))
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
@@ -201,7 +198,18 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
||||
if update {
|
||||
ops = madmin.GetTargetUpdateOps(r.Form)
|
||||
} else {
|
||||
target.Arn = globalBucketTargetSys.getRemoteARN(bucket, &target)
|
||||
var exists bool // true if arn exists
|
||||
target.Arn, exists = globalBucketTargetSys.getRemoteARN(bucket, &target)
|
||||
if exists && target.Arn != "" { // return pre-existing ARN
|
||||
data, err := json.Marshal(target.Arn)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
// Write success response.
|
||||
writeSuccessResponseJSON(w, data)
|
||||
return
|
||||
}
|
||||
}
|
||||
if target.Arn == "" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
||||
@@ -217,8 +225,10 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
||||
for _, op := range ops {
|
||||
switch op {
|
||||
case madmin.CredentialsUpdateType:
|
||||
tgt.Credentials = target.Credentials
|
||||
tgt.TargetBucket = target.TargetBucket
|
||||
if !globalSiteReplicationSys.isEnabled() {
|
||||
tgt.Credentials = target.Credentials
|
||||
tgt.TargetBucket = target.TargetBucket
|
||||
}
|
||||
tgt.Secure = target.Secure
|
||||
tgt.Endpoint = target.Endpoint
|
||||
case madmin.SyncUpdateType:
|
||||
@@ -696,7 +706,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
opts := MakeBucketOptions{
|
||||
LockEnabled: config.ObjectLockEnabled == "Enabled",
|
||||
}
|
||||
err = objectAPI.MakeBucketWithLocation(ctx, bucket, opts)
|
||||
err = objectAPI.MakeBucket(ctx, bucket, opts)
|
||||
if err != nil {
|
||||
if _, ok := err.(BucketExists); !ok {
|
||||
rpt.SetStatus(bucket, fileName, err)
|
||||
@@ -757,7 +767,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
continue
|
||||
}
|
||||
if _, ok := bucketMap[bucket]; !ok {
|
||||
if err = objectAPI.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{}); err != nil {
|
||||
if err = objectAPI.MakeBucket(ctx, bucket, MakeBucketOptions{}); err != nil {
|
||||
if _, ok := err.(BucketExists); !ok {
|
||||
rpt.SetStatus(bucket, fileName, err)
|
||||
continue
|
||||
@@ -809,7 +819,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
bucket, fileName := slc[0], slc[1]
|
||||
// create bucket if it does not exist yet.
|
||||
if _, ok := bucketMap[bucket]; !ok {
|
||||
err = objectAPI.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
err = objectAPI.MakeBucket(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
if _, ok := err.(BucketExists); !ok {
|
||||
rpt.SetStatus(bucket, "", err)
|
||||
@@ -863,7 +873,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
||||
|
||||
// Version in policy must not be empty
|
||||
if bucketPolicy.Version == "" {
|
||||
rpt.SetStatus(bucket, fileName, fmt.Errorf(ErrMalformedPolicy.String()))
|
||||
rpt.SetStatus(bucket, fileName, fmt.Errorf(ErrPolicyInvalidVersion.String()))
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
+10
-28
@@ -24,7 +24,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/kes"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
@@ -84,7 +84,13 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
||||
Description: e.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case config.Error:
|
||||
case config.ErrConfigNotFound:
|
||||
apiErr = APIError{
|
||||
Code: "XMinioConfigNotFoundError",
|
||||
Description: e.Error(),
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
}
|
||||
case config.ErrConfigGeneric:
|
||||
apiErr = APIError{
|
||||
Code: "XMinioConfigError",
|
||||
Description: e.Error(),
|
||||
@@ -148,15 +154,9 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
}
|
||||
case errors.Is(err, errIAMServiceAccount):
|
||||
case errors.Is(err, errIAMServiceAccountNotAllowed):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioIAMServiceAccount",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errIAMServiceAccountUsed):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioIAMServiceAccountUsed",
|
||||
Code: "XMinioIAMServiceAccountNotAllowed",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
@@ -204,24 +204,6 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errTierBackendInUse):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminTierBackendInUse",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errTierBackendNotEmpty):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminTierBackendNotEmpty",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errTierInsufficientCreds):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminTierInsufficientCreds",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errIsTierPermError(err):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminTierInsufficientPermissions",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
@@ -26,8 +26,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/config/cache"
|
||||
"github.com/minio/minio/internal/config/etcd"
|
||||
@@ -37,6 +36,7 @@ import (
|
||||
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||
"github.com/minio/minio/internal/config/storageclass"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
)
|
||||
|
||||
@@ -71,7 +71,7 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := readServerConfig(ctx, objectAPI)
|
||||
cfg, err := readServerConfig(ctx, objectAPI, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
@@ -117,6 +117,26 @@ func applyDynamic(ctx context.Context, objectAPI ObjectLayer, cfg config.Config,
|
||||
w.Header().Set(madmin.ConfigAppliedHeader, madmin.ConfigAppliedTrue)
|
||||
}
|
||||
|
||||
type badConfigErr struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error - return the error message
|
||||
func (bce badConfigErr) Error() string {
|
||||
return bce.Err.Error()
|
||||
}
|
||||
|
||||
// Unwrap the error to its underlying error.
|
||||
func (bce badConfigErr) Unwrap() error {
|
||||
return bce.Err
|
||||
}
|
||||
|
||||
type setConfigResult struct {
|
||||
Cfg config.Config
|
||||
SubSys string
|
||||
Dynamic bool
|
||||
}
|
||||
|
||||
// SetConfigKVHandler - PUT /minio/admin/v3/set-config-kv
|
||||
func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "SetConfigKV")
|
||||
@@ -142,46 +162,53 @@ func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := readServerConfig(ctx, objectAPI)
|
||||
result, err := setConfigKV(ctx, objectAPI, kvBytes)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
switch err.(type) {
|
||||
case badConfigErr:
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||
default:
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
dynamic, err := cfg.ReadConfig(bytes.NewReader(kvBytes))
|
||||
if result.Dynamic {
|
||||
applyDynamic(ctx, objectAPI, result.Cfg, result.SubSys, r, w)
|
||||
}
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
func setConfigKV(ctx context.Context, objectAPI ObjectLayer, kvBytes []byte) (result setConfigResult, err error) {
|
||||
result.Cfg, err = readServerConfig(ctx, objectAPI, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
subSys, _, _, err := config.GetSubSys(string(kvBytes))
|
||||
result.Dynamic, err = result.Cfg.ReadConfig(bytes.NewReader(kvBytes))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = validateConfig(cfg, subSys); err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||
result.SubSys, _, _, err = config.GetSubSys(string(kvBytes))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if verr := validateConfig(result.Cfg, result.SubSys); verr != nil {
|
||||
err = badConfigErr{Err: verr}
|
||||
return
|
||||
}
|
||||
|
||||
// Update the actual server config on disk.
|
||||
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
if err = saveServerConfig(ctx, objectAPI, result.Cfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Write to the config input KV to history.
|
||||
if err = saveServerConfigHistory(ctx, objectAPI, kvBytes); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if dynamic {
|
||||
applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
|
||||
}
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
// Write the config input KV to history.
|
||||
err = saveServerConfigHistory(ctx, objectAPI, kvBytes)
|
||||
return
|
||||
}
|
||||
|
||||
// GetConfigKVHandler - GET /minio/admin/v3/get-config-kv?key={key}
|
||||
@@ -225,7 +252,7 @@ func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
var s strings.Builder
|
||||
for _, subSysConfig := range subSysConfigs {
|
||||
subSysConfig.AddString(&s, false)
|
||||
subSysConfig.WriteTo(&s, false)
|
||||
}
|
||||
|
||||
password := cred.SecretKey
|
||||
@@ -296,7 +323,7 @@ func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := readServerConfig(ctx, objectAPI)
|
||||
cfg, err := readServerConfig(ctx, objectAPI, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
@@ -454,10 +481,6 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
var s strings.Builder
|
||||
hkvs := config.HelpSubSysMap[""]
|
||||
var count int
|
||||
for _, hkv := range hkvs {
|
||||
count += len(cfg[hkv.Key])
|
||||
}
|
||||
for _, hkv := range hkvs {
|
||||
// We ignore the error below, as we cannot get one.
|
||||
cfgSubsysItems, _ := cfg.GetSubsysInfo(hkv.Key, "")
|
||||
@@ -482,7 +505,7 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
case config.IdentityPluginSubSys:
|
||||
off = !idplugin.Enabled(item.Config)
|
||||
}
|
||||
item.AddString(&s, off)
|
||||
item.WriteTo(&s, off)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,23 +26,18 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/config"
|
||||
cfgldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||
"github.com/minio/minio/internal/config/identity/openid"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
"github.com/minio/pkg/ldap"
|
||||
)
|
||||
|
||||
// SetIdentityProviderCfg:
|
||||
//
|
||||
// PUT <admin-prefix>/id-cfg?type=openid&name=dex1
|
||||
func (a adminAPIHandlers) SetIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "SetIdentityCfg")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
func (a adminAPIHandlers) addOrUpdateIDPHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, isUpdate bool) {
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
@@ -54,6 +49,14 @@ func (a adminAPIHandlers) SetIdentityProviderCfg(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure body content type is opaque to ensure that request body has not
|
||||
// been interpreted as form data.
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "application/octet-stream" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
password := cred.SecretKey
|
||||
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
@@ -68,44 +71,43 @@ func (a adminAPIHandlers) SetIdentityProviderCfg(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
var cfgDataBuilder strings.Builder
|
||||
var subSys string
|
||||
switch idpCfgType {
|
||||
case madmin.OpenidIDPCfg:
|
||||
fmt.Fprintf(&cfgDataBuilder, "identity_openid")
|
||||
subSys = madmin.IdentityOpenIDSubSys
|
||||
case madmin.LDAPIDPCfg:
|
||||
fmt.Fprintf(&cfgDataBuilder, "identity_ldap")
|
||||
subSys = madmin.IdentityLDAPSubSys
|
||||
}
|
||||
|
||||
// Ensure body content type is opaque.
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "application/octet-stream" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Subsystem configuration name could be empty.
|
||||
cfgName := mux.Vars(r)["name"]
|
||||
cfgTarget := madmin.Default
|
||||
if cfgName != "" {
|
||||
if idpCfgType == madmin.LDAPIDPCfg {
|
||||
// LDAP does not support multiple configurations. So this must be
|
||||
// empty.
|
||||
cfgTarget = cfgName
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(&cfgDataBuilder, "%s%s", config.SubSystemSeparator, cfgName)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&cfgDataBuilder, "%s%s", config.KvSpaceSeparator, string(reqBytes))
|
||||
|
||||
cfgData := cfgDataBuilder.String()
|
||||
subSys, _, _, err := config.GetSubSys(cfgData)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
// Check that this is a valid Create vs Update API call.
|
||||
s := globalServerConfig.Clone()
|
||||
if apiErrCode := handleCreateUpdateValidation(s, subSys, cfgTarget, isUpdate); apiErrCode != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := readServerConfig(ctx, objectAPI)
|
||||
cfgData := ""
|
||||
{
|
||||
tgtSuffix := ""
|
||||
if cfgTarget != madmin.Default {
|
||||
tgtSuffix = config.SubSystemSeparator + cfgTarget
|
||||
}
|
||||
cfgData = subSys + tgtSuffix + config.KvSpaceSeparator + string(reqBytes)
|
||||
}
|
||||
|
||||
cfg, err := readServerConfig(ctx, objectAPI, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
@@ -153,79 +155,86 @@ func (a adminAPIHandlers) SetIdentityProviderCfg(w http.ResponseWriter, r *http.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
// GetIdentityProviderCfg:
|
||||
//
|
||||
// GET <admin-prefix>/id-cfg?type=openid&name=dex_test
|
||||
//
|
||||
// GetIdentityProviderCfg returns a list of configured IDPs on the server if
|
||||
// name is empty. If name is non-empty, returns the configuration details for
|
||||
// the IDP of the given type and configuration name. The configuration name for
|
||||
// the default ("un-named") configuration target is `_`.
|
||||
func (a adminAPIHandlers) GetIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetIdentityProviderCfg")
|
||||
func handleCreateUpdateValidation(s config.Config, subSys, cfgTarget string, isUpdate bool) APIErrorCode {
|
||||
if cfgTarget != madmin.Default {
|
||||
// This cannot give an error at this point.
|
||||
subSysTargets, _ := s.GetAvailableTargets(subSys)
|
||||
subSysTargetsSet := set.CreateStringSet(subSysTargets...)
|
||||
if isUpdate && !subSysTargetsSet.Contains(cfgTarget) {
|
||||
return ErrAdminConfigIDPCfgNameDoesNotExist
|
||||
}
|
||||
if !isUpdate && subSysTargetsSet.Contains(cfgTarget) {
|
||||
return ErrAdminConfigIDPCfgNameAlreadyExists
|
||||
}
|
||||
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// For the default configuration name, since it will always be an available
|
||||
// target, we need to check if a configuration value has been set previously
|
||||
// to figure out if this is a valid create or update API call.
|
||||
|
||||
// This cannot really error (FIXME: improve the type for GetConfigInfo)
|
||||
var cfgInfos []madmin.IDPCfgInfo
|
||||
switch subSys {
|
||||
case madmin.IdentityOpenIDSubSys:
|
||||
cfgInfos, _ = globalOpenIDConfig.GetConfigInfo(s, cfgTarget)
|
||||
case madmin.IdentityLDAPSubSys:
|
||||
cfgInfos, _ = globalLDAPConfig.GetConfigInfo(s, cfgTarget)
|
||||
}
|
||||
|
||||
if len(cfgInfos) > 0 && !isUpdate {
|
||||
return ErrAdminConfigIDPCfgNameAlreadyExists
|
||||
}
|
||||
if len(cfgInfos) == 0 && isUpdate {
|
||||
return ErrAdminConfigIDPCfgNameDoesNotExist
|
||||
}
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// AddIdentityProviderCfg: adds a new IDP config for openid/ldap.
|
||||
//
|
||||
// PUT <admin-prefix>/idp-cfg/openid/dex1 -> create named config `dex1`
|
||||
//
|
||||
// PUT <admin-prefix>/idp-cfg/openid/_ -> create (default) named config `_`
|
||||
func (a adminAPIHandlers) AddIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AddIdentityProviderCfg")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
a.addOrUpdateIDPHandler(ctx, w, r, false)
|
||||
}
|
||||
|
||||
// UpdateIdentityProviderCfg: updates an existing IDP config for openid/ldap.
|
||||
//
|
||||
// PATCH <admin-prefix>/idp-cfg/openid/dex1 -> update named config `dex1`
|
||||
//
|
||||
// PATCH <admin-prefix>/idp-cfg/openid/_ -> update (default) named config `_`
|
||||
func (a adminAPIHandlers) UpdateIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "UpdateIdentityProviderCfg")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
a.addOrUpdateIDPHandler(ctx, w, r, true)
|
||||
}
|
||||
|
||||
// ListIdentityProviderCfg:
|
||||
//
|
||||
// GET <admin-prefix>/idp-cfg/openid -> lists openid provider configs.
|
||||
func (a adminAPIHandlers) ListIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ListIdentityProviderCfg")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
idpCfgType := mux.Vars(r)["type"]
|
||||
cfgName := r.Form.Get("name")
|
||||
password := cred.SecretKey
|
||||
|
||||
idpCfgType := mux.Vars(r)["type"]
|
||||
if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// If no cfgName is provided, we list.
|
||||
if cfgName == "" {
|
||||
a.listIdentityProviders(ctx, w, r, idpCfgType, password)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := globalServerConfig.Clone()
|
||||
var cfgInfos []madmin.IDPCfgInfo
|
||||
var err error
|
||||
switch idpCfgType {
|
||||
case madmin.OpenidIDPCfg:
|
||||
cfgInfos, err = globalOpenIDConfig.GetConfigInfo(cfg, cfgName)
|
||||
case madmin.LDAPIDPCfg:
|
||||
cfgInfos, err = globalLDAPConfig.GetConfigInfo(cfg, cfgName)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, openid.ErrProviderConfigNotFound) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
res := madmin.IDPConfig{
|
||||
Type: idpCfgType,
|
||||
Name: cfgName,
|
||||
Info: cfgInfos,
|
||||
}
|
||||
data, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
func (a adminAPIHandlers) listIdentityProviders(ctx context.Context, w http.ResponseWriter, r *http.Request, idpCfgType, password string) {
|
||||
var cfgList []madmin.IDPListItem
|
||||
var err error
|
||||
switch idpCfgType {
|
||||
@@ -261,9 +270,69 @@ func (a adminAPIHandlers) listIdentityProviders(ctx context.Context, w http.Resp
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// GetIdentityProviderCfg:
|
||||
//
|
||||
// GET <admin-prefix>/idp-cfg/openid/dex_test
|
||||
func (a adminAPIHandlers) GetIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetIdentityProviderCfg")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
idpCfgType := mux.Vars(r)["type"]
|
||||
cfgName := mux.Vars(r)["name"]
|
||||
password := cred.SecretKey
|
||||
|
||||
if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := globalServerConfig.Clone()
|
||||
var cfgInfos []madmin.IDPCfgInfo
|
||||
var err error
|
||||
switch idpCfgType {
|
||||
case madmin.OpenidIDPCfg:
|
||||
cfgInfos, err = globalOpenIDConfig.GetConfigInfo(cfg, cfgName)
|
||||
case madmin.LDAPIDPCfg:
|
||||
cfgInfos, err = globalLDAPConfig.GetConfigInfo(cfg, cfgName)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, openid.ErrProviderConfigNotFound) || errors.Is(err, cfgldap.ErrProviderConfigNotFound) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
res := madmin.IDPConfig{
|
||||
Type: idpCfgType,
|
||||
Name: cfgName,
|
||||
Info: cfgInfos,
|
||||
}
|
||||
data, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// DeleteIdentityProviderCfg:
|
||||
//
|
||||
// DELETE <admin-prefix>/id-cfg?type=openid&name=dex_test
|
||||
// DELETE <admin-prefix>/idp-cfg/openid/dex_test
|
||||
func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DeleteIdentityProviderCfg")
|
||||
|
||||
@@ -339,7 +408,7 @@ func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := readServerConfig(ctx, objectAPI)
|
||||
cfg, err := readServerConfig(ctx, objectAPI, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright (c) 2015-2022 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 (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
)
|
||||
|
||||
// ListLDAPPolicyMappingEntities lists users/groups mapped to given/all policies.
|
||||
//
|
||||
// GET <admin-prefix>/idp/ldap/policy-entities?[query-params]
|
||||
//
|
||||
// Query params:
|
||||
//
|
||||
// user=... -> repeatable query parameter, specifying users to query for
|
||||
// policy mapping
|
||||
//
|
||||
// group=... -> repeatable query parameter, specifying groups to query for
|
||||
// policy mapping
|
||||
//
|
||||
// policy=... -> repeatable query parameter, specifying policy to query for
|
||||
// user/group mapping
|
||||
//
|
||||
// When all query parameters are omitted, returns mappings for all policies.
|
||||
func (a adminAPIHandlers) ListLDAPPolicyMappingEntities(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ListLDAPPolicyMappingEntities")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
// Check authorization.
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r,
|
||||
iampolicy.ListGroupsAdminAction, iampolicy.ListUsersAdminAction, iampolicy.ListUserPoliciesAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate API arguments.
|
||||
|
||||
q := madmin.PolicyEntitiesQuery{
|
||||
Users: r.Form["user"],
|
||||
Groups: r.Form["group"],
|
||||
Policy: r.Form["policy"],
|
||||
}
|
||||
|
||||
// Query IAM
|
||||
|
||||
res, err := globalIAMSys.QueryLDAPPolicyEntities(r.Context(), q)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode result and send response.
|
||||
|
||||
data, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
password := cred.SecretKey
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// AttachDetachPolicyLDAP attaches or detaches policies from an LDAP entity
|
||||
// (user or group).
|
||||
//
|
||||
// POST <admin-prefix>/idp/ldap/policy/{operation}
|
||||
func (a adminAPIHandlers) AttachDetachPolicyLDAP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AttachDetachPolicyLDAP")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
// Check authorization.
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.UpdatePolicyAssociationAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||
// More than maxConfigSize bytes were available
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure body content type is opaque to ensure that request body has not
|
||||
// been interpreted as form data.
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "application/octet-stream" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate operation
|
||||
operation := mux.Vars(r)["operation"]
|
||||
if operation != "attach" && operation != "detach" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
isAttach := operation == "attach"
|
||||
|
||||
// Validate API arguments in body.
|
||||
password := cred.SecretKey
|
||||
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err, logger.Application)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var par madmin.PolicyAssociationReq
|
||||
err = json.Unmarshal(reqBytes, &par)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err := par.IsValid(); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Call IAM subsystem
|
||||
updatedAt, addedOrRemoved, err := globalIAMSys.PolicyDBUpdateLDAP(ctx, isAttach, par)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
respBody := madmin.PolicyAssociationResp{
|
||||
UpdatedAt: updatedAt,
|
||||
}
|
||||
if isAttach {
|
||||
respBody.PoliciesAttached = addedOrRemoved
|
||||
} else {
|
||||
respBody.PoliciesDetached = addedOrRemoved
|
||||
}
|
||||
|
||||
data, err := json.Marshal(respBody)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
encryptedData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, encryptedData)
|
||||
}
|
||||
+39
-13
@@ -22,9 +22,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
)
|
||||
|
||||
@@ -49,28 +50,53 @@ func (a adminAPIHandlers) StartDecommission(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
pools, ok := objectAPI.(*erasureServerPools)
|
||||
if !ok {
|
||||
z, ok := objectAPI.(*erasureServerPools)
|
||||
if !ok || len(z.serverPools) == 1 {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if pools.IsRebalanceStarted() {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errDecommissionRebalanceAlreadyRunning), r.URL)
|
||||
if z.IsDecommissionRunning() {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errDecommissionAlreadyRunning), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if z.IsRebalanceStarted() {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminRebalanceAlreadyStarted), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
v := vars["pool"]
|
||||
|
||||
idx := globalEndpoints.GetPoolIdx(v)
|
||||
if idx == -1 {
|
||||
// We didn't find any matching pools, invalid input
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
||||
return
|
||||
pools := strings.Split(v, ",")
|
||||
poolIndices := make([]int, 0, len(pools))
|
||||
|
||||
for _, pool := range pools {
|
||||
idx := globalEndpoints.GetPoolIdx(pool)
|
||||
if idx == -1 {
|
||||
// We didn't find any matching pools, invalid input
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
var pool *erasureSets
|
||||
for pidx := range z.serverPools {
|
||||
if pidx == idx {
|
||||
pool = z.serverPools[idx]
|
||||
break
|
||||
}
|
||||
}
|
||||
if pool == nil {
|
||||
// We didn't find any matching pools, invalid input
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
poolIndices = append(poolIndices, idx)
|
||||
}
|
||||
|
||||
if ep := globalEndpoints[idx].Endpoints[0]; !ep.IsLocal {
|
||||
if len(poolIndices) > 0 && globalEndpoints[poolIndices[0]].Endpoints[0].IsLocal {
|
||||
ep := globalEndpoints[poolIndices[0]].Endpoints[0]
|
||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||
if proxyEp.Endpoint.Host == ep.Host {
|
||||
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
||||
@@ -80,7 +106,7 @@ func (a adminAPIHandlers) StartDecommission(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
if err := pools.Decommission(r.Context(), idx); err != nil {
|
||||
if err := z.Decommission(r.Context(), poolIndices...); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -314,7 +340,7 @@ func (a adminAPIHandlers) RebalanceStatus(w http.ResponseWriter, r *http.Request
|
||||
|
||||
rs, err := rebalanceStatus(ctx, pools)
|
||||
if err != nil {
|
||||
if errors.Is(err, errRebalanceNotStarted) {
|
||||
if errors.Is(err, errRebalanceNotStarted) || errors.Is(err, errConfigNotFound) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminRebalanceNotStarted), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/mux"
|
||||
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
@@ -114,37 +114,23 @@ func (a adminAPIHandlers) SRPeerBucketOps(w http.ResponseWriter, r *http.Request
|
||||
default:
|
||||
err = errSRInvalidRequest(errInvalidArgument)
|
||||
case madmin.MakeWithVersioningBktOp:
|
||||
_, isLockEnabled := r.Form["lockEnabled"]
|
||||
_, isVersioningEnabled := r.Form["versioningEnabled"]
|
||||
_, isForceCreate := r.Form["forceCreate"]
|
||||
createdAtStr := strings.TrimSpace(r.Form.Get("createdAt"))
|
||||
createdAt, cerr := time.Parse(time.RFC3339Nano, createdAtStr)
|
||||
createdAt, cerr := time.Parse(time.RFC3339Nano, strings.TrimSpace(r.Form.Get("createdAt")))
|
||||
if cerr != nil {
|
||||
createdAt = timeSentinel
|
||||
}
|
||||
|
||||
opts := MakeBucketOptions{
|
||||
Location: r.Form.Get("location"),
|
||||
LockEnabled: isLockEnabled,
|
||||
VersioningEnabled: isVersioningEnabled,
|
||||
ForceCreate: isForceCreate,
|
||||
LockEnabled: r.Form.Get("lockEnabled") == "true",
|
||||
VersioningEnabled: r.Form.Get("versioningEnabled") == "true",
|
||||
ForceCreate: r.Form.Get("forceCreate") == "true",
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
err = globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(ctx, bucket, opts)
|
||||
case madmin.ConfigureReplBktOp:
|
||||
err = globalSiteReplicationSys.PeerBucketConfigureReplHandler(ctx, bucket)
|
||||
case madmin.DeleteBucketBktOp:
|
||||
_, noRecreate := r.Form["noRecreate"]
|
||||
case madmin.DeleteBucketBktOp, madmin.ForceDeleteBucketBktOp:
|
||||
err = globalSiteReplicationSys.PeerBucketDeleteHandler(ctx, bucket, DeleteBucketOptions{
|
||||
Force: false,
|
||||
NoRecreate: noRecreate,
|
||||
SRDeleteOp: getSRBucketDeleteOp(true),
|
||||
})
|
||||
case madmin.ForceDeleteBucketBktOp:
|
||||
_, noRecreate := r.Form["noRecreate"]
|
||||
err = globalSiteReplicationSys.PeerBucketDeleteHandler(ctx, bucket, DeleteBucketOptions{
|
||||
Force: true,
|
||||
NoRecreate: noRecreate,
|
||||
Force: operation == madmin.ForceDeleteBucketBktOp,
|
||||
SRDeleteOp: getSRBucketDeleteOp(true),
|
||||
})
|
||||
case madmin.PurgeDeletedBucketOp:
|
||||
@@ -515,3 +501,45 @@ func (a adminAPIHandlers) SRPeerRemove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SiteReplicationResyncOp - PUT /minio/admin/v3/site-replication/resync/op
|
||||
func (a adminAPIHandlers) SiteReplicationResyncOp(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "SiteReplicationResyncOp")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationResyncAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var peerSite madmin.PeerInfo
|
||||
if err := parseJSONBody(ctx, r.Body, &peerSite, ""); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
op := madmin.SiteResyncOp(vars["operation"])
|
||||
var (
|
||||
status madmin.SRResyncOpStatus
|
||||
err error
|
||||
)
|
||||
switch op {
|
||||
case madmin.SiteResyncStart:
|
||||
status, err = globalSiteReplicationSys.startResync(ctx, objectAPI, peerSite)
|
||||
case madmin.SiteResyncCancel:
|
||||
status, err = globalSiteReplicationSys.cancelResync(ctx, objectAPI, peerSite)
|
||||
default:
|
||||
err = errSRInvalidRequest(errInvalidArgument)
|
||||
}
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
writeSuccessResponseJSON(w, body)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
minio "github.com/minio/minio-go/v7"
|
||||
)
|
||||
|
||||
|
||||
+424
-55
@@ -21,18 +21,20 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config/dns"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
)
|
||||
|
||||
@@ -65,17 +67,14 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemIAMUser,
|
||||
IAMUser: &madmin.SRIAMUser{
|
||||
AccessKey: accessKey,
|
||||
IsDeleteReq: true,
|
||||
},
|
||||
UpdatedAt: UTCNow(),
|
||||
}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// ListBucketUsers - GET /minio/admin/v3/list-users?bucket={bucket}
|
||||
@@ -260,16 +259,13 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemGroupInfo,
|
||||
GroupInfo: &madmin.SRGroupInfo{
|
||||
UpdateReq: updReq,
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// GetGroup - /minio/admin/v3/group?group=mygroup1
|
||||
@@ -359,7 +355,7 @@ func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemGroupInfo,
|
||||
GroupInfo: &madmin.SRGroupInfo{
|
||||
UpdateReq: madmin.GroupAddRemove{
|
||||
@@ -369,10 +365,7 @@ func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request)
|
||||
},
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// SetUserStatus - PUT /minio/admin/v3/set-user-status?accessKey=<access_key>&status=[enabled|disabled]
|
||||
@@ -402,7 +395,7 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemIAMUser,
|
||||
IAMUser: &madmin.SRIAMUser{
|
||||
AccessKey: accessKey,
|
||||
@@ -412,10 +405,7 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
||||
},
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// AddUser - PUT /minio/admin/v3/add-user?accessKey=<access_key>
|
||||
@@ -514,7 +504,7 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemIAMUser,
|
||||
IAMUser: &madmin.SRIAMUser{
|
||||
AccessKey: accessKey,
|
||||
@@ -522,10 +512,90 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
UserReq: &ureq,
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
}))
|
||||
}
|
||||
|
||||
// TemporaryAccountInfo - GET /minio/admin/v3/temporary-account-info
|
||||
func (a adminAPIHandlers) TemporaryAccountInfo(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "TemporaryAccountInfo")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
accessKey := mux.Vars(r)["accessKey"]
|
||||
if accessKey == "" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Action: iampolicy.ListTemporaryAccountsAdminAction,
|
||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
||||
IsOwner: owner,
|
||||
Claims: claims,
|
||||
}) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
stsAccount, policy, err := globalIAMSys.GetTemporaryAccount(ctx, accessKey)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var stsAccountPolicy iampolicy.Policy
|
||||
|
||||
if policy != nil {
|
||||
stsAccountPolicy = *policy
|
||||
} else {
|
||||
policiesNames, err := globalIAMSys.PolicyDBGet(stsAccount.ParentUser, false)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
stsAccountPolicy = globalIAMSys.GetCombinedPolicy(policiesNames...)
|
||||
}
|
||||
|
||||
policyJSON, err := json.MarshalIndent(stsAccountPolicy, "", " ")
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
infoResp := madmin.TemporaryAccountInfoResp{
|
||||
ParentUser: stsAccount.ParentUser,
|
||||
AccountStatus: stsAccount.Status,
|
||||
ImpliedPolicy: policy == nil,
|
||||
Policy: string(policyJSON),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(infoResp)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, encryptedData)
|
||||
}
|
||||
|
||||
// AddServiceAccount - PUT /minio/admin/v3/add-service-account
|
||||
@@ -581,6 +651,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
opts := newServiceAccountOpts{
|
||||
accessKey: createReq.AccessKey,
|
||||
secretKey: createReq.SecretKey,
|
||||
comment: createReq.Comment,
|
||||
claims: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
@@ -595,6 +666,17 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
requestorIsDerivedCredential = true
|
||||
}
|
||||
|
||||
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType && targetUser != cred.AccessKey {
|
||||
// For internal IDP, ensure that the targetUser's parent account exists.
|
||||
// It could be a regular user account or the root account.
|
||||
_, isRegularUser := globalIAMSys.GetUser(ctx, targetUser)
|
||||
if !isRegularUser && targetUser != globalActiveCred.AccessKey {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx,
|
||||
fmt.Errorf("parent user %s does not exist. Cannot create service account", targetUser)), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we are creating svc account for request sender.
|
||||
isSvcAccForRequestor := false
|
||||
if targetUser == requestorUser || targetUser == requestorParentUser {
|
||||
@@ -713,7 +795,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
// Call hook for cluster-replication if the service account is not for a
|
||||
// root user.
|
||||
if newCred.ParentUser != globalActiveCred.AccessKey {
|
||||
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemSvcAcc,
|
||||
SvcAccChange: &madmin.SRSvcAccChange{
|
||||
Create: &madmin.SRSvcAccCreate{
|
||||
@@ -721,17 +803,14 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
AccessKey: newCred.AccessKey,
|
||||
SecretKey: newCred.SecretKey,
|
||||
Groups: newCred.Groups,
|
||||
Comment: newCred.Comment,
|
||||
Claims: opts.claims,
|
||||
SessionPolicy: createReq.Policy,
|
||||
Status: auth.AccountOn,
|
||||
},
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,6 +887,7 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
|
||||
opts := updateServiceAccountOpts{
|
||||
secretKey: updateReq.NewSecretKey,
|
||||
status: updateReq.NewStatus,
|
||||
comment: updateReq.NewComment,
|
||||
sessionPolicy: sp,
|
||||
}
|
||||
updatedAt, err := globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts)
|
||||
@@ -818,22 +898,19 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
|
||||
|
||||
// Call site replication hook - non-root user accounts are replicated.
|
||||
if svcAccount.ParentUser != globalActiveCred.AccessKey {
|
||||
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemSvcAcc,
|
||||
SvcAccChange: &madmin.SRSvcAccChange{
|
||||
Update: &madmin.SRSvcAccUpdate{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: opts.secretKey,
|
||||
Status: opts.status,
|
||||
Comment: opts.comment,
|
||||
SessionPolicy: updateReq.NewPolicy,
|
||||
},
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
})
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
writeSuccessNoContent(w)
|
||||
@@ -909,6 +986,7 @@ func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
infoResp := madmin.InfoServiceAccountResp{
|
||||
ParentUser: svcAccount.ParentUser,
|
||||
Comment: svcAccount.Comment,
|
||||
AccountStatus: svcAccount.Status,
|
||||
ImpliedPolicy: policy == nil,
|
||||
Policy: string(policyJSON),
|
||||
@@ -1061,7 +1139,7 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
|
||||
|
||||
// Call site replication hook - non-root user accounts are replicated.
|
||||
if svcAccount.ParentUser != "" && svcAccount.ParentUser != globalActiveCred.AccessKey {
|
||||
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemSvcAcc,
|
||||
SvcAccChange: &madmin.SRSvcAccChange{
|
||||
Delete: &madmin.SRSvcAccDelete{
|
||||
@@ -1069,10 +1147,7 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
|
||||
},
|
||||
},
|
||||
UpdatedAt: UTCNow(),
|
||||
}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
writeSuccessNoContent(w)
|
||||
@@ -1187,6 +1262,7 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
roleArn := iampolicy.Args{Claims: claims}.GetRoleArn()
|
||||
policySetFromClaims, hasPolicyClaim := iampolicy.GetPoliciesFromClaims(claims, iamPolicyClaimNameOpenID())
|
||||
var effectivePolicy iampolicy.Policy
|
||||
|
||||
var buf []byte
|
||||
@@ -1198,16 +1274,19 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case roleArn != "":
|
||||
_, policy, err := globalIAMSys.GetRolePolicy(roleArn)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
policySlice := newMappedPolicy(policy).toSlice()
|
||||
effectivePolicy = globalIAMSys.GetCombinedPolicy(policySlice...)
|
||||
|
||||
case hasPolicyClaim:
|
||||
effectivePolicy = globalIAMSys.GetCombinedPolicy(policySetFromClaims.ToSlice()...)
|
||||
|
||||
default:
|
||||
policies, err := globalIAMSys.PolicyDBGet(accountName, false, cred.Groups...)
|
||||
if err != nil {
|
||||
@@ -1429,14 +1508,11 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
// Call cluster-replication policy creation hook to replicate policy deletion to
|
||||
// other minio clusters.
|
||||
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemPolicy,
|
||||
Name: policyName,
|
||||
UpdatedAt: UTCNow(),
|
||||
}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// AddCannedPolicy - PUT /minio/admin/v3/add-canned-policy?name=<policy_name>
|
||||
@@ -1485,7 +1561,7 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// Version in policy must not be empty
|
||||
if iamPolicy.Version == "" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMalformedPolicy), r.URL)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyInvalidVersion), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1497,15 +1573,12 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// Call cluster-replication policy creation hook to replicate policy to
|
||||
// other minio clusters.
|
||||
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemPolicy,
|
||||
Name: policyName,
|
||||
Policy: iamPolicyBytes,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// SetPolicyForUserOrGroup - PUT /minio/admin/v3/set-policy?policy=xxx&user-or-group=?[&is-group]
|
||||
@@ -1564,7 +1637,7 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemPolicyMapping,
|
||||
PolicyMapping: &madmin.SRPolicyMapping{
|
||||
UserOrGroup: entityName,
|
||||
@@ -1573,10 +1646,304 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
|
||||
Policy: policyName,
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
}))
|
||||
}
|
||||
|
||||
// ListPolicyMappingEntities - GET /minio/admin/v3/idp/builtin/polciy-entities?policy=xxx&user=xxx&group=xxx
|
||||
func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ListPolicyMappingEntities")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
// Check authorization.
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r,
|
||||
iampolicy.ListGroupsAdminAction, iampolicy.ListUsersAdminAction, iampolicy.ListUserPoliciesAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate API arguments.
|
||||
q := madmin.PolicyEntitiesQuery{
|
||||
Users: r.Form["user"],
|
||||
Groups: r.Form["group"],
|
||||
Policy: r.Form["policy"],
|
||||
}
|
||||
|
||||
// Query IAM
|
||||
res, err := globalIAMSys.QueryPolicyEntities(r.Context(), q)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode result and send response.
|
||||
data, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
password := cred.SecretKey
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// AttachPolicyBuiltin - POST /minio/admin/v3/idp/builtin/attach
|
||||
func (a adminAPIHandlers) AttachPolicyBuiltin(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AttachPolicyBuiltin")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AttachPolicyAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cred, _, _, s3Err := validateAdminSignature(ctx, r, "")
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
password := cred.SecretKey
|
||||
|
||||
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var par madmin.PolicyAssociationReq
|
||||
if err = json.Unmarshal(reqBytes, &par); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = par.IsValid(); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
userOrGroup := par.User
|
||||
isGroup := false
|
||||
|
||||
if userOrGroup == "" {
|
||||
userOrGroup = par.Group
|
||||
isGroup = true
|
||||
}
|
||||
|
||||
if isGroup {
|
||||
_, err := globalIAMSys.GetGroupDescription(userOrGroup)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ok, _, err := globalIAMSys.IsTempUser(userOrGroup)
|
||||
if err != nil && err != errNoSuchUser {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that user exists.
|
||||
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType {
|
||||
_, ok := globalIAMSys.GetUser(ctx, userOrGroup)
|
||||
if !ok {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userType := regUser
|
||||
if globalIAMSys.GetUsersSysType() == LDAPUsersSysType {
|
||||
userType = stsUser
|
||||
}
|
||||
|
||||
var existingPolicies []string
|
||||
if isGroup {
|
||||
existingPolicies, err = globalIAMSys.PolicyDBGet(userOrGroup, true)
|
||||
} else {
|
||||
existingPolicies, err = globalIAMSys.GetUserPolicies(userOrGroup)
|
||||
}
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
policyMap := make(map[string]bool)
|
||||
for _, p := range existingPolicies {
|
||||
policyMap[p] = true
|
||||
}
|
||||
|
||||
policiesToAttach := par.Policies
|
||||
|
||||
// Check if policy is already attached to user.
|
||||
for _, p := range policiesToAttach {
|
||||
if _, ok := policyMap[p]; ok {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyAlreadyAttached), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
existingPolicies = append(existingPolicies, policiesToAttach...)
|
||||
newPolicies := strings.Join(existingPolicies, ",")
|
||||
|
||||
updatedAt, err := globalIAMSys.PolicyDBSet(ctx, userOrGroup, newPolicies, userType, isGroup)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemPolicyMapping,
|
||||
PolicyMapping: &madmin.SRPolicyMapping{
|
||||
UserOrGroup: userOrGroup,
|
||||
UserType: int(userType),
|
||||
IsGroup: isGroup,
|
||||
Policy: strings.Join(policiesToAttach, ","),
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
}))
|
||||
|
||||
writeResponse(w, http.StatusCreated, nil, mimeNone)
|
||||
}
|
||||
|
||||
// DetachPolicyBuiltin - POST /minio/admin/v3/idp/builtin/detach
|
||||
func (a adminAPIHandlers) DetachPolicyBuiltin(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DetachPolicyBuiltin")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AttachPolicyAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cred, _, _, s3Err := validateAdminSignature(ctx, r, "")
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
password := cred.SecretKey
|
||||
|
||||
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var par madmin.PolicyAssociationReq
|
||||
if err = json.Unmarshal(reqBytes, &par); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = par.IsValid(); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
userOrGroup := par.User
|
||||
isGroup := false
|
||||
|
||||
if userOrGroup == "" {
|
||||
userOrGroup = par.Group
|
||||
isGroup = true
|
||||
}
|
||||
|
||||
if isGroup {
|
||||
_, err := globalIAMSys.GetGroupDescription(userOrGroup)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ok, _, err := globalIAMSys.IsTempUser(userOrGroup)
|
||||
if err != nil && err != errNoSuchUser {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that user exists.
|
||||
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType {
|
||||
_, ok := globalIAMSys.GetUser(ctx, userOrGroup)
|
||||
if !ok {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userType := regUser
|
||||
if globalIAMSys.GetUsersSysType() == LDAPUsersSysType {
|
||||
userType = stsUser
|
||||
}
|
||||
|
||||
var existingPolicies []string
|
||||
if isGroup {
|
||||
existingPolicies, err = globalIAMSys.PolicyDBGet(userOrGroup, true)
|
||||
} else {
|
||||
existingPolicies, err = globalIAMSys.GetUserPolicies(userOrGroup)
|
||||
}
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
policyMap := make(map[string]bool)
|
||||
for _, p := range existingPolicies {
|
||||
policyMap[p] = true
|
||||
}
|
||||
|
||||
policiesToDetach := par.Policies
|
||||
|
||||
// Check if policy is already attached to user.
|
||||
for _, p := range policiesToDetach {
|
||||
if _, ok := policyMap[p]; ok {
|
||||
delete(policyMap, p)
|
||||
} else {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyNotAttached), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
newPoliciesSl := []string{}
|
||||
for p := range policyMap {
|
||||
newPoliciesSl = append(newPoliciesSl, p)
|
||||
}
|
||||
|
||||
newPolicies := strings.Join(newPoliciesSl, ",")
|
||||
|
||||
updatedAt, err := globalIAMSys.PolicyDBSet(ctx, userOrGroup, newPolicies, userType, isGroup)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemPolicyMapping,
|
||||
PolicyMapping: &madmin.SRPolicyMapping{
|
||||
UserOrGroup: userOrGroup,
|
||||
UserType: int(userType),
|
||||
IsGroup: isGroup,
|
||||
Policy: strings.Join(policiesToDetach, ","),
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
}))
|
||||
|
||||
// Return successful JSON response
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -2076,6 +2443,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
opts := updateServiceAccountOpts{
|
||||
secretKey: svcAcctReq.SecretKey,
|
||||
status: svcAcctReq.Status,
|
||||
comment: svcAcctReq.Comment,
|
||||
sessionPolicy: sp,
|
||||
}
|
||||
_, err = globalIAMSys.UpdateServiceAccount(ctx, svcAcctReq.AccessKey, opts)
|
||||
@@ -2090,6 +2458,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
secretKey: svcAcctReq.SecretKey,
|
||||
sessionPolicy: sp,
|
||||
claims: svcAcctReq.Claims,
|
||||
comment: svcAcctReq.Comment,
|
||||
}
|
||||
|
||||
// In case of LDAP we need to resolve the targetUser to a DN and
|
||||
|
||||
@@ -33,7 +33,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
cr "github.com/minio/minio-go/v7/pkg/credentials"
|
||||
@@ -1136,7 +1136,7 @@ func (s *TestSuiteIAM) TestAccMgmtPlugin(c *check) {
|
||||
c.assertSvcAccDeletion(ctx, s, userAdmClient, accessKey, bucket)
|
||||
|
||||
// 6. Check that service account **can** be created for some other user.
|
||||
// This is possible because of the policy enforced in the plugin.
|
||||
// This is possible because the policy enforced in the plugin.
|
||||
c.mustCreateSvcAccount(ctx, globalActiveCred.AccessKey, userAdmClient)
|
||||
}
|
||||
|
||||
|
||||
+250
-249
@@ -21,14 +21,17 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/subtle"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -41,15 +44,17 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v2/estream"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/dsync"
|
||||
"github.com/minio/minio/internal/handlers"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/logger/message/log"
|
||||
"github.com/minio/mux"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
xnet "github.com/minio/pkg/net"
|
||||
"github.com/secure-io/sio-go"
|
||||
@@ -339,8 +344,7 @@ func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
// ignores any errors here.
|
||||
storageInfo, _ := objectAPI.StorageInfo(ctx)
|
||||
storageInfo := objectAPI.StorageInfo(ctx)
|
||||
|
||||
// Collect any disk healing.
|
||||
healing, _ := getAggregatedBackgroundHealState(ctx, nil)
|
||||
@@ -423,7 +427,7 @@ func (a adminAPIHandlers) MetricsHandler(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dID := r.Form.Get("by-depID")
|
||||
done := ctx.Done()
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
@@ -436,15 +440,16 @@ func (a adminAPIHandlers) MetricsHandler(w http.ResponseWriter, r *http.Request)
|
||||
hosts: hostMap,
|
||||
disks: diskMap,
|
||||
jobID: jobID,
|
||||
depID: dID,
|
||||
})
|
||||
m.Merge(&mLocal)
|
||||
|
||||
// Allow half the interval for collecting remote...
|
||||
cctx, cancel := context.WithTimeout(ctx, interval/2)
|
||||
mRemote := collectRemoteMetrics(cctx, types, collectMetricsOpts{
|
||||
hosts: hostMap,
|
||||
disks: diskMap,
|
||||
jobID: jobID,
|
||||
depID: dID,
|
||||
})
|
||||
cancel()
|
||||
m.Merge(&mRemote)
|
||||
@@ -506,9 +511,10 @@ func (a adminAPIHandlers) DataUsageInfoHandler(w http.ResponseWriter, r *http.Re
|
||||
writeSuccessResponseJSON(w, dataUsageInfoJSON)
|
||||
}
|
||||
|
||||
func lriToLockEntry(l lockRequesterInfo, resource, server string) *madmin.LockEntry {
|
||||
func lriToLockEntry(l lockRequesterInfo, now time.Time, resource, server string) *madmin.LockEntry {
|
||||
entry := &madmin.LockEntry{
|
||||
Timestamp: l.Timestamp,
|
||||
Elapsed: now.Sub(l.Timestamp),
|
||||
Resource: resource,
|
||||
ServerList: []string{server},
|
||||
Source: l.Source,
|
||||
@@ -525,6 +531,7 @@ func lriToLockEntry(l lockRequesterInfo, resource, server string) *madmin.LockEn
|
||||
}
|
||||
|
||||
func topLockEntries(peerLocks []*PeerLocks, stale bool) madmin.LockEntries {
|
||||
now := time.Now().UTC()
|
||||
entryMap := make(map[string]*madmin.LockEntry)
|
||||
for _, peerLock := range peerLocks {
|
||||
if peerLock == nil {
|
||||
@@ -535,7 +542,7 @@ func topLockEntries(peerLocks []*PeerLocks, stale bool) madmin.LockEntries {
|
||||
if val, ok := entryMap[lockReqInfo.Name]; ok {
|
||||
val.ServerList = append(val.ServerList, peerLock.Addr)
|
||||
} else {
|
||||
entryMap[lockReqInfo.Name] = lriToLockEntry(lockReqInfo, k, peerLock.Addr)
|
||||
entryMap[lockReqInfo.Name] = lriToLockEntry(lockReqInfo, now, k, peerLock.Addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1095,7 +1102,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// If no ObjectLayer is provided no set status is returned.
|
||||
func getAggregatedBackgroundHealState(ctx context.Context, o ObjectLayer) (madmin.BgHealState, error) {
|
||||
// Get local heal status first
|
||||
bgHealStates, ok := getBackgroundHealStatus(ctx, o)
|
||||
bgHealStates, ok := getLocalBackgroundHealStatus(ctx, o)
|
||||
if !ok {
|
||||
return bgHealStates, errServerNotInitialized
|
||||
}
|
||||
@@ -1163,7 +1170,7 @@ func (a adminAPIHandlers) NetperfHandler(w http.ResponseWriter, r *http.Request)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(toAPIErrorCode(ctx, err)), r.URL)
|
||||
return
|
||||
}
|
||||
defer nsLock.Unlock(lkctx.Cancel)
|
||||
defer nsLock.Unlock(lkctx)
|
||||
|
||||
durationStr := r.Form.Get(peerRESTDuration)
|
||||
duration, err := time.ParseDuration(durationStr)
|
||||
@@ -1226,7 +1233,7 @@ func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.
|
||||
duration = time.Second * 10
|
||||
}
|
||||
|
||||
storageInfo, _ := objectAPI.StorageInfo(ctx)
|
||||
storageInfo := objectAPI.StorageInfo(ctx)
|
||||
|
||||
sufficientCapacity, canAutotune, capacityErrMsg := validateObjPerfOptions(ctx, storageInfo, concurrent, size, autotune)
|
||||
if !sufficientCapacity {
|
||||
@@ -1312,7 +1319,7 @@ func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
func makeObjectPerfBucket(ctx context.Context, objectAPI ObjectLayer, bucketName string) (bucketExists bool, err error) {
|
||||
if err = objectAPI.MakeBucketWithLocation(ctx, bucketName, MakeBucketOptions{}); err != nil {
|
||||
if err = objectAPI.MakeBucket(ctx, bucketName, MakeBucketOptions{}); err != nil {
|
||||
if _, ok := err.(BucketExists); !ok {
|
||||
// Only BucketExists error can be ignored.
|
||||
return false, err
|
||||
@@ -1325,7 +1332,6 @@ func makeObjectPerfBucket(ctx context.Context, objectAPI ObjectLayer, bucketName
|
||||
func deleteObjectPerfBucket(objectAPI ObjectLayer) {
|
||||
objectAPI.DeleteBucket(context.Background(), globalObjectPerfBucket, DeleteBucketOptions{
|
||||
Force: true,
|
||||
NoRecreate: true,
|
||||
SRDeleteOp: getSRBucketDeleteOp(globalSiteReplicationSys.isEnabled()),
|
||||
})
|
||||
}
|
||||
@@ -1606,7 +1612,10 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
for {
|
||||
select {
|
||||
case log := <-logCh:
|
||||
case log, ok := <-logCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if log.SendLog(node, logKind) {
|
||||
if err := enc.Encode(log); err != nil {
|
||||
return
|
||||
@@ -1900,67 +1909,7 @@ func getKubernetesInfo(dctx context.Context) madmin.KubernetesInfo {
|
||||
return ki
|
||||
}
|
||||
|
||||
// HealthInfoHandler - GET /minio/admin/v3/healthinfo
|
||||
// ----------
|
||||
// Get server health info
|
||||
func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "HealthInfo")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
query := r.Form
|
||||
healthInfo := madmin.HealthInfo{
|
||||
Version: madmin.HealthInfoVersion,
|
||||
Minio: madmin.MinioHealthInfo{
|
||||
Info: madmin.MinioInfo{
|
||||
DeploymentID: globalDeploymentID,
|
||||
},
|
||||
},
|
||||
}
|
||||
healthInfoCh := make(chan madmin.HealthInfo)
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
|
||||
setCommonHeaders(w)
|
||||
|
||||
setEventStreamHeaders(w)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
errResp := func(err error) {
|
||||
errorResponse := getAPIErrorResponse(ctx, toAdminAPIErr(ctx, err), r.URL.String(),
|
||||
w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
|
||||
encodedErrorResponse := encodeResponse(errorResponse)
|
||||
healthInfo.Error = string(encodedErrorResponse)
|
||||
logger.LogIf(ctx, enc.Encode(healthInfo))
|
||||
}
|
||||
|
||||
deadline := 10 * time.Second // Default deadline is 10secs for health diagnostics.
|
||||
if dstr := r.Form.Get("deadline"); dstr != "" {
|
||||
var err error
|
||||
deadline, err = time.ParseDuration(dstr)
|
||||
if err != nil {
|
||||
errResp(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
nsLock := objectAPI.NewNSLock(minioMetaBucket, "health-check-in-progress")
|
||||
lkctx, err := nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline))
|
||||
if err != nil { // returns a locked lock
|
||||
errResp(err)
|
||||
return
|
||||
}
|
||||
defer nsLock.Unlock(lkctx.Cancel)
|
||||
|
||||
healthCtx, healthCancel := context.WithTimeout(lkctx.Context(), deadline)
|
||||
defer healthCancel()
|
||||
|
||||
func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *url.Values, healthInfoCh chan madmin.HealthInfo, healthInfo madmin.HealthInfo) {
|
||||
hostAnonymizer := createHostAnonymizer()
|
||||
// anonAddr - Anonymizes hosts in given input string.
|
||||
anonAddr := func(addr string) string {
|
||||
@@ -2185,7 +2134,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
getAndWriteMinioConfig := func() {
|
||||
if query.Get("minioconfig") == "true" {
|
||||
config, err := readServerConfig(ctx, objectAPI)
|
||||
config, err := readServerConfig(healthCtx, objectAPI, nil)
|
||||
if err != nil {
|
||||
healthInfo.Minio.Config = madmin.MinioConfig{
|
||||
Error: err.Error(),
|
||||
@@ -2233,7 +2182,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
getAndWriteSysConfig()
|
||||
|
||||
if query.Get("minioinfo") == "true" {
|
||||
infoMessage := getServerInfo(ctx, r)
|
||||
infoMessage := getServerInfo(healthCtx, nil)
|
||||
servers := make([]madmin.ServerInfo, 0, len(infoMessage.Servers))
|
||||
for _, server := range infoMessage.Servers {
|
||||
anonEndpoint := anonAddr(server.Endpoint)
|
||||
@@ -2283,6 +2232,68 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
partialWrite(healthInfo)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// HealthInfoHandler - GET /minio/admin/v3/healthinfo
|
||||
// ----------
|
||||
// Get server health info
|
||||
func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "HealthInfo")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
query := r.Form
|
||||
healthInfoCh := make(chan madmin.HealthInfo)
|
||||
enc := json.NewEncoder(w)
|
||||
|
||||
healthInfo := madmin.HealthInfo{
|
||||
TimeStamp: time.Now().UTC(),
|
||||
Version: madmin.HealthInfoVersion,
|
||||
Minio: madmin.MinioHealthInfo{
|
||||
Info: madmin.MinioInfo{
|
||||
DeploymentID: globalDeploymentID,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errResp := func(err error) {
|
||||
errorResponse := getAPIErrorResponse(ctx, toAdminAPIErr(ctx, err), r.URL.String(),
|
||||
w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
|
||||
encodedErrorResponse := encodeResponse(errorResponse)
|
||||
healthInfo.Error = string(encodedErrorResponse)
|
||||
logger.LogIf(ctx, enc.Encode(healthInfo))
|
||||
}
|
||||
|
||||
deadline := 10 * time.Second // Default deadline is 10secs for health diagnostics.
|
||||
if dstr := query.Get("deadline"); dstr != "" {
|
||||
var err error
|
||||
deadline, err = time.ParseDuration(dstr)
|
||||
if err != nil {
|
||||
errResp(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
nsLock := objectAPI.NewNSLock(minioMetaBucket, "health-check-in-progress")
|
||||
lkctx, err := nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline))
|
||||
if err != nil { // returns a locked lock
|
||||
errResp(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer nsLock.Unlock(lkctx)
|
||||
healthCtx, healthCancel := context.WithTimeout(lkctx.Context(), deadline)
|
||||
defer healthCancel()
|
||||
|
||||
go fetchHealthInfo(healthCtx, objectAPI, &query, healthInfoCh, healthInfo)
|
||||
|
||||
setCommonHeaders(w)
|
||||
setEventStreamHeaders(w)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
@@ -2330,66 +2341,6 @@ func getTLSInfo() madmin.TLSInfo {
|
||||
return tlsInfo
|
||||
}
|
||||
|
||||
// BandwidthMonitorHandler - GET /minio/admin/v3/bandwidth
|
||||
// ----------
|
||||
// Get bandwidth consumption information
|
||||
func (a adminAPIHandlers) BandwidthMonitorHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "BandwidthMonitor")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
// Validate request signature.
|
||||
_, adminAPIErr := checkAdminRequestAuth(ctx, r, iampolicy.BandwidthMonitorAction, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
setEventStreamHeaders(w)
|
||||
reportCh := make(chan madmin.BucketBandwidthReport)
|
||||
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
|
||||
defer keepAliveTicker.Stop()
|
||||
bucketsRequestedString := r.Form.Get("buckets")
|
||||
bucketsRequested := strings.Split(bucketsRequestedString, ",")
|
||||
go func() {
|
||||
defer close(reportCh)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case reportCh <- globalNotificationSys.GetBandwidthReports(ctx, bucketsRequested...):
|
||||
time.Sleep(time.Duration(rnd.Float64() * float64(2*time.Second)))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
for {
|
||||
select {
|
||||
case report, ok := <-reportCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := enc.Encode(report); err != nil {
|
||||
return
|
||||
}
|
||||
if len(reportCh) == 0 {
|
||||
// Flush if nothing is queued
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
case <-keepAliveTicker.C:
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ServerInfoHandler - GET /minio/admin/v3/info
|
||||
// ----------
|
||||
// Get server information
|
||||
@@ -2506,63 +2457,21 @@ func fetchKMSStatus() madmin.KMS {
|
||||
func fetchLoggerInfo() ([]madmin.Logger, []madmin.Audit) {
|
||||
var loggerInfo []madmin.Logger
|
||||
var auditloggerInfo []madmin.Audit
|
||||
for _, target := range logger.SystemTargets() {
|
||||
if target.Endpoint() != "" {
|
||||
tgt := target.String()
|
||||
err := checkConnection(target.Endpoint(), 15*time.Second)
|
||||
if err == nil {
|
||||
mapLog := make(map[string]madmin.Status)
|
||||
mapLog[tgt] = madmin.Status{Status: string(madmin.ItemOnline)}
|
||||
loggerInfo = append(loggerInfo, mapLog)
|
||||
} else {
|
||||
mapLog := make(map[string]madmin.Status)
|
||||
mapLog[tgt] = madmin.Status{Status: string(madmin.ItemOffline)}
|
||||
loggerInfo = append(loggerInfo, mapLog)
|
||||
}
|
||||
for _, tgt := range logger.SystemTargets() {
|
||||
if tgt.Endpoint() != "" {
|
||||
loggerInfo = append(loggerInfo, madmin.Logger{tgt.String(): logger.TargetStatus(tgt)})
|
||||
}
|
||||
}
|
||||
|
||||
for _, target := range logger.AuditTargets() {
|
||||
if target.Endpoint() != "" {
|
||||
tgt := target.String()
|
||||
err := checkConnection(target.Endpoint(), 15*time.Second)
|
||||
if err == nil {
|
||||
mapAudit := make(map[string]madmin.Status)
|
||||
mapAudit[tgt] = madmin.Status{Status: string(madmin.ItemOnline)}
|
||||
auditloggerInfo = append(auditloggerInfo, mapAudit)
|
||||
} else {
|
||||
mapAudit := make(map[string]madmin.Status)
|
||||
mapAudit[tgt] = madmin.Status{Status: string(madmin.ItemOffline)}
|
||||
auditloggerInfo = append(auditloggerInfo, mapAudit)
|
||||
}
|
||||
for _, tgt := range logger.AuditTargets() {
|
||||
if tgt.Endpoint() != "" {
|
||||
auditloggerInfo = append(auditloggerInfo, madmin.Audit{tgt.String(): logger.TargetStatus(tgt)})
|
||||
}
|
||||
}
|
||||
|
||||
return loggerInfo, auditloggerInfo
|
||||
}
|
||||
|
||||
// checkConnection - ping an endpoint , return err in case of no connection
|
||||
func checkConnection(endpointStr string, timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(GlobalContext, timeout)
|
||||
defer cancel()
|
||||
|
||||
client := &http.Client{
|
||||
Transport: globalProxyTransport,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodHead, endpointStr, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer xhttp.DrainBody(resp.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
func embedFileInZip(zipWriter *zip.Writer, name string, data []byte) error {
|
||||
// Send profiling data to zip as file
|
||||
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
||||
@@ -2585,13 +2494,14 @@ func embedFileInZip(zipWriter *zip.Writer, name string, data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// appendClusterMetaInfoToZip gets information of the current cluster and embedded
|
||||
// it in the passed zipwriter, This is not a critical function and it is allowed
|
||||
// to fail with a ten seconds timeout.
|
||||
func appendClusterMetaInfoToZip(ctx context.Context, zipWriter *zip.Writer) {
|
||||
// getClusterMetaInfo gets information of the current cluster and
|
||||
// returns it.
|
||||
// This is not a critical function, and it is allowed
|
||||
// to fail with a ten seconds timeout, returning nil.
|
||||
func getClusterMetaInfo(ctx context.Context) []byte {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add a ten seconds timeout because getting profiling data
|
||||
@@ -2600,13 +2510,16 @@ func appendClusterMetaInfoToZip(ctx context.Context, zipWriter *zip.Writer) {
|
||||
defer cancel()
|
||||
|
||||
resultCh := make(chan madmin.ClusterRegistrationInfo)
|
||||
|
||||
go func() {
|
||||
defer close(resultCh)
|
||||
|
||||
ci := madmin.ClusterRegistrationInfo{}
|
||||
ci.Info.NoOfServerPools = len(globalEndpoints)
|
||||
ci.Info.NoOfServers = len(globalEndpoints.Hostnames())
|
||||
ci.Info.MinioVersion = Version
|
||||
|
||||
si, _ := objectAPI.StorageInfo(ctx)
|
||||
si := objectAPI.StorageInfo(ctx)
|
||||
|
||||
ci.Info.NoOfDrives = len(si.Disks)
|
||||
for _, disk := range si.Disks {
|
||||
@@ -2622,25 +2535,39 @@ func appendClusterMetaInfoToZip(ctx context.Context, zipWriter *zip.Writer) {
|
||||
|
||||
ci.DeploymentID = globalDeploymentID
|
||||
ci.ClusterName = fmt.Sprintf("%d-servers-%d-disks-%s", ci.Info.NoOfServers, ci.Info.NoOfDrives, ci.Info.MinioVersion)
|
||||
resultCh <- ci
|
||||
|
||||
select {
|
||||
case resultCh <- ci:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
return nil
|
||||
case ci := <-resultCh:
|
||||
out, err := json.MarshalIndent(ci, "", " ")
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
err = embedFileInZip(zipWriter, "cluster.info", out)
|
||||
out, err := json.MarshalIndent(ci, "", " ")
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
func bytesToPublicKey(pub []byte) (*rsa.PublicKey, error) {
|
||||
block, _ := pem.Decode(pub)
|
||||
if block != nil {
|
||||
pub = block.Bytes
|
||||
}
|
||||
key, err := x509.ParsePKCS1PublicKey(pub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// getRawDataer provides an interface for getting raw FS files.
|
||||
type getRawDataer interface {
|
||||
GetRawData(ctx context.Context, volume, file string, fn func(r io.Reader, host string, disk string, filename string, info StatInfo) error) error
|
||||
@@ -2667,12 +2594,17 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
if err := parseForm(r); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
volume := r.Form.Get("volume")
|
||||
file := r.Form.Get("file")
|
||||
if len(volume) == 0 {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketName), r.URL)
|
||||
return
|
||||
}
|
||||
file := r.Form.Get("file")
|
||||
if len(file) == 0 {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
@@ -2685,41 +2617,102 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
var key [32]byte
|
||||
// MUST use crypto/rand
|
||||
n, err := crand.Read(key[:])
|
||||
if err != nil || n != len(key) {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
|
||||
return
|
||||
}
|
||||
stream, err := sio.AES_256_GCM.Stream(key[:])
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
|
||||
return
|
||||
}
|
||||
// Zero nonce, we only use each key once, and 32 bytes is plenty.
|
||||
nonce := make([]byte, stream.NonceSize())
|
||||
encw := stream.EncryptWriter(w, nonce, nil)
|
||||
var publicKey *rsa.PublicKey
|
||||
|
||||
defer encw.Close()
|
||||
publicKeyB64 := r.Form.Get("public-key")
|
||||
if publicKeyB64 != "" {
|
||||
publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyB64)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
publicKey, err = bytesToPublicKey(publicKeyBytes)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Write a version for making *incompatible* changes.
|
||||
// The AdminClient will reject any version it does not know.
|
||||
w.Write([]byte{1})
|
||||
var inspectZipW *zip.Writer
|
||||
if publicKey != nil {
|
||||
w.WriteHeader(200)
|
||||
stream := estream.NewWriter(w)
|
||||
defer stream.Close()
|
||||
|
||||
// Write key first (without encryption)
|
||||
_, err = w.Write(key[:])
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
|
||||
return
|
||||
clusterKey, err := bytesToPublicKey(getSubnetAdminPublicKey())
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, stream.AddError(err.Error()))
|
||||
return
|
||||
}
|
||||
err = stream.AddKeyEncrypted(clusterKey)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, stream.AddError(err.Error()))
|
||||
return
|
||||
}
|
||||
if b := getClusterMetaInfo(ctx); len(b) > 0 {
|
||||
w, err := stream.AddEncryptedStream("cluster.info", nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
w.Write(b)
|
||||
w.Close()
|
||||
}
|
||||
|
||||
// Add new key for inspect data.
|
||||
if err := stream.AddKeyEncrypted(publicKey); err != nil {
|
||||
logger.LogIf(ctx, stream.AddError(err.Error()))
|
||||
return
|
||||
}
|
||||
encStream, err := stream.AddEncryptedStream("inspect.zip", nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, stream.AddError(err.Error()))
|
||||
return
|
||||
}
|
||||
defer encStream.Close()
|
||||
|
||||
inspectZipW = zip.NewWriter(encStream)
|
||||
defer inspectZipW.Close()
|
||||
} else {
|
||||
// Legacy: Remove if we stop supporting inspection without public key.
|
||||
var key [32]byte
|
||||
// MUST use crypto/rand
|
||||
n, err := crand.Read(key[:])
|
||||
if err != nil || n != len(key) {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write a version for making *incompatible* changes.
|
||||
// The AdminClient will reject any version it does not know.
|
||||
if publicKey == nil {
|
||||
w.Write([]byte{1})
|
||||
w.Write(key[:])
|
||||
}
|
||||
|
||||
stream, err := sio.AES_256_GCM.Stream(key[:])
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
// Zero nonce, we only use each key once, and 32 bytes is plenty.
|
||||
nonce := make([]byte, stream.NonceSize())
|
||||
encw := stream.EncryptWriter(w, nonce, nil)
|
||||
defer encw.Close()
|
||||
|
||||
// Initialize a zip writer which will provide a zipped content
|
||||
// of profiling data of all nodes
|
||||
inspectZipW = zip.NewWriter(encw)
|
||||
defer inspectZipW.Close()
|
||||
|
||||
if b := getClusterMetaInfo(ctx); len(b) > 0 {
|
||||
logger.LogIf(ctx, embedFileInZip(inspectZipW, "cluster.info", b))
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize a zip writer which will provide a zipped content
|
||||
// of profiling data of all nodes
|
||||
zipWriter := zip.NewWriter(encw)
|
||||
defer zipWriter.Close()
|
||||
rawDataFn := func(r io.Reader, host, disk, filename string, si StatInfo) error {
|
||||
// Prefix host+disk
|
||||
filename = path.Join(host, disk, filename)
|
||||
@@ -2748,17 +2741,17 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
|
||||
return nil
|
||||
}
|
||||
header.Method = zip.Deflate
|
||||
zwriter, zerr := zipWriter.CreateHeader(header)
|
||||
zwriter, zerr := inspectZipW.CreateHeader(header)
|
||||
if zerr != nil {
|
||||
logger.LogIf(ctx, zerr)
|
||||
return nil
|
||||
}
|
||||
if _, err = io.Copy(zwriter, r); err != nil {
|
||||
if _, err := io.Copy(zwriter, r); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err = o.GetRawData(ctx, volume, file, rawDataFn)
|
||||
err := o.GetRawData(ctx, volume, file, rawDataFn)
|
||||
if !errors.Is(err, errFileNotFound) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
@@ -2770,22 +2763,24 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
|
||||
if !errors.Is(err, errFileNotFound) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
// save args passed to inspect command
|
||||
inspectArgs := []string{fmt.Sprintf(" Inspect path: %s%s%s\n", volume, slashSeparator, file)}
|
||||
cmdLine := []string{"Server command line args: "}
|
||||
for _, pool := range globalEndpoints {
|
||||
cmdLine = append(cmdLine, pool.CmdLine)
|
||||
}
|
||||
cmdLine = append(cmdLine, "\n")
|
||||
inspectArgs = append(inspectArgs, cmdLine...)
|
||||
inspectArgsBytes := []byte(strings.Join(inspectArgs, " "))
|
||||
if err = rawDataFn(bytes.NewReader(inspectArgsBytes), "", "", "inspect-input.txt", StatInfo{
|
||||
Size: int64(len(inspectArgsBytes)),
|
||||
}); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
|
||||
appendClusterMetaInfoToZip(ctx, zipWriter)
|
||||
// save args passed to inspect command
|
||||
var sb bytes.Buffer
|
||||
fmt.Fprintf(&sb, "Inspect path: %s%s%s\n", volume, slashSeparator, file)
|
||||
sb.WriteString("Server command line args:")
|
||||
for _, pool := range globalEndpoints {
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(pool.CmdLine)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
logger.LogIf(ctx, embedFileInZip(inspectZipW, "inspect-input.txt", sb.Bytes()))
|
||||
}
|
||||
|
||||
func getSubnetAdminPublicKey() []byte {
|
||||
if globalIsCICD {
|
||||
return subnetAdminPublicKeyDev
|
||||
}
|
||||
return subnetAdminPublicKey
|
||||
}
|
||||
|
||||
func createHostAnonymizerForFSMode() map[string]string {
|
||||
@@ -2865,10 +2860,16 @@ func createHostAnonymizer() map[string]string {
|
||||
}
|
||||
|
||||
hostAnonymizer := map[string]string{}
|
||||
hosts := set.NewStringSet()
|
||||
srvrIdx := 0
|
||||
|
||||
for poolIdx, pool := range globalEndpoints {
|
||||
for srvrIdx, endpoint := range pool.Endpoints {
|
||||
anonymizeHost(hostAnonymizer, endpoint, poolIdx+1, srvrIdx+1)
|
||||
for _, endpoint := range pool.Endpoints {
|
||||
if !hosts.Contains(endpoint.Host) {
|
||||
hosts.Add(endpoint.Host)
|
||||
srvrIdx++
|
||||
}
|
||||
anonymizeHost(hostAnonymizer, endpoint, poolIdx+1, srvrIdx)
|
||||
}
|
||||
}
|
||||
return hostAnonymizer
|
||||
|
||||
@@ -29,9 +29,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/mux"
|
||||
)
|
||||
|
||||
// adminErasureTestBed - encapsulates subsystems that need to be setup for
|
||||
|
||||
+32
-14
@@ -27,7 +27,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
@@ -176,11 +176,12 @@ func (ahs *allHealState) getHealLocalDiskEndpoints() Endpoints {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func (ahs *allHealState) markDiskForHealing(ep Endpoint) {
|
||||
// Set, in the memory, the state of the disk as currently healing or not
|
||||
func (ahs *allHealState) setDiskHealingStatus(ep Endpoint, healing bool) {
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
|
||||
ahs.healLocalDisks[ep] = true
|
||||
ahs.healLocalDisks[ep] = healing
|
||||
}
|
||||
|
||||
func (ahs *allHealState) pushHealLocalDisks(healLocalDisks ...Endpoint) {
|
||||
@@ -222,8 +223,8 @@ func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) {
|
||||
// getHealSequenceByToken - Retrieve a heal sequence by token. The second
|
||||
// argument returns if a heal sequence actually exists.
|
||||
func (ahs *allHealState) getHealSequenceByToken(token string) (h *healSequence, exists bool) {
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
ahs.RLock()
|
||||
defer ahs.RUnlock()
|
||||
for _, healSeq := range ahs.healSeqMap {
|
||||
if healSeq.clientToken == token {
|
||||
return healSeq, true
|
||||
@@ -235,8 +236,8 @@ func (ahs *allHealState) getHealSequenceByToken(token string) (h *healSequence,
|
||||
// getHealSequence - Retrieve a heal sequence by path. The second
|
||||
// argument returns if a heal sequence actually exists.
|
||||
func (ahs *allHealState) getHealSequence(path string) (h *healSequence, exists bool) {
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
ahs.RLock()
|
||||
defer ahs.RUnlock()
|
||||
h, exists = ahs.healSeqMap[path]
|
||||
return h, exists
|
||||
}
|
||||
@@ -396,6 +397,7 @@ type healSource struct {
|
||||
bucket string
|
||||
object string
|
||||
versionID string
|
||||
noWait bool // a non blocking call, if task queue is full return right away.
|
||||
opts *madmin.HealOpts // optional heal option overrides default setting
|
||||
}
|
||||
|
||||
@@ -699,7 +701,6 @@ func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItem
|
||||
object: source.object,
|
||||
versionID: source.versionID,
|
||||
opts: h.settings,
|
||||
respCh: h.respCh,
|
||||
}
|
||||
if source.opts != nil {
|
||||
task.opts = *source.opts
|
||||
@@ -712,15 +713,32 @@ func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItem
|
||||
h.lastHealActivity = UTCNow()
|
||||
h.mutex.Unlock()
|
||||
|
||||
select {
|
||||
case globalBackgroundHealRoutine.tasks <- task:
|
||||
if serverDebugLog {
|
||||
logger.Info("Task in the queue: %#v", task)
|
||||
if source.noWait {
|
||||
select {
|
||||
case globalBackgroundHealRoutine.tasks <- task:
|
||||
if serverDebugLog {
|
||||
logger.Info("Task in the queue: %#v", task)
|
||||
}
|
||||
case <-h.ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
// task queue is full, no more workers, we shall move on and heal later.
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// respCh must be set for guaranteed result
|
||||
task.respCh = h.respCh
|
||||
select {
|
||||
case globalBackgroundHealRoutine.tasks <- task:
|
||||
if serverDebugLog {
|
||||
logger.Info("Task in the queue: %#v", task)
|
||||
}
|
||||
case <-h.ctx.Done():
|
||||
return nil
|
||||
}
|
||||
case <-h.ctx.Done():
|
||||
return nil
|
||||
}
|
||||
|
||||
// task queued, now wait for the response.
|
||||
select {
|
||||
case res := <-h.respCh:
|
||||
if !h.reportProgress {
|
||||
|
||||
+24
-8
@@ -20,11 +20,11 @@ package cmd
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/klauspost/compress/gzhttp"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -60,7 +60,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
|
||||
// Info operations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/info").HandlerFunc(gz(httpTraceAll(adminAPI.ServerInfoHandler)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/inspect-data").HandlerFunc(httpTraceHdrs(adminAPI.InspectDataHandler)).Queries("volume", "{volume:.*}", "file", "{file:.*}")
|
||||
adminRouter.Methods(http.MethodGet, http.MethodPost).Path(adminVersion + "/inspect-data").HandlerFunc(httpTraceAll(adminAPI.InspectDataHandler))
|
||||
|
||||
// StorageInfo operations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/storageinfo").HandlerFunc(gz(httpTraceAll(adminAPI.StorageInfoHandler)))
|
||||
@@ -142,12 +142,18 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-service-accounts").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListServiceAccounts)))
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/delete-service-account").HandlerFunc(gz(httpTraceHdrs(adminAPI.DeleteServiceAccount))).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
// STS accounts ops
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/temporary-account-info").HandlerFunc(gz(httpTraceHdrs(adminAPI.TemporaryAccountInfo))).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
// Info policy IAM latest
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(gz(httpTraceHdrs(adminAPI.InfoCannedPolicy))).Queries("name", "{name:.*}")
|
||||
// List policies latest
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-canned-policies").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListBucketPolicies))).Queries("bucket", "{bucket:.*}")
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListCannedPolicies)))
|
||||
|
||||
// Builtin IAM policy associations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/builtin/policy-entities").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListPolicyMappingEntities)))
|
||||
|
||||
// Remove policy IAM
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-canned-policy").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveCannedPolicy))).Queries("name", "{name:.*}")
|
||||
|
||||
@@ -156,6 +162,12 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
HandlerFunc(gz(httpTraceHdrs(adminAPI.SetPolicyForUserOrGroup))).
|
||||
Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}")
|
||||
|
||||
// Attach policies to user or group
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/builtin/policy/attach").HandlerFunc(gz(httpTraceHdrs(adminAPI.AttachPolicyBuiltin)))
|
||||
|
||||
// Detach policies from user or group
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/builtin/policy/detach").HandlerFunc(gz(httpTraceHdrs(adminAPI.DetachPolicyBuiltin)))
|
||||
|
||||
// Remove user IAM
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-user").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveUser))).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
@@ -184,10 +196,15 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-iam").HandlerFunc(httpTraceHdrs(adminAPI.ImportIAM))
|
||||
|
||||
// IDentity Provider configuration APIs
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/idp-config").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetIdentityProviderCfg))).Queries("type", "{type:.*}").Queries("name", "{name:.*}")
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp-config").HandlerFunc(gz(httpTraceHdrs(adminAPI.GetIdentityProviderCfg))).Queries("type", "{type:.*}")
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/idp-config").HandlerFunc(gz(httpTraceHdrs(adminAPI.DeleteIdentityProviderCfg))).Queries("type", "{type:.*}").Queries("name", "{name:.*}")
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(gz(httpTraceHdrs(adminAPI.AddIdentityProviderCfg)))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(gz(httpTraceHdrs(adminAPI.UpdateIdentityProviderCfg)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp-config/{type}").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListIdentityProviderCfg)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(gz(httpTraceHdrs(adminAPI.GetIdentityProviderCfg)))
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(gz(httpTraceHdrs(adminAPI.DeleteIdentityProviderCfg)))
|
||||
|
||||
// LDAP IAM operations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/ldap/policy-entities").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListLDAPPolicyMappingEntities)))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/ldap/policy/{operation}").HandlerFunc(gz(httpTraceHdrs(adminAPI.AttachDetachPolicyLDAP)))
|
||||
// -- END IAM APIs --
|
||||
|
||||
// GetBucketQuotaConfig
|
||||
@@ -253,6 +270,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationEdit)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerEdit)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerRemove)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/site-replication/resync/op").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationResyncOp))).Queries("operation", "{operation:.*}")
|
||||
|
||||
if globalIsDistErasure {
|
||||
// Top locks
|
||||
@@ -285,8 +303,6 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
// -- Health API --
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/healthinfo").
|
||||
HandlerFunc(gz(httpTraceHdrs(adminAPI.HealthInfoHandler)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/bandwidth").
|
||||
HandlerFunc(gz(httpTraceHdrs(adminAPI.BandwidthMonitorHandler)))
|
||||
}
|
||||
|
||||
// If none of the routes match add default error handler routes
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
@@ -34,7 +34,6 @@ import (
|
||||
// getLocalServerProperty - returns madmin.ServerProperties for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Request) madmin.ServerProperties {
|
||||
var localEndpoints Endpoints
|
||||
addr := globalLocalNodeName
|
||||
if r != nil {
|
||||
addr = r.Host
|
||||
@@ -52,7 +51,6 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
|
||||
if endpoint.IsLocal {
|
||||
// Only proceed for local endpoints
|
||||
network[nodeName] = string(madmin.ItemOnline)
|
||||
localEndpoints = append(localEndpoints, endpoint)
|
||||
continue
|
||||
}
|
||||
_, present := network[nodeName]
|
||||
@@ -143,10 +141,11 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
|
||||
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer != nil {
|
||||
// only need Disks information in server mode.
|
||||
storageInfo, _ := objLayer.LocalStorageInfo(GlobalContext)
|
||||
storageInfo := objLayer.LocalStorageInfo(GlobalContext)
|
||||
props.State = string(madmin.ItemOnline)
|
||||
props.Disks = storageInfo.Disks
|
||||
} else {
|
||||
props.State = string(madmin.ItemOffline)
|
||||
}
|
||||
|
||||
return props
|
||||
|
||||
+48
-16
@@ -30,7 +30,7 @@ import (
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"google.golang.org/api/googleapi"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
@@ -151,7 +151,7 @@ const (
|
||||
ErrSignatureVersionNotSupported
|
||||
ErrBucketNotEmpty
|
||||
ErrAllAccessDisabled
|
||||
ErrMalformedPolicy
|
||||
ErrPolicyInvalidVersion
|
||||
ErrMissingFields
|
||||
ErrMissingCredTag
|
||||
ErrCredMalformed
|
||||
@@ -177,7 +177,6 @@ const (
|
||||
ErrBucketAlreadyOwnedByYou
|
||||
ErrInvalidDuration
|
||||
ErrBucketAlreadyExists
|
||||
ErrTooManyBuckets
|
||||
ErrMetadataTooLarge
|
||||
ErrUnsupportedMetadata
|
||||
ErrMaximumExpires
|
||||
@@ -196,6 +195,8 @@ const (
|
||||
ErrBucketTaggingNotFound
|
||||
ErrObjectLockInvalidHeaders
|
||||
ErrInvalidTagDirective
|
||||
ErrPolicyAlreadyAttached
|
||||
ErrPolicyNotAttached
|
||||
// Add new error codes here.
|
||||
|
||||
// SSE-S3/SSE-KMS related API errors
|
||||
@@ -207,6 +208,8 @@ const (
|
||||
ErrSSEMultipartEncrypted
|
||||
ErrSSEEncryptedObject
|
||||
ErrInvalidEncryptionParameters
|
||||
ErrInvalidEncryptionParametersSSEC
|
||||
|
||||
ErrInvalidSSECustomerAlgorithm
|
||||
ErrInvalidSSECustomerKey
|
||||
ErrMissingSSECustomerKey
|
||||
@@ -266,6 +269,7 @@ const (
|
||||
ErrAdminGroupNotEmpty
|
||||
ErrAdminNoSuchJob
|
||||
ErrAdminNoSuchPolicy
|
||||
ErrAdminPolicyChangeAlreadyApplied
|
||||
ErrAdminInvalidArgument
|
||||
ErrAdminInvalidAccessKey
|
||||
ErrAdminInvalidSecretKey
|
||||
@@ -277,6 +281,8 @@ const (
|
||||
ErrAdminConfigDuplicateKeys
|
||||
ErrAdminConfigInvalidIDPType
|
||||
ErrAdminConfigLDAPValidation
|
||||
ErrAdminConfigIDPCfgNameAlreadyExists
|
||||
ErrAdminConfigIDPCfgNameDoesNotExist
|
||||
ErrAdminCredentialsMismatch
|
||||
ErrInsecureClientRequest
|
||||
ErrObjectTampered
|
||||
@@ -693,11 +699,6 @@ var errorCodes = errorCodeMap{
|
||||
Description: "The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrTooManyBuckets: {
|
||||
Code: "TooManyBuckets",
|
||||
Description: "You have attempted to create more buckets than allowed",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrBucketNotEmpty: {
|
||||
Code: "BucketNotEmpty",
|
||||
Description: "The bucket you tried to delete is not empty",
|
||||
@@ -713,9 +714,9 @@ var errorCodes = errorCodeMap{
|
||||
Description: "All access to this resource has been disabled.",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
ErrMalformedPolicy: {
|
||||
ErrPolicyInvalidVersion: {
|
||||
Code: "MalformedPolicy",
|
||||
Description: "Policy has invalid resource.",
|
||||
Description: "The policy must contain a valid version string",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingFields: {
|
||||
@@ -1115,6 +1116,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "The encryption parameters are not applicable to this object.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidEncryptionParametersSSEC: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "SSE-C encryption parameters are not supported on replicated bucket.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidSSECustomerAlgorithm: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Requests specifying Server Side Encryption with Customer provided keys must provide a valid encryption algorithm.",
|
||||
@@ -1249,6 +1255,12 @@ var errorCodes = errorCodeMap{
|
||||
Description: "The canned policy does not exist.",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrAdminPolicyChangeAlreadyApplied: {
|
||||
Code: "XMinioAdminPolicyChangeAlreadyApplied",
|
||||
Description: "The specified policy change is already in effect.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
ErrAdminInvalidArgument: {
|
||||
Code: "XMinioAdminInvalidArgument",
|
||||
Description: "Invalid arguments specified.",
|
||||
@@ -1305,6 +1317,16 @@ var errorCodes = errorCodeMap{
|
||||
Description: "LDAP Configuration validation failed",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminConfigIDPCfgNameAlreadyExists: {
|
||||
Code: "XMinioAdminConfigIDPCfgNameAlreadyExists",
|
||||
Description: "An IDP configuration with the given name aleady exists",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminConfigIDPCfgNameDoesNotExist: {
|
||||
Code: "XMinioAdminConfigIDPCfgNameDoesNotExist",
|
||||
Description: "No such IDP configuration exists",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminConfigNotificationTargetsFailed: {
|
||||
Code: "XMinioAdminNotificationTargetsTestFailed",
|
||||
Description: "Configuration update failed due an unsuccessful attempt to connect to one or more notification servers",
|
||||
@@ -1369,7 +1391,7 @@ var errorCodes = errorCodeMap{
|
||||
ErrSiteReplicationPeerResp: {
|
||||
Code: "XMinioSiteReplicationPeerResp",
|
||||
Description: "Error received when contacting a peer site",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSiteReplicationBackendIssue: {
|
||||
Code: "XMinioSiteReplicationBackendIssue",
|
||||
@@ -1926,6 +1948,16 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Invalid checksum provided.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrPolicyAlreadyAttached: {
|
||||
Code: "XMinioPolicyAlreadyAttached",
|
||||
Description: "The specified policy is already attached.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrPolicyNotAttached: {
|
||||
Code: "XMinioPolicyNotAttached",
|
||||
Description: "The specified policy is not found.",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
// Add your error structure here.
|
||||
}
|
||||
|
||||
@@ -1960,6 +1992,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrAdminNoSuchJob
|
||||
case errNoSuchPolicy:
|
||||
apiErr = ErrAdminNoSuchPolicy
|
||||
case errNoPolicyToAttachOrDetach:
|
||||
apiErr = ErrAdminPolicyChangeAlreadyApplied
|
||||
case errSignatureMismatch:
|
||||
apiErr = ErrSignatureDoesNotMatch
|
||||
case errInvalidRange:
|
||||
@@ -1979,6 +2013,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
// SSE errors
|
||||
case errInvalidEncryptionParameters:
|
||||
apiErr = ErrInvalidEncryptionParameters
|
||||
case errInvalidEncryptionParametersSSEC:
|
||||
apiErr = ErrInvalidEncryptionParametersSSEC
|
||||
case crypto.ErrInvalidEncryptionMethod:
|
||||
apiErr = ErrInvalidEncryptionMethod
|
||||
case crypto.ErrInvalidEncryptionKeyID:
|
||||
@@ -2255,11 +2291,7 @@ func toAPIError(ctx context.Context, err error) APIError {
|
||||
// their internal error types.
|
||||
switch e := err.(type) {
|
||||
case batchReplicationJobError:
|
||||
apiErr = APIError{
|
||||
Code: e.Code,
|
||||
Description: e.Description,
|
||||
HTTPStatusCode: e.HTTPStatusCode,
|
||||
}
|
||||
apiErr = APIError(e)
|
||||
case InvalidArgument:
|
||||
apiErr = APIError{
|
||||
Code: "InvalidArgument",
|
||||
|
||||
+23
-6
@@ -20,6 +20,7 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -64,15 +65,31 @@ func setCommonHeaders(w http.ResponseWriter) {
|
||||
|
||||
// Encodes the response headers into XML format.
|
||||
func encodeResponse(response interface{}) []byte {
|
||||
var bytesBuffer bytes.Buffer
|
||||
bytesBuffer.WriteString(xxml.Header)
|
||||
buf, err := xxml.Marshal(response)
|
||||
if err != nil {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(xml.Header)
|
||||
if err := xml.NewEncoder(&buf).Encode(response); err != nil {
|
||||
logger.LogIf(GlobalContext, err)
|
||||
return nil
|
||||
}
|
||||
bytesBuffer.Write(buf)
|
||||
return bytesBuffer.Bytes()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// Use this encodeResponseList() to support control characters
|
||||
// this function must be used by only ListObjects() for objects
|
||||
// with control characters, this is a specialized extension
|
||||
// to support AWS S3 compatible behavior.
|
||||
//
|
||||
// Do not use this function for anything other than ListObjects()
|
||||
// variants, please open a github discussion if you wish to use
|
||||
// this in other places.
|
||||
func encodeResponseList(response interface{}) []byte {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(xxml.Header)
|
||||
if err := xxml.NewEncoder(&buf).Encode(response); err != nil {
|
||||
logger.LogIf(GlobalContext, err)
|
||||
return nil
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// Encodes the response headers into JSON format.
|
||||
|
||||
+16
-17
@@ -29,6 +29,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/amztime"
|
||||
"github.com/minio/minio/internal/crypto"
|
||||
"github.com/minio/minio/internal/handlers"
|
||||
"github.com/minio/minio/internal/hash"
|
||||
@@ -38,12 +39,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// RFC3339 a subset of the ISO8601 timestamp format. e.g 2014-04-29T18:30:38Z
|
||||
iso8601TimeFormat = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
|
||||
maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
|
||||
maxDeleteList = 1000 // Limit number of objects deleted in a delete call.
|
||||
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
|
||||
maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
|
||||
maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
|
||||
maxDeleteList = 1000 // Limit number of objects deleted in a delete call.
|
||||
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
|
||||
maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
|
||||
)
|
||||
|
||||
// LocationResponse - format for location response.
|
||||
@@ -315,12 +314,12 @@ func (s *Metadata) Set(k, v string) {
|
||||
}
|
||||
|
||||
type xmlKeyEntry struct {
|
||||
XMLName xml.Name
|
||||
XMLName xxml.Name
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// MarshalXML - StringMap marshals into XML.
|
||||
func (s *Metadata) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
func (s *Metadata) MarshalXML(e *xxml.Encoder, start xxml.StartElement) error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -335,7 +334,7 @@ func (s *Metadata) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
|
||||
for _, item := range s.Items {
|
||||
if err := e.Encode(xmlKeyEntry{
|
||||
XMLName: xml.Name{Local: item.Key},
|
||||
XMLName: xxml.Name{Local: item.Key},
|
||||
Value: item.Value,
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -482,7 +481,7 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
|
||||
for _, bucket := range buckets {
|
||||
listbuckets = append(listbuckets, Bucket{
|
||||
Name: bucket.Name,
|
||||
CreationDate: bucket.Created.UTC().Format(iso8601TimeFormat),
|
||||
CreationDate: amztime.ISO8601Format(bucket.Created.UTC()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -508,7 +507,7 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
|
||||
}
|
||||
content := ObjectVersion{}
|
||||
content.Key = s3EncodeName(object.Name, encodingType)
|
||||
content.LastModified = object.ModTime.UTC().Format(iso8601TimeFormat)
|
||||
content.LastModified = amztime.ISO8601Format(object.ModTime.UTC())
|
||||
if object.ETag != "" {
|
||||
content.ETag = "\"" + object.ETag + "\""
|
||||
}
|
||||
@@ -566,7 +565,7 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingTy
|
||||
continue
|
||||
}
|
||||
content.Key = s3EncodeName(object.Name, encodingType)
|
||||
content.LastModified = object.ModTime.UTC().Format(iso8601TimeFormat)
|
||||
content.LastModified = amztime.ISO8601Format(object.ModTime.UTC())
|
||||
if object.ETag != "" {
|
||||
content.ETag = "\"" + object.ETag + "\""
|
||||
}
|
||||
@@ -615,7 +614,7 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
||||
continue
|
||||
}
|
||||
content.Key = s3EncodeName(object.Name, encodingType)
|
||||
content.LastModified = object.ModTime.UTC().Format(iso8601TimeFormat)
|
||||
content.LastModified = amztime.ISO8601Format(object.ModTime.UTC())
|
||||
if object.ETag != "" {
|
||||
content.ETag = "\"" + object.ETag + "\""
|
||||
}
|
||||
@@ -678,7 +677,7 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
||||
func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse {
|
||||
return CopyObjectResponse{
|
||||
ETag: "\"" + etag + "\"",
|
||||
LastModified: lastModified.UTC().Format(iso8601TimeFormat),
|
||||
LastModified: amztime.ISO8601Format(lastModified.UTC()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,7 +685,7 @@ func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectR
|
||||
func generateCopyObjectPartResponse(etag string, lastModified time.Time) CopyObjectPartResponse {
|
||||
return CopyObjectPartResponse{
|
||||
ETag: "\"" + etag + "\"",
|
||||
LastModified: lastModified.UTC().Format(iso8601TimeFormat),
|
||||
LastModified: amztime.ISO8601Format(lastModified.UTC()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -746,7 +745,7 @@ func generateListPartsResponse(partsInfo ListPartsInfo, encodingType string) Lis
|
||||
newPart.PartNumber = part.PartNumber
|
||||
newPart.ETag = "\"" + part.ETag + "\""
|
||||
newPart.Size = part.Size
|
||||
newPart.LastModified = part.LastModified.UTC().Format(iso8601TimeFormat)
|
||||
newPart.LastModified = amztime.ISO8601Format(part.LastModified.UTC())
|
||||
newPart.ChecksumCRC32 = part.ChecksumCRC32
|
||||
newPart.ChecksumCRC32C = part.ChecksumCRC32C
|
||||
newPart.ChecksumSHA1 = part.ChecksumSHA1
|
||||
@@ -780,7 +779,7 @@ func generateListMultipartUploadsResponse(bucket string, multipartsInfo ListMult
|
||||
newUpload := Upload{}
|
||||
newUpload.UploadID = upload.UploadID
|
||||
newUpload.Key = s3EncodeName(upload.Object, encodingType)
|
||||
newUpload.Initiated = upload.Initiated.UTC().Format(iso8601TimeFormat)
|
||||
newUpload.Initiated = amztime.ISO8601Format(upload.Initiated.UTC())
|
||||
listMultipartUploadsResponse.Uploads[index] = newUpload
|
||||
}
|
||||
return listMultipartUploadsResponse
|
||||
|
||||
+1
-1
@@ -22,11 +22,11 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/klauspost/compress/gzhttp"
|
||||
"github.com/minio/console/restapi"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/wildcard"
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
+205
-200
File diff suppressed because one or more lines are too long
+24
-11
@@ -39,6 +39,7 @@ import (
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
xjwt "github.com/minio/minio/internal/jwt"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/mcontext"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
)
|
||||
@@ -150,9 +151,6 @@ func validateAdminSignature(ctx context.Context, r *http.Request, region string)
|
||||
s3Err = isReqAuthenticated(ctx, r, region, serviceS3)
|
||||
}
|
||||
if s3Err != ErrNone {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("requestHeaders", dumpRequest(r))
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, errors.New(getAPIError(s3Err).Description), logger.Application)
|
||||
return cred, nil, owner, s3Err
|
||||
}
|
||||
|
||||
@@ -373,6 +371,7 @@ func authorizeRequest(ctx context.Context, r *http.Request, action policy.Action
|
||||
region := reqInfo.Region
|
||||
bucket := reqInfo.BucketName
|
||||
object := reqInfo.ObjectName
|
||||
versionID := reqInfo.VersionID
|
||||
|
||||
if action != policy.ListAllMyBucketsAction && cred.AccessKey == "" {
|
||||
// Anonymous checks are not meant for ListAllBuckets action
|
||||
@@ -406,7 +405,21 @@ func authorizeRequest(ctx context.Context, r *http.Request, action policy.Action
|
||||
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
if action == policy.DeleteObjectAction && versionID != "" {
|
||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
Action: iampolicy.Action(policy.DeleteObjectVersionAction),
|
||||
BucketName: bucket,
|
||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
||||
ObjectName: object,
|
||||
IsOwner: owner,
|
||||
Claims: cred.Claims,
|
||||
DenyOnly: true,
|
||||
}) { // Request is not allowed if Deny action on DeleteObjectVersionAction
|
||||
return ErrAccessDenied
|
||||
}
|
||||
}
|
||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
@@ -544,7 +557,7 @@ func isSupportedS3AuthType(aType authType) bool {
|
||||
func setAuthHandler(h http.Handler) http.Handler {
|
||||
// handler for validating incoming authorization headers.
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tc, ok := r.Context().Value(contextTraceReqKey).(*traceCtxt)
|
||||
tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
|
||||
|
||||
aType := getRequestAuthType(r)
|
||||
if aType == authTypeSigned || aType == authTypeSignedV2 || aType == authTypeStreamingSigned {
|
||||
@@ -552,8 +565,8 @@ func setAuthHandler(h http.Handler) http.Handler {
|
||||
amzDate, errCode := parseAmzDateHeader(r)
|
||||
if errCode != ErrNone {
|
||||
if ok {
|
||||
tc.funcName = "handler.Auth"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
tc.FuncName = "handler.Auth"
|
||||
tc.ResponseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
// All our internal APIs are sensitive towards Date
|
||||
@@ -568,8 +581,8 @@ func setAuthHandler(h http.Handler) http.Handler {
|
||||
curTime := UTCNow()
|
||||
if curTime.Sub(amzDate) > globalMaxSkewTime || amzDate.Sub(curTime) > globalMaxSkewTime {
|
||||
if ok {
|
||||
tc.funcName = "handler.Auth"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
tc.FuncName = "handler.Auth"
|
||||
tc.ResponseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrRequestTimeTooSkewed), r.URL)
|
||||
@@ -583,8 +596,8 @@ func setAuthHandler(h http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
if ok {
|
||||
tc.funcName = "handler.Auth"
|
||||
tc.responseRecorder.LogErrBody = true
|
||||
tc.FuncName = "handler.Auth"
|
||||
tc.ResponseRecorder.LogErrBody = true
|
||||
}
|
||||
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL)
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"context"
|
||||
"runtime"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
)
|
||||
|
||||
// healTask represents what to heal along with options
|
||||
@@ -88,8 +88,7 @@ func (h *healRoutine) AddWorker(ctx context.Context, objAPI ObjectLayer) {
|
||||
var err error
|
||||
switch task.bucket {
|
||||
case nopHeal:
|
||||
task.respCh <- healResult{err: errSkipFile}
|
||||
continue
|
||||
err = errSkipFile
|
||||
case SlashSeparator:
|
||||
res, err = healDiskFormat(ctx, objAPI, task.opts)
|
||||
default:
|
||||
@@ -100,7 +99,10 @@ func (h *healRoutine) AddWorker(ctx context.Context, objAPI ObjectLayer) {
|
||||
}
|
||||
}
|
||||
|
||||
task.respCh <- healResult{result: res, err: err}
|
||||
if task.respCh != nil {
|
||||
task.respCh <- healResult{result: res, err: err}
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
@@ -79,6 +79,10 @@ type healingTracker struct {
|
||||
|
||||
// Filled during heal.
|
||||
HealedBuckets []string
|
||||
|
||||
// ID of the current healing operation
|
||||
HealID string
|
||||
|
||||
// Add future tracking capabilities
|
||||
// Be sure that they are included in toHealingDisk
|
||||
}
|
||||
@@ -112,11 +116,12 @@ func loadHealingTracker(ctx context.Context, disk StorageAPI) (*healingTracker,
|
||||
}
|
||||
|
||||
// newHealingTracker will create a new healing tracker for the disk.
|
||||
func newHealingTracker(disk StorageAPI) *healingTracker {
|
||||
func newHealingTracker(disk StorageAPI, healID string) *healingTracker {
|
||||
diskID, _ := disk.GetDiskID()
|
||||
h := healingTracker{
|
||||
disk: disk,
|
||||
ID: diskID,
|
||||
HealID: healID,
|
||||
Path: disk.String(),
|
||||
Endpoint: disk.Endpoint().String(),
|
||||
Started: time.Now().UTC(),
|
||||
@@ -227,6 +232,7 @@ func (h *healingTracker) printTo(writer io.Writer) {
|
||||
func (h *healingTracker) toHealingDisk() madmin.HealingDisk {
|
||||
return madmin.HealingDisk{
|
||||
ID: h.ID,
|
||||
HealID: h.HealID,
|
||||
Endpoint: h.Endpoint,
|
||||
PoolIndex: h.PoolIndex,
|
||||
SetIndex: h.SetIndex,
|
||||
@@ -286,13 +292,11 @@ func getLocalDisksToHeal() (disksToHeal Endpoints) {
|
||||
var newDiskHealingTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
||||
|
||||
func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint) error {
|
||||
logger.Info(fmt.Sprintf("Proceeding to heal '%s' - 'mc admin heal alias/ --verbose' to check the status.", endpoint))
|
||||
|
||||
disk, format, err := connectEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error: %w, %s", err, endpoint)
|
||||
}
|
||||
|
||||
defer disk.Close()
|
||||
poolIdx := globalEndpoints.GetLocalPoolIdx(disk.Endpoint())
|
||||
if poolIdx < 0 {
|
||||
return fmt.Errorf("unexpected pool index (%d) found in %s", poolIdx, disk.Endpoint())
|
||||
@@ -316,7 +320,21 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
||||
return err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer locker.Unlock(lkctx.Cancel)
|
||||
defer locker.Unlock(lkctx)
|
||||
|
||||
// Load healing tracker in this disk
|
||||
tracker, err := loadHealingTracker(ctx, disk)
|
||||
if err != nil {
|
||||
// A healing track can be not found when another disk in the same
|
||||
// erasure set and same healing-id successfully finished healing.
|
||||
if err == errFileNotFound {
|
||||
return nil
|
||||
}
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to load a healing tracker on '%s': %w", disk, err))
|
||||
tracker = newHealingTracker(disk, mustGetUUID())
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Proceeding to heal '%s' - 'mc admin heal alias/ --verbose' to check the status.", endpoint))
|
||||
|
||||
buckets, _ := z.ListBuckets(ctx, BucketOptions{})
|
||||
// Buckets data are dispersed in multiple zones/sets, make
|
||||
@@ -340,15 +358,6 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
||||
logger.Info("Healing drive '%v' on %s pool", disk, humanize.Ordinal(poolIdx+1))
|
||||
}
|
||||
|
||||
// Load healing tracker in this disk
|
||||
tracker, err := loadHealingTracker(ctx, disk)
|
||||
if err != nil {
|
||||
// So someone changed the drives underneath, healing tracker missing.
|
||||
logger.LogIf(ctx, fmt.Errorf("Healing tracker missing on '%s', drive was swapped again on %s pool: %w",
|
||||
disk, humanize.Ordinal(poolIdx+1), err))
|
||||
tracker = newHealingTracker(disk)
|
||||
}
|
||||
|
||||
// Load bucket totals
|
||||
cache := dataUsageCache{}
|
||||
if err := cache.load(ctx, z.serverPools[poolIdx].sets[setIdx], dataUsageCacheName); err == nil {
|
||||
@@ -379,7 +388,24 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
||||
logger.Info("\n")
|
||||
}
|
||||
|
||||
logger.LogIf(ctx, tracker.delete(ctx))
|
||||
if tracker.HealID == "" { // HealID is empty only before Feb 2023
|
||||
logger.LogIf(ctx, tracker.delete(ctx))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove .healing.bin from all disks with similar heal-id
|
||||
for _, disk := range z.serverPools[poolIdx].sets[setIdx].getDisks() {
|
||||
t, err := loadHealingTracker(ctx, disk)
|
||||
if err != nil {
|
||||
if err != errFileNotFound {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if t.HealID == tracker.HealID {
|
||||
t.delete(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -415,9 +441,10 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools) {
|
||||
|
||||
for _, disk := range healDisks {
|
||||
go func(disk Endpoint) {
|
||||
globalBackgroundHealState.markDiskForHealing(disk)
|
||||
globalBackgroundHealState.setDiskHealingStatus(disk, true)
|
||||
err := healFreshDisk(ctx, z, disk)
|
||||
if err != nil {
|
||||
globalBackgroundHealState.setDiskHealingStatus(disk, false)
|
||||
printEndpointError(disk, err, false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -182,6 +182,12 @@ func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
case "HealID":
|
||||
z.HealID, err = dc.ReadString()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "HealID")
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = dc.Skip()
|
||||
if err != nil {
|
||||
@@ -195,9 +201,9 @@ func (z *healingTracker) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// map header, size 22
|
||||
// map header, size 23
|
||||
// write "ID"
|
||||
err = en.Append(0xde, 0x0, 0x16, 0xa2, 0x49, 0x44)
|
||||
err = en.Append(0xde, 0x0, 0x17, 0xa2, 0x49, 0x44)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -430,15 +436,25 @@ func (z *healingTracker) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// write "HealID"
|
||||
err = en.Append(0xa6, 0x48, 0x65, 0x61, 0x6c, 0x49, 0x44)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteString(z.HealID)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "HealID")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z *healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// map header, size 22
|
||||
// map header, size 23
|
||||
// string "ID"
|
||||
o = append(o, 0xde, 0x0, 0x16, 0xa2, 0x49, 0x44)
|
||||
o = append(o, 0xde, 0x0, 0x17, 0xa2, 0x49, 0x44)
|
||||
o = msgp.AppendString(o, z.ID)
|
||||
// string "PoolIndex"
|
||||
o = append(o, 0xa9, 0x50, 0x6f, 0x6f, 0x6c, 0x49, 0x6e, 0x64, 0x65, 0x78)
|
||||
@@ -509,6 +525,9 @@ func (z *healingTracker) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
for za0002 := range z.HealedBuckets {
|
||||
o = msgp.AppendString(o, z.HealedBuckets[za0002])
|
||||
}
|
||||
// string "HealID"
|
||||
o = append(o, 0xa6, 0x48, 0x65, 0x61, 0x6c, 0x49, 0x44)
|
||||
o = msgp.AppendString(o, z.HealID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -688,6 +707,12 @@ func (z *healingTracker) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
case "HealID":
|
||||
z.HealID, bts, err = msgp.ReadStringBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "HealID")
|
||||
return
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
@@ -710,5 +735,6 @@ func (z *healingTracker) Msgsize() (s int) {
|
||||
for za0002 := range z.HealedBuckets {
|
||||
s += msgp.StringPrefixSize + len(z.HealedBuckets[za0002])
|
||||
}
|
||||
s += 7 + msgp.StringPrefixSize + len(z.HealID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
miniogo "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
|
||||
@@ -35,7 +35,7 @@ func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, MakeBucketOptions{})
|
||||
err = obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
||||
object := getRandomObjectName()
|
||||
|
||||
// create bucket.
|
||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, MakeBucketOptions{})
|
||||
err = obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -196,7 +196,7 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err := obj.MakeBucketWithLocation(context.Background(), bucket, MakeBucketOptions{})
|
||||
err := obj.MakeBucket(context.Background(), bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
@@ -25,14 +25,13 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/rest"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/env"
|
||||
)
|
||||
|
||||
@@ -53,23 +52,22 @@ type bootstrapRESTServer struct{}
|
||||
|
||||
// ServerSystemConfig - captures information about server configuration.
|
||||
type ServerSystemConfig struct {
|
||||
MinioPlatform string
|
||||
MinioEndpoints EndpointServerPools
|
||||
MinioEnv map[string]string
|
||||
}
|
||||
|
||||
// Diff - returns error on first difference found in two configs.
|
||||
func (s1 ServerSystemConfig) Diff(s2 ServerSystemConfig) error {
|
||||
if s1.MinioPlatform != s2.MinioPlatform {
|
||||
return fmt.Errorf("Expected platform '%s', found to be running '%s'",
|
||||
s1.MinioPlatform, s2.MinioPlatform)
|
||||
}
|
||||
if s1.MinioEndpoints.NEndpoints() != s2.MinioEndpoints.NEndpoints() {
|
||||
return fmt.Errorf("Expected number of endpoints %d, seen %d", s1.MinioEndpoints.NEndpoints(),
|
||||
s2.MinioEndpoints.NEndpoints())
|
||||
}
|
||||
|
||||
for i, ep := range s1.MinioEndpoints {
|
||||
if ep.CmdLine != s2.MinioEndpoints[i].CmdLine {
|
||||
return fmt.Errorf("Expected command line argument %s, seen %s", ep.CmdLine,
|
||||
s2.MinioEndpoints[i].CmdLine)
|
||||
}
|
||||
if ep.SetCount != s2.MinioEndpoints[i].SetCount {
|
||||
return fmt.Errorf("Expected set count %d, seen %d", ep.SetCount,
|
||||
s2.MinioEndpoints[i].SetCount)
|
||||
@@ -78,11 +76,9 @@ func (s1 ServerSystemConfig) Diff(s2 ServerSystemConfig) error {
|
||||
return fmt.Errorf("Expected drives pet set %d, seen %d", ep.DrivesPerSet,
|
||||
s2.MinioEndpoints[i].DrivesPerSet)
|
||||
}
|
||||
for j, endpoint := range ep.Endpoints {
|
||||
if endpoint.String() != s2.MinioEndpoints[i].Endpoints[j].String() {
|
||||
return fmt.Errorf("Expected endpoint %s, seen %s", endpoint,
|
||||
s2.MinioEndpoints[i].Endpoints[j])
|
||||
}
|
||||
if ep.Platform != s2.MinioEndpoints[i].Platform {
|
||||
return fmt.Errorf("Expected platform '%s', found to be on '%s'",
|
||||
ep.Platform, s2.MinioEndpoints[i].Platform)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(s1.MinioEnv, s2.MinioEnv) {
|
||||
@@ -125,7 +121,6 @@ func getServerSystemCfg() ServerSystemConfig {
|
||||
envValues[envK] = env.Get(envK, "")
|
||||
}
|
||||
return ServerSystemConfig{
|
||||
MinioPlatform: fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH),
|
||||
MinioEndpoints: globalEndpoints,
|
||||
MinioEnv: envValues,
|
||||
}
|
||||
@@ -200,15 +195,18 @@ func verifyServerSystemConfig(ctx context.Context, endpointServerPools EndpointS
|
||||
srcCfg := getServerSystemCfg()
|
||||
clnts := newBootstrapRESTClients(endpointServerPools)
|
||||
var onlineServers int
|
||||
var offlineEndpoints []string
|
||||
var offlineEndpoints []error
|
||||
var incorrectConfigs []error
|
||||
var retries int
|
||||
for onlineServers < len(clnts)/2 {
|
||||
for _, clnt := range clnts {
|
||||
if err := clnt.Verify(ctx, srcCfg); err != nil {
|
||||
if !isNetworkError(err) {
|
||||
logger.LogIf(ctx, fmt.Errorf("%s has incorrect configuration: %w", clnt.String(), err))
|
||||
logger.LogOnceIf(ctx, fmt.Errorf("%s has incorrect configuration: %w", clnt.String(), err), clnt.String())
|
||||
incorrectConfigs = append(incorrectConfigs, fmt.Errorf("%s has incorrect configuration: %w", clnt.String(), err))
|
||||
} else {
|
||||
offlineEndpoints = append(offlineEndpoints, fmt.Errorf("%s is unreachable: %w", clnt.String(), err))
|
||||
}
|
||||
offlineEndpoints = append(offlineEndpoints, clnt.String())
|
||||
continue
|
||||
}
|
||||
onlineServers++
|
||||
@@ -221,15 +219,19 @@ func verifyServerSystemConfig(ctx context.Context, endpointServerPools EndpointS
|
||||
// 100% CPU when half the endpoints are offline.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
retries++
|
||||
// after 5 retries start logging that servers are not reachable yet
|
||||
if retries >= 5 {
|
||||
logger.Info(fmt.Sprintf("Waiting for atleast %d remote servers to be online for bootstrap check", len(clnts)/2))
|
||||
// after 20 retries start logging that servers are not reachable yet
|
||||
if retries >= 20 {
|
||||
logger.Info(fmt.Sprintf("Waiting for atleast %d remote servers with valid configuration to be online", len(clnts)/2))
|
||||
if len(offlineEndpoints) > 0 {
|
||||
logger.Info(fmt.Sprintf("Following servers are currently offline or unreachable %s", offlineEndpoints))
|
||||
}
|
||||
if len(incorrectConfigs) > 0 {
|
||||
logger.Info(fmt.Sprintf("Following servers have mismatching configuration %s", incorrectConfigs))
|
||||
}
|
||||
retries = 0 // reset to log again after 5 retries.
|
||||
}
|
||||
offlineEndpoints = nil
|
||||
incorrectConfigs = nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -25,11 +25,11 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/kes"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
@@ -51,11 +51,6 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
if !objAPI.IsEncryptionSupported() {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
@@ -119,15 +114,12 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
||||
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||
// errors.
|
||||
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeSSEConfig,
|
||||
Bucket: bucket,
|
||||
SSEConfig: &cfgStr,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
@@ -209,16 +201,14 @@ func (api objectAPIHandlers) DeleteBucketEncryptionHandler(w http.ResponseWriter
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Call site replication hook.
|
||||
//
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeSSEConfig,
|
||||
Bucket: bucket,
|
||||
SSEConfig: nil,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
+68
-83
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
@@ -34,9 +34,9 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/mux"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
sse "github.com/minio/minio/internal/bucket/encryption"
|
||||
@@ -431,7 +431,6 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
// Unmarshal list of keys to be deleted.
|
||||
deleteObjectsReq := &DeleteObjectsRequest{}
|
||||
if err := xmlDecoder(r.Body, deleteObjectsReq, maxBodySize); err != nil {
|
||||
logger.LogIf(ctx, err, logger.Application)
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -511,11 +510,10 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
}
|
||||
if object.VersionID != "" && object.VersionID != nullVersionID {
|
||||
if _, err := uuid.Parse(object.VersionID); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("invalid version-id specified %w", err))
|
||||
apiErr := errorCodes.ToAPIErr(ErrNoSuchVersion)
|
||||
deleteResults[index].errInfo = DeleteError{
|
||||
Code: apiErr.Code,
|
||||
Message: apiErr.Description,
|
||||
Message: fmt.Sprintf("%s (%s)", apiErr.Description, err),
|
||||
Key: object.ObjectName,
|
||||
VersionID: object.VersionID,
|
||||
}
|
||||
@@ -769,16 +767,12 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
// check if client is attempting to create more buckets than allowed maximum.
|
||||
// check if client is attempting to create more buckets, complain about it.
|
||||
if currBuckets := globalBucketMetadataSys.Count(); currBuckets+1 > maxBuckets {
|
||||
apiErr := errorCodes.ToAPIErr(ErrTooManyBuckets)
|
||||
apiErr.Description = fmt.Sprintf("You have attempted to create %d buckets than allowed %d", currBuckets+1, maxBuckets)
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
logger.LogIf(ctx, fmt.Errorf("An attempt to create %d buckets beyond recommended %d", currBuckets+1, maxBuckets))
|
||||
}
|
||||
|
||||
opts := MakeBucketOptions{
|
||||
Location: location,
|
||||
LockEnabled: objectLockEnabled,
|
||||
ForceCreate: forceCreate,
|
||||
}
|
||||
@@ -790,15 +784,14 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
// exists elsewhere
|
||||
if err == dns.ErrNoEntriesFound || err == dns.ErrNotImplemented {
|
||||
// Proceed to creating a bucket.
|
||||
if err = objectAPI.MakeBucketWithLocation(ctx, bucket, opts); err != nil {
|
||||
if err = objectAPI.MakeBucket(ctx, bucket, opts); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = globalDNSConfig.Put(bucket); err != nil {
|
||||
objectAPI.DeleteBucket(context.Background(), bucket, DeleteBucketOptions{
|
||||
Force: false,
|
||||
NoRecreate: true,
|
||||
Force: true,
|
||||
SRDeleteOp: getSRBucketDeleteOp(globalSiteReplicationSys.isEnabled()),
|
||||
})
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
@@ -842,7 +835,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
// Proceed to creating a bucket.
|
||||
if err := objectAPI.MakeBucketWithLocation(ctx, bucket, opts); err != nil {
|
||||
if err := objectAPI.MakeBucket(ctx, bucket, opts); err != nil {
|
||||
if _, ok := err.(BucketExists); ok {
|
||||
// Though bucket exists locally, we send the site-replication
|
||||
// hook to ensure all sites have this bucket. If the hook
|
||||
@@ -858,7 +851,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
||||
|
||||
// Call site replication hook
|
||||
globalSiteReplicationSys.MakeBucketHook(ctx, bucket, opts)
|
||||
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 != "" {
|
||||
@@ -897,11 +890,6 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
if crypto.Requested(r.Header) && !objectAPI.IsEncryptionSupported() {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
bucket := mux.Vars(r)["bucket"]
|
||||
|
||||
// Require Content-Length to be set in the request
|
||||
@@ -927,16 +915,18 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
// be loaded in memory, the remaining being put in temporary files.
|
||||
reader, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL)
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
||||
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Read multipart data and save in memory and in the disk if needed
|
||||
form, err := reader.ReadForm(maxFormMemory)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err, logger.Application)
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL)
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
||||
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -946,8 +936,9 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
// Extract all form fields
|
||||
fileBody, fileName, fileSize, formValues, err := extractPostPolicyFormValues(ctx, form)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err, logger.Application)
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL)
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
||||
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1070,51 +1061,56 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
writeErrorResponseHeadersOnly(w, toAPIError(ctx, err))
|
||||
return
|
||||
}
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if crypto.Requested(formValues) && !HasSuffix(object, SlashSeparator) { // handle SSE requests
|
||||
if crypto.SSECopy.IsRequested(r.Header) {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParameters), r.URL)
|
||||
return
|
||||
}
|
||||
var (
|
||||
reader io.Reader
|
||||
keyID string
|
||||
key []byte
|
||||
kmsCtx kms.Context
|
||||
)
|
||||
kind, _ := crypto.IsRequested(formValues)
|
||||
switch kind {
|
||||
case crypto.SSEC:
|
||||
key, err = ParseSSECustomerHeader(formValues)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
case crypto.S3KMS:
|
||||
keyID, kmsCtx, err = crypto.S3KMS.ParseHTTP(formValues)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
reader, objectEncryptionKey, err = newEncryptReader(ctx, hashReader, kind, keyID, key, bucket, object, metadata, kmsCtx)
|
||||
|
||||
if crypto.Requested(formValues) {
|
||||
if crypto.SSECopy.IsRequested(r.Header) {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParameters), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if crypto.SSEC.IsRequested(r.Header) && isReplicationEnabled(ctx, bucket) {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParametersSSEC), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
reader io.Reader
|
||||
keyID string
|
||||
key []byte
|
||||
kmsCtx kms.Context
|
||||
)
|
||||
kind, _ := crypto.IsRequested(formValues)
|
||||
switch kind {
|
||||
case crypto.SSEC:
|
||||
key, err = ParseSSECustomerHeader(formValues)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
info := ObjectInfo{Size: fileSize}
|
||||
// do not try to verify encrypted content
|
||||
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", fileSize)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
pReader, err = pReader.WithEncryption(hashReader, &objectEncryptionKey)
|
||||
case crypto.S3KMS:
|
||||
keyID, kmsCtx, err = crypto.S3KMS.ParseHTTP(formValues)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
reader, objectEncryptionKey, err = newEncryptReader(ctx, hashReader, kind, keyID, key, bucket, object, metadata, kmsCtx)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
info := ObjectInfo{Size: fileSize}
|
||||
// do not try to verify encrypted content
|
||||
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", fileSize)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
pReader, err = pReader.WithEncryption(hashReader, &objectEncryptionKey)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
objInfo, err := objectAPI.PutObject(ctx, bucket, object, pReader, opts)
|
||||
@@ -1356,11 +1352,9 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
|
||||
globalNotificationSys.DeleteBucketMetadata(ctx, bucket)
|
||||
globalReplicationPool.deleteResyncMetadata(ctx, bucket)
|
||||
|
||||
// Call site replication hook.
|
||||
if err := globalSiteReplicationSys.DeleteBucketHook(ctx, bucket, forceDelete); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.DeleteBucketHook(ctx, bucket, forceDelete))
|
||||
|
||||
// Write success response.
|
||||
writeSuccessNoContent(w)
|
||||
@@ -1429,15 +1423,12 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||
// errors.
|
||||
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
||||
Bucket: bucket,
|
||||
ObjectLockConfig: &cfgStr,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
@@ -1536,15 +1527,12 @@ func (api objectAPIHandlers) PutBucketTaggingHandler(w http.ResponseWriter, r *h
|
||||
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||
// errors.
|
||||
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeTags,
|
||||
Bucket: bucket,
|
||||
Tags: &cfgStr,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
@@ -1615,14 +1603,11 @@ func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeTags,
|
||||
Bucket: bucket,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
|
||||
+24
-11
@@ -662,6 +662,9 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
var objectNames []string
|
||||
for i := 0; i < 10; i++ {
|
||||
objectName := "test-object-" + strconv.Itoa(i)
|
||||
if i == 0 {
|
||||
objectName += "/"
|
||||
}
|
||||
// uploading the object.
|
||||
_, err = obj.PutObject(GlobalContext, bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{})
|
||||
// if object upload fails stop the test.
|
||||
@@ -793,9 +796,9 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
expectedContent []byte
|
||||
expectedRespStatus int
|
||||
}{
|
||||
// Test case - 1.
|
||||
// Test case - 0.
|
||||
// Delete objects with invalid access key.
|
||||
{
|
||||
0: {
|
||||
bucket: bucketName,
|
||||
objects: successRequest0,
|
||||
accessKey: "Invalid-AccessID",
|
||||
@@ -803,9 +806,19 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
expectedContent: nil,
|
||||
expectedRespStatus: http.StatusForbidden,
|
||||
},
|
||||
// Test case - 2.
|
||||
// Test case - 1.
|
||||
// Delete valid objects with quiet flag off.
|
||||
{
|
||||
1: {
|
||||
bucket: bucketName,
|
||||
objects: successRequest0,
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedContent: encodedSuccessResponse0,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 2.
|
||||
// Delete deleted objects with quiet flag off.
|
||||
2: {
|
||||
bucket: bucketName,
|
||||
objects: successRequest0,
|
||||
accessKey: credentials.AccessKey,
|
||||
@@ -815,7 +828,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
},
|
||||
// Test case - 3.
|
||||
// Delete valid objects with quiet flag on.
|
||||
{
|
||||
3: {
|
||||
bucket: bucketName,
|
||||
objects: successRequest1,
|
||||
accessKey: credentials.AccessKey,
|
||||
@@ -825,7 +838,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
},
|
||||
// Test case - 4.
|
||||
// Delete previously deleted objects.
|
||||
{
|
||||
4: {
|
||||
bucket: bucketName,
|
||||
objects: successRequest1,
|
||||
accessKey: credentials.AccessKey,
|
||||
@@ -836,7 +849,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
// Test case - 5.
|
||||
// Anonymous user access denied response
|
||||
// Currently anonymous users cannot delete multiple objects in MinIO server
|
||||
{
|
||||
5: {
|
||||
bucket: bucketName,
|
||||
objects: anonRequest,
|
||||
accessKey: "",
|
||||
@@ -847,7 +860,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
// Test case - 6.
|
||||
// Anonymous user has access to some public folder, issue removing with
|
||||
// another private object as well
|
||||
{
|
||||
6: {
|
||||
bucket: bucketName,
|
||||
objects: anonRequestWithPartialPublicAccess,
|
||||
accessKey: "",
|
||||
@@ -881,19 +894,19 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
// Assert the response code with the expected status.
|
||||
if rec.Code != testCase.expectedRespStatus {
|
||||
t.Errorf("Test %d: MinIO %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
|
||||
t.Errorf("Test %d: MinIO %s: Expected the response status to be `%d`, but instead found `%d`", i, instanceType, testCase.expectedRespStatus, rec.Code)
|
||||
}
|
||||
|
||||
// read the response body.
|
||||
actualContent, err = io.ReadAll(rec.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d : MinIO %s: Failed parsing response body: <ERROR> %v", i+1, instanceType, err)
|
||||
t.Fatalf("Test %d : MinIO %s: Failed parsing response body: <ERROR> %v", i, instanceType, err)
|
||||
}
|
||||
|
||||
// Verify whether the bucket obtained object is same as the one created.
|
||||
if testCase.expectedContent != nil && !bytes.Equal(testCase.expectedContent, actualContent) {
|
||||
t.Log(string(testCase.expectedContent), string(actualContent))
|
||||
t.Errorf("Test %d : MinIO %s: Object content differs from expected value.", i+1, instanceType)
|
||||
t.Errorf("Test %d : MinIO %s: Object content differs from expected value.", i, instanceType)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,10 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ func validateTransitionTier(lc *lifecycle.Lifecycle) error {
|
||||
// This is to be called after a successful upload of an object (version).
|
||||
func enqueueTransitionImmediate(obj ObjectInfo) {
|
||||
if lc, err := globalLifecycleSys.Get(obj.Bucket); err == nil {
|
||||
event := lc.Eval(obj.ToLifecycleOpts(), time.Now())
|
||||
event := lc.Eval(obj.ToLifecycleOpts())
|
||||
switch event.Action {
|
||||
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
|
||||
globalTransitionState.queueTransitionTask(obj, event.StorageClass)
|
||||
@@ -540,7 +540,7 @@ type RestoreObjectRequest struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ RestoreRequest" json:"-"`
|
||||
Days int `xml:"Days,omitempty"`
|
||||
Type RestoreRequestType `xml:"Type,omitempty"`
|
||||
Tier string `xml:"Tier,-"`
|
||||
Tier string `xml:"Tier"`
|
||||
Description string `xml:"Description,omitempty"`
|
||||
SelectParameters *SelectParameters `xml:"SelectParameters,omitempty"`
|
||||
OutputLocation OutputLocation `xml:"OutputLocation,omitempty"`
|
||||
@@ -660,7 +660,9 @@ func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo O
|
||||
if len(objInfo.UserTags) != 0 {
|
||||
meta[xhttp.AmzObjectTagging] = objInfo.UserTags
|
||||
}
|
||||
|
||||
// Set restore object status
|
||||
restoreExpiry := lifecycle.ExpectedExpiryTime(time.Now().UTC(), rreq.Days)
|
||||
meta[xhttp.AmzRestore] = completedRestoreObj(restoreExpiry).String()
|
||||
return ObjectOptions{
|
||||
Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, object),
|
||||
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, object),
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
)
|
||||
@@ -35,7 +35,7 @@ import (
|
||||
// - delimiter if set should be equal to '/', otherwise the request is rejected.
|
||||
// - marker if set should have a common prefix with 'prefix' param, otherwise
|
||||
// the request is rejected.
|
||||
func validateListObjectsArgs(marker, delimiter, encodingType string, maxKeys int) APIErrorCode {
|
||||
func validateListObjectsArgs(prefix, marker, delimiter, encodingType string, maxKeys int) APIErrorCode {
|
||||
// Max keys cannot be negative.
|
||||
if maxKeys < 0 {
|
||||
return ErrInvalidMaxKeys
|
||||
@@ -48,6 +48,14 @@ func validateListObjectsArgs(marker, delimiter, encodingType string, maxKeys int
|
||||
}
|
||||
}
|
||||
|
||||
if !IsValidObjectPrefix(prefix) {
|
||||
return ErrInvalidObjectName
|
||||
}
|
||||
|
||||
if marker != "" && !HasPrefix(marker, prefix) {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
@@ -83,7 +91,7 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Validate the query params before beginning to serve the request.
|
||||
if s3Error := validateListObjectsArgs(marker, delimiter, encodingType, maxkeys); s3Error != ErrNone {
|
||||
if s3Error := validateListObjectsArgs(prefix, marker, delimiter, encodingType, maxkeys); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -107,7 +115,7 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
||||
response := generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType, maxkeys, listObjectVersionsInfo)
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, encodeResponse(response))
|
||||
writeSuccessResponseXML(w, encodeResponseList(response))
|
||||
}
|
||||
|
||||
// ListObjectsV2MHandler - GET Bucket (List Objects) Version 2 with metadata.
|
||||
@@ -148,7 +156,7 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
|
||||
|
||||
// Validate the query params before beginning to serve the request.
|
||||
// fetch-owner is not validated since it is a boolean
|
||||
if s3Error := validateListObjectsArgs(token, delimiter, encodingType, maxKeys); s3Error != ErrNone {
|
||||
if s3Error := validateListObjectsArgs(prefix, token, delimiter, encodingType, maxKeys); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -177,7 +185,7 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
|
||||
maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes, true)
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, encodeResponse(response))
|
||||
writeSuccessResponseXML(w, encodeResponseList(response))
|
||||
}
|
||||
|
||||
// ListObjectsV2Handler - GET Bucket (List Objects) Version 2.
|
||||
@@ -218,7 +226,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
||||
|
||||
// Validate the query params before beginning to serve the request.
|
||||
// fetch-owner is not validated since it is a boolean
|
||||
if s3Error := validateListObjectsArgs(token, delimiter, encodingType, maxKeys); s3Error != ErrNone {
|
||||
if s3Error := validateListObjectsArgs(prefix, token, delimiter, encodingType, maxKeys); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -252,7 +260,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
||||
maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes, false)
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, encodeResponse(response))
|
||||
writeSuccessResponseXML(w, encodeResponseList(response))
|
||||
}
|
||||
|
||||
func parseRequestToken(token string) (subToken string, nodeIndex int) {
|
||||
@@ -325,7 +333,7 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// Validate all the query params before beginning to serve the request.
|
||||
if s3Error := validateListObjectsArgs(marker, delimiter, encodingType, maxKeys); s3Error != ErrNone {
|
||||
if s3Error := validateListObjectsArgs(prefix, marker, delimiter, encodingType, maxKeys); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -349,5 +357,5 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
||||
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, encodeResponse(response))
|
||||
writeSuccessResponseXML(w, encodeResponseList(response))
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
bucketsse "github.com/minio/minio/internal/bucket/encryption"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
@@ -66,7 +66,7 @@ func (sys *BucketMetadataSys) Remove(bucket string) {
|
||||
// so they should be replaced atomically and not appended to, etc.
|
||||
// Data is not persisted to disk.
|
||||
func (sys *BucketMetadataSys) Set(bucket string, meta BucketMetadata) {
|
||||
if bucket != minioMetaBucket {
|
||||
if !isMinioMetaBucketName(bucket) {
|
||||
sys.Lock()
|
||||
sys.metadataMap[bucket] = meta
|
||||
sys.Unlock()
|
||||
@@ -79,7 +79,7 @@ func (sys *BucketMetadataSys) updateAndParse(ctx context.Context, bucket string,
|
||||
return updatedAt, errServerNotInitialized
|
||||
}
|
||||
|
||||
if bucket == minioMetaBucket {
|
||||
if isMinioMetaBucketName(bucket) {
|
||||
return updatedAt, errInvalidArgument
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ func (sys *BucketMetadataSys) Update(ctx context.Context, bucket string, configF
|
||||
// For all other bucket specific metadata, use the relevant
|
||||
// calls implemented specifically for each of those features.
|
||||
func (sys *BucketMetadataSys) Get(bucket string) (BucketMetadata, error) {
|
||||
if bucket == minioMetaBucket {
|
||||
if isMinioMetaBucketName(bucket) {
|
||||
return newBucketMetadata(bucket), errConfigNotFound
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (Buc
|
||||
return newBucketMetadata(bucket), errServerNotInitialized
|
||||
}
|
||||
|
||||
if bucket == minioMetaBucket {
|
||||
if isMinioMetaBucketName(bucket) {
|
||||
return newBucketMetadata(bucket), errInvalidArgument
|
||||
}
|
||||
|
||||
|
||||
+2
-19
@@ -29,7 +29,7 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
bucketsse "github.com/minio/minio/internal/bucket/encryption"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
@@ -135,7 +135,7 @@ func (b *BucketMetadata) SetCreatedAt(createdAt time.Time) {
|
||||
func (b *BucketMetadata) Load(ctx context.Context, api ObjectLayer, name string) error {
|
||||
if name == "" {
|
||||
logger.LogIf(ctx, errors.New("bucket name cannot be empty"))
|
||||
return errors.New("bucket name cannot be empty")
|
||||
return errInvalidArgument
|
||||
}
|
||||
configFile := path.Join(bucketMetaPrefix, name, bucketMetadataFile)
|
||||
data, err := readConfig(ctx, api, configFile)
|
||||
@@ -442,23 +442,6 @@ func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error {
|
||||
return saveConfig(ctx, api, configFile, data)
|
||||
}
|
||||
|
||||
// deleteBucketMetadata deletes bucket metadata
|
||||
// If config does not exist no error is returned.
|
||||
func deleteBucketMetadata(ctx context.Context, obj objectDeleter, bucket string) error {
|
||||
metadataFiles := []string{
|
||||
dataUsageCacheName,
|
||||
bucketMetadataFile,
|
||||
path.Join(replicationDir, resyncFileName),
|
||||
}
|
||||
for _, metaFile := range metadataFiles {
|
||||
configFile := path.Join(bucketMetaPrefix, bucket, metaFile)
|
||||
if err := deleteConfig(ctx, obj, configFile); err != nil && err != errConfigNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrate config for remote targets by encrypting data if currently unencrypted and kms is configured.
|
||||
func (b *BucketMetadata) migrateTargetConfig(ctx context.Context, objectAPI ObjectLayer) error {
|
||||
var err error
|
||||
|
||||
@@ -23,9 +23,9 @@ import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/internal/event"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
@@ -50,11 +50,6 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
if !objAPI.IsNotificationSupported() {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
@@ -119,11 +114,6 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
if !objectAPI.IsNotificationSupported() {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucketName := vars["bucket"]
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ import (
|
||||
"net/http"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
@@ -92,7 +92,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
|
||||
// Version in policy must not be empty
|
||||
if bucketPolicy.Version == "" {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPolicy), r.URL)
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPolicyInvalidVersion), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,15 +109,12 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
}
|
||||
|
||||
// Call site replication hook.
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypePolicy,
|
||||
Bucket: bucket,
|
||||
Policy: bucketPolicyBytes,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
||||
// Success.
|
||||
writeSuccessNoContent(w)
|
||||
@@ -156,14 +153,11 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Call site replication hook.
|
||||
if err := globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypePolicy,
|
||||
Bucket: bucket,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
||||
// Success.
|
||||
writeSuccessNoContent(w)
|
||||
|
||||
@@ -107,7 +107,7 @@ func getAnonWriteOnlyObjectPolicy(bucketName, prefix string) *policy.Policy {
|
||||
|
||||
// Wrapper for calling Create Bucket and ensure we get one and only one success.
|
||||
func TestCreateBucket(t *testing.T) {
|
||||
ExecObjectLayerAPITest(t, testCreateBucket, []string{"MakeBucketWithLocation"})
|
||||
ExecObjectLayerAPITest(t, testCreateBucket, []string{"MakeBucket"})
|
||||
}
|
||||
|
||||
// testCreateBucket - Test for calling Create Bucket and ensure we get one and only one success.
|
||||
@@ -127,7 +127,7 @@ func testCreateBucket(obj ObjectLayer, instanceType, bucketName string, apiRoute
|
||||
defer wg.Done()
|
||||
// Sync start.
|
||||
<-start
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName1, MakeBucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucket(GlobalContext, bucketName1, MakeBucketOptions{}); err != nil {
|
||||
if _, ok := err.(BucketExists); !ok {
|
||||
t.Logf("unexpected error: %T: %v", err, err)
|
||||
return
|
||||
@@ -162,7 +162,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
||||
credentials auth.Credentials, t *testing.T,
|
||||
) {
|
||||
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName1, MakeBucketOptions{}); err != nil {
|
||||
if err := obj.MakeBucket(GlobalContext, bucketName1, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
|
||||
@@ -25,10 +25,10 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
@@ -169,6 +169,21 @@ func (api objectAPIHandlers) DeleteBucketReplicationConfigHandler(w http.Respons
|
||||
return
|
||||
}
|
||||
|
||||
targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
for _, tgt := range targets.Targets {
|
||||
if err := globalBucketTargetSys.RemoveTarget(ctx, bucket, tgt.Arn); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, err := globalBucketMetadataSys.Delete(ctx, bucket, bucketTargetsFile); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
@@ -216,7 +231,19 @@ func (api objectAPIHandlers) GetBucketReplicationMetricsHandler(w http.ResponseW
|
||||
w.Header().Set(xhttp.ContentType, string(mimeJSON))
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
if err = enc.Encode(globalReplicationStats.getLatestReplicationStats(bucket, usageInfo)); err != nil {
|
||||
stats := globalReplicationStats.getLatestReplicationStats(bucket, usageInfo)
|
||||
bwRpt := globalNotificationSys.GetBandwidthReports(ctx, bucket)
|
||||
bwMap := bwRpt.BucketStats[bucket]
|
||||
for arn, st := range stats.Stats {
|
||||
if bwMap != nil {
|
||||
if bw, ok := bwMap[arn]; ok {
|
||||
st.BandWidthLimitInBytesPerSecond = bw.LimitInBytesPerSecond
|
||||
st.CurrentBandwidthInBytesPerSecond = bw.CurrentBandwidthInBytesPerSecond
|
||||
stats.Stats[arn] = st
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = enc.Encode(stats); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -321,7 +348,27 @@ func (api objectAPIHandlers) ResetBucketReplicationStartHandler(w http.ResponseW
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
}
|
||||
}
|
||||
if err := startReplicationResync(ctx, bucket, arn, resetID, resetBeforeDate, objectAPI); err != nil {
|
||||
targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
tgtBytes, err := json.Marshal(&targets)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
||||
return
|
||||
}
|
||||
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalReplicationPool.resyncer.start(ctx, objectAPI, resyncOpts{
|
||||
bucket: bucket,
|
||||
arn: arn,
|
||||
resyncID: resetID,
|
||||
resyncBefore: resetBeforeDate,
|
||||
}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Err: err,
|
||||
@@ -370,10 +417,13 @@ func (api objectAPIHandlers) ResetBucketReplicationStatusHandler(w http.Response
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
globalReplicationPool.resyncState.RLock()
|
||||
brs, ok := globalReplicationPool.resyncState.statusMap[bucket]
|
||||
globalReplicationPool.resyncState.RUnlock()
|
||||
var tgtStats map[string]TargetReplicationResyncStatus
|
||||
globalReplicationPool.resyncer.RLock()
|
||||
brs, ok := globalReplicationPool.resyncer.statusMap[bucket]
|
||||
if ok {
|
||||
tgtStats = brs.cloneTgtStats()
|
||||
}
|
||||
globalReplicationPool.resyncer.RUnlock()
|
||||
if !ok {
|
||||
brs, err = loadBucketResyncMetadata(ctx, bucket, objectAPI)
|
||||
if err != nil {
|
||||
@@ -383,10 +433,11 @@ func (api objectAPIHandlers) ResetBucketReplicationStatusHandler(w http.Response
|
||||
}), r.URL)
|
||||
return
|
||||
}
|
||||
tgtStats = brs.cloneTgtStats()
|
||||
}
|
||||
|
||||
var rinfo ResyncTargetsInfo
|
||||
for tarn, st := range brs.TargetsMap {
|
||||
for tarn, st := range tgtStats {
|
||||
if arn != "" && tarn != arn {
|
||||
continue
|
||||
}
|
||||
@@ -394,7 +445,7 @@ func (api objectAPIHandlers) ResetBucketReplicationStatusHandler(w http.Response
|
||||
Arn: tarn,
|
||||
ResetID: st.ResyncID,
|
||||
StartTime: st.StartTime,
|
||||
EndTime: st.EndTime,
|
||||
EndTime: st.LastUpdate,
|
||||
ResyncStatus: st.ResyncStatus.String(),
|
||||
ReplicatedSize: st.ReplicatedSize,
|
||||
ReplicatedCount: st.ReplicatedCount,
|
||||
|
||||
@@ -19,6 +19,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -37,12 +38,12 @@ func (b *BucketReplicationStats) hasReplicationUsage() bool {
|
||||
|
||||
// ReplicationStats holds the global in-memory replication stats
|
||||
type ReplicationStats struct {
|
||||
Cache map[string]*BucketReplicationStats
|
||||
UsageCache map[string]*BucketReplicationStats
|
||||
mostRecentStats BucketStatsMap
|
||||
sync.RWMutex // mutex for Cache
|
||||
ulock sync.RWMutex // mutex for UsageCache
|
||||
dlock sync.RWMutex // mutex for mostRecentStats
|
||||
Cache map[string]*BucketReplicationStats
|
||||
UsageCache map[string]*BucketReplicationStats
|
||||
mostRecentStats BucketStatsMap
|
||||
sync.RWMutex // mutex for Cache
|
||||
ulock sync.RWMutex // mutex for UsageCache
|
||||
mostRecentStatsMu sync.Mutex // mutex for mostRecentStats
|
||||
}
|
||||
|
||||
// Delete deletes in-memory replication statistics for a bucket.
|
||||
@@ -196,7 +197,8 @@ func (r *ReplicationStats) loadInitialReplicationMetrics(ctx context.Context) {
|
||||
m := make(map[string]*BucketReplicationStats)
|
||||
if stats, err := globalReplicationPool.loadStatsFromDisk(); err == nil {
|
||||
for b, st := range stats {
|
||||
m[b] = &st
|
||||
c := st.Clone()
|
||||
m[b] = &c
|
||||
}
|
||||
r.ulock.Lock()
|
||||
r.UsageCache = m
|
||||
@@ -245,13 +247,23 @@ outer:
|
||||
r.ulock.Unlock()
|
||||
}
|
||||
|
||||
func (r *ReplicationStats) getAllCachedLatest() BucketStatsMap {
|
||||
// serializeStats will serialize the current stats.
|
||||
// Will return (nil, nil) if no data.
|
||||
func (r *ReplicationStats) serializeStats() ([]byte, error) {
|
||||
if r == nil {
|
||||
return BucketStatsMap{}
|
||||
return nil, nil
|
||||
}
|
||||
r.dlock.RLock()
|
||||
defer r.dlock.RUnlock()
|
||||
return r.mostRecentStats
|
||||
r.mostRecentStatsMu.Lock()
|
||||
defer r.mostRecentStatsMu.Unlock()
|
||||
if len(r.mostRecentStats.Stats) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
data := make([]byte, 4, 4+r.mostRecentStats.Msgsize())
|
||||
// Add the replication stats meta header.
|
||||
binary.LittleEndian.PutUint16(data[0:2], replStatsMetaFormat)
|
||||
binary.LittleEndian.PutUint16(data[2:4], replStatsVersion)
|
||||
// Add data
|
||||
return r.mostRecentStats.MarshalMsg(data)
|
||||
}
|
||||
|
||||
func (r *ReplicationStats) getAllLatest(bucketsUsage map[string]BucketUsageInfo) (bucketsReplicationStats map[string]BucketReplicationStats) {
|
||||
@@ -321,6 +333,7 @@ func (r *ReplicationStats) calculateBucketReplicationStats(bucket string, u Buck
|
||||
s = BucketReplicationStats{
|
||||
Stats: make(map[string]*BucketReplicationStat, len(stats)),
|
||||
}
|
||||
|
||||
var latestTotReplicatedSize int64
|
||||
for _, st := range u.ReplicationInfo {
|
||||
latestTotReplicatedSize += int64(st.ReplicatedSize)
|
||||
@@ -356,13 +369,15 @@ func (r *ReplicationStats) calculateBucketReplicationStats(bucket string, u Buck
|
||||
// normalize overall stats
|
||||
s.ReplicaSize = int64(math.Max(float64(totReplicaSize), float64(u.ReplicaSize)))
|
||||
s.ReplicatedSize = int64(math.Max(float64(s.ReplicatedSize), float64(latestTotReplicatedSize)))
|
||||
r.dlock.Lock()
|
||||
r.mostRecentStatsMu.Lock()
|
||||
if len(r.mostRecentStats.Stats) == 0 {
|
||||
r.mostRecentStats = BucketStatsMap{Stats: make(map[string]BucketStats, 1), Timestamp: UTCNow()}
|
||||
}
|
||||
r.mostRecentStats.Stats[bucket] = BucketStats{ReplicationStats: s}
|
||||
if len(s.Stats) > 0 {
|
||||
r.mostRecentStats.Stats[bucket] = BucketStats{ReplicationStats: s}
|
||||
}
|
||||
r.mostRecentStats.Timestamp = UTCNow()
|
||||
r.dlock.Unlock()
|
||||
r.mostRecentStatsMu.Unlock()
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
)
|
||||
@@ -628,26 +628,40 @@ func (v VersionPurgeStatusType) Pending() bool {
|
||||
return v == Pending || v == Failed
|
||||
}
|
||||
|
||||
type replicationResyncState struct {
|
||||
type replicationResyncer struct {
|
||||
// map of bucket to their resync status
|
||||
statusMap map[string]BucketReplicationResyncStatus
|
||||
statusMap map[string]BucketReplicationResyncStatus
|
||||
workerSize int
|
||||
resyncCancelCh chan struct{}
|
||||
workerCh chan struct{}
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
const (
|
||||
replicationDir = "replication"
|
||||
replicationDir = ".replication"
|
||||
resyncFileName = "resync.bin"
|
||||
resyncMetaFormat = 1
|
||||
resyncMetaVersionV1 = 1
|
||||
resyncMetaVersion = resyncMetaVersionV1
|
||||
)
|
||||
|
||||
type resyncOpts struct {
|
||||
bucket string
|
||||
arn string
|
||||
resyncID string
|
||||
resyncBefore time.Time
|
||||
}
|
||||
|
||||
// ResyncStatusType status of resync operation
|
||||
type ResyncStatusType int
|
||||
|
||||
const (
|
||||
// NoResync - no resync in progress
|
||||
NoResync ResyncStatusType = iota
|
||||
// ResyncPending - resync pending
|
||||
ResyncPending
|
||||
// ResyncCanceled - resync canceled
|
||||
ResyncCanceled
|
||||
// ResyncStarted - resync in progress
|
||||
ResyncStarted
|
||||
// ResyncCompleted - resync finished
|
||||
@@ -656,6 +670,10 @@ const (
|
||||
ResyncFailed
|
||||
)
|
||||
|
||||
func (rt ResyncStatusType) isValid() bool {
|
||||
return rt != NoResync
|
||||
}
|
||||
|
||||
func (rt ResyncStatusType) String() string {
|
||||
switch rt {
|
||||
case ResyncStarted:
|
||||
@@ -664,6 +682,10 @@ func (rt ResyncStatusType) String() string {
|
||||
return "Completed"
|
||||
case ResyncFailed:
|
||||
return "Failed"
|
||||
case ResyncPending:
|
||||
return "Pending"
|
||||
case ResyncCanceled:
|
||||
return "Canceled"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -671,8 +693,8 @@ func (rt ResyncStatusType) String() string {
|
||||
|
||||
// TargetReplicationResyncStatus status of resync of bucket for a specific target
|
||||
type TargetReplicationResyncStatus struct {
|
||||
StartTime time.Time `json:"startTime" msg:"st"`
|
||||
EndTime time.Time `json:"endTime" msg:"et"`
|
||||
StartTime time.Time `json:"startTime" msg:"st"`
|
||||
LastUpdate time.Time `json:"lastUpdated" msg:"lst"`
|
||||
// Resync ID assigned to this reset
|
||||
ResyncID string `json:"resyncID" msg:"id"`
|
||||
// ResyncBeforeDate - resync all objects created prior to this date
|
||||
@@ -701,6 +723,14 @@ type BucketReplicationResyncStatus struct {
|
||||
LastUpdate time.Time `json:"lastUpdate" msg:"lu"`
|
||||
}
|
||||
|
||||
func (rs *BucketReplicationResyncStatus) cloneTgtStats() (m map[string]TargetReplicationResyncStatus) {
|
||||
m = make(map[string]TargetReplicationResyncStatus)
|
||||
for arn, st := range rs.TargetsMap {
|
||||
m[arn] = st
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newBucketResyncStatus(bucket string) BucketReplicationResyncStatus {
|
||||
return BucketReplicationResyncStatus{
|
||||
TargetsMap: make(map[string]TargetReplicationResyncStatus),
|
||||
@@ -767,9 +797,8 @@ func (ri ReplicateObjectInfo) ToMRFEntry() MRFReplicateEntry {
|
||||
}
|
||||
}
|
||||
|
||||
func getReplicationStatsPath(nodeName string) string {
|
||||
nodeStr := strings.ReplaceAll(nodeName, ":", "_")
|
||||
return bucketMetaPrefix + SlashSeparator + replicationDir + SlashSeparator + nodeStr + ".stats"
|
||||
func getReplicationStatsPath() string {
|
||||
return bucketMetaPrefix + SlashSeparator + replicationDir + SlashSeparator + "replication.stats"
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -2117,10 +2117,10 @@ func (z *TargetReplicationResyncStatus) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err, "StartTime")
|
||||
return
|
||||
}
|
||||
case "et":
|
||||
z.EndTime, err = dc.ReadTime()
|
||||
case "lst":
|
||||
z.LastUpdate, err = dc.ReadTime()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "EndTime")
|
||||
err = msgp.WrapError(err, "LastUpdate")
|
||||
return
|
||||
}
|
||||
case "id":
|
||||
@@ -2205,14 +2205,14 @@ func (z *TargetReplicationResyncStatus) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = msgp.WrapError(err, "StartTime")
|
||||
return
|
||||
}
|
||||
// write "et"
|
||||
err = en.Append(0xa2, 0x65, 0x74)
|
||||
// write "lst"
|
||||
err = en.Append(0xa3, 0x6c, 0x73, 0x74)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteTime(z.EndTime)
|
||||
err = en.WriteTime(z.LastUpdate)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "EndTime")
|
||||
err = msgp.WrapError(err, "LastUpdate")
|
||||
return
|
||||
}
|
||||
// write "id"
|
||||
@@ -2315,9 +2315,9 @@ func (z *TargetReplicationResyncStatus) MarshalMsg(b []byte) (o []byte, err erro
|
||||
// string "st"
|
||||
o = append(o, 0x8b, 0xa2, 0x73, 0x74)
|
||||
o = msgp.AppendTime(o, z.StartTime)
|
||||
// string "et"
|
||||
o = append(o, 0xa2, 0x65, 0x74)
|
||||
o = msgp.AppendTime(o, z.EndTime)
|
||||
// string "lst"
|
||||
o = append(o, 0xa3, 0x6c, 0x73, 0x74)
|
||||
o = msgp.AppendTime(o, z.LastUpdate)
|
||||
// string "id"
|
||||
o = append(o, 0xa2, 0x69, 0x64)
|
||||
o = msgp.AppendString(o, z.ResyncID)
|
||||
@@ -2372,10 +2372,10 @@ func (z *TargetReplicationResyncStatus) UnmarshalMsg(bts []byte) (o []byte, err
|
||||
err = msgp.WrapError(err, "StartTime")
|
||||
return
|
||||
}
|
||||
case "et":
|
||||
z.EndTime, bts, err = msgp.ReadTimeBytes(bts)
|
||||
case "lst":
|
||||
z.LastUpdate, bts, err = msgp.ReadTimeBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "EndTime")
|
||||
err = msgp.WrapError(err, "LastUpdate")
|
||||
return
|
||||
}
|
||||
case "id":
|
||||
@@ -2450,7 +2450,7 @@ func (z *TargetReplicationResyncStatus) UnmarshalMsg(bts []byte) (o []byte, err
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z *TargetReplicationResyncStatus) Msgsize() (s int) {
|
||||
s = 1 + 3 + msgp.TimeSize + 3 + msgp.TimeSize + 3 + msgp.StringPrefixSize + len(z.ResyncID) + 4 + msgp.TimeSize + 4 + msgp.IntSize + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 4 + msgp.StringPrefixSize + len(z.Bucket) + 4 + msgp.StringPrefixSize + len(z.Object)
|
||||
s = 1 + 3 + msgp.TimeSize + 4 + msgp.TimeSize + 3 + msgp.StringPrefixSize + len(z.ResyncID) + 4 + msgp.TimeSize + 4 + msgp.IntSize + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 4 + msgp.StringPrefixSize + len(z.Bucket) + 4 + msgp.StringPrefixSize + len(z.Object)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+422
-284
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
)
|
||||
|
||||
@@ -120,6 +120,10 @@ type BucketReplicationStat struct {
|
||||
FailedCount int64 `json:"failedReplicationCount"`
|
||||
// Replication latency information
|
||||
Latency ReplicationLatency `json:"replicationLatency"`
|
||||
// bandwidth limit for target
|
||||
BandWidthLimitInBytesPerSecond int64 `json:"limitInBits"`
|
||||
// current bandwidth reported
|
||||
CurrentBandwidthInBytesPerSecond float64 `json:"currentBandwidth"`
|
||||
}
|
||||
|
||||
func (bs *BucketReplicationStat) hasReplicationUsage() bool {
|
||||
|
||||
+55
-5
@@ -89,6 +89,18 @@ func (z *BucketReplicationStat) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
case "BandWidthLimitInBytesPerSecond":
|
||||
z.BandWidthLimitInBytesPerSecond, err = dc.ReadInt64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "BandWidthLimitInBytesPerSecond")
|
||||
return
|
||||
}
|
||||
case "CurrentBandwidthInBytesPerSecond":
|
||||
z.CurrentBandwidthInBytesPerSecond, err = dc.ReadFloat64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "CurrentBandwidthInBytesPerSecond")
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = dc.Skip()
|
||||
if err != nil {
|
||||
@@ -102,9 +114,9 @@ func (z *BucketReplicationStat) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z *BucketReplicationStat) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// map header, size 7
|
||||
// map header, size 9
|
||||
// write "PendingSize"
|
||||
err = en.Append(0x87, 0xab, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65)
|
||||
err = en.Append(0x89, 0xab, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -179,15 +191,35 @@ func (z *BucketReplicationStat) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = msgp.WrapError(err, "Latency", "UploadHistogram")
|
||||
return
|
||||
}
|
||||
// write "BandWidthLimitInBytesPerSecond"
|
||||
err = en.Append(0xbe, 0x42, 0x61, 0x6e, 0x64, 0x57, 0x69, 0x64, 0x74, 0x68, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x49, 0x6e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x50, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteInt64(z.BandWidthLimitInBytesPerSecond)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "BandWidthLimitInBytesPerSecond")
|
||||
return
|
||||
}
|
||||
// write "CurrentBandwidthInBytesPerSecond"
|
||||
err = en.Append(0xd9, 0x20, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x64, 0x77, 0x69, 0x64, 0x74, 0x68, 0x49, 0x6e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x50, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteFloat64(z.CurrentBandwidthInBytesPerSecond)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "CurrentBandwidthInBytesPerSecond")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z *BucketReplicationStat) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// map header, size 7
|
||||
// map header, size 9
|
||||
// string "PendingSize"
|
||||
o = append(o, 0x87, 0xab, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65)
|
||||
o = append(o, 0x89, 0xab, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65)
|
||||
o = msgp.AppendInt64(o, z.PendingSize)
|
||||
// string "ReplicatedSize"
|
||||
o = append(o, 0xae, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65)
|
||||
@@ -214,6 +246,12 @@ func (z *BucketReplicationStat) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "Latency", "UploadHistogram")
|
||||
return
|
||||
}
|
||||
// string "BandWidthLimitInBytesPerSecond"
|
||||
o = append(o, 0xbe, 0x42, 0x61, 0x6e, 0x64, 0x57, 0x69, 0x64, 0x74, 0x68, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x49, 0x6e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x50, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64)
|
||||
o = msgp.AppendInt64(o, z.BandWidthLimitInBytesPerSecond)
|
||||
// string "CurrentBandwidthInBytesPerSecond"
|
||||
o = append(o, 0xd9, 0x20, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x64, 0x77, 0x69, 0x64, 0x74, 0x68, 0x49, 0x6e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x50, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64)
|
||||
o = msgp.AppendFloat64(o, z.CurrentBandwidthInBytesPerSecond)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -300,6 +338,18 @@ func (z *BucketReplicationStat) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
case "BandWidthLimitInBytesPerSecond":
|
||||
z.BandWidthLimitInBytesPerSecond, bts, err = msgp.ReadInt64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "BandWidthLimitInBytesPerSecond")
|
||||
return
|
||||
}
|
||||
case "CurrentBandwidthInBytesPerSecond":
|
||||
z.CurrentBandwidthInBytesPerSecond, bts, err = msgp.ReadFloat64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "CurrentBandwidthInBytesPerSecond")
|
||||
return
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
@@ -314,7 +364,7 @@ func (z *BucketReplicationStat) 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 *BucketReplicationStat) Msgsize() (s int) {
|
||||
s = 1 + 12 + msgp.Int64Size + 15 + msgp.Int64Size + 12 + msgp.Int64Size + 11 + msgp.Int64Size + 13 + msgp.Int64Size + 12 + msgp.Int64Size + 8 + 1 + 16 + z.Latency.UploadHistogram.Msgsize()
|
||||
s = 1 + 12 + msgp.Int64Size + 15 + msgp.Int64Size + 12 + msgp.Int64Size + 11 + msgp.Int64Size + 13 + msgp.Int64Size + 12 + msgp.Int64Size + 8 + 1 + 16 + z.Latency.UploadHistogram.Msgsize() + 31 + msgp.Int64Size + 34 + msgp.Float64Size
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+46
-28
@@ -25,9 +25,8 @@ import (
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio-go/v7"
|
||||
miniogo "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
"github.com/minio/minio/internal/crypto"
|
||||
@@ -99,7 +98,7 @@ func (sys *BucketTargetSys) heartBeat(ctx context.Context) {
|
||||
select {
|
||||
case <-hcTimer.C:
|
||||
sys.hMutex.RLock()
|
||||
var eps []madmin.ServerProperties
|
||||
eps := make([]madmin.ServerProperties, 0, len(sys.hc))
|
||||
for _, ep := range sys.hc {
|
||||
eps = append(eps, madmin.ServerProperties{Endpoint: ep.Endpoint, Scheme: ep.Scheme})
|
||||
}
|
||||
@@ -107,16 +106,12 @@ func (sys *BucketTargetSys) heartBeat(ctx context.Context) {
|
||||
|
||||
if len(eps) > 0 {
|
||||
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
m := map[string]epHealth{}
|
||||
m := make(map[string]epHealth, len(eps))
|
||||
for result := range sys.hcClient.Alive(cctx, madmin.AliveOpts{}, eps...) {
|
||||
var online bool
|
||||
if result.Error == nil {
|
||||
online = result.Online
|
||||
}
|
||||
m[result.Endpoint.Host] = epHealth{
|
||||
Endpoint: result.Endpoint.Host,
|
||||
Scheme: result.Endpoint.Scheme,
|
||||
Online: online,
|
||||
Online: result.Online,
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
@@ -218,10 +213,13 @@ func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *m
|
||||
// validate if target credentials are ok
|
||||
exists, err := clnt.BucketExists(ctx, tgt.TargetBucket)
|
||||
if err != nil {
|
||||
if minio.ToErrorResponse(err).Code == "NoSuchBucket" {
|
||||
switch minio.ToErrorResponse(err).Code {
|
||||
case "NoSuchBucket":
|
||||
return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket}
|
||||
case "AccessDenied":
|
||||
return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, AccessKey: tgt.Credentials.AccessKey, Err: err}
|
||||
}
|
||||
return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, Err: err}
|
||||
return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, AccessKey: tgt.Credentials.AccessKey, Err: err}
|
||||
}
|
||||
if !exists {
|
||||
return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket}
|
||||
@@ -232,7 +230,7 @@ func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *m
|
||||
}
|
||||
vcfg, err := clnt.GetBucketVersioning(ctx, tgt.TargetBucket)
|
||||
if err != nil {
|
||||
return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, Err: err}
|
||||
return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, Err: err, AccessKey: tgt.Credentials.AccessKey}
|
||||
}
|
||||
if !vcfg.Enabled() {
|
||||
return BucketRemoteTargetNotVersioned{Bucket: tgt.TargetBucket}
|
||||
@@ -254,6 +252,10 @@ func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *m
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
// fail if endpoint is already present in list of targets and not a matching ARN
|
||||
if t.Endpoint == tgt.Endpoint {
|
||||
return BucketRemoteAlreadyExists{Bucket: t.TargetBucket}
|
||||
}
|
||||
}
|
||||
newtgts[idx] = t
|
||||
}
|
||||
@@ -263,18 +265,18 @@ func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *m
|
||||
|
||||
sys.targetsMap[bucket] = newtgts
|
||||
sys.arnRemotesMap[tgt.Arn] = clnt
|
||||
sys.updateBandwidthLimit(bucket, tgt.BandwidthLimit)
|
||||
sys.updateBandwidthLimit(bucket, tgt.Arn, tgt.BandwidthLimit)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sys *BucketTargetSys) updateBandwidthLimit(bucket string, limit int64) {
|
||||
func (sys *BucketTargetSys) updateBandwidthLimit(bucket, arn string, limit int64) {
|
||||
if limit == 0 {
|
||||
globalBucketMonitor.DeleteBucket(bucket)
|
||||
globalBucketMonitor.DeleteBucketThrottle(bucket, arn)
|
||||
return
|
||||
}
|
||||
// Setup bandwidth throttling
|
||||
|
||||
globalBucketMonitor.SetBandwidthLimit(bucket, limit)
|
||||
globalBucketMonitor.SetBandwidthLimit(bucket, arn, limit)
|
||||
}
|
||||
|
||||
// RemoveTarget - removes a remote bucket target for this source bucket.
|
||||
@@ -326,7 +328,7 @@ func (sys *BucketTargetSys) RemoveTarget(ctx context.Context, bucket, arnStr str
|
||||
}
|
||||
sys.targetsMap[bucket] = targets
|
||||
delete(sys.arnRemotesMap, arnStr)
|
||||
sys.updateBandwidthLimit(bucket, 0)
|
||||
sys.updateBandwidthLimit(bucket, arnStr, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -396,7 +398,7 @@ func (sys *BucketTargetSys) UpdateAllTargets(bucket string, tgts *madmin.BucketT
|
||||
|
||||
// No need for more if not adding anything
|
||||
if tgts == nil || tgts.Empty() {
|
||||
sys.updateBandwidthLimit(bucket, 0)
|
||||
globalBucketMonitor.DeleteBucket(bucket)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -409,7 +411,7 @@ func (sys *BucketTargetSys) UpdateAllTargets(bucket string, tgts *madmin.BucketT
|
||||
continue
|
||||
}
|
||||
sys.arnRemotesMap[tgt.Arn] = tgtClient
|
||||
sys.updateBandwidthLimit(bucket, tgt.BandwidthLimit)
|
||||
sys.updateBandwidthLimit(bucket, tgt.Arn, tgt.BandwidthLimit)
|
||||
}
|
||||
sys.targetsMap[bucket] = tgts.Targets
|
||||
}
|
||||
@@ -432,7 +434,7 @@ func (sys *BucketTargetSys) set(bucket BucketInfo, meta BucketMetadata) {
|
||||
continue
|
||||
}
|
||||
sys.arnRemotesMap[tgt.Arn] = tgtClient
|
||||
sys.updateBandwidthLimit(bucket.Name, tgt.BandwidthLimit)
|
||||
sys.updateBandwidthLimit(bucket.Name, tgt.Arn, tgt.BandwidthLimit)
|
||||
}
|
||||
sys.targetsMap[bucket.Name] = cfg.Targets
|
||||
}
|
||||
@@ -442,7 +444,7 @@ func (sys *BucketTargetSys) getRemoteTargetClient(tcfg *madmin.BucketTarget) (*T
|
||||
config := tcfg.Credentials
|
||||
creds := credentials.NewStaticV4(config.AccessKey, config.SecretKey, "")
|
||||
|
||||
api, err := minio.New(tcfg.Endpoint, &miniogo.Options{
|
||||
api, err := minio.New(tcfg.Endpoint, &minio.Options{
|
||||
Creds: creds,
|
||||
Secure: tcfg.Secure,
|
||||
Region: tcfg.Region,
|
||||
@@ -471,20 +473,36 @@ func (sys *BucketTargetSys) getRemoteTargetClient(tcfg *madmin.BucketTarget) (*T
|
||||
}
|
||||
|
||||
// getRemoteARN gets existing ARN for an endpoint or generates a new one.
|
||||
func (sys *BucketTargetSys) getRemoteARN(bucket string, target *madmin.BucketTarget) string {
|
||||
func (sys *BucketTargetSys) getRemoteARN(bucket string, target *madmin.BucketTarget) (arn string, exists bool) {
|
||||
if target == nil {
|
||||
return ""
|
||||
return
|
||||
}
|
||||
tgts := sys.targetsMap[bucket]
|
||||
for _, tgt := range tgts {
|
||||
if tgt.Type == target.Type && tgt.TargetBucket == target.TargetBucket && target.URL().String() == tgt.URL().String() {
|
||||
return tgt.Arn
|
||||
if tgt.Type == target.Type && tgt.TargetBucket == target.TargetBucket && target.URL().String() == tgt.URL().String() && tgt.Credentials.AccessKey == target.Credentials.AccessKey {
|
||||
return tgt.Arn, true
|
||||
}
|
||||
}
|
||||
if !target.Type.IsValid() {
|
||||
return ""
|
||||
return
|
||||
}
|
||||
return generateARN(target)
|
||||
return generateARN(target), false
|
||||
}
|
||||
|
||||
// getRemoteARNForPeer returns the remote target for a peer site in site replication
|
||||
func (sys *BucketTargetSys) getRemoteARNForPeer(bucket string, peer madmin.PeerInfo) string {
|
||||
tgts := sys.targetsMap[bucket]
|
||||
for _, target := range tgts {
|
||||
ep, _ := url.Parse(peer.Endpoint)
|
||||
if target.SourceBucket == bucket &&
|
||||
target.TargetBucket == bucket &&
|
||||
target.Endpoint == ep.Host &&
|
||||
target.Secure == (ep.Scheme == "https") &&
|
||||
target.Type == madmin.ReplicationService {
|
||||
return target.Arn
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// generate ARN that is unique to this target type
|
||||
@@ -533,7 +551,7 @@ func parseBucketTargetConfig(bucket string, cdata, cmetadata []byte) (*madmin.Bu
|
||||
|
||||
// TargetClient is the struct for remote target client.
|
||||
type TargetClient struct {
|
||||
*miniogo.Client
|
||||
*minio.Client
|
||||
healthCheckDuration time.Duration
|
||||
Bucket string // remote bucket target
|
||||
replicateSync bool
|
||||
|
||||
@@ -24,10 +24,10 @@ import (
|
||||
"net/http"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/bucket/versioning"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
@@ -108,15 +108,12 @@ func (api objectAPIHandlers) PutBucketVersioningHandler(w http.ResponseWriter, r
|
||||
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||
// errors.
|
||||
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeVersionConfig,
|
||||
Bucket: bucket,
|
||||
Versioning: &cfgStr,
|
||||
UpdatedAt: updatedAt,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
+86
-29
@@ -18,29 +18,20 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// callhomeSchemaVersion1 is callhome schema version 1
|
||||
callhomeSchemaVersion1 = "1"
|
||||
|
||||
// callhomeSchemaVersion is current callhome schema version.
|
||||
callhomeSchemaVersion = callhomeSchemaVersion1
|
||||
)
|
||||
|
||||
// CallhomeInfo - Contains callhome information
|
||||
type CallhomeInfo struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
AdminInfo madmin.InfoMessage `json:"admin_info"`
|
||||
}
|
||||
|
||||
var callhomeLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
||||
|
||||
// initCallhome will start the callhome task in the background.
|
||||
@@ -88,7 +79,7 @@ func runCallhome(ctx context.Context, objAPI ObjectLayer) bool {
|
||||
}
|
||||
|
||||
ctx = lkctx.Context()
|
||||
defer locker.Unlock(lkctx.Cancel)
|
||||
defer locker.Unlock(lkctx)
|
||||
|
||||
callhomeTimer := time.NewTimer(globalCallhomeConfig.FrequencyDur())
|
||||
defer callhomeTimer.Stop()
|
||||
@@ -118,26 +109,92 @@ func runCallhome(ctx context.Context, objAPI ObjectLayer) bool {
|
||||
}
|
||||
|
||||
func performCallhome(ctx context.Context) {
|
||||
err := sendCallhomeInfo(
|
||||
CallhomeInfo{
|
||||
SchemaVersion: callhomeSchemaVersion,
|
||||
AdminInfo: getServerInfo(ctx, nil),
|
||||
})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to perform callhome: %w", err))
|
||||
deadline := 10 * time.Second // Default deadline is 10secs for callhome
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
logger.LogIf(ctx, errors.New("Callhome: object layer not ready"))
|
||||
return
|
||||
}
|
||||
|
||||
healthCtx, healthCancel := context.WithTimeout(ctx, deadline)
|
||||
defer healthCancel()
|
||||
|
||||
healthInfoCh := make(chan madmin.HealthInfo)
|
||||
|
||||
query := url.Values{}
|
||||
for _, k := range madmin.HealthDataTypesList {
|
||||
query.Set(string(k), "true")
|
||||
}
|
||||
|
||||
healthInfo := madmin.HealthInfo{
|
||||
TimeStamp: time.Now().UTC(),
|
||||
Version: madmin.HealthInfoVersion,
|
||||
Minio: madmin.MinioHealthInfo{
|
||||
Info: madmin.MinioInfo{
|
||||
DeploymentID: globalDeploymentID,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
go fetchHealthInfo(healthCtx, objectAPI, &query, healthInfoCh, healthInfo)
|
||||
|
||||
for {
|
||||
select {
|
||||
case hi, hasMore := <-healthInfoCh:
|
||||
if !hasMore {
|
||||
// Received all data. Send to SUBNET and return
|
||||
err := sendHealthInfo(ctx, healthInfo)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to perform callhome: %w", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
healthInfo = hi
|
||||
case <-healthCtx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
callhomeURL = "https://subnet.min.io/api/callhome"
|
||||
callhomeURLDev = "http://localhost:9000/api/callhome"
|
||||
healthURL = "https://subnet.min.io/api/health/upload"
|
||||
healthURLDev = "http://localhost:9000/api/health/upload"
|
||||
)
|
||||
|
||||
func sendCallhomeInfo(ch CallhomeInfo) error {
|
||||
url := callhomeURL
|
||||
func sendHealthInfo(ctx context.Context, healthInfo madmin.HealthInfo) error {
|
||||
url := healthURL
|
||||
if globalIsCICD {
|
||||
url = callhomeURLDev
|
||||
url = healthURLDev
|
||||
}
|
||||
_, err := globalSubnetConfig.Post(url, ch)
|
||||
|
||||
filename := fmt.Sprintf("health_%s.json.gz", UTCNow().Format("20060102150405"))
|
||||
url += "?filename=" + filename
|
||||
|
||||
_, err := globalSubnetConfig.Upload(url, filename, createHealthJSONGzip(ctx, healthInfo))
|
||||
return err
|
||||
}
|
||||
|
||||
func createHealthJSONGzip(ctx context.Context, healthInfo madmin.HealthInfo) []byte {
|
||||
var b bytes.Buffer
|
||||
gzWriter := gzip.NewWriter(&b)
|
||||
|
||||
header := struct {
|
||||
Version string `json:"version"`
|
||||
}{Version: healthInfo.Version}
|
||||
|
||||
enc := json.NewEncoder(gzWriter)
|
||||
if e := enc.Encode(header); e != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Could not encode health info header: %w", e))
|
||||
return nil
|
||||
}
|
||||
|
||||
if e := enc.Encode(healthInfo); e != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Could not encode health info: %w", e))
|
||||
return nil
|
||||
}
|
||||
|
||||
gzWriter.Flush()
|
||||
gzWriter.Close()
|
||||
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
+8
-3
@@ -52,7 +52,7 @@ import (
|
||||
"github.com/minio/console/restapi"
|
||||
"github.com/minio/console/restapi/operations"
|
||||
"github.com/minio/kes"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
@@ -117,7 +117,8 @@ func init() {
|
||||
go func() {
|
||||
var t *time.Ticker
|
||||
if containers {
|
||||
t = time.NewTicker(1 * time.Minute)
|
||||
// k8s DNS TTL is 30s (Attempt a refresh only after)
|
||||
t = time.NewTicker(30 * time.Second)
|
||||
} else {
|
||||
t = time.NewTicker(10 * time.Minute)
|
||||
}
|
||||
@@ -134,7 +135,7 @@ func init() {
|
||||
|
||||
globalForwarder = handlers.NewForwarder(&handlers.Forwarder{
|
||||
PassHost: true,
|
||||
RoundTripper: newHTTPTransport(1 * time.Hour),
|
||||
RoundTripper: NewHTTPTransportWithTimeout(1 * time.Hour),
|
||||
Logger: func(err error) {
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
logger.LogIf(GlobalContext, err)
|
||||
@@ -231,6 +232,7 @@ func buildOpenIDConsoleConfig() consoleoauth2.OpenIDPCfg {
|
||||
Userinfo: cfg.ClaimUserinfo,
|
||||
RedirectCallbackDynamic: cfg.RedirectURIDynamic,
|
||||
RedirectCallback: callback,
|
||||
EndSessionEndpoint: cfg.DiscoveryDoc.EndSessionEndpoint,
|
||||
RoleArn: cfg.GetRoleArn(),
|
||||
}
|
||||
}
|
||||
@@ -772,6 +774,8 @@ func handleCommonEnvVars() {
|
||||
logger.Info(color.RedBold(msg))
|
||||
}
|
||||
globalActiveCred = cred
|
||||
} else {
|
||||
globalActiveCred = auth.DefaultCredentials
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,6 +858,7 @@ func handleKMSConfig() {
|
||||
defaultKeyID := env.Get(config.EnvKESKeyName, "")
|
||||
KMS, err := kms.NewWithConfig(kms.Config{
|
||||
Endpoints: endpoints,
|
||||
Enclave: env.Get(config.EnvKESEnclave, ""),
|
||||
DefaultKeyID: defaultKeyID,
|
||||
Certificate: certificate,
|
||||
ReloadCertEvents: reloadCertEvents,
|
||||
|
||||
+42
-67
@@ -24,8 +24,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/config/api"
|
||||
"github.com/minio/minio/internal/config/cache"
|
||||
@@ -65,7 +64,6 @@ func initHelp() {
|
||||
config.SiteSubSys: config.DefaultSiteKVS,
|
||||
config.RegionSubSys: config.DefaultRegionKVS,
|
||||
config.APISubSys: api.DefaultKVS,
|
||||
config.CredentialsSubSys: config.DefaultCredentialKVS,
|
||||
config.LoggerWebhookSubSys: logger.DefaultLoggerWebhookKVS,
|
||||
config.AuditWebhookSubSys: logger.DefaultAuditWebhookKVS,
|
||||
config.AuditKafkaSubSys: logger.DefaultAuditKafkaKVS,
|
||||
@@ -84,22 +82,34 @@ func initHelp() {
|
||||
|
||||
// Captures help for each sub-system
|
||||
helpSubSys := config.HelpKVS{
|
||||
config.HelpKV{
|
||||
Key: config.SubnetSubSys,
|
||||
Type: "string",
|
||||
Description: "register the cluster to MinIO SUBNET",
|
||||
Optional: true,
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.CallhomeSubSys,
|
||||
Type: "string",
|
||||
Description: "enable callhome to MinIO SUBNET",
|
||||
Optional: true,
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.SiteSubSys,
|
||||
Description: "label the server and its location",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.CacheSubSys,
|
||||
Description: "add caching storage tier",
|
||||
Key: config.APISubSys,
|
||||
Description: "manage global HTTP API call specific features, such as throttling, authentication types, etc.",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.ScannerSubSys,
|
||||
Description: "manage namespace scanning for usage calculation, lifecycle, healing and more",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.CompressionSubSys,
|
||||
Description: "enable server side compression of objects",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.EtcdSubSys,
|
||||
Description: "federate multiple clusters for IAM and Bucket DNS",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.IdentityOpenIDSubSys,
|
||||
Description: "enable OpenID SSO support",
|
||||
@@ -121,14 +131,6 @@ func initHelp() {
|
||||
Key: config.PolicyPluginSubSys,
|
||||
Description: "enable Access Management Plugin for policy enforcement",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.APISubSys,
|
||||
Description: "manage global HTTP API call specific features, such as throttling, authentication types, etc.",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.ScannerSubSys,
|
||||
Description: "manage namespace scanning for usage calculation, lifecycle, healing and more",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.LoggerWebhookSubSys,
|
||||
Description: "send server logs to webhook endpoints",
|
||||
@@ -195,16 +197,12 @@ func initHelp() {
|
||||
MultipleTargets: true,
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.SubnetSubSys,
|
||||
Type: "string",
|
||||
Description: "set subnet config for the cluster e.g. api key",
|
||||
Optional: true,
|
||||
Key: config.EtcdSubSys,
|
||||
Description: "persist IAM assets externally to etcd",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.CallhomeSubSys,
|
||||
Type: "string",
|
||||
Description: "enable callhome for the cluster",
|
||||
Optional: true,
|
||||
Key: config.CacheSubSys,
|
||||
Description: "[DEPRECATED] add caching storage tier",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -299,16 +297,9 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
|
||||
return err
|
||||
}
|
||||
case config.CompressionSubSys:
|
||||
compCfg, err := compress.LookupConfig(s[config.CompressionSubSys][config.Default])
|
||||
if err != nil {
|
||||
if _, err := compress.LookupConfig(s[config.CompressionSubSys][config.Default]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if objAPI != nil {
|
||||
if compCfg.Enabled && !objAPI.IsCompressionSupported() {
|
||||
return fmt.Errorf("Backend does not support compression")
|
||||
}
|
||||
}
|
||||
case config.HealSubSys:
|
||||
if _, err := heal.LookupConfig(s[config.HealSubSys][config.Default]); err != nil {
|
||||
return err
|
||||
@@ -374,8 +365,7 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
|
||||
subSys = config.PolicyPluginSubSys
|
||||
fallthrough
|
||||
case config.PolicyPluginSubSys:
|
||||
if ppargs, err := polplugin.LookupConfig(s[config.PolicyPluginSubSys][config.Default],
|
||||
NewHTTPTransport(), xhttp.DrainBody); err != nil {
|
||||
if ppargs, err := polplugin.LookupConfig(s, GetDefaultConnSettings(), xhttp.DrainBody); err != nil {
|
||||
return err
|
||||
} else if ppargs.URL == nil {
|
||||
// Check if legacy opa is configured.
|
||||
@@ -428,15 +418,6 @@ func validateConfig(s config.Config, subSys string) error {
|
||||
func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
||||
ctx := GlobalContext
|
||||
|
||||
var err error
|
||||
if !globalActiveCred.IsValid() {
|
||||
// Env doesn't seem to be set, we fallback to lookup creds from the config.
|
||||
globalActiveCred, err = config.LookupCreds(s[config.CredentialsSubSys][config.Default])
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Invalid credentials configuration: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
dnsURL, dnsUser, dnsPass, err := env.LookupEnv(config.EnvDNSWebhook)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize remote webhook DNS config %w", err))
|
||||
@@ -523,11 +504,13 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
||||
|
||||
transport := NewHTTPTransport()
|
||||
|
||||
bootstrapTrace("lookup the event notification targets")
|
||||
globalConfigTargetList, err = notify.FetchEnabledTargets(GlobalContext, s, transport)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
|
||||
}
|
||||
|
||||
bootstrapTrace("applying the dynamic configuration")
|
||||
// Apply dynamic config values
|
||||
if err := applyDynamicConfig(ctx, objAPI, s); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -551,17 +534,13 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
||||
|
||||
// Initialize remote instance transport once.
|
||||
getRemoteInstanceTransportOnce.Do(func() {
|
||||
getRemoteInstanceTransport = newHTTPTransport(apiConfig.RemoteTransportDeadline)
|
||||
getRemoteInstanceTransport = NewHTTPTransportWithTimeout(apiConfig.RemoteTransportDeadline)
|
||||
})
|
||||
case config.CompressionSubSys:
|
||||
cmpCfg, err := compress.LookupConfig(s[config.CompressionSubSys][config.Default])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to setup Compression: %w", err)
|
||||
}
|
||||
// Validate if the object layer supports compression.
|
||||
if cmpCfg.Enabled && !objAPI.IsCompressionSupported() {
|
||||
return fmt.Errorf("Backend does not support compression")
|
||||
}
|
||||
globalCompressConfigMu.Lock()
|
||||
globalCompressConfig = cmpCfg
|
||||
globalCompressConfigMu.Unlock()
|
||||
@@ -590,11 +569,11 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
||||
l.LogOnce = logger.LogOnceConsoleIf
|
||||
l.UserAgent = userAgent
|
||||
l.Transport = NewHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
|
||||
loggerCfg.HTTP[n] = l
|
||||
}
|
||||
loggerCfg.HTTP[n] = l
|
||||
}
|
||||
if err = logger.UpdateSystemTargets(loggerCfg); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to update logger webhook config: %w", err))
|
||||
if errs := logger.UpdateSystemTargets(loggerCfg); len(errs) > 0 {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to update logger webhook config: %v", errs))
|
||||
}
|
||||
case config.AuditWebhookSubSys:
|
||||
loggerCfg, err := logger.LookupConfigForSubSys(s, config.AuditWebhookSubSys)
|
||||
@@ -607,12 +586,12 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
||||
l.LogOnce = logger.LogOnceConsoleIf
|
||||
l.UserAgent = userAgent
|
||||
l.Transport = NewHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
|
||||
loggerCfg.AuditWebhook[n] = l
|
||||
}
|
||||
loggerCfg.AuditWebhook[n] = l
|
||||
}
|
||||
|
||||
if err = logger.UpdateAuditWebhookTargets(loggerCfg); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to update audit webhook targets: %w", err))
|
||||
if errs := logger.UpdateAuditWebhookTargets(loggerCfg); len(errs) > 0 {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to update audit webhook targets: %v", errs))
|
||||
}
|
||||
case config.AuditKafkaSubSys:
|
||||
loggerCfg, err := logger.LookupConfigForSubSys(s, config.AuditKafkaSubSys)
|
||||
@@ -625,8 +604,8 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
||||
loggerCfg.AuditKafka[n] = l
|
||||
}
|
||||
}
|
||||
if err = logger.UpdateAuditKafkaTargets(loggerCfg); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to update audit kafka targets: %w", err))
|
||||
if errs := logger.UpdateAuditKafkaTargets(loggerCfg); len(errs) > 0 {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to update audit kafka targets: %v", errs))
|
||||
}
|
||||
case config.StorageClassSubSys:
|
||||
for i, setDriveCount := range setDriveCounts {
|
||||
@@ -766,13 +745,6 @@ func newSrvConfig(objAPI ObjectLayer) error {
|
||||
// Initialize server config.
|
||||
srvCfg := newServerConfig()
|
||||
|
||||
if globalActiveCred.IsValid() && !globalActiveCred.Equal(auth.DefaultCredentials) {
|
||||
kvs := srvCfg[config.CredentialsSubSys][config.Default]
|
||||
kvs.Set(config.AccessKey, globalActiveCred.AccessKey)
|
||||
kvs.Set(config.SecretKey, globalActiveCred.SecretKey)
|
||||
srvCfg[config.CredentialsSubSys][config.Default] = kvs
|
||||
}
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
globalServerConfigMu.Lock()
|
||||
globalServerConfig = srvCfg
|
||||
@@ -783,17 +755,20 @@ func newSrvConfig(objAPI ObjectLayer) error {
|
||||
}
|
||||
|
||||
func getValidConfig(objAPI ObjectLayer) (config.Config, error) {
|
||||
return readServerConfig(GlobalContext, objAPI)
|
||||
return readServerConfig(GlobalContext, objAPI, nil)
|
||||
}
|
||||
|
||||
// loadConfig - loads a new config from disk, overrides params
|
||||
// from env if found and valid
|
||||
func loadConfig(objAPI ObjectLayer) error {
|
||||
srvCfg, err := getValidConfig(objAPI)
|
||||
// data is optional. If nil it will be loaded from backend.
|
||||
func loadConfig(objAPI ObjectLayer, data []byte) error {
|
||||
bootstrapTrace("load the configuration")
|
||||
srvCfg, err := readServerConfig(GlobalContext, objAPI, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bootstrapTrace("lookup the configuration")
|
||||
// Override any values from ENVs.
|
||||
lookupConfigs(srvCfg, objAPI)
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestServerConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
// Initialize server config.
|
||||
if err := loadConfig(objLayer); err != nil {
|
||||
if err := loadConfig(objLayer, nil); err != nil {
|
||||
t.Fatalf("Unable to initialize from updated config file %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-10
@@ -25,7 +25,7 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
@@ -37,7 +37,10 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to encrypt config %w", err)
|
||||
}
|
||||
if err = migrateConfigPrefixToEncrypted(objAPI, encrypted); err != nil {
|
||||
if !encrypted {
|
||||
return nil
|
||||
}
|
||||
if err = migrateConfigPrefixToEncrypted(objAPI); err != nil {
|
||||
return fmt.Errorf("Unable to migrate all config at .minio.sys/config/: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -48,6 +51,7 @@ const backendEncryptedFile = "backend-encrypted"
|
||||
var backendEncryptedMigrationComplete = []byte("encrypted")
|
||||
|
||||
func checkBackendEtcdEncrypted(ctx context.Context, client *etcd.Client) (bool, error) {
|
||||
bootstrapTrace("check if etcd backend is encrypted")
|
||||
data, err := readKeyEtcd(ctx, client, backendEncryptedFile)
|
||||
if err != nil && err != errConfigNotFound {
|
||||
return false, err
|
||||
@@ -56,6 +60,7 @@ func checkBackendEtcdEncrypted(ctx context.Context, client *etcd.Client) (bool,
|
||||
}
|
||||
|
||||
func checkBackendEncrypted(objAPI ObjectLayer) (bool, error) {
|
||||
bootstrapTrace("check if the config backend is encrypted")
|
||||
data, err := readConfig(GlobalContext, objAPI, backendEncryptedFile)
|
||||
if err != nil && err != errConfigNotFound {
|
||||
return false, err
|
||||
@@ -75,7 +80,9 @@ func migrateIAMConfigsEtcdToEncrypted(ctx context.Context, client *etcd.Client)
|
||||
return nil
|
||||
}
|
||||
|
||||
if encrypted && GlobalKMS != nil {
|
||||
bootstrapTrace("encrypt etcd config")
|
||||
|
||||
if GlobalKMS != nil {
|
||||
stat, err := GlobalKMS.Stat(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -136,18 +143,16 @@ func migrateIAMConfigsEtcdToEncrypted(ctx context.Context, client *etcd.Client)
|
||||
}
|
||||
}
|
||||
|
||||
if encrypted && GlobalKMS != nil {
|
||||
if GlobalKMS != nil {
|
||||
logger.Info("Migration of encrypted IAM config data completed. All data is now encrypted on etcd.")
|
||||
}
|
||||
|
||||
return deleteKeyEtcd(ctx, client, backendEncryptedFile)
|
||||
}
|
||||
|
||||
func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, encrypted bool) error {
|
||||
if !encrypted {
|
||||
return nil
|
||||
}
|
||||
if encrypted && GlobalKMS != nil {
|
||||
func migrateConfigPrefixToEncrypted(objAPI ObjectLayer) error {
|
||||
bootstrapTrace("migrating config prefix to encrypted")
|
||||
if GlobalKMS != nil {
|
||||
stat, err := GlobalKMS.Stat(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -202,7 +207,7 @@ func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, encrypted bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
if encrypted && GlobalKMS != nil {
|
||||
if GlobalKMS != nil {
|
||||
logger.Info("Migration of encrypted config data completed. All config data is now encrypted.")
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
)
|
||||
|
||||
|
||||
+19
-3
@@ -27,7 +27,7 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/config/cache"
|
||||
@@ -2425,6 +2425,7 @@ func migrateV27ToV28() error {
|
||||
// Migrates ${HOME}/.minio/config.json to '<export_path>/.minio.sys/config/config.json'
|
||||
// if etcd is configured then migrates /config/config.json to '<export_path>/.minio.sys/config/config.json'
|
||||
func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
||||
bootstrapTrace("migrate config to .minio.sys/config/config.json")
|
||||
// Construct path to config.json for the given bucket.
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
|
||||
@@ -2472,6 +2473,7 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
||||
|
||||
// Migrates '.minio.sys/config.json' to v33.
|
||||
func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
||||
bootstrapTrace("migrate .minio.sys/config/config.json to latest version")
|
||||
// Construct path to config.json for the given bucket.
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
|
||||
@@ -2533,6 +2535,21 @@ func checkConfigVersion(objAPI ObjectLayer, configFile string, version string) (
|
||||
}
|
||||
}
|
||||
|
||||
if version == "kvs" {
|
||||
// Check api config values are present.
|
||||
var vcfg struct {
|
||||
API struct {
|
||||
E []struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
} `json:"_"`
|
||||
} `json:"api"`
|
||||
}
|
||||
if err = json.Unmarshal(data, &vcfg); err != nil {
|
||||
return false, nil, nil
|
||||
}
|
||||
return len(vcfg.API.E) > 0, data, nil
|
||||
}
|
||||
var versionConfig struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
@@ -2722,6 +2739,7 @@ func migrateV32ToV33MinioSys(objAPI ObjectLayer) error {
|
||||
}
|
||||
|
||||
func migrateMinioSysConfigToKV(objAPI ObjectLayer) error {
|
||||
bootstrapTrace("migrate config to KV style")
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
|
||||
// Check if the config version is latest, if not migrate.
|
||||
@@ -2740,9 +2758,7 @@ func migrateMinioSysConfigToKV(objAPI ObjectLayer) error {
|
||||
|
||||
newCfg := newServerConfig()
|
||||
|
||||
config.SetCredentials(newCfg, cfg.Credential)
|
||||
config.SetRegion(newCfg, cfg.Region)
|
||||
|
||||
storageclass.SetStorageClass(newCfg, cfg.StorageClass)
|
||||
|
||||
for k, loggerArgs := range cfg.Logger.HTTP {
|
||||
|
||||
@@ -22,8 +22,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/internal/config"
|
||||
)
|
||||
|
||||
// Test if config v1 is purged
|
||||
@@ -65,7 +63,7 @@ func TestServerConfigMigrateV1(t *testing.T) {
|
||||
}
|
||||
|
||||
// Initialize server config and check again if everything is fine
|
||||
if err := loadConfig(objLayer); err != nil {
|
||||
if err := loadConfig(objLayer, nil); err != nil {
|
||||
t.Fatalf("Unable to initialize from updated config file %s", err)
|
||||
}
|
||||
}
|
||||
@@ -207,20 +205,9 @@ func TestServerConfigMigrateV2toV33(t *testing.T) {
|
||||
}
|
||||
|
||||
// Initialize server config and check again if everything is fine
|
||||
if err := loadConfig(objLayer); err != nil {
|
||||
if err := loadConfig(objLayer, nil); err != nil {
|
||||
t.Fatalf("Unable to initialize from updated config file %s", err)
|
||||
}
|
||||
|
||||
// Check if accessKey and secretKey are not altered during migration
|
||||
caccessKey := globalServerConfig[config.CredentialsSubSys][config.Default].Get(config.AccessKey)
|
||||
if caccessKey != accessKey {
|
||||
t.Fatalf("Access key lost during migration, expected: %v, found:%v", accessKey, caccessKey)
|
||||
}
|
||||
|
||||
csecretKey := globalServerConfig[config.CredentialsSubSys][config.Default].Get(config.SecretKey)
|
||||
if csecretKey != secretKey {
|
||||
t.Fatalf("Secret key lost during migration, expected: %v, found: %v", secretKey, csecretKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Test if all migrate code returns error with corrupted config files
|
||||
|
||||
+34
-19
@@ -28,7 +28,7 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
)
|
||||
@@ -152,30 +152,34 @@ func saveServerConfig(ctx context.Context, objAPI ObjectLayer, cfg interface{})
|
||||
return saveConfig(ctx, objAPI, configFile, data)
|
||||
}
|
||||
|
||||
func readServerConfig(ctx context.Context, objAPI ObjectLayer) (config.Config, error) {
|
||||
// data is optional. If nil it will be loaded from backend.
|
||||
func readServerConfig(ctx context.Context, objAPI ObjectLayer, data []byte) (config.Config, error) {
|
||||
srvCfg := config.New()
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
data, err := readConfig(ctx, objAPI, configFile)
|
||||
if err != nil {
|
||||
if errors.Is(err, errConfigNotFound) {
|
||||
lookupConfigs(srvCfg, objAPI)
|
||||
return srvCfg, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if GlobalKMS != nil && !utf8.Valid(data) {
|
||||
data, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
|
||||
minioMetaBucket: path.Join(minioMetaBucket, configFile),
|
||||
})
|
||||
var err error
|
||||
if len(data) == 0 {
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
data, err = readConfig(ctx, objAPI, configFile)
|
||||
if err != nil {
|
||||
lookupConfigs(srvCfg, objAPI)
|
||||
if errors.Is(err, errConfigNotFound) {
|
||||
lookupConfigs(srvCfg, objAPI)
|
||||
return srvCfg, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if GlobalKMS != nil && !utf8.Valid(data) {
|
||||
data, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
|
||||
minioMetaBucket: path.Join(minioMetaBucket, configFile),
|
||||
})
|
||||
if err != nil {
|
||||
lookupConfigs(srvCfg, objAPI)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
if err = json.Unmarshal(data, &srvCfg); err != nil {
|
||||
if err := json.Unmarshal(data, &srvCfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -202,6 +206,8 @@ func NewConfigSys() *ConfigSys {
|
||||
|
||||
// Initialize and load config from remote etcd or local config directory
|
||||
func initConfig(objAPI ObjectLayer) error {
|
||||
bootstrapTrace("load the configuration")
|
||||
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
@@ -212,6 +218,15 @@ func initConfig(objAPI ObjectLayer) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the config version is latest (kvs), if not migrate.
|
||||
ok, data, err := checkConfigVersion(objAPI, path.Join(minioConfigPrefix, minioConfigFile), "kvs")
|
||||
if err != nil && !errors.Is(err, errConfigNotFound) {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
return loadConfig(objAPI, data)
|
||||
}
|
||||
|
||||
// Migrates ${HOME}/.minio/config.json or config.json.deprecated
|
||||
// to '<export_path>/.minio.sys/config/config.json'
|
||||
// ignore if the file doesn't exist.
|
||||
@@ -232,5 +247,5 @@ func initConfig(objAPI ObjectLayer) error {
|
||||
return fmt.Errorf("migrateMinioSysConfigToKV: %w", err)
|
||||
}
|
||||
|
||||
return loadConfig(objAPI)
|
||||
return loadConfig(objAPI, nil)
|
||||
}
|
||||
|
||||
+25
-3
@@ -21,8 +21,9 @@ import (
|
||||
"container/ring"
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/logger/message/log"
|
||||
"github.com/minio/minio/internal/logger/target/console"
|
||||
@@ -36,6 +37,9 @@ const defaultLogBufferCount = 10000
|
||||
|
||||
// HTTPConsoleLoggerSys holds global console logger state
|
||||
type HTTPConsoleLoggerSys struct {
|
||||
totalMessages int64
|
||||
failedMessages int64
|
||||
|
||||
sync.RWMutex
|
||||
pubsub *pubsub.PubSub[log.Info, madmin.LogMask]
|
||||
console *console.Target
|
||||
@@ -53,6 +57,11 @@ func NewConsoleLogger(ctx context.Context) *HTTPConsoleLoggerSys {
|
||||
}
|
||||
}
|
||||
|
||||
// IsOnline always true in case of console logger
|
||||
func (sys *HTTPConsoleLoggerSys) IsOnline() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SetNodeName - sets the node name if any after distributed setup has initialized
|
||||
func (sys *HTTPConsoleLoggerSys) SetNodeName(nodeName string) {
|
||||
if !globalIsDistErasure {
|
||||
@@ -133,6 +142,15 @@ func (sys *HTTPConsoleLoggerSys) String() string {
|
||||
return logger.ConsoleLoggerTgt
|
||||
}
|
||||
|
||||
// Stats returns the target statistics.
|
||||
func (sys *HTTPConsoleLoggerSys) Stats() types.TargetStats {
|
||||
return types.TargetStats{
|
||||
TotalMessages: atomic.LoadInt64(&sys.totalMessages),
|
||||
FailedMessages: atomic.LoadInt64(&sys.failedMessages),
|
||||
QueueLength: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Content returns the console stdout log
|
||||
func (sys *HTTPConsoleLoggerSys) Content() (logs []log.Entry) {
|
||||
sys.RLock()
|
||||
@@ -170,6 +188,7 @@ func (sys *HTTPConsoleLoggerSys) Send(entry interface{}) error {
|
||||
case string:
|
||||
lg = log.Info{ConsoleMsg: e, NodeName: sys.nodeName}
|
||||
}
|
||||
atomic.AddInt64(&sys.totalMessages, 1)
|
||||
|
||||
sys.pubsub.Publish(lg)
|
||||
sys.Lock()
|
||||
@@ -177,6 +196,9 @@ func (sys *HTTPConsoleLoggerSys) Send(entry interface{}) error {
|
||||
sys.logBuf.Value = lg
|
||||
sys.logBuf = sys.logBuf.Next()
|
||||
sys.Unlock()
|
||||
|
||||
return sys.console.Send(entry, string(logger.All))
|
||||
err := sys.console.Send(entry, string(logger.All))
|
||||
if err != nil {
|
||||
atomic.AddInt64(&sys.failedMessages, 1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/mux"
|
||||
)
|
||||
|
||||
// Test cross domain xml handler.
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
)
|
||||
|
||||
@@ -47,6 +47,8 @@ const (
|
||||
scannerMetricILM
|
||||
scannerMetricCheckReplication
|
||||
scannerMetricYield
|
||||
scannerMetricCleanAbandoned
|
||||
scannerMetricApplyNonCurrent
|
||||
|
||||
// START Trace metrics:
|
||||
scannerMetricStartTrace
|
||||
@@ -56,9 +58,10 @@ const (
|
||||
scannerMetricLastRealtime
|
||||
|
||||
// Trace only metrics:
|
||||
scannerMetricScanFolder // Scan a folder on disk, recursively.
|
||||
scannerMetricScanCycle // Full cycle, cluster global
|
||||
scannerMetricScanBucketDisk // Single bucket on one disk
|
||||
scannerMetricScanFolder // Scan a folder on disk, recursively.
|
||||
scannerMetricScanCycle // Full cycle, cluster global
|
||||
scannerMetricScanBucketDrive // Single bucket on one drive
|
||||
scannerMetricCompactFolder // Folder compacted.
|
||||
|
||||
// Must be last:
|
||||
scannerMetricLast
|
||||
@@ -179,9 +182,9 @@ func (p *scannerMetrics) getCurrentPaths() []string {
|
||||
return res
|
||||
}
|
||||
|
||||
// activeDisks returns the number of currently active disks.
|
||||
// activeDrives returns the number of currently active disks.
|
||||
// (since this is concurrent it may not be 100% reliable)
|
||||
func (p *scannerMetrics) activeDisks() int {
|
||||
func (p *scannerMetrics) activeDrives() int {
|
||||
var i int
|
||||
p.currentPaths.Range(func(k, v interface{}) bool {
|
||||
i++
|
||||
@@ -192,7 +195,7 @@ func (p *scannerMetrics) activeDisks() int {
|
||||
|
||||
// lifetime returns the lifetime count of the specified metric.
|
||||
func (p *scannerMetrics) lifetime(m scannerMetric) uint64 {
|
||||
if m < 0 || m >= scannerMetricLast {
|
||||
if m >= scannerMetricLast {
|
||||
return 0
|
||||
}
|
||||
val := atomic.LoadUint64(&p.operations[m])
|
||||
@@ -202,7 +205,7 @@ func (p *scannerMetrics) lifetime(m scannerMetric) uint64 {
|
||||
// lastMinute returns the last minute statistics of a metric.
|
||||
// m should be < scannerMetricLastRealtime
|
||||
func (p *scannerMetrics) lastMinute(m scannerMetric) AccElem {
|
||||
if m < 0 || m >= scannerMetricLastRealtime {
|
||||
if m >= scannerMetricLastRealtime {
|
||||
return AccElem{}
|
||||
}
|
||||
val := p.latency[m].total()
|
||||
|
||||
+41
-38
@@ -35,7 +35,7 @@ import (
|
||||
|
||||
"github.com/bits-and-blooms/bloom/v3"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/bucket/object/lock"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
@@ -64,11 +64,6 @@ const (
|
||||
var (
|
||||
globalHealConfig heal.Config
|
||||
|
||||
dataScannerLeaderLockTimeout = newDynamicTimeoutWithOpts(dynamicTimeoutOpts{
|
||||
timeout: 30 * time.Second,
|
||||
minimum: 10 * time.Second,
|
||||
retryInterval: time.Second,
|
||||
})
|
||||
// Sleeper values are updated when config is loaded.
|
||||
scannerSleeper = newDynamicSleeper(10, 10*time.Second, true)
|
||||
scannerCycle = uatomic.NewDuration(dataScannerStartDelay)
|
||||
@@ -118,6 +113,10 @@ type backgroundHealInfo struct {
|
||||
}
|
||||
|
||||
func readBackgroundHealInfo(ctx context.Context, objAPI ObjectLayer) backgroundHealInfo {
|
||||
if globalIsErasureSD {
|
||||
return backgroundHealInfo{}
|
||||
}
|
||||
|
||||
// Get last healing information
|
||||
buf, err := readConfig(ctx, objAPI, backgroundHealInfoPath)
|
||||
if err != nil {
|
||||
@@ -127,15 +126,17 @@ func readBackgroundHealInfo(ctx context.Context, objAPI ObjectLayer) backgroundH
|
||||
return backgroundHealInfo{}
|
||||
}
|
||||
var info backgroundHealInfo
|
||||
err = json.Unmarshal(buf, &info)
|
||||
if err != nil {
|
||||
if err = json.Unmarshal(buf, &info); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return backgroundHealInfo{}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func saveBackgroundHealInfo(ctx context.Context, objAPI ObjectLayer, info backgroundHealInfo) {
|
||||
if globalIsErasureSD {
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -151,19 +152,9 @@ func saveBackgroundHealInfo(ctx context.Context, objAPI ObjectLayer, info backgr
|
||||
// runDataScanner will start a data scanner.
|
||||
// The function will block until the context is canceled.
|
||||
// There should only ever be one scanner running per cluster.
|
||||
func runDataScanner(pctx context.Context, objAPI ObjectLayer) {
|
||||
// Make sure only 1 scanner is running on the cluster.
|
||||
locker := objAPI.NewNSLock(minioMetaBucket, "scanner/runDataScanner.lock")
|
||||
lkctx, err := locker.GetLock(pctx, dataScannerLeaderLockTimeout)
|
||||
if err != nil {
|
||||
if intDataUpdateTracker.debug {
|
||||
logger.LogIf(pctx, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx := lkctx.Context()
|
||||
defer lkctx.Cancel()
|
||||
// No unlock for "leader" lock.
|
||||
func runDataScanner(ctx context.Context, objAPI ObjectLayer) {
|
||||
ctx, cancel := globalLeaderLock.GetLock(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Load current bloom cycle
|
||||
var cycleInfo currentScannerCycle
|
||||
@@ -175,7 +166,7 @@ func runDataScanner(pctx context.Context, objAPI ObjectLayer) {
|
||||
} else if len(buf) > 8 {
|
||||
cycleInfo.next = binary.LittleEndian.Uint64(buf[:8])
|
||||
buf = buf[8:]
|
||||
_, err = cycleInfo.UnmarshalMsg(buf)
|
||||
_, err := cycleInfo.UnmarshalMsg(buf)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
|
||||
@@ -429,7 +420,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
||||
filter := f.withFilter
|
||||
_, prefix := path2BucketObjectWithBasePath(f.root, folder.name)
|
||||
var activeLifeCycle *lifecycle.Lifecycle
|
||||
if f.oldCache.Info.lifeCycle != nil && f.oldCache.Info.lifeCycle.HasActiveRules(prefix, true) {
|
||||
if f.oldCache.Info.lifeCycle != nil && f.oldCache.Info.lifeCycle.HasActiveRules(prefix) {
|
||||
if f.dataUsageScannerDebug {
|
||||
console.Debugf(scannerLogPrefix+" Prefix %q has active rules\n", prefix)
|
||||
}
|
||||
@@ -770,7 +761,9 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
||||
object: entry.name,
|
||||
versionID: "",
|
||||
}, madmin.HealItemObject)
|
||||
logger.LogIf(ctx, err)
|
||||
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
foundObjs = foundObjs || err == nil
|
||||
return
|
||||
}
|
||||
@@ -785,7 +778,9 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
||||
object: fiv.Name,
|
||||
versionID: ver.VersionID,
|
||||
}, madmin.HealItemObject)
|
||||
logger.LogIf(ctx, err)
|
||||
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
foundObjs = foundObjs || err == nil
|
||||
}
|
||||
},
|
||||
@@ -822,9 +817,8 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
||||
flat.Compacted = true
|
||||
var compact bool
|
||||
if flat.Objects < dataScannerCompactLeastObject {
|
||||
if f.dataUsageScannerDebug && flat.Objects > 1 {
|
||||
// Disabled, rather chatty:
|
||||
// console.Debugf(scannerLogPrefix+" Only %d objects, compacting %s -> %+v\n", flat.Objects, folder.name, flat)
|
||||
if flat.Objects > 1 {
|
||||
globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
|
||||
}
|
||||
compact = true
|
||||
} else {
|
||||
@@ -838,9 +832,8 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
||||
}
|
||||
}
|
||||
}
|
||||
if f.dataUsageScannerDebug && compact {
|
||||
// Disabled, rather chatty:
|
||||
// console.Debugf(scannerLogPrefix+" Only objects (%d), compacting %s -> %+v\n", flat.Objects, folder.name, flat)
|
||||
if compact {
|
||||
globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
|
||||
}
|
||||
}
|
||||
if compact {
|
||||
@@ -954,10 +947,6 @@ func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, oi Obje
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
if i.lifeCycle == nil {
|
||||
if i.debug {
|
||||
// disabled, very chatty:
|
||||
// console.Debugf(applyActionsLogPrefix+" no lifecycle rules to apply: %q\n", i.objectPath())
|
||||
}
|
||||
return false, size
|
||||
}
|
||||
|
||||
@@ -1008,7 +997,8 @@ func (i *scannerItem) applyTierObjSweep(ctx context.Context, o ObjectLayer, oi O
|
||||
|
||||
// Remove this free version
|
||||
_, err = o.DeleteObject(ctx, oi.Bucket, oi.Name, ObjectOptions{
|
||||
VersionID: oi.VersionID,
|
||||
VersionID: oi.VersionID,
|
||||
InclFreeVersions: true,
|
||||
})
|
||||
if err == nil {
|
||||
auditLogLifecycle(ctx, oi, ILMFreeVersionDelete)
|
||||
@@ -1024,6 +1014,8 @@ func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ Ob
|
||||
if i.lifeCycle == nil {
|
||||
return fivs, nil
|
||||
}
|
||||
done := globalScannerMetrics.time(scannerMetricApplyNonCurrent)
|
||||
defer done()
|
||||
|
||||
_, days, lim := i.lifeCycle.NoncurrentVersionsExpirationLimit(lifecycle.ObjectOpts{Name: i.objectPath()})
|
||||
if lim == 0 || len(fivs) <= lim+1 { // fewer than lim _noncurrent_ versions
|
||||
@@ -1080,6 +1072,17 @@ func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ Ob
|
||||
// applyVersionActions will apply lifecycle checks on all versions of a scanned item. Returns versions that remain
|
||||
// after applying lifecycle checks configured.
|
||||
func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fivs []FileInfo) ([]FileInfo, error) {
|
||||
if i.heal.enabled {
|
||||
if healDeleteDangling {
|
||||
done := globalScannerMetrics.time(scannerMetricCleanAbandoned)
|
||||
err := o.CheckAbandonedParts(ctx, i.bucket, i.objectPath(), madmin.HealOpts{Remove: healDeleteDangling})
|
||||
done()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("unable to check object %s/%s for abandoned data: %w", i.bucket, i.objectPath(), err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return i.applyNewerNoncurrentVersionLimit(ctx, o, fivs)
|
||||
}
|
||||
|
||||
@@ -1110,7 +1113,7 @@ func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, oi Object
|
||||
}
|
||||
|
||||
func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr lock.Retention, obj ObjectInfo) lifecycle.Event {
|
||||
event := lc.Eval(obj.ToLifecycleOpts(), time.Now().UTC())
|
||||
event := lc.Eval(obj.ToLifecycleOpts())
|
||||
if serverDebugLog {
|
||||
console.Debugf(applyActionsLogPrefix+" lifecycle: Secondary scan: %v\n", event.Action)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,15 @@ func (t *testingLogger) Type() types.TargetType {
|
||||
return types.TargetHTTP
|
||||
}
|
||||
|
||||
func (t *testingLogger) IsOnline() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Stats returns the target statistics.
|
||||
func (t *testingLogger) Stats() types.TargetStats {
|
||||
return types.TargetStats{}
|
||||
}
|
||||
|
||||
func (t *testingLogger) Send(entry interface{}) error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
+24
-8
@@ -18,6 +18,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -31,7 +32,7 @@ import (
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/minio/madmin-go"
|
||||
"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"
|
||||
@@ -879,13 +880,28 @@ func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string)
|
||||
|
||||
// save the content of the cache to minioMetaBackgroundOpsBucket with the provided name.
|
||||
func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string) error {
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
pw.CloseWithError(d.serializeTo(pw))
|
||||
}()
|
||||
defer pr.Close()
|
||||
var r io.Reader
|
||||
|
||||
r, err := hash.NewReader(pr, -1, "", "", -1)
|
||||
// If big, do streaming...
|
||||
size := int64(-1)
|
||||
if len(d.Cache) > 10000 {
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
pw.CloseWithError(d.serializeTo(pw))
|
||||
}()
|
||||
defer pr.Close()
|
||||
r = pr
|
||||
} else {
|
||||
var buf bytes.Buffer
|
||||
err := d.serializeTo(&buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r = &buf
|
||||
size = int64(buf.Len())
|
||||
}
|
||||
|
||||
hr, err := hash.NewReader(r, size, "", "", size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -896,7 +912,7 @@ func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string)
|
||||
_, err = store.PutObject(ctx,
|
||||
dataUsageBucket,
|
||||
name,
|
||||
NewPutObjReader(r),
|
||||
NewPutObjReader(hr),
|
||||
ObjectOptions{})
|
||||
if isErrBucketNotFound(err) {
|
||||
return nil
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
)
|
||||
|
||||
// BucketTargetUsageInfo - bucket target usage info provides
|
||||
|
||||
@@ -512,7 +512,7 @@ func (c *diskCache) statCachedMeta(ctx context.Context, cacheObjPath string) (me
|
||||
return
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer cLock.RUnlock(lkctx.Cancel)
|
||||
defer cLock.RUnlock(lkctx)
|
||||
return c.statCache(ctx, cacheObjPath)
|
||||
}
|
||||
|
||||
@@ -597,7 +597,7 @@ func (c *diskCache) SaveMetadata(ctx context.Context, bucket, object string, met
|
||||
return err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer cLock.Unlock(lkctx.Cancel)
|
||||
defer cLock.Unlock(lkctx)
|
||||
if err = c.saveMetadata(ctx, bucket, object, meta, actualSize, rs, rsFileName, incHitsOnly); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -842,7 +842,7 @@ func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Read
|
||||
return oi, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer cLock.Unlock(lkctx.Cancel)
|
||||
defer cLock.Unlock(lkctx)
|
||||
|
||||
return c.put(ctx, bucket, object, data, size, rs, opts, incHitsOnly, writeback)
|
||||
}
|
||||
@@ -1003,6 +1003,7 @@ func (c *diskCache) bitrotReadFromCache(ctx context.Context, filePath string, of
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
bufp := c.pool.Get().(*[]byte)
|
||||
defer c.pool.Put(bufp)
|
||||
|
||||
@@ -1075,7 +1076,7 @@ func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRang
|
||||
return nil, numHits, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer cLock.RUnlock(lkctx.Cancel)
|
||||
defer cLock.RUnlock(lkctx)
|
||||
|
||||
var objInfo ObjectInfo
|
||||
var rngInfo RangeInfo
|
||||
@@ -1212,7 +1213,7 @@ func (c *diskCache) Delete(ctx context.Context, bucket, object string) (err erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cLock.Unlock(lkctx.Cancel)
|
||||
defer cLock.Unlock(lkctx)
|
||||
return removeAll(cacheObjPath)
|
||||
}
|
||||
|
||||
@@ -1323,7 +1324,7 @@ func (c *diskCache) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
||||
}
|
||||
|
||||
ctx = lkctx.Context()
|
||||
defer partIDLock.Unlock(lkctx.Cancel)
|
||||
defer partIDLock.Unlock(lkctx)
|
||||
meta, _, _, err := c.statCache(ctx, uploadIDDir)
|
||||
// Case where object not yet cached
|
||||
if err != nil {
|
||||
@@ -1380,7 +1381,7 @@ func (c *diskCache) SavePartMetadata(ctx context.Context, bucket, object, upload
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer uploadLock.Unlock(ulkctx.Cancel)
|
||||
defer uploadLock.Unlock(ulkctx)
|
||||
|
||||
metaPath := pathJoin(uploadDir, cacheMetaJSONFile)
|
||||
f, err := OpenFile(metaPath, os.O_RDWR|writeMode, 0o666)
|
||||
@@ -1479,7 +1480,7 @@ func (c *diskCache) CompleteMultipartUpload(ctx context.Context, bucket, object,
|
||||
}
|
||||
|
||||
ctx = lkctx.Context()
|
||||
defer cLock.Unlock(lkctx.Cancel)
|
||||
defer cLock.Unlock(lkctx)
|
||||
mpartCachePath := getMultipartCacheSHADir(c.dir, bucket, object)
|
||||
uploadIDDir := path.Join(mpartCachePath, uploadID)
|
||||
|
||||
|
||||
+1
-1
@@ -728,7 +728,7 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
|
||||
if cerr != nil {
|
||||
return putObjectFn(ctx, bucket, object, r, opts)
|
||||
}
|
||||
defer cLock.Unlock(lkctx.Cancel)
|
||||
defer cLock.Unlock(lkctx)
|
||||
// Initialize pipe to stream data to backend
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
hashReader, err := hash.NewReader(pipeReader, size, "", "", r.ActualSize())
|
||||
|
||||
@@ -20,8 +20,8 @@ package cmd
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
|
||||
+40
-1
@@ -55,7 +55,8 @@ var (
|
||||
// Additional MinIO errors for SSE-C requests.
|
||||
errObjectTampered = errors.New("The requested object was modified and may be compromised")
|
||||
// error returned when invalid encryption parameters are specified
|
||||
errInvalidEncryptionParameters = errors.New("The encryption parameters are not applicable to this object")
|
||||
errInvalidEncryptionParameters = errors.New("The encryption parameters are not applicable to this object")
|
||||
errInvalidEncryptionParametersSSEC = errors.New("SSE-C encryption parameters are not supported on this bucket")
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1083,6 +1084,44 @@ func (o *ObjectInfo) metadataDecrypter() objectMetaDecryptFn {
|
||||
}
|
||||
}
|
||||
|
||||
// metadataEncryptFn provides an encryption function for metadata.
|
||||
// Will return nil, nil if unencrypted.
|
||||
func (o *ObjectInfo) metadataEncryptFn(headers http.Header) (objectMetaEncryptFn, error) {
|
||||
kind, _ := crypto.IsEncrypted(o.UserDefined)
|
||||
switch kind {
|
||||
case crypto.SSEC:
|
||||
if crypto.SSECopy.IsRequested(headers) {
|
||||
key, err := crypto.SSECopy.ParseHTTP(headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objectEncryptionKey, err := decryptObjectMeta(key[:], o.Bucket, o.Name, o.UserDefined)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(objectEncryptionKey) == 32 {
|
||||
var key crypto.ObjectKey
|
||||
copy(key[:], objectEncryptionKey)
|
||||
return metadataEncrypter(key), nil
|
||||
}
|
||||
return nil, errors.New("metadataEncryptFn: unexpected key size")
|
||||
}
|
||||
case crypto.S3, crypto.S3KMS:
|
||||
objectEncryptionKey, err := decryptObjectMeta(nil, o.Bucket, o.Name, o.UserDefined)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(objectEncryptionKey) == 32 {
|
||||
var key crypto.ObjectKey
|
||||
copy(key[:], objectEncryptionKey)
|
||||
return metadataEncrypter(key), nil
|
||||
}
|
||||
return nil, errors.New("metadataEncryptFn: unexpected key size")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// decryptChecksums will attempt to decode checksums and return it/them if set.
|
||||
func (o *ObjectInfo) decryptChecksums() map[string]string {
|
||||
data := o.Checksum
|
||||
|
||||
@@ -19,6 +19,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -207,7 +208,7 @@ func getSetIndexes(args []string, totalSizes []uint64, customSetDriveCount uint6
|
||||
}
|
||||
|
||||
// Returns all the expanded endpoints, each argument is expanded separately.
|
||||
func (s endpointSet) getEndpoints() (endpoints []string) {
|
||||
func (s *endpointSet) getEndpoints() (endpoints []string) {
|
||||
if len(s.endpoints) != 0 {
|
||||
return s.endpoints
|
||||
}
|
||||
@@ -343,7 +344,13 @@ func createServerEndpoints(serverAddr string, args ...string) (
|
||||
return nil, -1, errInvalidArgument
|
||||
}
|
||||
|
||||
if !ellipses.HasEllipses(args...) {
|
||||
ok := true
|
||||
for _, arg := range args {
|
||||
ok = ok && !ellipses.HasEllipses(arg)
|
||||
}
|
||||
|
||||
// None of the args have ellipses use the old style.
|
||||
if ok {
|
||||
setArgs, err := GetAllSets(args...)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
@@ -358,6 +365,7 @@ func createServerEndpoints(serverAddr string, args ...string) (
|
||||
DrivesPerSet: len(setArgs[0]),
|
||||
Endpoints: endpointList,
|
||||
CmdLine: strings.Join(args, " "),
|
||||
Platform: fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH),
|
||||
})
|
||||
setupType = newSetupType
|
||||
return endpointServerPools, setupType, nil
|
||||
@@ -365,6 +373,10 @@ func createServerEndpoints(serverAddr string, args ...string) (
|
||||
|
||||
var foundPrevLocal bool
|
||||
for _, arg := range args {
|
||||
if !ellipses.HasEllipses(arg) && len(args) > 1 {
|
||||
// TODO: support SNSD deployments to be decommissioned in future
|
||||
return nil, -1, fmt.Errorf("all args must have ellipses for pool expansion (%w) args: %s", errInvalidArgument, args)
|
||||
}
|
||||
setArgs, err := GetAllSets(arg)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
@@ -379,6 +391,7 @@ func createServerEndpoints(serverAddr string, args ...string) (
|
||||
DrivesPerSet: len(setArgs[0]),
|
||||
Endpoints: endpointList,
|
||||
CmdLine: arg,
|
||||
Platform: fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH),
|
||||
}); err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
|
||||
+46
-4
@@ -203,6 +203,7 @@ type PoolEndpoints struct {
|
||||
DrivesPerSet int
|
||||
Endpoints Endpoints
|
||||
CmdLine string
|
||||
Platform string
|
||||
}
|
||||
|
||||
// EndpointServerPools - list of list of endpoints
|
||||
@@ -417,6 +418,27 @@ func (endpoints Endpoints) atleastOneEndpointLocal() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func hostResolveToLocalhost(endpoint Endpoint) bool {
|
||||
hostIPs, err := getHostIP(endpoint.Hostname())
|
||||
if err != nil {
|
||||
// Log the message to console about the host resolving
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags(
|
||||
"host",
|
||||
endpoint.Hostname(),
|
||||
)
|
||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||
logger.LogOnceIf(ctx, err, endpoint.Hostname(), logger.Application)
|
||||
return false
|
||||
}
|
||||
var loopback int
|
||||
for _, hostIP := range hostIPs.ToSlice() {
|
||||
if net.ParseIP(hostIP).IsLoopback() {
|
||||
loopback++
|
||||
}
|
||||
}
|
||||
return loopback == len(hostIPs)
|
||||
}
|
||||
|
||||
// UpdateIsLocal - resolves the host and discovers the local host.
|
||||
func (endpoints Endpoints) UpdateIsLocal(foundPrevLocal bool) error {
|
||||
orchestrated := IsDocker() || IsKubernetes()
|
||||
@@ -452,6 +474,26 @@ func (endpoints Endpoints) UpdateIsLocal(foundPrevLocal bool) error {
|
||||
endpoints[i].Hostname(),
|
||||
)
|
||||
|
||||
if orchestrated && hostResolveToLocalhost(endpoints[i]) {
|
||||
// time elapsed
|
||||
timeElapsed := time.Since(startTime)
|
||||
// log error only if more than a second has elapsed
|
||||
if timeElapsed > time.Second {
|
||||
reqInfo.AppendTags("elapsedTime",
|
||||
humanize.RelTime(startTime,
|
||||
startTime.Add(timeElapsed),
|
||||
"elapsed",
|
||||
"",
|
||||
))
|
||||
ctx := logger.SetReqInfo(GlobalContext,
|
||||
reqInfo)
|
||||
logger.LogOnceIf(ctx, fmt.Errorf("%s resolves to localhost in a containerized deployment, waiting for it to resolve to a valid IP",
|
||||
endpoints[i].Hostname()), endpoints[i].Hostname(), logger.Application)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// return err if not Docker or Kubernetes
|
||||
// We use IsDocker() to check for Docker environment
|
||||
// We use IsKubernetes() to check for Kubernetes environment
|
||||
@@ -465,7 +507,7 @@ func (endpoints Endpoints) UpdateIsLocal(foundPrevLocal bool) error {
|
||||
if err != nil {
|
||||
// time elapsed
|
||||
timeElapsed := time.Since(startTime)
|
||||
// log error only if more than 1s elapsed
|
||||
// log error only if more than a second has elapsed
|
||||
if timeElapsed > time.Second {
|
||||
reqInfo.AppendTags("elapsedTime",
|
||||
humanize.RelTime(startTime,
|
||||
@@ -475,7 +517,7 @@ func (endpoints Endpoints) UpdateIsLocal(foundPrevLocal bool) error {
|
||||
))
|
||||
ctx := logger.SetReqInfo(GlobalContext,
|
||||
reqInfo)
|
||||
logger.LogIf(ctx, err, logger.Application)
|
||||
logger.LogOnceIf(ctx, err, endpoints[i].Hostname(), logger.Application)
|
||||
}
|
||||
} else {
|
||||
resolvedList[i] = true
|
||||
@@ -592,14 +634,14 @@ func CreateEndpoints(serverAddr string, foundLocal bool, args ...[]string) (Endp
|
||||
return endpoints, setupType, err
|
||||
}
|
||||
if endpoint.Type() != PathEndpointType {
|
||||
return endpoints, setupType, config.ErrInvalidFSEndpoint(nil).Msg("use path style endpoint for FS setup")
|
||||
return endpoints, setupType, config.ErrInvalidEndpoint(nil).Msg("use path style endpoint for single node setup")
|
||||
}
|
||||
endpoints = append(endpoints, endpoint)
|
||||
setupType = ErasureSDSetupType
|
||||
|
||||
// Check for cross device mounts if any.
|
||||
if err = checkCrossDeviceMounts(endpoints); err != nil {
|
||||
return endpoints, setupType, config.ErrInvalidFSEndpoint(nil).Msg(err.Error())
|
||||
return endpoints, setupType, config.ErrInvalidEndpoint(nil).Msg(err.Error())
|
||||
}
|
||||
|
||||
return endpoints, setupType, nil
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/minio/minio-go/v7/pkg/s3utils"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/sync/errgroup"
|
||||
)
|
||||
|
||||
// list all errors that can be ignore in a bucket operation.
|
||||
var bucketOpIgnoredErrs = append(baseIgnoredErrs, errDiskAccessDenied, errUnformattedDisk)
|
||||
|
||||
// list all errors that can be ignored in a bucket metadata operation.
|
||||
var bucketMetadataOpIgnoredErrs = append(bucketOpIgnoredErrs, errVolumeNotFound)
|
||||
|
||||
// Bucket operations
|
||||
|
||||
// MakeBucket - make a bucket.
|
||||
func (er erasureObjects) MakeBucketWithLocation(ctx context.Context, bucket string, opts MakeBucketOptions) error {
|
||||
defer NSUpdated(bucket, slashSeparator)
|
||||
|
||||
// Verify if bucket is valid.
|
||||
if !isMinioMetaBucketName(bucket) {
|
||||
if err := s3utils.CheckValidBucketNameStrict(bucket); err != nil {
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
}
|
||||
|
||||
storageDisks := er.getDisks()
|
||||
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
|
||||
// Make a volume entry on all underlying storage disks.
|
||||
for index := range storageDisks {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if storageDisks[index] != nil {
|
||||
if err := storageDisks[index].MakeVol(ctx, bucket); err != nil {
|
||||
if opts.ForceCreate && errors.Is(err, errVolumeExists) {
|
||||
// No need to return error when force create was
|
||||
// requested.
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, errVolumeExists) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return errDiskNotFound
|
||||
}, index)
|
||||
}
|
||||
|
||||
err := reduceWriteQuorumErrs(ctx, g.Wait(), bucketOpIgnoredErrs, er.defaultWQuorum())
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
func undoDeleteBucket(storageDisks []StorageAPI, bucket string) {
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
// Undo previous make bucket entry on all underlying storage disks.
|
||||
for index := range storageDisks {
|
||||
if storageDisks[index] == nil {
|
||||
continue
|
||||
}
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
_ = storageDisks[index].MakeVol(context.Background(), bucket)
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
|
||||
// Wait for all make vol to finish.
|
||||
g.Wait()
|
||||
}
|
||||
|
||||
// getBucketInfo - returns the BucketInfo from one of the load balanced disks.
|
||||
func (er erasureObjects) getBucketInfo(ctx context.Context, bucketName string, opts BucketOptions) (bucketInfo BucketInfo, err error) {
|
||||
storageDisks := er.getDisks()
|
||||
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
bucketsInfo := make([]BucketInfo, len(storageDisks))
|
||||
// Undo previous make bucket entry on all underlying storage disks.
|
||||
for index := range storageDisks {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if storageDisks[index] == nil {
|
||||
return errDiskNotFound
|
||||
}
|
||||
volInfo, err := storageDisks[index].StatVol(ctx, bucketName)
|
||||
if err != nil {
|
||||
if opts.Deleted {
|
||||
dvi, derr := storageDisks[index].StatVol(ctx, pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, bucketName))
|
||||
if derr != nil {
|
||||
return err
|
||||
}
|
||||
bucketsInfo[index] = BucketInfo{Name: bucketName, Deleted: dvi.Created}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
bucketsInfo[index] = BucketInfo{Name: volInfo.Name, Created: volInfo.Created}
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
|
||||
errs := g.Wait()
|
||||
|
||||
for i, err := range errs {
|
||||
if err == nil {
|
||||
return bucketsInfo[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
// If all our errors were ignored, then we try to
|
||||
// reduce to one error based on read quorum.
|
||||
// `nil` is deliberately passed for ignoredErrs
|
||||
// because these errors were already ignored.
|
||||
return BucketInfo{}, reduceReadQuorumErrs(ctx, errs, nil, er.defaultRQuorum())
|
||||
}
|
||||
|
||||
// GetBucketInfo - returns BucketInfo for a bucket.
|
||||
func (er erasureObjects) GetBucketInfo(ctx context.Context, bucket string, opts BucketOptions) (bi BucketInfo, e error) {
|
||||
bucketInfo, err := er.getBucketInfo(ctx, bucket, opts)
|
||||
if err != nil {
|
||||
return bi, toObjectErr(err, bucket)
|
||||
}
|
||||
return bucketInfo, nil
|
||||
}
|
||||
|
||||
// DeleteBucket - deletes a bucket.
|
||||
func (er erasureObjects) DeleteBucket(ctx context.Context, bucket string, opts DeleteBucketOptions) error {
|
||||
// Collect if all disks report volume not found.
|
||||
defer NSUpdated(bucket, slashSeparator)
|
||||
|
||||
storageDisks := er.getDisks()
|
||||
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
|
||||
for index := range storageDisks {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if storageDisks[index] != nil {
|
||||
return storageDisks[index].DeleteVol(ctx, bucket, opts.Force)
|
||||
}
|
||||
return errDiskNotFound
|
||||
}, index)
|
||||
}
|
||||
|
||||
// Wait for all the delete vols to finish.
|
||||
dErrs := g.Wait()
|
||||
|
||||
if opts.Force {
|
||||
for _, err := range dErrs {
|
||||
if err != nil {
|
||||
undoDeleteBucket(storageDisks, bucket)
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err := reduceWriteQuorumErrs(ctx, dErrs, bucketOpIgnoredErrs, er.defaultWQuorum())
|
||||
if err == errErasureWriteQuorum && !opts.NoRecreate {
|
||||
undoDeleteBucket(storageDisks, bucket)
|
||||
}
|
||||
|
||||
if err == nil || errors.Is(err, errVolumeNotFound) {
|
||||
var purgedDangling bool
|
||||
// At this point we have `err == nil` but some errors might be `errVolumeNotEmpty`
|
||||
// we should proceed to attempt a force delete of such buckets.
|
||||
for index, err := range dErrs {
|
||||
if err == errVolumeNotEmpty && storageDisks[index] != nil {
|
||||
storageDisks[index].RenameFile(ctx, bucket, "", minioMetaTmpDeletedBucket, mustGetUUID())
|
||||
purgedDangling = true
|
||||
}
|
||||
}
|
||||
// if we purged dangling buckets, ignore errVolumeNotFound error.
|
||||
if purgedDangling {
|
||||
err = nil
|
||||
}
|
||||
if opts.SRDeleteOp == MarkDelete {
|
||||
er.markDelete(ctx, minioMetaBucket, pathJoin(bucketMetaPrefix, deletedBucketsPrefix, bucket))
|
||||
}
|
||||
}
|
||||
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// markDelete creates a vol entry in .minio.sys/buckets/.deleted until site replication
|
||||
// syncs the delete to peers
|
||||
func (er erasureObjects) markDelete(ctx context.Context, bucket, prefix string) error {
|
||||
storageDisks := er.getDisks()
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
// Make a volume entry on all underlying storage disks.
|
||||
for index := range storageDisks {
|
||||
index := index
|
||||
if storageDisks[index] == nil {
|
||||
continue
|
||||
}
|
||||
g.Go(func() error {
|
||||
if err := storageDisks[index].MakeVol(ctx, pathJoin(bucket, prefix)); err != nil {
|
||||
if errors.Is(err, errVolumeExists) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
err := reduceWriteQuorumErrs(ctx, g.Wait(), bucketOpIgnoredErrs, er.defaultWQuorum())
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// purgeDelete deletes vol entry in .minio.sys/buckets/.deleted after site replication
|
||||
// syncs the delete to peers OR on a new MakeBucket call.
|
||||
func (er erasureObjects) purgeDelete(ctx context.Context, bucket, prefix string) error {
|
||||
storageDisks := er.getDisks()
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
// Make a volume entry on all underlying storage disks.
|
||||
for index := range storageDisks {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if storageDisks[index] != nil {
|
||||
return storageDisks[index].DeleteVol(ctx, pathJoin(bucket, prefix), true)
|
||||
}
|
||||
return errDiskNotFound
|
||||
}, index)
|
||||
}
|
||||
err := reduceWriteQuorumErrs(ctx, g.Wait(), bucketOpIgnoredErrs, er.defaultWQuorum())
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// IsNotificationSupported returns whether bucket notification is applicable for this layer.
|
||||
func (er erasureObjects) IsNotificationSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsListenSupported returns whether listen bucket notification is applicable for this layer.
|
||||
func (er erasureObjects) IsListenSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsEncryptionSupported returns whether server side encryption is implemented for this layer.
|
||||
func (er erasureObjects) IsEncryptionSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||
func (er erasureObjects) IsCompressionSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsTaggingSupported indicates whether erasureObjects implements tagging support.
|
||||
func (er erasureObjects) IsTaggingSupported() bool {
|
||||
return true
|
||||
}
|
||||
+11
-3
@@ -22,6 +22,7 @@ import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio/internal/hash"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
@@ -83,9 +84,16 @@ func (e *Erasure) Encode(ctx context.Context, src io.Reader, writers []io.Writer
|
||||
for {
|
||||
var blocks [][]byte
|
||||
n, err := io.ReadFull(src, buf)
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
logger.LogIf(ctx, err)
|
||||
return 0, err
|
||||
if err != nil {
|
||||
if !IsErrIgnored(err, []error{
|
||||
io.EOF,
|
||||
io.ErrUnexpectedEOF,
|
||||
}...) {
|
||||
if !hash.IsChecksumMismatch(err) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
eof := err == io.EOF || err == io.ErrUnexpectedEOF
|
||||
if n == 0 && total != 0 {
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
)
|
||||
|
||||
// commonTime returns a maximally occurring time from a list of time.
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
)
|
||||
|
||||
// Returns the latest updated FileInfo files and error in case of failure.
|
||||
@@ -223,7 +223,7 @@ func TestListOnlineDisks(t *testing.T) {
|
||||
}
|
||||
|
||||
bucket := "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket %v", err)
|
||||
}
|
||||
@@ -398,7 +398,7 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
|
||||
}
|
||||
|
||||
bucket := "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket %v", err)
|
||||
}
|
||||
@@ -519,7 +519,7 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
z := obj.(*erasureServerPools)
|
||||
s := z.serverPools[0].sets[0]
|
||||
erasureDisks := s.getDisks()
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket %v", err)
|
||||
}
|
||||
|
||||
+65
-18
@@ -26,7 +26,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/sync/errgroup"
|
||||
)
|
||||
@@ -40,6 +40,7 @@ type healingMetric uint8
|
||||
const (
|
||||
healingMetricBucket healingMetric = iota
|
||||
healingMetricObject
|
||||
healingMetricCheckAbandonedParts
|
||||
)
|
||||
|
||||
// AcceptableDelta returns 'true' if the fi.DiskMTime is under
|
||||
@@ -93,7 +94,7 @@ func (er erasureObjects) healBucket(ctx context.Context, storageDisks []StorageA
|
||||
if globalTrace.NumSubscribers(madmin.TraceHealing) > 0 {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
healTrace(healingMetricBucket, startTime, bucket, "", "", opts, err, &res)
|
||||
healTrace(healingMetricBucket, startTime, bucket, "", &opts, err, &res)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -234,6 +235,10 @@ func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets
|
||||
// we ignore disk not found errors
|
||||
return nil
|
||||
}
|
||||
if storageDisks[index].Healing() != nil {
|
||||
// we ignore disks under healing
|
||||
return nil
|
||||
}
|
||||
volsInfo, err := storageDisks[index].ListVols(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -294,7 +299,7 @@ func shouldHealObjectOnDisk(erErr, dataErr error, meta FileInfo, latestMeta File
|
||||
}
|
||||
|
||||
// Heals an object by re-writing corrupt/missing erasure blocks.
|
||||
func (er erasureObjects) healObject(ctx context.Context, bucket string, object string, versionID string, opts madmin.HealOpts) (result madmin.HealResultItem, err error) {
|
||||
func (er *erasureObjects) healObject(ctx context.Context, bucket string, object string, versionID string, opts madmin.HealOpts) (result madmin.HealResultItem, err error) {
|
||||
dryRun := opts.DryRun
|
||||
scanMode := opts.ScanMode
|
||||
|
||||
@@ -304,7 +309,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
if globalTrace.NumSubscribers(madmin.TraceHealing) > 0 {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
healTrace(healingMetricObject, startTime, bucket, object, versionID, opts, err, &result)
|
||||
healTrace(healingMetricObject, startTime, bucket, object, &opts, err, &result)
|
||||
}()
|
||||
}
|
||||
// Initialize heal result object
|
||||
@@ -312,6 +317,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
Type: madmin.HealItemObject,
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
VersionID: versionID,
|
||||
DiskCount: len(storageDisks),
|
||||
}
|
||||
|
||||
@@ -322,7 +328,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
return result, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx.Cancel)
|
||||
defer lk.Unlock(lkctx)
|
||||
}
|
||||
|
||||
// Re-read when we have lock...
|
||||
@@ -622,7 +628,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
|
||||
}
|
||||
|
||||
defer er.renameAll(context.Background(), minioMetaTmpBucket, tmpID)
|
||||
defer er.deleteAll(context.Background(), minioMetaTmpBucket, tmpID)
|
||||
|
||||
// Rename from tmp location to the actual location.
|
||||
for i, disk := range outDatedDisks {
|
||||
@@ -673,9 +679,45 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// checkAbandonedParts will check if an object has abandoned parts,
|
||||
// meaning data-dirs or inlined data that are no longer referenced by the xl.meta
|
||||
// Errors are generally ignored by this function.
|
||||
func (er *erasureObjects) checkAbandonedParts(ctx context.Context, bucket string, object string, opts madmin.HealOpts) (err error) {
|
||||
if !opts.Remove || opts.DryRun {
|
||||
return nil
|
||||
}
|
||||
if globalTrace.NumSubscribers(madmin.TraceHealing) > 0 {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
healTrace(healingMetricCheckAbandonedParts, startTime, bucket, object, nil, err, nil)
|
||||
}()
|
||||
}
|
||||
if !opts.NoLock {
|
||||
lk := er.NewNSLock(bucket, object)
|
||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for _, disk := range er.getDisks() {
|
||||
if disk != nil {
|
||||
wg.Add(1)
|
||||
go func(disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
_ = disk.CleanAbandonedData(ctx, bucket, object)
|
||||
}(disk)
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// healObjectDir - heals object directory specifically, this special call
|
||||
// is needed since we do not have a special backend format for directories.
|
||||
func (er erasureObjects) healObjectDir(ctx context.Context, bucket, object string, dryRun bool, remove bool) (hr madmin.HealResultItem, err error) {
|
||||
func (er *erasureObjects) healObjectDir(ctx context.Context, bucket, object string, dryRun bool, remove bool) (hr madmin.HealResultItem, err error) {
|
||||
storageDisks := er.getDisks()
|
||||
storageEndpoints := er.getEndpoints()
|
||||
|
||||
@@ -766,7 +808,7 @@ func (er erasureObjects) healObjectDir(ctx context.Context, bucket, object strin
|
||||
|
||||
// Populates default heal result item entries with possible values when we are returning prematurely.
|
||||
// This is to ensure that in any circumstance we are not returning empty arrays with wrong values.
|
||||
func (er erasureObjects) defaultHealResult(lfi FileInfo, storageDisks []StorageAPI, storageEndpoints []Endpoint, errs []error, bucket, object, versionID string) madmin.HealResultItem {
|
||||
func (er *erasureObjects) defaultHealResult(lfi FileInfo, storageDisks []StorageAPI, storageEndpoints []Endpoint, errs []error, bucket, object, versionID string) madmin.HealResultItem {
|
||||
// Initialize heal result object
|
||||
result := madmin.HealResultItem{
|
||||
Type: madmin.HealItemObject,
|
||||
@@ -895,19 +937,22 @@ func isObjectDangling(metaArr []FileInfo, errs []error, dataErrs []error) (valid
|
||||
// We can consider an object data not reliable
|
||||
// when xl.meta is not found in read quorum disks.
|
||||
// or when xl.meta is not readable in read quorum disks.
|
||||
danglingErrsCount := func(cerrs []error) (int, int) {
|
||||
danglingErrsCount := func(cerrs []error) (int, int, int) {
|
||||
var (
|
||||
notFoundCount int
|
||||
corruptedCount int
|
||||
notFoundCount int
|
||||
corruptedCount int
|
||||
diskNotFoundCount int
|
||||
)
|
||||
for _, readErr := range cerrs {
|
||||
if errors.Is(readErr, errFileNotFound) || errors.Is(readErr, errFileVersionNotFound) {
|
||||
notFoundCount++
|
||||
} else if errors.Is(readErr, errFileCorrupt) {
|
||||
corruptedCount++
|
||||
} else if errors.Is(readErr, errDiskNotFound) {
|
||||
diskNotFoundCount++
|
||||
}
|
||||
}
|
||||
return notFoundCount, corruptedCount
|
||||
return notFoundCount, corruptedCount, diskNotFoundCount
|
||||
}
|
||||
|
||||
ndataErrs := make([]error, len(dataErrs))
|
||||
@@ -921,9 +966,12 @@ func isObjectDangling(metaArr []FileInfo, errs []error, dataErrs []error) (valid
|
||||
}
|
||||
}
|
||||
|
||||
notFoundMetaErrs, corruptedMetaErrs := danglingErrsCount(errs)
|
||||
notFoundPartsErrs, corruptedPartsErrs := danglingErrsCount(ndataErrs)
|
||||
notFoundMetaErrs, corruptedMetaErrs, driveNotFoundMetaErrs := danglingErrsCount(errs)
|
||||
notFoundPartsErrs, corruptedPartsErrs, driveNotFoundPartsErrs := danglingErrsCount(ndataErrs)
|
||||
|
||||
if driveNotFoundMetaErrs > 0 || driveNotFoundPartsErrs > 0 {
|
||||
return validMeta, false
|
||||
}
|
||||
for _, m := range metaArr {
|
||||
if m.IsValid() {
|
||||
validMeta = m
|
||||
@@ -1005,18 +1053,17 @@ func (er erasureObjects) HealObject(ctx context.Context, bucket, object, version
|
||||
}
|
||||
|
||||
// healTrace sends healing results to trace output.
|
||||
func healTrace(funcName healingMetric, startTime time.Time, bucket, object, versionID string, opts madmin.HealOpts, err error, result *madmin.HealResultItem) {
|
||||
func healTrace(funcName healingMetric, startTime time.Time, bucket, object string, opts *madmin.HealOpts, err error, result *madmin.HealResultItem) {
|
||||
tr := madmin.TraceInfo{
|
||||
TraceType: madmin.TraceHealing,
|
||||
Time: startTime,
|
||||
NodeName: globalLocalNodeName,
|
||||
FuncName: "heal." + funcName.String(),
|
||||
Duration: time.Since(startTime),
|
||||
Message: fmt.Sprintf("dry:%v, rm:%v, recreate:%v mode:%v", opts.DryRun, opts.Remove, opts.Recreate, opts.ScanMode),
|
||||
Path: pathJoin(bucket, decodeDirObject(object)),
|
||||
}
|
||||
if versionID != "" && versionID != "null" {
|
||||
tr.Path += " v=" + versionID
|
||||
if opts != nil {
|
||||
tr.Message = fmt.Sprintf("dry:%v, rm:%v, recreate:%v mode:%v", opts.DryRun, opts.Remove, opts.Recreate, opts.ScanMode)
|
||||
}
|
||||
if err != nil {
|
||||
tr.Error = err.Error()
|
||||
|
||||
+225
-10
@@ -31,7 +31,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go"
|
||||
uuid2 "github.com/google/uuid"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
)
|
||||
|
||||
// Tests isObjectDangling function
|
||||
@@ -243,7 +244,7 @@ func TestHealing(t *testing.T) {
|
||||
er := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -276,6 +277,12 @@ func TestHealing(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Checking abandoned parts should do nothing
|
||||
err = er.checkAbandonedParts(ctx, bucket, object, madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = er.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -320,6 +327,214 @@ func TestHealing(t *testing.T) {
|
||||
t.Fatal("HealObject failed")
|
||||
}
|
||||
|
||||
uuid, _ := uuid2.NewRandom()
|
||||
for _, drive := range fsDirs {
|
||||
dir := path.Join(drive, bucket, object, uuid.String())
|
||||
err = os.MkdirAll(dir, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(pathJoin(dir, "part.1"), []byte("some data"), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// This should remove all the unreferenced parts.
|
||||
err = er.checkAbandonedParts(ctx, bucket, object, madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, drive := range fsDirs {
|
||||
dir := path.Join(drive, bucket, object, uuid.String())
|
||||
_, err := os.ReadFile(pathJoin(dir, "part.1"))
|
||||
if err == nil {
|
||||
t.Fatal("expected data dit to be cleaned up")
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the bucket - to simulate the case where bucket was
|
||||
// created when the disk was down.
|
||||
err = os.RemoveAll(path.Join(fsDirs[0], bucket))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// This would create the bucket.
|
||||
_, err = er.HealBucket(ctx, bucket, madmin.HealOpts{
|
||||
DryRun: false,
|
||||
Remove: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Stat the bucket to make sure that it was created.
|
||||
_, err = er.getDisks()[0].StatVol(context.Background(), bucket)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests both object and bucket healing.
|
||||
func TestHealingVersioned(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
obj, fsDirs, err := prepareErasure16(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer obj.Shutdown(context.Background())
|
||||
|
||||
// initialize the server and obtain the credentials and root.
|
||||
// credentials are necessary to sign the HTTP request.
|
||||
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
||||
t.Fatalf("Unable to initialize server config. %s", err)
|
||||
}
|
||||
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
z := obj.(*erasureServerPools)
|
||||
er := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{VersioningEnabled: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bucket := "bucket"
|
||||
object := "object"
|
||||
|
||||
data := make([]byte, 1*humanize.MiByte)
|
||||
length := int64(len(data))
|
||||
_, err = rand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oi1, err := obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), length, "", ""), ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 2nd version.
|
||||
_, _ = rand.Read(data)
|
||||
oi2, err := obj.PutObject(ctx, bucket, object, mustGetPutObjReader(t, bytes.NewReader(data), length, "", ""), ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
disk := er.getDisks()[0]
|
||||
fileInfoPreHeal1, err := disk.ReadVersion(context.Background(), bucket, object, oi1.VersionID, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fileInfoPreHeal2, err := disk.ReadVersion(context.Background(), bucket, object, oi2.VersionID, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Remove the object - to simulate the case where the disk was down when the object
|
||||
// was created.
|
||||
err = removeAll(pathJoin(disk.String(), bucket, object))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Checking abandoned parts should do nothing
|
||||
err = er.checkAbandonedParts(ctx, bucket, object, madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = er.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fileInfoPostHeal1, err := disk.ReadVersion(context.Background(), bucket, object, oi1.VersionID, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fileInfoPostHeal2, err := disk.ReadVersion(context.Background(), bucket, object, oi2.VersionID, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// After heal the meta file should be as expected.
|
||||
if !fileInfoPreHeal1.Equals(fileInfoPostHeal1) {
|
||||
t.Fatal("HealObject failed")
|
||||
}
|
||||
if !fileInfoPreHeal1.Equals(fileInfoPostHeal2) {
|
||||
t.Fatal("HealObject failed")
|
||||
}
|
||||
|
||||
err = os.RemoveAll(path.Join(fsDirs[0], bucket, object, "xl.meta"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write xl.meta with different modtime to simulate the case where a disk had
|
||||
// gone down when an object was replaced by a new object.
|
||||
fileInfoOutDated := fileInfoPreHeal1
|
||||
fileInfoOutDated.ModTime = time.Now()
|
||||
err = disk.WriteMetadata(context.Background(), bucket, object, fileInfoOutDated)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = er.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealDeepScan})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fileInfoPostHeal1, err = disk.ReadVersion(context.Background(), bucket, object, "", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// After heal the meta file should be as expected.
|
||||
if !fileInfoPreHeal1.Equals(fileInfoPostHeal1) {
|
||||
t.Fatal("HealObject failed")
|
||||
}
|
||||
|
||||
fileInfoPostHeal2, err = disk.ReadVersion(context.Background(), bucket, object, "", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// After heal the meta file should be as expected.
|
||||
if !fileInfoPreHeal2.Equals(fileInfoPostHeal2) {
|
||||
t.Fatal("HealObject failed")
|
||||
}
|
||||
|
||||
uuid, _ := uuid2.NewRandom()
|
||||
for _, drive := range fsDirs {
|
||||
dir := path.Join(drive, bucket, object, uuid.String())
|
||||
err = os.MkdirAll(dir, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(pathJoin(dir, "part.1"), []byte("some data"), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// This should remove all the unreferenced parts.
|
||||
err = er.checkAbandonedParts(ctx, bucket, object, madmin.HealOpts{ScanMode: madmin.HealNormalScan, Remove: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, drive := range fsDirs {
|
||||
dir := path.Join(drive, bucket, object, uuid.String())
|
||||
_, err := os.ReadFile(pathJoin(dir, "part.1"))
|
||||
if err == nil {
|
||||
t.Fatal("expected data dit to be cleaned up")
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the bucket - to simulate the case where bucket was
|
||||
// created when the disk was down.
|
||||
err = os.RemoveAll(path.Join(fsDirs[0], bucket))
|
||||
@@ -366,7 +581,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
||||
object := getRandomObjectName()
|
||||
data := bytes.Repeat([]byte("a"), 128*1024)
|
||||
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -561,7 +776,7 @@ func TestHealCorrectQuorum(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("a"), 5*1024*1024)
|
||||
var opts ObjectOptions
|
||||
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -692,7 +907,7 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("a"), 5*1024*1024)
|
||||
var opts ObjectOptions
|
||||
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -869,7 +1084,7 @@ func TestHealObjectCorruptedXLMeta(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("a"), 5*1024*1024)
|
||||
var opts ObjectOptions
|
||||
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -1012,7 +1227,7 @@ func TestHealObjectCorruptedParts(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("a"), 5*1024*1024)
|
||||
var opts ObjectOptions
|
||||
|
||||
err = objLayer.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
err = objLayer.MakeBucket(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -1169,7 +1384,7 @@ func TestHealObjectErasure(t *testing.T) {
|
||||
data := bytes.Repeat([]byte("a"), 5*1024*1024)
|
||||
var opts ObjectOptions
|
||||
|
||||
err = obj.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -1266,7 +1481,7 @@ func TestHealEmptyDirectoryErasure(t *testing.T) {
|
||||
object := "empty-dir/"
|
||||
var opts ObjectOptions
|
||||
|
||||
err = obj.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
@@ -1367,7 +1582,7 @@ func TestHealLastDataShard(t *testing.T) {
|
||||
}
|
||||
var opts ObjectOptions
|
||||
|
||||
err = obj.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket - %v", err)
|
||||
}
|
||||
|
||||
+17
-5
@@ -147,9 +147,9 @@ func (fi FileInfo) ToObjectInfo(bucket, object string, versioned bool) ObjectInf
|
||||
// Add replication status to the object info
|
||||
objInfo.ReplicationStatusInternal = fi.ReplicationState.ReplicationStatusInternal
|
||||
objInfo.VersionPurgeStatusInternal = fi.ReplicationState.VersionPurgeStatusInternal
|
||||
objInfo.ReplicationStatus = fi.ReplicationState.CompositeReplicationStatus()
|
||||
objInfo.ReplicationStatus = fi.ReplicationStatus()
|
||||
objInfo.VersionPurgeStatus = fi.VersionPurgeStatus()
|
||||
|
||||
objInfo.VersionPurgeStatus = fi.ReplicationState.CompositeVersionPurgeStatus()
|
||||
objInfo.TransitionedObject = TransitionedObject{
|
||||
Name: fi.TransitionedObjName,
|
||||
VersionID: fi.TransitionVersionID,
|
||||
@@ -168,13 +168,14 @@ func (fi FileInfo) ToObjectInfo(bucket, object string, versioned bool) ObjectInf
|
||||
objInfo.Parts = fi.Parts
|
||||
|
||||
// Update storage class
|
||||
if sc, ok := fi.Metadata[xhttp.AmzStorageClass]; ok {
|
||||
if fi.TransitionTier != "" {
|
||||
objInfo.StorageClass = fi.TransitionTier
|
||||
} else if sc, ok := fi.Metadata[xhttp.AmzStorageClass]; ok {
|
||||
objInfo.StorageClass = sc
|
||||
} else {
|
||||
objInfo.StorageClass = globalMinioDefaultStorageClass
|
||||
}
|
||||
|
||||
objInfo.VersionPurgeStatus = fi.VersionPurgeStatus()
|
||||
// set restore status for transitioned object
|
||||
restoreHdr, ok := fi.Metadata[xhttp.AmzRestore]
|
||||
if ok {
|
||||
@@ -427,7 +428,13 @@ func listObjectParities(partsMetadata []FileInfo, errs []error) (parities []int)
|
||||
parities[index] = -1
|
||||
continue
|
||||
}
|
||||
parities[index] = metadata.Erasure.ParityBlocks
|
||||
if !metadata.IsValid() {
|
||||
parities[index] = -1
|
||||
continue
|
||||
}
|
||||
if !metadata.Deleted {
|
||||
parities[index] = metadata.Erasure.ParityBlocks
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -532,6 +539,11 @@ func (fi *FileInfo) VersionPurgeStatus() VersionPurgeStatusType {
|
||||
return fi.ReplicationState.CompositeVersionPurgeStatus()
|
||||
}
|
||||
|
||||
// ReplicationStatus returns overall version replication status for this object version across targets
|
||||
func (fi *FileInfo) ReplicationStatus() replication.StatusType {
|
||||
return fi.ReplicationState.CompositeReplicationStatus()
|
||||
}
|
||||
|
||||
// DeleteMarkerReplicationStatus returns overall replication status for this delete marker version across targets
|
||||
func (fi *FileInfo) DeleteMarkerReplicationStatus() replication.StatusType {
|
||||
if fi.Deleted {
|
||||
|
||||
+75
-78
@@ -43,7 +43,7 @@ import (
|
||||
|
||||
func (er erasureObjects) getUploadIDDir(bucket, object, uploadID string) string {
|
||||
uploadUUID := uploadID
|
||||
uploadBytes, err := base64.StdEncoding.DecodeString(uploadID)
|
||||
uploadBytes, err := base64.RawURLEncoding.DecodeString(uploadID)
|
||||
if err == nil {
|
||||
slc := strings.SplitN(string(uploadBytes), ".", 2)
|
||||
if len(slc) == 2 {
|
||||
@@ -175,21 +175,6 @@ func (er erasureObjects) cleanupStaleUploads(ctx context.Context, expiry time.Du
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (er erasureObjects) renameAll(ctx context.Context, bucket, prefix string) {
|
||||
var wg sync.WaitGroup
|
||||
for _, disk := range er.getDisks() {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
disk.RenameFile(ctx, bucket, prefix, minioMetaTmpDeletedBucket, mustGetUUID())
|
||||
}(disk)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (er erasureObjects) deleteAll(ctx context.Context, bucket, prefix string) {
|
||||
var wg sync.WaitGroup
|
||||
for _, disk := range er.getDisks() {
|
||||
@@ -218,11 +203,12 @@ func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk Sto
|
||||
uploadIDPath := pathJoin(shaDir, uploadIDDir)
|
||||
fi, err := disk.ReadVersion(ctx, minioMetaMultipartBucket, uploadIDPath, "", false)
|
||||
if err != nil {
|
||||
er.deleteAll(ctx, minioMetaMultipartBucket, uploadIDPath)
|
||||
return nil
|
||||
}
|
||||
wait := er.deletedCleanupSleeper.Timer(ctx)
|
||||
wait := deletedCleanupSleeper.Timer(ctx)
|
||||
if now.Sub(fi.ModTime) > expiry {
|
||||
er.renameAll(ctx, minioMetaMultipartBucket, uploadIDPath)
|
||||
er.deleteAll(ctx, minioMetaMultipartBucket, uploadIDPath)
|
||||
}
|
||||
wait()
|
||||
return nil
|
||||
@@ -237,7 +223,7 @@ func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk Sto
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
wait := er.deletedCleanupSleeper.Timer(ctx)
|
||||
wait := deletedCleanupSleeper.Timer(ctx)
|
||||
if now.Sub(vi.Created) > expiry {
|
||||
er.deleteAll(ctx, minioMetaTmpBucket, tmpDir)
|
||||
}
|
||||
@@ -363,8 +349,8 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
||||
}
|
||||
rctx := lkctx.Context()
|
||||
obj, err := er.getObjectInfo(rctx, bucket, object, opts)
|
||||
lk.RUnlock(lkctx.Cancel)
|
||||
if err != nil {
|
||||
lk.RUnlock(lkctx)
|
||||
if err != nil && !isErrVersionNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
if opts.CheckPrecondFn(obj) {
|
||||
@@ -373,7 +359,9 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
||||
}
|
||||
|
||||
userDefined := cloneMSS(opts.UserDefined)
|
||||
|
||||
if opts.PreserveETag != "" {
|
||||
userDefined["etag"] = opts.PreserveETag
|
||||
}
|
||||
onlineDisks := er.getDisks()
|
||||
parityDrives := globalStorageClass.GetParityForSC(userDefined[xhttp.AmzStorageClass])
|
||||
if parityDrives < 0 {
|
||||
@@ -446,7 +434,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
||||
partsMetadata[index].Metadata = userDefined
|
||||
}
|
||||
uploadUUID := mustGetUUID()
|
||||
uploadID := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s.%s", globalDeploymentID, uploadUUID)))
|
||||
uploadID := base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%s.%s", globalDeploymentID, uploadUUID)))
|
||||
uploadIDPath := er.getUploadIDDir(bucket, object, uploadUUID)
|
||||
|
||||
// Write updated `xl.meta` to all disks.
|
||||
@@ -485,24 +473,6 @@ func (er erasureObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObjec
|
||||
return partInfo, nil
|
||||
}
|
||||
|
||||
func undoRenamePart(disks []StorageAPI, srcBucket, srcEntry, dstBucket, dstEntry string, errs []error) {
|
||||
// Undo rename object on disks where RenameFile succeeded.
|
||||
g := errgroup.WithNErrs(len(disks))
|
||||
for index, disk := range disks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if errs[index] == nil {
|
||||
_ = disks[index].RenameFile(context.TODO(), dstBucket, dstEntry, srcBucket, srcEntry)
|
||||
}
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
g.Wait()
|
||||
}
|
||||
|
||||
// renamePart - renames multipart part to its relevant location under uploadID.
|
||||
func renamePart(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, dstBucket, dstEntry string, writeQuorum int) ([]StorageAPI, error) {
|
||||
g := errgroup.WithNErrs(len(disks))
|
||||
@@ -521,15 +491,13 @@ func renamePart(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, ds
|
||||
// Wait for all renames to finish.
|
||||
errs := g.Wait()
|
||||
|
||||
// Do not need to undo partial successful operation since those will be cleaned up
|
||||
// in 24hrs via multipart cleaner, never rename() back to `.minio.sys/tmp` as there
|
||||
// is no way to clean them.
|
||||
|
||||
// We can safely allow RenameFile errors up to len(er.getDisks()) - writeQuorum
|
||||
// otherwise return failure. Cleanup successful renames.
|
||||
err := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||
if err == errErasureWriteQuorum {
|
||||
// Undo all the partial rename operations.
|
||||
undoRenamePart(disks, srcBucket, srcEntry, dstBucket, dstEntry, errs)
|
||||
}
|
||||
|
||||
return evalDisks(disks, errs), err
|
||||
return evalDisks(disks, errs), reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||
}
|
||||
|
||||
// writeAllDisks - writes 'b' to all provided disks.
|
||||
@@ -581,16 +549,6 @@ func writeAllDisks(ctx context.Context, disks []StorageAPI, dstBucket, dstEntry
|
||||
func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, r *PutObjReader, opts ObjectOptions) (pi PartInfo, err error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
// Write lock for this part ID.
|
||||
// Held throughout the operation.
|
||||
partIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID, strconv.Itoa(partID)))
|
||||
plkctx, err := partIDLock.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return PartInfo{}, err
|
||||
}
|
||||
pctx := plkctx.Context()
|
||||
defer partIDLock.Unlock(plkctx.Cancel)
|
||||
|
||||
// Read lock for upload id.
|
||||
// Only held while reading the upload metadata.
|
||||
uploadIDRLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
@@ -599,7 +557,14 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
return PartInfo{}, err
|
||||
}
|
||||
rctx := rlkctx.Context()
|
||||
defer uploadIDRLock.RUnlock(rlkctx.Cancel)
|
||||
defer uploadIDRLock.RUnlock(rlkctx)
|
||||
|
||||
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
|
||||
// Validates if upload ID exists.
|
||||
fi, _, err := er.checkUploadIDExists(rctx, bucket, object, uploadID, true)
|
||||
if err != nil {
|
||||
return pi, toObjectErr(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
data := r.Reader
|
||||
// Validate input data size and it can never be less than zero.
|
||||
@@ -608,13 +573,17 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
return pi, toObjectErr(errInvalidArgument)
|
||||
}
|
||||
|
||||
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
|
||||
|
||||
// Validates if upload ID exists.
|
||||
fi, _, err := er.checkUploadIDExists(rctx, bucket, object, uploadID, true)
|
||||
// Write lock for this part ID, only hold it if we are planning to read from the
|
||||
// streamto avoid any concurrent updates.
|
||||
//
|
||||
// Must be held throughout this call.
|
||||
partIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID, strconv.Itoa(partID)))
|
||||
plkctx, err := partIDLock.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return pi, toObjectErr(err, bucket, object, uploadID)
|
||||
return PartInfo{}, err
|
||||
}
|
||||
pctx := plkctx.Context()
|
||||
defer partIDLock.Unlock(plkctx)
|
||||
|
||||
onlineDisks := er.getDisks()
|
||||
writeQuorum := fi.WriteQuorum(er.defaultWQuorum())
|
||||
@@ -641,7 +610,7 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
var online int
|
||||
defer func() {
|
||||
if online != len(onlineDisks) {
|
||||
er.renameAll(context.Background(), minioMetaTmpBucket, tmpPart)
|
||||
er.deleteAll(context.Background(), minioMetaTmpBucket, tmpPart)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -786,7 +755,7 @@ func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, u
|
||||
return MultipartInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer uploadIDLock.RUnlock(lkctx.Cancel)
|
||||
defer uploadIDLock.RUnlock(lkctx)
|
||||
|
||||
fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
|
||||
if err != nil {
|
||||
@@ -813,7 +782,7 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
||||
return ListPartsInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer uploadIDLock.RUnlock(lkctx.Cancel)
|
||||
defer uploadIDLock.RUnlock(lkctx)
|
||||
|
||||
fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
|
||||
if err != nil {
|
||||
@@ -946,7 +915,7 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
return oi, err
|
||||
}
|
||||
wctx := wlkctx.Context()
|
||||
defer uploadIDLock.Unlock(wlkctx.Cancel)
|
||||
defer uploadIDLock.Unlock(wlkctx)
|
||||
|
||||
fi, partsMetadata, err := er.checkUploadIDExists(wctx, bucket, object, uploadID, true)
|
||||
if err != nil {
|
||||
@@ -1004,15 +973,35 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
//
|
||||
// Therefore, we adjust all ETags sent by the client to match what is stored
|
||||
// on the backend.
|
||||
kind, isEncrypted := crypto.IsEncrypted(fi.Metadata)
|
||||
kind, _ := crypto.IsEncrypted(fi.Metadata)
|
||||
|
||||
var objectEncryptionKey []byte
|
||||
if isEncrypted && kind == crypto.S3 {
|
||||
switch kind {
|
||||
case crypto.SSEC:
|
||||
if checksumType.IsSet() {
|
||||
if opts.EncryptFn == nil {
|
||||
return oi, crypto.ErrMissingCustomerKey
|
||||
}
|
||||
baseKey := opts.EncryptFn("", nil)
|
||||
if len(baseKey) != 32 {
|
||||
return oi, crypto.ErrInvalidCustomerKey
|
||||
}
|
||||
objectEncryptionKey, err = decryptObjectMeta(baseKey, bucket, object, fi.Metadata)
|
||||
if err != nil {
|
||||
return oi, err
|
||||
}
|
||||
}
|
||||
case crypto.S3, crypto.S3KMS:
|
||||
objectEncryptionKey, err = decryptObjectMeta(nil, bucket, object, fi.Metadata)
|
||||
if err != nil {
|
||||
return oi, err
|
||||
}
|
||||
}
|
||||
if len(objectEncryptionKey) == 32 {
|
||||
var key crypto.ObjectKey
|
||||
copy(key[:], objectEncryptionKey)
|
||||
opts.EncryptFn = metadataEncrypter(key)
|
||||
}
|
||||
|
||||
for i, part := range partInfoFiles {
|
||||
partID := parts[i].PartNumber
|
||||
@@ -1164,9 +1153,13 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
}
|
||||
|
||||
// Save successfully calculated md5sum.
|
||||
fi.Metadata["etag"] = opts.UserDefined["etag"]
|
||||
// for replica, newMultipartUpload would have already sent the replication ETag
|
||||
if fi.Metadata["etag"] == "" {
|
||||
fi.Metadata["etag"] = getCompleteMultipartMD5(parts)
|
||||
if opts.UserDefined["etag"] != "" {
|
||||
fi.Metadata["etag"] = opts.UserDefined["etag"]
|
||||
} else { // fallback if not already calculated in handler.
|
||||
fi.Metadata["etag"] = getCompleteMultipartMD5(parts)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the consolidated actual size.
|
||||
@@ -1191,7 +1184,7 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
return oi, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx.Cancel)
|
||||
defer lk.Unlock(lkctx)
|
||||
|
||||
// Write final `xl.meta` at uploadID location
|
||||
onlineDisks, err = writeUniqueFileInfo(ctx, onlineDisks, minioMetaMultipartBucket, uploadIDPath, partsMetadata, writeQuorum)
|
||||
@@ -1218,13 +1211,17 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
}
|
||||
|
||||
// Rename the multipart object to final location.
|
||||
if onlineDisks, err = renameData(ctx, onlineDisks, minioMetaMultipartBucket, uploadIDPath,
|
||||
partsMetadata, bucket, object, writeQuorum); err != nil {
|
||||
onlineDisks, versionsDisparity, err := renameData(ctx, onlineDisks, minioMetaMultipartBucket, uploadIDPath,
|
||||
partsMetadata, bucket, object, writeQuorum)
|
||||
if err != nil {
|
||||
return oi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
defer NSUpdated(bucket, object)
|
||||
|
||||
if !opts.Speedtest && versionsDisparity {
|
||||
listAndHeal(ctx, bucket, object, &er, healObjectVersionsDisparity)
|
||||
}
|
||||
|
||||
// Check if there is any offline disk and add it to the MRF list
|
||||
for _, disk := range onlineDisks {
|
||||
if disk != nil && disk.IsOnline() {
|
||||
@@ -1266,7 +1263,7 @@ func (er erasureObjects) AbortMultipartUpload(ctx context.Context, bucket, objec
|
||||
return err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx.Cancel)
|
||||
defer lk.Unlock(lkctx)
|
||||
|
||||
// Validates if upload ID exists.
|
||||
if _, _, err = er.checkUploadIDExists(ctx, bucket, object, uploadID, false); err != nil {
|
||||
@@ -1274,7 +1271,7 @@ func (er erasureObjects) AbortMultipartUpload(ctx context.Context, bucket, objec
|
||||
}
|
||||
|
||||
// Cleanup all uploaded parts.
|
||||
er.renameAll(ctx, minioMetaMultipartBucket, er.getUploadIDDir(bucket, object, uploadID))
|
||||
er.deleteAll(ctx, minioMetaMultipartBucket, er.getUploadIDDir(bucket, object, uploadID))
|
||||
|
||||
// Successfully purged.
|
||||
return nil
|
||||
|
||||
+137
-101
@@ -31,7 +31,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/klauspost/readahead"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/bucket/object/lock"
|
||||
@@ -81,7 +81,7 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
||||
return oi, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx.Cancel)
|
||||
defer lk.Unlock(lkctx)
|
||||
}
|
||||
// Read metadata associated with the object from all disks.
|
||||
storageDisks := er.getDisks()
|
||||
@@ -93,7 +93,7 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
||||
if srcOpts.VersionID != "" {
|
||||
metaArr, errs = readAllFileInfo(ctx, storageDisks, srcBucket, srcObject, srcOpts.VersionID, true)
|
||||
} else {
|
||||
metaArr, errs = readAllXL(ctx, storageDisks, srcBucket, srcObject, true)
|
||||
metaArr, errs = readAllXL(ctx, storageDisks, srcBucket, srcObject, true, false)
|
||||
}
|
||||
|
||||
readQuorum, writeQuorum, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||
@@ -206,14 +206,14 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
|
||||
return nil, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
nsUnlocker = func() { lock.Unlock(lkctx.Cancel) }
|
||||
nsUnlocker = func() { lock.Unlock(lkctx) }
|
||||
case readLock:
|
||||
lkctx, err := lock.GetRLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
nsUnlocker = func() { lock.RUnlock(lkctx.Cancel) }
|
||||
nsUnlocker = func() { lock.RUnlock(lkctx) }
|
||||
}
|
||||
unlockOnDefer = true
|
||||
}
|
||||
@@ -434,7 +434,7 @@ func (er erasureObjects) GetObjectInfo(ctx context.Context, bucket, object strin
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.RUnlock(lkctx.Cancel)
|
||||
defer lk.RUnlock(lkctx)
|
||||
}
|
||||
|
||||
return er.getObjectInfo(ctx, bucket, object, opts)
|
||||
@@ -501,7 +501,7 @@ func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object st
|
||||
return m, err
|
||||
}
|
||||
|
||||
func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, readData bool) ([]FileInfo, []error) {
|
||||
func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, readData, inclFreeVers bool) ([]FileInfo, []error) {
|
||||
metadataArray := make([]*xlMetaV2, len(disks))
|
||||
metaFileInfos := make([]FileInfo, len(metadataArray))
|
||||
metadataShallowVersions := make([][]xlMetaV2ShallowVersion, len(disks))
|
||||
@@ -533,6 +533,7 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
|
||||
|
||||
ignoredErrs := []error{
|
||||
errFileNotFound,
|
||||
errFileNameTooLong,
|
||||
errVolumeNotFound,
|
||||
errFileVersionNotFound,
|
||||
errDiskNotFound,
|
||||
@@ -565,7 +566,7 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
|
||||
|
||||
readQuorum := (len(disks) + 1) / 2
|
||||
meta := &xlMetaV2{versions: mergeXLV2Versions(readQuorum, false, 1, metadataShallowVersions...)}
|
||||
lfi, err := meta.ToFileInfo(bucket, object, "")
|
||||
lfi, err := meta.ToFileInfo(bucket, object, "", inclFreeVers)
|
||||
if err != nil {
|
||||
for i := range errs {
|
||||
if errs[i] == nil {
|
||||
@@ -595,7 +596,7 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
|
||||
|
||||
// make sure to preserve this for diskmtime based healing bugfix.
|
||||
diskMTime := metaFileInfos[index].DiskMTime
|
||||
metaFileInfos[index], errs[index] = metadataArray[index].ToFileInfo(bucket, object, versionID)
|
||||
metaFileInfos[index], errs[index] = metadataArray[index].ToFileInfo(bucket, object, versionID, inclFreeVers)
|
||||
if errs[index] != nil {
|
||||
continue
|
||||
}
|
||||
@@ -619,7 +620,7 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
||||
if opts.VersionID != "" {
|
||||
metaArr, errs = readAllFileInfo(ctx, disks, bucket, object, opts.VersionID, readData)
|
||||
} else {
|
||||
metaArr, errs = readAllXL(ctx, disks, bucket, object, readData)
|
||||
metaArr, errs = readAllXL(ctx, disks, bucket, object, readData, opts.InclFreeVersions)
|
||||
}
|
||||
|
||||
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||
@@ -637,7 +638,7 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
||||
if errors.Is(reducedErr, errErasureReadQuorum) && !strings.HasPrefix(bucket, minioMetaBucket) {
|
||||
_, derr := er.deleteIfDangling(ctx, bucket, object, metaArr, errs, nil, opts)
|
||||
if derr != nil {
|
||||
err = derr
|
||||
reducedErr = derr
|
||||
}
|
||||
}
|
||||
return fi, nil, nil, toObjectErr(reducedErr, bucket, object)
|
||||
@@ -731,7 +732,7 @@ func (er erasureObjects) getObjectInfoAndQuorum(ctx context.Context, bucket, obj
|
||||
}
|
||||
|
||||
// Similar to rename but renames data from srcEntry to dstEntry at dataDir
|
||||
func renameData(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry string, metadata []FileInfo, dstBucket, dstEntry string, writeQuorum int) ([]StorageAPI, error) {
|
||||
func renameData(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry string, metadata []FileInfo, dstBucket, dstEntry string, writeQuorum int) ([]StorageAPI, bool, error) {
|
||||
g := errgroup.WithNErrs(len(disks))
|
||||
|
||||
fvID := mustGetUUID()
|
||||
@@ -770,20 +771,25 @@ func renameData(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry str
|
||||
// Wait for all renames to finish.
|
||||
errs := g.Wait()
|
||||
|
||||
var versionsDisparity bool
|
||||
|
||||
err := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||
if err == nil {
|
||||
versions := reduceCommonVersions(diskVersions, writeQuorum)
|
||||
for index, dversions := range diskVersions {
|
||||
if errs[index] != nil {
|
||||
continue
|
||||
}
|
||||
if versions != dversions {
|
||||
errs[index] = errFileNeedsHealing
|
||||
versionsDisparity = true
|
||||
break
|
||||
}
|
||||
}
|
||||
err = reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||
}
|
||||
|
||||
// We can safely allow RenameData errors up to len(er.getDisks()) - writeQuorum
|
||||
// otherwise return failure.
|
||||
return evalDisks(disks, errs), err
|
||||
return evalDisks(disks, errs), versionsDisparity, err
|
||||
}
|
||||
|
||||
func (er erasureObjects) putMetacacheObject(ctx context.Context, key string, r *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
@@ -936,13 +942,54 @@ func (er erasureObjects) PutObject(ctx context.Context, bucket string, object st
|
||||
return er.putObject(ctx, bucket, object, data, opts)
|
||||
}
|
||||
|
||||
// Heal up to two versions of one object when there is disparity between disks
|
||||
func healObjectVersionsDisparity(bucket string, entry metaCacheEntry) error {
|
||||
if entry.isDir() {
|
||||
return nil
|
||||
}
|
||||
// We might land at .metacache, .trash, .multipart
|
||||
// no need to heal them skip, only when bucket
|
||||
// is '.minio.sys'
|
||||
if bucket == minioMetaBucket {
|
||||
if wildcard.Match("buckets/*/.metacache/*", entry.name) {
|
||||
return nil
|
||||
}
|
||||
if wildcard.Match("tmp/*", entry.name) {
|
||||
return nil
|
||||
}
|
||||
if wildcard.Match("multipart/*", entry.name) {
|
||||
return nil
|
||||
}
|
||||
if wildcard.Match("tmp-old/*", entry.name) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fivs, err := entry.fileInfoVersions(bucket)
|
||||
if err != nil {
|
||||
go healObject(bucket, entry.name, "", madmin.HealNormalScan)
|
||||
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)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// putObject wrapper for erasureObjects PutObject
|
||||
func (er erasureObjects) putObject(ctx context.Context, bucket string, object string, r *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
if opts.CheckPrecondFn != nil {
|
||||
obj, err := er.getObjectInfo(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
if err != nil && !isErrVersionNotFound(err) {
|
||||
return objInfo, err
|
||||
}
|
||||
if opts.CheckPrecondFn(obj) {
|
||||
@@ -1072,7 +1119,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
var online int
|
||||
defer func() {
|
||||
if online != len(onlineDisks) {
|
||||
er.renameAll(context.Background(), minioMetaTmpBucket, tempObj)
|
||||
er.deleteAll(context.Background(), minioMetaTmpBucket, tempObj)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -1154,7 +1201,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx.Cancel)
|
||||
defer lk.Unlock(lkctx)
|
||||
}
|
||||
|
||||
modTime := opts.MTime
|
||||
@@ -1207,7 +1254,17 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
}
|
||||
|
||||
// Rename the successfully written temporary object to final location.
|
||||
onlineDisks, err = renameData(ctx, onlineDisks, minioMetaTmpBucket, tempObj, partsMetadata, bucket, object, writeQuorum)
|
||||
onlineDisks, versionsDisparity, err := renameData(ctx, onlineDisks, minioMetaTmpBucket, tempObj, partsMetadata, bucket, object, writeQuorum)
|
||||
if err != nil {
|
||||
if errors.Is(err, errFileNotFound) {
|
||||
return ObjectInfo{}, toObjectErr(errErasureWriteQuorum, bucket, object)
|
||||
}
|
||||
logger.LogIf(ctx, err)
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
defer NSUpdated(bucket, object)
|
||||
|
||||
for i := 0; i < len(onlineDisks); i++ {
|
||||
if onlineDisks[i] != nil && onlineDisks[i].IsOnline() {
|
||||
// Object info is the same in all disks, so we can pick
|
||||
@@ -1229,59 +1286,12 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
er.addPartial(bucket, object, fi.VersionID, fi.Size)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
healEntry := func(entry metaCacheEntry) error {
|
||||
if entry.isDir() {
|
||||
return nil
|
||||
}
|
||||
// We might land at .metacache, .trash, .multipart
|
||||
// no need to heal them skip, only when bucket
|
||||
// is '.minio.sys'
|
||||
if bucket == minioMetaBucket {
|
||||
if wildcard.Match("buckets/*/.metacache/*", entry.name) {
|
||||
return nil
|
||||
}
|
||||
if wildcard.Match("tmp/*", entry.name) {
|
||||
return nil
|
||||
}
|
||||
if wildcard.Match("multipart/*", entry.name) {
|
||||
return nil
|
||||
}
|
||||
if wildcard.Match("tmp-old/*", entry.name) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
fivs, err := entry.fileInfoVersions(bucket)
|
||||
if err != nil {
|
||||
_, err = er.HealObject(ctx, bucket, entry.name, "", madmin.HealOpts{NoLock: true})
|
||||
return err
|
||||
}
|
||||
|
||||
for _, version := range fivs.Versions {
|
||||
_, err = er.HealObject(ctx, bucket, version.Name, version.VersionID, madmin.HealOpts{NoLock: true})
|
||||
if err != nil && !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, errFileNotFound) {
|
||||
return ObjectInfo{}, toObjectErr(errErasureWriteQuorum, bucket, object)
|
||||
}
|
||||
if errors.Is(err, errFileNeedsHealing) && !opts.Speedtest {
|
||||
listAndHeal(ctx, bucket, object, &er, healEntry)
|
||||
} else {
|
||||
logger.LogIf(ctx, err)
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
if versionsDisparity {
|
||||
listAndHeal(ctx, bucket, object, &er, healObjectVersionsDisparity)
|
||||
}
|
||||
}
|
||||
|
||||
defer NSUpdated(bucket, object)
|
||||
|
||||
fi.ReplicationState = opts.PutReplicationState()
|
||||
online = countOnlineDisks(onlineDisks)
|
||||
|
||||
@@ -1554,13 +1564,24 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
||||
defer NSUpdated(bucket, object)
|
||||
|
||||
storageDisks := er.getDisks()
|
||||
|
||||
// Determine whether to mark object deleted for replication
|
||||
var markDelete bool
|
||||
versionFound := true
|
||||
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:
|
||||
return objInfo, InsufficientWriteQuorum{}
|
||||
}
|
||||
// For delete marker replication, versionID being replicated will not exist on disk
|
||||
if opts.DeleteMarker {
|
||||
versionFound = false
|
||||
} else {
|
||||
return objInfo, gerr
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Expiration.Expire {
|
||||
goi, _, err := er.getObjectInfoAndQuorum(ctx, bucket, object, opts)
|
||||
if err == nil {
|
||||
if gerr == nil {
|
||||
evt := evalActionFromLifecycle(ctx, *lc, rcfg, goi)
|
||||
var isErr bool
|
||||
switch evt.Action {
|
||||
@@ -1582,21 +1603,16 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
||||
Object: object,
|
||||
}
|
||||
}
|
||||
markDelete = goi.VersionID != ""
|
||||
}
|
||||
}
|
||||
|
||||
versionFound := !(opts.DeleteMarker && opts.VersionID != "")
|
||||
// Determine whether to mark object deleted for replication
|
||||
markDelete := goi.VersionID != ""
|
||||
|
||||
if !markDelete {
|
||||
markDelete = !opts.DeleteMarker && opts.VersionID != ""
|
||||
}
|
||||
// Default deleteMarker to true if object is under versioning
|
||||
// versioning suspended means we add `null` version as
|
||||
// delete marker, if its not decided already.
|
||||
deleteMarker := (opts.Versioned || opts.VersionSuspended) && opts.VersionID == "" || (opts.DeleteMarker && opts.VersionID != "")
|
||||
deleteMarker := opts.Versioned
|
||||
|
||||
if markDelete && opts.VersionID != "" {
|
||||
if opts.VersionID != "" {
|
||||
// case where replica version needs to be deleted on target cluster
|
||||
if versionFound && opts.DeleteMarkerReplicationStatus() == replication.Replica {
|
||||
markDelete = false
|
||||
@@ -1607,6 +1623,22 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
||||
if opts.VersionPurgeStatus() == Complete {
|
||||
markDelete = false
|
||||
}
|
||||
// now, since VersionPurgeStatus() is already set, we can let the
|
||||
// lower layers decide this. This fixes a regression that was introduced
|
||||
// in PR #14555 where !VersionPurgeStatus.Empty() is automatically
|
||||
// considered as Delete marker true to avoid listing such objects by
|
||||
// regular ListObjects() calls. However for delete replication this
|
||||
// ends up being a problem because "upon" a successful delete this
|
||||
// ends up creating a new delete marker that is spurious and unnecessary.
|
||||
//
|
||||
// Regression introduced by #14555 was reintroduced in #15564
|
||||
if versionFound {
|
||||
if !goi.VersionPurgeStatus.Empty() {
|
||||
deleteMarker = false
|
||||
} else if !goi.DeleteMarker { // implies a versioned delete of object
|
||||
deleteMarker = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modTime := opts.MTime
|
||||
@@ -1616,9 +1648,14 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
||||
fvID := mustGetUUID()
|
||||
|
||||
if markDelete && (opts.Versioned || opts.VersionSuspended) {
|
||||
if !deleteMarker {
|
||||
// versioning suspended means we add `null` version as
|
||||
// delete marker, if its not decided already.
|
||||
deleteMarker = opts.VersionSuspended && opts.VersionID == ""
|
||||
}
|
||||
fi := FileInfo{
|
||||
Name: object,
|
||||
Deleted: true,
|
||||
Deleted: deleteMarker,
|
||||
MarkDeleted: markDelete,
|
||||
ModTime: modTime,
|
||||
ReplicationState: opts.DeleteReplication,
|
||||
@@ -1626,11 +1663,10 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
||||
ExpireRestored: opts.Transition.ExpireRestored,
|
||||
}
|
||||
fi.SetTierFreeVersionID(fvID)
|
||||
if opts.Versioned {
|
||||
if opts.VersionID != "" {
|
||||
fi.VersionID = opts.VersionID
|
||||
} else if opts.Versioned {
|
||||
fi.VersionID = mustGetUUID()
|
||||
if opts.VersionID != "" {
|
||||
fi.VersionID = opts.VersionID
|
||||
}
|
||||
}
|
||||
// versioning suspended means we add `null` version as
|
||||
// delete marker. Add delete marker, since we don't have
|
||||
@@ -1691,7 +1727,7 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx.Cancel)
|
||||
defer lk.Unlock(lkctx)
|
||||
}
|
||||
|
||||
disks := er.getDisks()
|
||||
@@ -1703,7 +1739,7 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
|
||||
if opts.VersionID != "" {
|
||||
metaArr, errs = readAllFileInfo(ctx, disks, bucket, object, opts.VersionID, false)
|
||||
} else {
|
||||
metaArr, errs = readAllXL(ctx, disks, bucket, object, false)
|
||||
metaArr, errs = readAllXL(ctx, disks, bucket, object, false, false)
|
||||
}
|
||||
|
||||
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||
@@ -1739,7 +1775,7 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
|
||||
|
||||
objInfo := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
|
||||
if opts.EvalMetadataFn != nil {
|
||||
if err := opts.EvalMetadataFn(objInfo); err != nil {
|
||||
if err := opts.EvalMetadataFn(&objInfo); err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
}
|
||||
@@ -1765,7 +1801,7 @@ func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object strin
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx.Cancel)
|
||||
defer lk.Unlock(lkctx)
|
||||
|
||||
disks := er.getDisks()
|
||||
|
||||
@@ -1776,7 +1812,7 @@ func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object strin
|
||||
if opts.VersionID != "" {
|
||||
metaArr, errs = readAllFileInfo(ctx, disks, bucket, object, opts.VersionID, false)
|
||||
} else {
|
||||
metaArr, errs = readAllXL(ctx, disks, bucket, object, false)
|
||||
metaArr, errs = readAllXL(ctx, disks, bucket, object, false, false)
|
||||
}
|
||||
|
||||
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||
@@ -1875,7 +1911,7 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
|
||||
return err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx.Cancel)
|
||||
defer lk.Unlock(lkctx)
|
||||
|
||||
fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts, true)
|
||||
if err != nil {
|
||||
@@ -1970,15 +2006,13 @@ func (er erasureObjects) RestoreTransitionedObject(ctx context.Context, bucket,
|
||||
}
|
||||
|
||||
// update restore status header in the metadata
|
||||
func (er erasureObjects) updateRestoreMetadata(ctx context.Context, bucket, object string, objInfo ObjectInfo, opts ObjectOptions, rerr error) error {
|
||||
func (er erasureObjects) updateRestoreMetadata(ctx context.Context, bucket, object string, objInfo ObjectInfo, opts ObjectOptions) error {
|
||||
oi := objInfo.Clone()
|
||||
oi.metadataOnly = true // Perform only metadata updates.
|
||||
|
||||
if rerr == nil {
|
||||
oi.UserDefined[xhttp.AmzRestore] = completedRestoreObj(opts.Transition.RestoreExpiry).String()
|
||||
} else { // allow retry in the case of failure to restore
|
||||
delete(oi.UserDefined, xhttp.AmzRestore)
|
||||
}
|
||||
// allow retry in the case of failure to restore
|
||||
delete(oi.UserDefined, xhttp.AmzRestore)
|
||||
|
||||
if _, err := er.CopyObject(ctx, bucket, object, bucket, object, oi, ObjectOptions{
|
||||
VersionID: oi.VersionID,
|
||||
}, ObjectOptions{
|
||||
@@ -1994,7 +2028,10 @@ func (er erasureObjects) updateRestoreMetadata(ctx context.Context, bucket, obje
|
||||
// as in the xl.meta for this version and rehydrates the part.n into the fi.DataDir for this version as in the xl.meta
|
||||
func (er erasureObjects) restoreTransitionedObject(ctx context.Context, bucket string, object string, opts ObjectOptions) error {
|
||||
setRestoreHeaderFn := func(oi ObjectInfo, rerr error) error {
|
||||
er.updateRestoreMetadata(ctx, bucket, object, oi, opts, rerr)
|
||||
if rerr == nil {
|
||||
return nil // nothing to do; restore object was successful
|
||||
}
|
||||
er.updateRestoreMetadata(ctx, bucket, object, oi, opts)
|
||||
return rerr
|
||||
}
|
||||
var oi ObjectInfo
|
||||
@@ -2018,7 +2055,6 @@ func (er erasureObjects) restoreTransitionedObject(ctx context.Context, bucket s
|
||||
return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object))
|
||||
}
|
||||
pReader := NewPutObjReader(hashReader)
|
||||
ropts.UserDefined[xhttp.AmzRestore] = completedRestoreObj(opts.Transition.RestoreExpiry).String()
|
||||
_, err = er.PutObject(ctx, bucket, object, pReader, ropts)
|
||||
return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object))
|
||||
}
|
||||
|
||||
+34
-33
@@ -27,6 +27,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
@@ -52,7 +53,7 @@ func TestRepeatPutObjectPart(t *testing.T) {
|
||||
defer objLayer.Shutdown(context.Background())
|
||||
defer removeRoots(disks)
|
||||
|
||||
err = objLayer.MakeBucketWithLocation(ctx, "bucket1", MakeBucketOptions{})
|
||||
err = objLayer.MakeBucket(ctx, "bucket1", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -100,7 +101,7 @@ func TestErasureDeleteObjectBasic(t *testing.T) {
|
||||
}
|
||||
defer xl.Shutdown(context.Background())
|
||||
|
||||
err = xl.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
err = xl.MakeBucket(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -154,7 +155,7 @@ func TestDeleteObjectsVersioned(t *testing.T) {
|
||||
{bucketName, "dir/obj1"},
|
||||
}
|
||||
|
||||
err = obj.MakeBucketWithLocation(ctx, bucketName, MakeBucketOptions{
|
||||
err = obj.MakeBucket(ctx, bucketName, MakeBucketOptions{
|
||||
VersioningEnabled: true,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -213,22 +214,19 @@ func TestDeleteObjectsVersioned(t *testing.T) {
|
||||
func TestErasureDeleteObjectsErasureSet(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
var objs []*erasureObjects
|
||||
for i := 0; i < 32; i++ {
|
||||
obj, fsDirs, err := prepareErasure(ctx, 16)
|
||||
if err != nil {
|
||||
t.Fatal("Unable to initialize 'Erasure' object layer.", err)
|
||||
}
|
||||
// Remove all dirs.
|
||||
for _, dir := range fsDirs {
|
||||
defer os.RemoveAll(dir)
|
||||
}
|
||||
z := obj.(*erasureServerPools)
|
||||
xl := z.serverPools[0].sets[0]
|
||||
objs = append(objs, xl)
|
||||
|
||||
obj, fsDirs, err := prepareErasureSets32(ctx)
|
||||
if err != nil {
|
||||
t.Fatal("Unable to initialize 'Erasure' object layer.", err)
|
||||
}
|
||||
|
||||
erasureSets := &erasureSets{sets: objs, distributionAlgo: "CRCMOD"}
|
||||
setObjectLayer(obj)
|
||||
initConfigSubsystem(ctx, obj)
|
||||
|
||||
// Remove all dirs.
|
||||
for _, dir := range fsDirs {
|
||||
defer os.RemoveAll(dir)
|
||||
}
|
||||
|
||||
type testCaseType struct {
|
||||
bucket string
|
||||
@@ -243,13 +241,12 @@ func TestErasureDeleteObjectsErasureSet(t *testing.T) {
|
||||
{bucketName, "obj_4"},
|
||||
}
|
||||
|
||||
err := erasureSets.MakeBucketWithLocation(ctx, bucketName, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
if err = obj.MakeBucket(ctx, bucketName, MakeBucketOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
_, err = erasureSets.PutObject(ctx, testCase.bucket, testCase.object,
|
||||
_, err = obj.PutObject(ctx, testCase.bucket, testCase.object,
|
||||
mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Erasure Object upload failed: <ERROR> %s", err)
|
||||
@@ -269,7 +266,7 @@ func TestErasureDeleteObjectsErasureSet(t *testing.T) {
|
||||
}
|
||||
|
||||
objectNames := toObjectNames(testCases)
|
||||
_, delErrs := erasureSets.DeleteObjects(ctx, bucketName, objectNames, ObjectOptions{})
|
||||
_, delErrs := obj.DeleteObjects(ctx, bucketName, objectNames, ObjectOptions{})
|
||||
|
||||
for i := range delErrs {
|
||||
if delErrs[i] != nil {
|
||||
@@ -278,7 +275,7 @@ func TestErasureDeleteObjectsErasureSet(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
_, statErr := erasureSets.GetObjectInfo(ctx, test.bucket, test.object, ObjectOptions{})
|
||||
_, statErr := obj.GetObjectInfo(ctx, test.bucket, test.object, ObjectOptions{})
|
||||
switch statErr.(type) {
|
||||
case ObjectNotFound:
|
||||
default:
|
||||
@@ -304,7 +301,7 @@ func TestErasureDeleteObjectDiskNotFound(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -373,7 +370,7 @@ func TestErasureDeleteObjectDiskNotFoundErasure4(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -433,7 +430,7 @@ func TestErasureDeleteObjectDiskNotFoundErr(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -504,7 +501,7 @@ func TestGetObjectNoQuorum(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -613,7 +610,7 @@ func TestHeadObjectNoQuorum(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -691,7 +688,7 @@ func TestPutObjectNoQuorum(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -754,7 +751,7 @@ func TestPutObjectNoQuorumSmall(t *testing.T) {
|
||||
xl := z.serverPools[0].sets[0]
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, "bucket", MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, "bucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -823,7 +820,7 @@ func TestPutObjectSmallInlineData(t *testing.T) {
|
||||
object := "object"
|
||||
|
||||
// Create "bucket"
|
||||
err = obj.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
err = obj.MakeBucket(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -891,7 +888,7 @@ func testObjectQuorumFromMeta(obj ObjectLayer, instanceType string, dirs []strin
|
||||
ctx, cancel := context.WithCancel(GlobalContext)
|
||||
defer cancel()
|
||||
|
||||
err := obj.MakeBucketWithLocation(ctx, bucket, MakeBucketOptions{})
|
||||
err := obj.MakeBucket(ctx, bucket, MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make a bucket %v", err)
|
||||
}
|
||||
@@ -1090,7 +1087,7 @@ func TestGetObjectInlineNotInline(t *testing.T) {
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
// Create a testbucket
|
||||
err = objLayer.MakeBucketWithLocation(ctx, "testbucket", MakeBucketOptions{})
|
||||
err = objLayer.MakeBucket(ctx, "testbucket", MakeBucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1123,6 +1120,10 @@ func TestGetObjectInlineNotInline(t *testing.T) {
|
||||
|
||||
// Test reading an object with some outdated data in some disks
|
||||
func TestGetObjectWithOutdatedDisks(t *testing.T) {
|
||||
if runtime.GOOS == globalWindowsOSName {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -1156,7 +1157,7 @@ func TestGetObjectWithOutdatedDisks(t *testing.T) {
|
||||
|
||||
for i, testCase := range testCases {
|
||||
// Step 1: create a bucket
|
||||
err = z.MakeBucketWithLocation(ctx, testCase.bucket, MakeBucketOptions{VersioningEnabled: testCase.versioned})
|
||||
err = z.MakeBucket(ctx, testCase.bucket, MakeBucketOptions{VersioningEnabled: testCase.versioned})
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to create a bucket: %v", i+1, err)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user