mirror of
https://github.com/pgsty/minio.git
synced 2026-11-13 02:55:42 +03:00
Compare commits
351 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d96b18df0 | |||
| ad2ab6eb3e | |||
| f037c9b286 | |||
| 85912985b6 | |||
| 876f51a708 | |||
| f7d29b4a53 | |||
| 06557fe8be | |||
| 2131046427 | |||
| aaf1abc993 | |||
| 413549bcf5 | |||
| 9a799065b3 | |||
| fd2959fa3a | |||
| 07927e032a | |||
| 15bec32bb4 | |||
| e2b7a08c10 | |||
| ef2fc0f99e | |||
| d063596430 | |||
| bd2dc6c670 | |||
| 684399433b | |||
| b62791617c | |||
| e07c2ab868 | |||
| 203755793c | |||
| 883c98e26f | |||
| f5a20a5d06 | |||
| ef7177ebbd | |||
| 3637aad36e | |||
| 77db9686fb | |||
| c326e5a34e | |||
| c23c982593 | |||
| a3d666356c | |||
| 3cdbc2f414 | |||
| b92cdea578 | |||
| 5e629a99af | |||
| 99c4ffa34f | |||
| a7f266c907 | |||
| 57acacd5a7 | |||
| 42fb3cd95e | |||
| 855ed642c3 | |||
| 629503ff73 | |||
| e3a070e3de | |||
| 5b364bca1f | |||
| c5c1426262 | |||
| be18d435a2 | |||
| 7eea6cdb12 | |||
| 824c55b3a4 | |||
| 76913a9fd5 | |||
| 2f44dac14f | |||
| 5569acd95c | |||
| 1d0211d395 | |||
| 06cd0a636e | |||
| 7f7b489a3d | |||
| bb6f4d7633 | |||
| 6e24dff26a | |||
| e372e4e592 | |||
| 9571b0825e | |||
| 90e2cc3d4c | |||
| 0c0820caef | |||
| 9112ca4e29 | |||
| 8203cb9990 | |||
| d5bce978a8 | |||
| ec84bad882 | |||
| b53376a3a4 | |||
| 0ec722bc54 | |||
| 4640b13c66 | |||
| 1704abaf6b | |||
| ab34f0065c | |||
| e8c0a50862 | |||
| b963f69f34 | |||
| 02d8f3cdc8 | |||
| 7ae69accc0 | |||
| 701b89f377 | |||
| 46d45a6923 | |||
| 7fad0c8b41 | |||
| 6e27264c6b | |||
| 5c83c9724f | |||
| d5aff735be | |||
| 98c26df53e | |||
| 2448a9e047 | |||
| b28d391a22 | |||
| c8b92f6067 | |||
| e7cac8acef | |||
| 31b5acc245 | |||
| 6105997299 | |||
| 8c874884fc | |||
| ebfe81e5fd | |||
| 0b7ca094e4 | |||
| 72802a5972 | |||
| b1f3935c5b | |||
| dbd53af369 | |||
| b09fe0e50e | |||
| fae9000304 | |||
| 8fd07bcd51 | |||
| 6addc7a35d | |||
| 477230c82e | |||
| d1737199ed | |||
| 61101d82d9 | |||
| cebb948da2 | |||
| c61c4b71b2 | |||
| 84f31ed45d | |||
| 8a81e317d6 | |||
| 224d9a752f | |||
| 0db34e4b85 | |||
| 8a9b9832fd | |||
| f66625be67 | |||
| 6825bd7e75 | |||
| 18515a4e3b | |||
| 839b9c9271 | |||
| dd9ed85e22 | |||
| e96c88e914 | |||
| 6c1410f7f5 | |||
| f92450d8b3 | |||
| c133979b8e | |||
| a9269cee29 | |||
| cf42ede92c | |||
| 958a480e53 | |||
| 62151a751d | |||
| f1ab9df2ee | |||
| a42650c065 | |||
| bdad3730f7 | |||
| a5835cecbf | |||
| b19620b324 | |||
| cd6dec49c0 | |||
| d350654aee | |||
| 6877578bbc | |||
| 10693fddfa | |||
| f3682b6149 | |||
| 056ca0c68e | |||
| 25f7a8e406 | |||
| 1f1c267b6c | |||
| eab1dc927b | |||
| fc94ea1ced | |||
| 67d2cf8f30 | |||
| 2c85b84cbc | |||
| 09a25ea7b7 | |||
| 260a63ca73 | |||
| d3f70ea340 | |||
| ceebd35ef7 | |||
| 91b6fe1af3 | |||
| 9803f68522 | |||
| 47b7469a60 | |||
| 4c204707fd | |||
| 8fd6be0827 | |||
| c06e0bfef9 | |||
| 8625a9dbb3 | |||
| 2b71b659e0 | |||
| 0320ac43cb | |||
| 111c7d4026 | |||
| 62c3df0ca3 | |||
| ae011663e8 | |||
| 12591cd241 | |||
| 0499e1c4b0 | |||
| 3158f2d12e | |||
| 51f7f9aaa3 | |||
| 6e359c586e | |||
| 699a24f7e5 | |||
| 5fa3665074 | |||
| 3b7781835e | |||
| 5fe1b46bfd | |||
| f65cce4317 | |||
| 27d0d22e5d | |||
| 407c9ddcbf | |||
| d90d0c8931 | |||
| 216a471bbb | |||
| a7b7860e0e | |||
| d703daa480 | |||
| dc8fdcb9c9 | |||
| 7a6c4e438e | |||
| c468b4e2a8 | |||
| b04956a676 | |||
| 518f6e4d39 | |||
| 483b226cc1 | |||
| 13151cbb2b | |||
| 8e02660a0d | |||
| 16feef2a2c | |||
| 66ff17e452 | |||
| 4c5edacae2 | |||
| 8b4d0255b7 | |||
| 58c129f94a | |||
| 74040b457b | |||
| c259a8ea38 | |||
| 2d51e42305 | |||
| 5e3bfd2148 | |||
| 8b0ab6ead6 | |||
| b1b0aadabf | |||
| ac7d9c449a | |||
| 1346561b9d | |||
| 4bc52897b2 | |||
| 035791669e | |||
| 6017b63a06 | |||
| 11d04279c8 | |||
| 0448728228 | |||
| 12047702f5 | |||
| fb1492f531 | |||
| d14ead7bec | |||
| 05444a0f6a | |||
| b3c54ec81e | |||
| 6c11dbffd5 | |||
| 3b5dbf9046 | |||
| 09c733677a | |||
| 8d6558b236 | |||
| 67f4ba154a | |||
| 440ad20c1d | |||
| 31fba6f434 | |||
| 280442e533 | |||
| 850a945a18 | |||
| 46f9049fb4 | |||
| 58266c9e2c | |||
| d1e775313d | |||
| a65df1e67b | |||
| e700be8cd6 | |||
| 3fdd574f54 | |||
| e0f4dd6027 | |||
| de02eca467 | |||
| 50dbd2cacc | |||
| c2f9cc5824 | |||
| c7f7e67a10 | |||
| 628042e65e | |||
| cde7eeb660 | |||
| 6305b206e1 | |||
| d85da9236e | |||
| 9800760cb3 | |||
| 5c087bdcad | |||
| a547bf517d | |||
| b984bf8d1a | |||
| 18f9cccfa7 | |||
| fb6ab1cca2 | |||
| 56c57e2c53 | |||
| a6057c35cc | |||
| 901887e6bf | |||
| ee54643004 | |||
| 0a17acdb34 | |||
| 72e5212842 | |||
| 714283fae2 | |||
| 3423028713 | |||
| 9d062b37d7 | |||
| 4636d3a9c3 | |||
| c95ede35c1 | |||
| 7415e1aa56 | |||
| f350953a19 | |||
| 958bba5b42 | |||
| 0f2b95b497 | |||
| d07089ceac | |||
| 47dfa62384 | |||
| 3a3265cf88 | |||
| 0ff931dc76 | |||
| 4d708cebe9 | |||
| 4d7c8e3bb8 | |||
| 8cde38404d | |||
| fe7bf6cbbc | |||
| 8b4eb2304b | |||
| ae029191a3 | |||
| bfedea9bad | |||
| 7777d3b43a | |||
| 9ed4fc9687 | |||
| b49b39e99d | |||
| cd3a2de5a3 | |||
| 6e8960ccdd | |||
| e05f3d5d84 | |||
| 94c6cb1323 | |||
| 3f81cd1b22 | |||
| 8da0f4c5bb | |||
| 9acf1024e4 | |||
| b21d3f9b82 | |||
| 2bbf380262 | |||
| f678bcf7ba | |||
| a0f06eac2a | |||
| 83fe1a2732 | |||
| 5c98223c89 | |||
| 663a0b7783 | |||
| 6efe4d1df6 | |||
| fb17f97cf3 | |||
| 6b65ba1551 | |||
| 9202c6e26a | |||
| 59a5456091 | |||
| 8bfe972bab | |||
| fd6622458b | |||
| 8a08861dd9 | |||
| 82dcfd4e10 | |||
| b66d7dc708 | |||
| eebdd2b31d | |||
| b94733ab31 | |||
| 7f2c90a0ed | |||
| 84bb7d05a9 | |||
| 98a84d88e2 | |||
| e470268c7c | |||
| 3a6cd4f73d | |||
| 6ea150fd68 | |||
| a7188bc9d0 | |||
| e1e9ddd4a4 | |||
| a1dd08f2e6 | |||
| d136ac0596 | |||
| c33a237067 | |||
| eb7d3da994 | |||
| 37134e42d4 | |||
| 626a4efaad | |||
| 0c1f8b4e0f | |||
| 857674c3a0 | |||
| 15a75bd79b | |||
| 74887c7372 | |||
| 31188e9327 | |||
| ee6d96eb46 | |||
| 11fe2fd79a | |||
| c2863cc6ef | |||
| d6d01067a0 | |||
| 1d3b18c3f4 | |||
| a15b6f21b8 | |||
| 689179bf18 | |||
| bf749eec61 | |||
| d0f4cc89a5 | |||
| d65debb6bc | |||
| 72daccd468 | |||
| b363400587 | |||
| 6b41f941b6 | |||
| b9f5f9ba3f | |||
| c8ffa59d28 | |||
| 1141187bf2 | |||
| 52aeebebea | |||
| e101384aa4 | |||
| 71f02adfca | |||
| 9de26531e4 | |||
| fadc46b906 | |||
| b1d98febfd | |||
| 1828fb212a | |||
| c97f50e274 | |||
| be92046dfd | |||
| 990fc415f7 | |||
| d8daabae9b | |||
| 84fe4fd156 | |||
| 747d475e76 | |||
| 095b518802 | |||
| 0319ae756a | |||
| 11c7ecb5cf | |||
| 422c396d73 | |||
| ffd57fde90 | |||
| a451d1cb8d | |||
| 14cf8f1b22 | |||
| 5996c8c4d5 | |||
| 21885f9457 | |||
| 6ac48aff46 | |||
| 85ff76e7b0 | |||
| e47a31f9fc | |||
| 517fcd423d | |||
| b780359598 | |||
| aa8b9572b9 | |||
| 8ca14e6267 | |||
| 876e1a91b2 | |||
| 0b7989aa4b | |||
| 2278fc8f47 | |||
| a91f353621 | |||
| cdb1b48ad9 | |||
| a24037bfec |
@@ -1,5 +0,0 @@
|
|||||||
# Config file for markdownlint-cli
|
|
||||||
MD033:
|
|
||||||
allowed_elements:
|
|
||||||
- details
|
|
||||||
- summary
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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,11 +20,11 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.19.x]
|
go-version: [1.20.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
|
||||||
- uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 # v2
|
- uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
@@ -20,21 +20,28 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.19.x]
|
go-version: [1.20.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Setup dockerfile for build test
|
- name: Setup dockerfile for build test
|
||||||
run: |
|
run: |
|
||||||
echo "FROM golang:1.19.4" >> Dockerfile.fips.test
|
GO_VERSION=$(go version | cut -d ' ' -f 3 | sed 's/go//')
|
||||||
echo "COPY . /minio" >> Dockerfile.fips.test
|
echo Detected go version $GO_VERSION
|
||||||
echo "WORKDIR /minio" >> Dockerfile.fips.test
|
cat > Dockerfile.fips.test <<EOF
|
||||||
echo "ENV GOEXPERIMENT=boringcrypto" >> Dockerfile.fips.test
|
FROM golang:${GO_VERSION}
|
||||||
echo "RUN make" >> Dockerfile.fips.test
|
COPY . /minio
|
||||||
|
WORKDIR /minio
|
||||||
|
ENV GOEXPERIMENT=boringcrypto
|
||||||
|
RUN make
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
@@ -49,4 +56,4 @@ jobs:
|
|||||||
- name: Test binary
|
- name: Test binary
|
||||||
run: |
|
run: |
|
||||||
docker run --rm minio/fips-test:latest ./minio --version
|
docker run --rm minio/fips-test:latest ./minio --version
|
||||||
docker run --rm -i minio/fips-test:latest /bin/bash -c 'go tool nm ./minio' | grep -q FIPS
|
docker run --rm -i minio/fips-test:latest /bin/bash -c 'go tool nm ./minio | grep FIPS | grep -q FIPS'
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.19.x]
|
go-version: [1.20.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.19.x]
|
go-version: [1.20.x]
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.19.x]
|
go-version: [1.20.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
# are turned off - i.e. if ldap="", then ldap server is not enabled for
|
# are turned off - i.e. if ldap="", then ldap server is not enabled for
|
||||||
# the tests.
|
# the tests.
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.19.x]
|
go-version: [1.20.x]
|
||||||
ldap: ["", "localhost:389"]
|
ldap: ["", "localhost:389"]
|
||||||
etcd: ["", "http://localhost:2379"]
|
etcd: ["", "http://localhost:2379"]
|
||||||
openid: ["", "http://127.0.0.1:5556/dex"]
|
openid: ["", "http://127.0.0.1:5556/dex"]
|
||||||
@@ -75,7 +75,7 @@ jobs:
|
|||||||
openid: "http://127.0.0.1:5556/dex"
|
openid: "http://127.0.0.1:5556/dex"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
name: Markdown Linter
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
# This ensures that previous jobs for the PR are canceled when the PR is
|
|
||||||
# updated.
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: Lint all docs
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Lint all docs
|
|
||||||
run: |
|
|
||||||
npm install -g markdownlint-cli
|
|
||||||
markdownlint --fix '**/*.md' \
|
|
||||||
--config /home/runner/work/minio/minio/.github/markdown-lint-cfg.yaml \
|
|
||||||
--disable MD013 MD040 MD051
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
name: Mint Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
mint-test:
|
||||||
|
runs-on: mint
|
||||||
|
timeout-minutes: 120
|
||||||
|
steps:
|
||||||
|
- name: cleanup #https://github.com/actions/checkout/issues/273
|
||||||
|
run: |
|
||||||
|
sudo -S rm -rf ${GITHUB_WORKSPACE}
|
||||||
|
mkdir ${GITHUB_WORKSPACE}
|
||||||
|
- name: checkout-step
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: setup-go-step
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.20.x
|
||||||
|
|
||||||
|
- name: github sha short
|
||||||
|
id: vars
|
||||||
|
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: build-minio
|
||||||
|
run: |
|
||||||
|
make install
|
||||||
|
docker build . -t "minio/minio:${{ steps.vars.outputs.sha_short }}"
|
||||||
|
|
||||||
|
- name: compress and encrypt
|
||||||
|
run: |
|
||||||
|
${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "compress-encrypt" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
|
||||||
|
|
||||||
|
- name: multiple pools
|
||||||
|
run: |
|
||||||
|
${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "pools" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
|
||||||
|
|
||||||
|
- name: standalone erasure
|
||||||
|
run: |
|
||||||
|
${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "erasure" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
|
||||||
|
docker rmi -f minio/minio:${{ steps.vars.outputs.sha_short }}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
# Settings and configurations that are common for all containers
|
||||||
|
x-minio-common: &minio-common
|
||||||
|
image: minio/minio:${JOB_NAME}
|
||||||
|
command: server --console-address ":9001" http://minio{1...4}/cdata{1...2}
|
||||||
|
expose:
|
||||||
|
- "9000"
|
||||||
|
- "9001"
|
||||||
|
environment:
|
||||||
|
MINIO_CI_CD: "on"
|
||||||
|
MINIO_ROOT_USER: "minio"
|
||||||
|
MINIO_ROOT_PASSWORD: "minio123"
|
||||||
|
MINIO_COMPRESS: "true"
|
||||||
|
MINIO_COMPRESS_MIMETYPES: "*"
|
||||||
|
MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
# starts 4 docker containers running minio server instances.
|
||||||
|
# using nginx reverse proxy, load balancing, you can access
|
||||||
|
# it through port 9000.
|
||||||
|
services:
|
||||||
|
minio1:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio1
|
||||||
|
volumes:
|
||||||
|
- cdata1-1:/cdata1
|
||||||
|
- cdata1-2:/cdata2
|
||||||
|
|
||||||
|
minio2:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio2
|
||||||
|
volumes:
|
||||||
|
- cdata2-1:/cdata1
|
||||||
|
- cdata2-2:/cdata2
|
||||||
|
|
||||||
|
minio3:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio3
|
||||||
|
volumes:
|
||||||
|
- cdata3-1:/cdata1
|
||||||
|
- cdata3-2:/cdata2
|
||||||
|
|
||||||
|
minio4:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio4
|
||||||
|
volumes:
|
||||||
|
- cdata4-1:/cdata1
|
||||||
|
- cdata4-2:/cdata2
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.19.2-alpine
|
||||||
|
hostname: nginx
|
||||||
|
volumes:
|
||||||
|
- ./nginx-4-node.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
depends_on:
|
||||||
|
- minio1
|
||||||
|
- minio2
|
||||||
|
- minio3
|
||||||
|
- minio4
|
||||||
|
|
||||||
|
## By default this config uses default local driver,
|
||||||
|
## For custom volumes replace with volume driver configuration.
|
||||||
|
volumes:
|
||||||
|
cdata1-1:
|
||||||
|
cdata1-2:
|
||||||
|
cdata2-1:
|
||||||
|
cdata2-2:
|
||||||
|
cdata3-1:
|
||||||
|
cdata3-2:
|
||||||
|
cdata4-1:
|
||||||
|
cdata4-2:
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
# Settings and configurations that are common for all containers
|
||||||
|
x-minio-common: &minio-common
|
||||||
|
image: minio/minio:${JOB_NAME}
|
||||||
|
command: server --console-address ":9001" edata{1...4}
|
||||||
|
expose:
|
||||||
|
- "9000"
|
||||||
|
- "9001"
|
||||||
|
environment:
|
||||||
|
MINIO_CI_CD: "on"
|
||||||
|
MINIO_ROOT_USER: "minio"
|
||||||
|
MINIO_ROOT_PASSWORD: "minio123"
|
||||||
|
MINIO_COMPRESS: "true"
|
||||||
|
MINIO_COMPRESS_MIMETYPES: "*"
|
||||||
|
MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
# starts 4 docker containers running minio server instances.
|
||||||
|
# using nginx reverse proxy, load balancing, you can access
|
||||||
|
# it through port 9000.
|
||||||
|
services:
|
||||||
|
minio1:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio1
|
||||||
|
volumes:
|
||||||
|
- edata1-1:/edata1
|
||||||
|
- edata1-2:/edata2
|
||||||
|
- edata1-3:/edata3
|
||||||
|
- edata1-4:/edata4
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.19.2-alpine
|
||||||
|
hostname: nginx
|
||||||
|
volumes:
|
||||||
|
- ./nginx-1-node.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
depends_on:
|
||||||
|
- minio1
|
||||||
|
|
||||||
|
## By default this config uses default local driver,
|
||||||
|
## For custom volumes replace with volume driver configuration.
|
||||||
|
volumes:
|
||||||
|
edata1-1:
|
||||||
|
edata1-2:
|
||||||
|
edata1-3:
|
||||||
|
edata1-4:
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
# Settings and configurations that are common for all containers
|
||||||
|
x-minio-common: &minio-common
|
||||||
|
image: minio/minio:${JOB_NAME}
|
||||||
|
command: server --console-address ":9001" http://minio{1...4}/pdata{1...2} http://minio{5...8}/pdata{1...2}
|
||||||
|
expose:
|
||||||
|
- "9000"
|
||||||
|
- "9001"
|
||||||
|
environment:
|
||||||
|
MINIO_CI_CD: "on"
|
||||||
|
MINIO_ROOT_USER: "minio"
|
||||||
|
MINIO_ROOT_PASSWORD: "minio123"
|
||||||
|
MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
# starts 4 docker containers running minio server instances.
|
||||||
|
# using nginx reverse proxy, load balancing, you can access
|
||||||
|
# it through port 9000.
|
||||||
|
services:
|
||||||
|
minio1:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio1
|
||||||
|
volumes:
|
||||||
|
- pdata1-1:/pdata1
|
||||||
|
- pdata1-2:/pdata2
|
||||||
|
|
||||||
|
minio2:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio2
|
||||||
|
volumes:
|
||||||
|
- pdata2-1:/pdata1
|
||||||
|
- pdata2-2:/pdata2
|
||||||
|
|
||||||
|
minio3:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio3
|
||||||
|
volumes:
|
||||||
|
- pdata3-1:/pdata1
|
||||||
|
- pdata3-2:/pdata2
|
||||||
|
|
||||||
|
minio4:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio4
|
||||||
|
volumes:
|
||||||
|
- pdata4-1:/pdata1
|
||||||
|
- pdata4-2:/pdata2
|
||||||
|
|
||||||
|
minio5:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio5
|
||||||
|
volumes:
|
||||||
|
- pdata5-1:/pdata1
|
||||||
|
- pdata5-2:/pdata2
|
||||||
|
|
||||||
|
minio6:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio6
|
||||||
|
volumes:
|
||||||
|
- pdata6-1:/pdata1
|
||||||
|
- pdata6-2:/pdata2
|
||||||
|
|
||||||
|
minio7:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio7
|
||||||
|
volumes:
|
||||||
|
- pdata7-1:/pdata1
|
||||||
|
- pdata7-2:/pdata2
|
||||||
|
|
||||||
|
minio8:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio8
|
||||||
|
volumes:
|
||||||
|
- pdata8-1:/pdata1
|
||||||
|
- pdata8-2:/pdata2
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.19.2-alpine
|
||||||
|
hostname: nginx
|
||||||
|
volumes:
|
||||||
|
- ./nginx-8-node.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
depends_on:
|
||||||
|
- minio1
|
||||||
|
- minio2
|
||||||
|
- minio3
|
||||||
|
- minio4
|
||||||
|
- minio5
|
||||||
|
- minio6
|
||||||
|
- minio7
|
||||||
|
- minio8
|
||||||
|
|
||||||
|
## By default this config uses default local driver,
|
||||||
|
## For custom volumes replace with volume driver configuration.
|
||||||
|
volumes:
|
||||||
|
pdata1-1:
|
||||||
|
pdata1-2:
|
||||||
|
pdata2-1:
|
||||||
|
pdata2-2:
|
||||||
|
pdata3-1:
|
||||||
|
pdata3-2:
|
||||||
|
pdata4-1:
|
||||||
|
pdata4-2:
|
||||||
|
pdata5-1:
|
||||||
|
pdata5-2:
|
||||||
|
pdata6-1:
|
||||||
|
pdata6-2:
|
||||||
|
pdata7-1:
|
||||||
|
pdata7-2:
|
||||||
|
pdata8-1:
|
||||||
|
pdata8-2:
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
# include /etc/nginx/conf.d/*.conf;
|
||||||
|
|
||||||
|
upstream minio {
|
||||||
|
server minio1:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream console {
|
||||||
|
ip_hash;
|
||||||
|
server minio1:9001;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 9000;
|
||||||
|
listen [::]:9000;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# To allow special characters in headers
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
# Allow any size file to be uploaded.
|
||||||
|
# Set to a value such as 1000m; to restrict file size to a specific value
|
||||||
|
client_max_body_size 0;
|
||||||
|
# To disable buffering
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
|
||||||
|
proxy_pass http://minio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 9001;
|
||||||
|
listen [::]:9001;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# To allow special characters in headers
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
# Allow any size file to be uploaded.
|
||||||
|
# Set to a value such as 1000m; to restrict file size to a specific value
|
||||||
|
client_max_body_size 0;
|
||||||
|
# To disable buffering
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-NginX-Proxy true;
|
||||||
|
|
||||||
|
# This is necessary to pass the correct IP to be hashed
|
||||||
|
real_ip_header X-Real-IP;
|
||||||
|
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
|
||||||
|
# To support websocket
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
|
||||||
|
proxy_pass http://console;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
# include /etc/nginx/conf.d/*.conf;
|
||||||
|
|
||||||
|
upstream minio {
|
||||||
|
server minio1:9000;
|
||||||
|
server minio2:9000;
|
||||||
|
server minio3:9000;
|
||||||
|
server minio4:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream console {
|
||||||
|
ip_hash;
|
||||||
|
server minio1:9001;
|
||||||
|
server minio2:9001;
|
||||||
|
server minio3:9001;
|
||||||
|
server minio4:9001;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 9000;
|
||||||
|
listen [::]:9000;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# To allow special characters in headers
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
# Allow any size file to be uploaded.
|
||||||
|
# Set to a value such as 1000m; to restrict file size to a specific value
|
||||||
|
client_max_body_size 0;
|
||||||
|
# To disable buffering
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
|
||||||
|
proxy_pass http://minio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 9001;
|
||||||
|
listen [::]:9001;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# To allow special characters in headers
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
# Allow any size file to be uploaded.
|
||||||
|
# Set to a value such as 1000m; to restrict file size to a specific value
|
||||||
|
client_max_body_size 0;
|
||||||
|
# To disable buffering
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-NginX-Proxy true;
|
||||||
|
|
||||||
|
# This is necessary to pass the correct IP to be hashed
|
||||||
|
real_ip_header X-Real-IP;
|
||||||
|
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
|
||||||
|
# To support websocket
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
|
||||||
|
proxy_pass http://console;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
# include /etc/nginx/conf.d/*.conf;
|
||||||
|
|
||||||
|
upstream minio {
|
||||||
|
server minio1:9000;
|
||||||
|
server minio2:9000;
|
||||||
|
server minio3:9000;
|
||||||
|
server minio4:9000;
|
||||||
|
server minio5:9000;
|
||||||
|
server minio6:9000;
|
||||||
|
server minio7:9000;
|
||||||
|
server minio8:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream console {
|
||||||
|
ip_hash;
|
||||||
|
server minio1:9001;
|
||||||
|
server minio2:9001;
|
||||||
|
server minio3:9001;
|
||||||
|
server minio4:9001;
|
||||||
|
server minio5:9001;
|
||||||
|
server minio6:9001;
|
||||||
|
server minio7:9001;
|
||||||
|
server minio8:9001;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 9000;
|
||||||
|
listen [::]:9000;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# To allow special characters in headers
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
# Allow any size file to be uploaded.
|
||||||
|
# Set to a value such as 1000m; to restrict file size to a specific value
|
||||||
|
client_max_body_size 0;
|
||||||
|
# To disable buffering
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
|
||||||
|
proxy_pass http://minio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 9001;
|
||||||
|
listen [::]:9001;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# To allow special characters in headers
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
# Allow any size file to be uploaded.
|
||||||
|
# Set to a value such as 1000m; to restrict file size to a specific value
|
||||||
|
client_max_body_size 0;
|
||||||
|
# To disable buffering
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-NginX-Proxy true;
|
||||||
|
|
||||||
|
# This is necessary to pass the correct IP to be hashed
|
||||||
|
real_ip_header X-Real-IP;
|
||||||
|
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
|
||||||
|
# To support websocket
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
|
||||||
|
proxy_pass http://console;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
# include /etc/nginx/conf.d/*.conf;
|
||||||
|
|
||||||
|
upstream minio {
|
||||||
|
server minio1:9000;
|
||||||
|
server minio2:9000;
|
||||||
|
server minio3:9000;
|
||||||
|
server minio4:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream console {
|
||||||
|
ip_hash;
|
||||||
|
server minio1:9001;
|
||||||
|
server minio2:9001;
|
||||||
|
server minio3:9001;
|
||||||
|
server minio4:9001;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 9000;
|
||||||
|
listen [::]:9000;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# To allow special characters in headers
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
# Allow any size file to be uploaded.
|
||||||
|
# Set to a value such as 1000m; to restrict file size to a specific value
|
||||||
|
client_max_body_size 0;
|
||||||
|
# To disable buffering
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
|
||||||
|
proxy_pass http://minio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 9001;
|
||||||
|
listen [::]:9001;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# To allow special characters in headers
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
# Allow any size file to be uploaded.
|
||||||
|
# Set to a value such as 1000m; to restrict file size to a specific value
|
||||||
|
client_max_body_size 0;
|
||||||
|
# To disable buffering
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-NginX-Proxy true;
|
||||||
|
|
||||||
|
# This is necessary to pass the correct IP to be hashed
|
||||||
|
real_ip_header X-Real-IP;
|
||||||
|
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
|
||||||
|
# To support websocket
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
|
||||||
|
proxy_pass http://console;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,10 +21,10 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.19.x]
|
go-version: [1.20.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: Root lockdown tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.20.x]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
|
- name: Start root lockdown tests
|
||||||
|
run: |
|
||||||
|
make test-root-disable
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
export MODE="$1"
|
||||||
|
export ACCESS_KEY="$2"
|
||||||
|
export SECRET_KEY="$3"
|
||||||
|
export JOB_NAME="$4"
|
||||||
|
export MINT_MODE="full"
|
||||||
|
|
||||||
|
docker system prune -f || true
|
||||||
|
docker volume prune -f || true
|
||||||
|
docker volume rm $(docker volume ls -f dangling=true) || true
|
||||||
|
|
||||||
|
## change working directory
|
||||||
|
cd .github/workflows/mint
|
||||||
|
|
||||||
|
docker-compose -f minio-${MODE}.yaml up -d
|
||||||
|
sleep 5m
|
||||||
|
|
||||||
|
docker run --rm --net=host \
|
||||||
|
--name="mint-${MODE}-${JOB_NAME}" \
|
||||||
|
-e SERVER_ENDPOINT="127.0.0.1:9000" \
|
||||||
|
-e ACCESS_KEY="${ACCESS_KEY}" \
|
||||||
|
-e SECRET_KEY="${SECRET_KEY}" \
|
||||||
|
-e ENABLE_HTTPS=0 \
|
||||||
|
-e MINT_MODE="${MINT_MODE}" \
|
||||||
|
docker.io/minio/mint:edge \
|
||||||
|
aws-sdk-go \
|
||||||
|
aws-sdk-java \
|
||||||
|
aws-sdk-php \
|
||||||
|
aws-sdk-ruby \
|
||||||
|
awscli \
|
||||||
|
healthcheck \
|
||||||
|
mc \
|
||||||
|
minio-go \
|
||||||
|
minio-java \
|
||||||
|
minio-py \
|
||||||
|
s3cmd \
|
||||||
|
s3select \
|
||||||
|
versioning
|
||||||
|
|
||||||
|
docker-compose -f minio-${MODE}.yaml down || true
|
||||||
|
sleep 10s
|
||||||
|
|
||||||
|
docker system prune -f || true
|
||||||
|
docker volume prune -f || true
|
||||||
|
docker volume rm $(docker volume ls -f dangling=true) || true
|
||||||
|
|
||||||
|
## change working directory
|
||||||
|
cd ../../../
|
||||||
|
|
||||||
@@ -20,11 +20,11 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.19.x]
|
go-version: [1.20.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: 1.19
|
go-version: 1.20.x
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Get official govulncheck
|
- name: Get official govulncheck
|
||||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
|||||||
@@ -38,3 +38,4 @@ docs/debugging/s3-check-md5/s3-check-md5
|
|||||||
docs/debugging/hash-set/hash-set
|
docs/debugging/hash-set/hash-set
|
||||||
docs/debugging/healing-bin/healing-bin
|
docs/debugging/healing-bin/healing-bin
|
||||||
docs/debugging/inspect/inspect
|
docs/debugging/inspect/inspect
|
||||||
|
.bin/
|
||||||
|
|||||||
+16
-21
@@ -1,39 +1,34 @@
|
|||||||
linters-settings:
|
linters-settings:
|
||||||
gofumpt:
|
gofumpt:
|
||||||
lang-version: "1.18"
|
simplify: true
|
||||||
|
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
locale: US
|
||||||
|
|
||||||
|
staticcheck:
|
||||||
|
checks: ['all', '-ST1005', '-ST1000', '-SA4000', '-SA9004', '-SA1019', '-SA1008', '-U1000', '-ST1016']
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- typecheck
|
- durationcheck
|
||||||
- goimports
|
- gocritic
|
||||||
- misspell
|
|
||||||
- govet
|
|
||||||
- revive
|
|
||||||
- ineffassign
|
|
||||||
- gomodguard
|
|
||||||
- gofmt
|
- gofmt
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
- gomodguard
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- revive
|
||||||
|
- staticcheck
|
||||||
|
- tenv
|
||||||
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unused
|
- unused
|
||||||
- gocritic
|
|
||||||
- gofumpt
|
|
||||||
- tenv
|
|
||||||
- durationcheck
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-use-default: false
|
exclude-use-default: false
|
||||||
exclude:
|
exclude:
|
||||||
- should have a package comment
|
- should have a package comment
|
||||||
- error strings should not be capitalized or end with punctuation or a newline
|
- error strings should not be capitalized or end with punctuation or a newline
|
||||||
# todo fix these when we get enough time.
|
|
||||||
- "singleCaseSwitch: should rewrite switch statement to if statement"
|
|
||||||
- "unlambda: replace"
|
|
||||||
- "captLocal:"
|
|
||||||
- "ifElseChain:"
|
|
||||||
- "elseif:"
|
|
||||||
|
|
||||||
service:
|
|
||||||
golangci-lint-version: 1.43.0 # use the fixed version to not introduce new linters unexpectedly
|
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ COPY LICENSE /licenses/LICENSE
|
|||||||
RUN \
|
RUN \
|
||||||
microdnf clean all && \
|
microdnf clean all && \
|
||||||
microdnf update --nodocs && \
|
microdnf update --nodocs && \
|
||||||
microdnf install curl ca-certificates shadow-utils util-linux gzip --nodocs && \
|
microdnf install curl ca-certificates shadow-utils util-linux gzip lsof tar net-tools --nodocs && \
|
||||||
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
|
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
|
||||||
microdnf install minisign --nodocs && \
|
microdnf install minisign --nodocs && \
|
||||||
mkdir -p /opt/bin && chmod -R 777 /opt/bin && \
|
mkdir -p /opt/bin && chmod -R 777 /opt/bin && \
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ GOOS := $(shell go env GOOS)
|
|||||||
VERSION ?= $(shell git describe --tags)
|
VERSION ?= $(shell git describe --tags)
|
||||||
TAG ?= "minio/minio:$(VERSION)"
|
TAG ?= "minio/minio:$(VERSION)"
|
||||||
|
|
||||||
|
GOLANGCI_VERSION = v1.51.2
|
||||||
|
GOLANGCI_DIR = .bin/golangci/$(GOLANGCI_VERSION)
|
||||||
|
GOLANGCI = $(GOLANGCI_DIR)/golangci-lint
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
checks: ## check dependencies
|
checks: ## check dependencies
|
||||||
@@ -19,30 +23,36 @@ help: ## print this help
|
|||||||
|
|
||||||
getdeps: ## fetch necessary dependencies
|
getdeps: ## fetch necessary dependencies
|
||||||
@mkdir -p ${GOPATH}/bin
|
@mkdir -p ${GOPATH}/bin
|
||||||
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin
|
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOLANGCI_DIR) $(GOLANGCI_VERSION)
|
||||||
@echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.1.7
|
@echo "Installing 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 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
|
crosscompile: ## cross compile minio
|
||||||
@(env bash $(PWD)/buildscripts/cross-compile.sh)
|
@(env bash $(PWD)/buildscripts/cross-compile.sh)
|
||||||
|
|
||||||
verifiers: getdeps lint check-gen
|
verifiers: lint check-gen
|
||||||
|
|
||||||
check-gen: ## check for updated autogenerated files
|
check-gen: ## check for updated autogenerated files
|
||||||
@go generate ./... >/dev/null
|
@go generate ./... >/dev/null
|
||||||
@(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false)
|
@(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false)
|
||||||
|
|
||||||
lint: ## runs golangci-lint suite of linters
|
lint: getdeps ## runs golangci-lint suite of linters
|
||||||
@echo "Running $@ check"
|
@echo "Running $@ check"
|
||||||
@${GOPATH}/bin/golangci-lint run --build-tags kqueue --timeout=10m --config ./.golangci.yml
|
@$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml
|
||||||
@${GOPATH}/bin/staticcheck --tests=false ./...
|
|
||||||
|
lint-fix: getdeps ## runs golangci-lint suite of linters with automatic fixes
|
||||||
|
@echo "Running $@ check"
|
||||||
|
@$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml --fix
|
||||||
|
|
||||||
check: test
|
check: test
|
||||||
test: verifiers build ## builds minio, runs linters, tests
|
test: verifiers build ## builds minio, runs linters, tests
|
||||||
@echo "Running unit tests"
|
@echo "Running unit tests"
|
||||||
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -tags kqueue ./...
|
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -tags kqueue ./...
|
||||||
|
|
||||||
|
test-root-disable: install
|
||||||
|
@echo "Running minio root lockdown tests"
|
||||||
|
@env bash $(PWD)/buildscripts/disable-root.sh
|
||||||
|
|
||||||
test-decom: install
|
test-decom: install
|
||||||
@echo "Running minio decom tests"
|
@echo "Running minio decom tests"
|
||||||
@env bash $(PWD)/docs/distributed/decom.sh
|
@env bash $(PWD)/docs/distributed/decom.sh
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
MinIO Project, (C) 2015-2021 MinIO, Inc.
|
MinIO Project, (C) 2015-2023 MinIO, Inc.
|
||||||
|
|
||||||
This product includes software developed at MinIO, Inc.
|
This product includes software developed at MinIO, Inc.
|
||||||
(https://min.io/).
|
(https://min.io/).
|
||||||
|
|||||||
@@ -86,8 +86,6 @@ chmod +x minio
|
|||||||
./minio server /data
|
./minio server /data
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace ``/data`` with the path to the drive or directory in which you want MinIO to store data.
|
|
||||||
|
|
||||||
The following table lists supported architectures. Replace the `wget` URL with the architecture for your Linux host.
|
The following table lists supported architectures. Replace the `wget` URL with the architecture for your Linux host.
|
||||||
|
|
||||||
| Architecture | URL |
|
| Architecture | URL |
|
||||||
|
|||||||
@@ -75,11 +75,11 @@ check_minimum_version() {
|
|||||||
|
|
||||||
assert_is_supported_arch() {
|
assert_is_supported_arch() {
|
||||||
case "${ARCH}" in
|
case "${ARCH}" in
|
||||||
x86_64 | amd64 | aarch64 | ppc64le | arm* | s390x )
|
x86_64 | amd64 | aarch64 | ppc64le | arm* | s390x | loong64 | loongarch64 )
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, ppc64le, arm*, s390x]"
|
echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, ppc64le, arm*, s390x, loong64, loongarch64]"
|
||||||
exit 1
|
exit 1
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -x
|
||||||
|
|
||||||
|
export MINIO_CI_CD=1
|
||||||
|
killall -9 minio
|
||||||
|
|
||||||
|
rm -rf ${HOME}/tmp/dist
|
||||||
|
|
||||||
|
scheme="http"
|
||||||
|
nr_servers=4
|
||||||
|
|
||||||
|
addr="localhost"
|
||||||
|
args=""
|
||||||
|
for ((i=0;i<$[${nr_servers}];i++)); do
|
||||||
|
args="$args $scheme://$addr:$[9100+$i]/${HOME}/tmp/dist/path1/$i"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo $args
|
||||||
|
|
||||||
|
for ((i=0;i<$[${nr_servers}];i++)); do
|
||||||
|
(minio server --address ":$[9100+$i]" $args 2>&1 > /tmp/log$i.txt) &
|
||||||
|
done
|
||||||
|
|
||||||
|
sleep 10s
|
||||||
|
|
||||||
|
if [ ! -f ./mc ]; then
|
||||||
|
wget --quiet -O ./mc https://dl.minio.io/client/mc/release/linux-amd64/./mc && \
|
||||||
|
chmod +x mc
|
||||||
|
fi
|
||||||
|
|
||||||
|
set +e
|
||||||
|
|
||||||
|
export MC_HOST_minioadm=http://minioadmin:minioadmin@localhost:9100/
|
||||||
|
|
||||||
|
./mc ls minioadm/
|
||||||
|
|
||||||
|
./mc admin config set minioadm/ api root_access=off
|
||||||
|
|
||||||
|
sleep 3s # let things settle a little
|
||||||
|
|
||||||
|
./mc ls minioadm/
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "listing succeeded, 'minioadmin' was not disabled"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
killall -9 minio
|
||||||
|
|
||||||
|
export MINIO_API_ROOT_ACCESS=on
|
||||||
|
for ((i=0;i<$[${nr_servers}];i++)); do
|
||||||
|
(minio server --address ":$[9100+$i]" $args 2>&1 > /tmp/log$i.txt) &
|
||||||
|
done
|
||||||
|
|
||||||
|
set +e
|
||||||
|
|
||||||
|
./mc ls minioadm/
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "listing failed, 'minioadmin' should be enabled"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
killall -9 minio
|
||||||
|
|
||||||
|
rm -rf /tmp/multisitea/
|
||||||
|
rm -rf /tmp/multisiteb/
|
||||||
|
|
||||||
|
echo "Setup site-replication and then disable root credentials"
|
||||||
|
|
||||||
|
minio server --address 127.0.0.1:9001 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_1.log 2>&1 &
|
||||||
|
minio server --address 127.0.0.1:9002 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_2.log 2>&1 &
|
||||||
|
|
||||||
|
minio server --address 127.0.0.1:9003 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_1.log 2>&1 &
|
||||||
|
minio server --address 127.0.0.1:9004 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_2.log 2>&1 &
|
||||||
|
|
||||||
|
sleep 20s
|
||||||
|
|
||||||
|
export MC_HOST_sitea=http://minioadmin:minioadmin@127.0.0.1:9001
|
||||||
|
export MC_HOST_siteb=http://minioadmin:minioadmin@127.0.0.1:9004
|
||||||
|
|
||||||
|
./mc admin replicate add sitea siteb
|
||||||
|
|
||||||
|
./mc admin user add sitea foobar foo12345
|
||||||
|
|
||||||
|
./mc admin policy attach sitea/ consoleAdmin --user=foobar
|
||||||
|
|
||||||
|
./mc admin user info siteb foobar
|
||||||
|
|
||||||
|
killall -9 minio
|
||||||
|
|
||||||
|
echo "turning off root access, however site replication must continue"
|
||||||
|
export MINIO_API_ROOT_ACCESS=off
|
||||||
|
|
||||||
|
minio server --address 127.0.0.1:9001 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_1.log 2>&1 &
|
||||||
|
minio server --address 127.0.0.1:9002 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_2.log 2>&1 &
|
||||||
|
|
||||||
|
minio server --address 127.0.0.1:9003 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_1.log 2>&1 &
|
||||||
|
minio server --address 127.0.0.1:9004 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_2.log 2>&1 &
|
||||||
|
|
||||||
|
sleep 20s
|
||||||
|
|
||||||
|
export MC_HOST_sitea=http://foobar:foo12345@127.0.0.1:9001
|
||||||
|
export MC_HOST_siteb=http://foobar:foo12345@127.0.0.1:9004
|
||||||
|
|
||||||
|
./mc admin user add sitea foobar-admin foo12345
|
||||||
|
|
||||||
|
sleep 2s
|
||||||
|
|
||||||
|
./mc admin user info siteb foobar-admin
|
||||||
@@ -5,7 +5,6 @@ set -e
|
|||||||
export GORACE="history_size=7"
|
export GORACE="history_size=7"
|
||||||
export MINIO_API_REQUESTS_MAX=10000
|
export MINIO_API_REQUESTS_MAX=10000
|
||||||
|
|
||||||
## TODO remove `dsync` from race detector once this is merged and released https://go-review.googlesource.com/c/go/+/333529/
|
for d in $(go list ./...); do
|
||||||
for d in $(go list ./... | grep -v dsync); do
|
|
||||||
CGO_ENABLED=1 go test -v -race --timeout 100m "$d"
|
CGO_ENABLED=1 go test -v -race --timeout 100m "$d"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -45,16 +45,18 @@ function start_minio() {
|
|||||||
|
|
||||||
# Prepare fake disks with losetup
|
# Prepare fake disks with losetup
|
||||||
function prepare_block_devices() {
|
function prepare_block_devices() {
|
||||||
|
set -e
|
||||||
mkdir -p ${WORK_DIR}/disks/ ${WORK_DIR}/mnt/
|
mkdir -p ${WORK_DIR}/disks/ ${WORK_DIR}/mnt/
|
||||||
|
sudo modprobe loop
|
||||||
for i in 1 2 3 4; do
|
for i in 1 2 3 4; do
|
||||||
dd if=/dev/zero of=${WORK_DIR}/disks/img.$i bs=1M count=2048
|
dd if=/dev/zero of=${WORK_DIR}/disks/img.${i} bs=1M count=2000
|
||||||
mkfs.ext4 -F ${WORK_DIR}/disks/img.$i
|
device=$(sudo losetup --find --show ${WORK_DIR}/disks/img.${i})
|
||||||
sudo mknod /dev/minio-loopdisk$i b 7 $[256-$i]
|
sudo mkfs.ext4 -F ${device}
|
||||||
sudo losetup /dev/minio-loopdisk$i ${WORK_DIR}/disks/img.$i
|
mkdir -p ${WORK_DIR}/mnt/disk${i}/
|
||||||
mkdir -p ${WORK_DIR}/mnt/disk$i/
|
sudo mount ${device} ${WORK_DIR}/mnt/disk${i}/
|
||||||
sudo mount /dev/minio-loopdisk$i ${WORK_DIR}/mnt/disk$i/
|
sudo chown "$(id -u):$(id -g)" ${device} ${WORK_DIR}/mnt/disk${i}/
|
||||||
sudo chown "$(id -u):$(id -g)" /dev/minio-loopdisk$i ${WORK_DIR}/mnt/disk$i/
|
|
||||||
done
|
done
|
||||||
|
set +e
|
||||||
}
|
}
|
||||||
|
|
||||||
# Start a distributed MinIO setup, unmount one disk and check if it is formatted
|
# Start a distributed MinIO setup, unmount one disk and check if it is formatted
|
||||||
|
|||||||
+2
-2
@@ -90,8 +90,8 @@ func (api objectAPIHandlers) PutBucketACLHandler(w http.ResponseWriter, r *http.
|
|||||||
if aclHeader == "" {
|
if aclHeader == "" {
|
||||||
acl := &accessControlPolicy{}
|
acl := &accessControlPolicy{}
|
||||||
if err = xmlDecoder(r.Body, acl, r.ContentLength); err != nil {
|
if err = xmlDecoder(r.Body, acl, r.ContentLength); err != nil {
|
||||||
if err == io.EOF {
|
if terr, ok := err.(*xml.SyntaxError); ok && terr.Msg == io.EOF.Error() {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingSecurityHeader),
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML),
|
||||||
r.URL)
|
r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import (
|
|||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/klauspost/compress/zip"
|
"github.com/klauspost/compress/zip"
|
||||||
"github.com/minio/kes"
|
"github.com/minio/kes-go"
|
||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
"github.com/minio/minio-go/v7/pkg/tags"
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||||
@@ -85,11 +85,6 @@ func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if quotaConfig.Type == "fifo" {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data)
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
@@ -169,7 +164,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, _, _, s3Err := validateAdminSignature(ctx, r, "")
|
cred, _, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -199,7 +194,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
|||||||
ops = madmin.GetTargetUpdateOps(r.Form)
|
ops = madmin.GetTargetUpdateOps(r.Form)
|
||||||
} else {
|
} else {
|
||||||
var exists bool // true if arn exists
|
var exists bool // true if arn exists
|
||||||
target.Arn, exists = globalBucketTargetSys.getRemoteARN(bucket, &target)
|
target.Arn, exists = globalBucketTargetSys.getRemoteARN(bucket, &target, "")
|
||||||
if exists && target.Arn != "" { // return pre-existing ARN
|
if exists && target.Arn != "" { // return pre-existing ARN
|
||||||
data, err := json.Marshal(target.Arn)
|
data, err := json.Marshal(target.Arn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -215,6 +210,11 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
|||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if globalSiteReplicationSys.isEnabled() && !update {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrRemoteTargetDenyAddError, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if update {
|
if update {
|
||||||
// overlay the updates on existing target
|
// overlay the updates on existing target
|
||||||
tgt := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, target.Arn)
|
tgt := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, target.Arn)
|
||||||
@@ -226,11 +226,13 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
|||||||
switch op {
|
switch op {
|
||||||
case madmin.CredentialsUpdateType:
|
case madmin.CredentialsUpdateType:
|
||||||
if !globalSiteReplicationSys.isEnabled() {
|
if !globalSiteReplicationSys.isEnabled() {
|
||||||
|
// credentials update is possible only in bucket replication. User will never
|
||||||
|
// know the site replicator creds.
|
||||||
tgt.Credentials = target.Credentials
|
tgt.Credentials = target.Credentials
|
||||||
tgt.TargetBucket = target.TargetBucket
|
tgt.TargetBucket = target.TargetBucket
|
||||||
}
|
|
||||||
tgt.Secure = target.Secure
|
tgt.Secure = target.Secure
|
||||||
tgt.Endpoint = target.Endpoint
|
tgt.Endpoint = target.Endpoint
|
||||||
|
}
|
||||||
case madmin.SyncUpdateType:
|
case madmin.SyncUpdateType:
|
||||||
tgt.ReplicationSync = target.ReplicationSync
|
tgt.ReplicationSync = target.ReplicationSync
|
||||||
case madmin.ProxyUpdateType:
|
case madmin.ProxyUpdateType:
|
||||||
@@ -684,8 +686,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bucket, fileName := slc[0], slc[1]
|
bucket, fileName := slc[0], slc[1]
|
||||||
switch fileName {
|
if fileName == objectLockConfig {
|
||||||
case objectLockConfig:
|
|
||||||
reader, err := file.Open()
|
reader, err := file.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rpt.SetStatus(bucket, fileName, err)
|
rpt.SetStatus(bucket, fileName, err)
|
||||||
@@ -754,8 +755,7 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bucket, fileName := slc[0], slc[1]
|
bucket, fileName := slc[0], slc[1]
|
||||||
switch fileName {
|
if fileName == bucketVersioningConfig {
|
||||||
case bucketVersioningConfig:
|
|
||||||
reader, err := file.Open()
|
reader, err := file.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rpt.SetStatus(bucket, fileName, err)
|
rpt.SetStatus(bucket, fileName, err)
|
||||||
@@ -1032,11 +1032,6 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if quotaConfig.Type == "fifo" {
|
|
||||||
rpt.SetStatus(bucket, fileName, fmt.Errorf("Detected older 'fifo' quota config, 'fifo' feature is removed and not supported anymore"))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data)
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rpt.SetStatus(bucket, fileName, err)
|
rpt.SetStatus(bucket, fileName, err)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/minio/kes"
|
"github.com/minio/kes-go"
|
||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
"github.com/minio/minio/internal/auth"
|
"github.com/minio/minio/internal/auth"
|
||||||
"github.com/minio/minio/internal/config"
|
"github.com/minio/minio/internal/config"
|
||||||
@@ -168,10 +168,16 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
|||||||
}
|
}
|
||||||
case errors.Is(err, errPolicyInUse):
|
case errors.Is(err, errPolicyInUse):
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "XMinioAdminPolicyInUse",
|
Code: "XMinioIAMPolicyInUse",
|
||||||
Description: "The policy cannot be removed, as it is in use",
|
Description: "The policy cannot be removed, as it is in use",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
}
|
}
|
||||||
|
case errors.Is(err, errSessionPolicyTooLarge):
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "XMinioIAMServiceAccountSessionPolicyTooLarge",
|
||||||
|
Description: err.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
case errors.Is(err, kes.ErrKeyExists):
|
case errors.Is(err, kes.ErrKeyExists):
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "XMinioKMSKeyExists",
|
Code: "XMinioKMSKeyExists",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import (
|
|||||||
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
||||||
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||||
"github.com/minio/minio/internal/config/storageclass"
|
"github.com/minio/minio/internal/config/storageclass"
|
||||||
|
"github.com/minio/minio/internal/config/subnet"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/mux"
|
"github.com/minio/mux"
|
||||||
iampolicy "github.com/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
@@ -87,6 +88,10 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if subnet proxy being deleted and if so the value of proxy of subnet
|
||||||
|
// target of logger webhook configuration also should be deleted
|
||||||
|
loggerWebhookProxyDeleted := setLoggerWebhookSubnetProxy(subSys, cfg)
|
||||||
|
|
||||||
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
|
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -101,6 +106,10 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
dynamic := config.SubSystemsDynamic.Contains(subSys)
|
dynamic := config.SubSystemsDynamic.Contains(subSys)
|
||||||
if dynamic {
|
if dynamic {
|
||||||
applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
|
applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
|
||||||
|
if subSys == config.SubnetSubSys && loggerWebhookProxyDeleted {
|
||||||
|
// Logger webhook proxy deleted, apply the dynamic changes
|
||||||
|
applyDynamic(ctx, objectAPI, cfg, config.LoggerWebhookSubSys, r, w)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +144,7 @@ type setConfigResult struct {
|
|||||||
Cfg config.Config
|
Cfg config.Config
|
||||||
SubSys string
|
SubSys string
|
||||||
Dynamic bool
|
Dynamic bool
|
||||||
|
LoggerWebhookCfgUpdated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfigKVHandler - PUT /minio/admin/v3/set-config-kv
|
// SetConfigKVHandler - PUT /minio/admin/v3/set-config-kv
|
||||||
@@ -175,6 +185,11 @@ func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
if result.Dynamic {
|
if result.Dynamic {
|
||||||
applyDynamic(ctx, objectAPI, result.Cfg, result.SubSys, r, w)
|
applyDynamic(ctx, objectAPI, result.Cfg, result.SubSys, r, w)
|
||||||
|
// If logger webhook config updated (proxy due to callhome), explicitly dynamically
|
||||||
|
// apply the config
|
||||||
|
if result.LoggerWebhookCfgUpdated {
|
||||||
|
applyDynamic(ctx, objectAPI, result.Cfg, config.LoggerWebhookSubSys, r, w)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
@@ -201,6 +216,10 @@ func setConfigKV(ctx context.Context, objectAPI ObjectLayer, kvBytes []byte) (re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if subnet proxy being set and if so set the same value to proxy of subnet
|
||||||
|
// target of logger webhook configuration
|
||||||
|
result.LoggerWebhookCfgUpdated = setLoggerWebhookSubnetProxy(result.SubSys, result.Cfg)
|
||||||
|
|
||||||
// Update the actual server config on disk.
|
// Update the actual server config on disk.
|
||||||
if err = saveServerConfig(ctx, objectAPI, result.Cfg); err != nil {
|
if err = saveServerConfig(ctx, objectAPI, result.Cfg); err != nil {
|
||||||
return
|
return
|
||||||
@@ -501,7 +520,7 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
case config.IdentityLDAPSubSys:
|
case config.IdentityLDAPSubSys:
|
||||||
off = !xldap.Enabled(item.Config)
|
off = !xldap.Enabled(item.Config)
|
||||||
case config.IdentityTLSSubSys:
|
case config.IdentityTLSSubSys:
|
||||||
off = !globalSTSTLSConfig.Enabled
|
off = !globalIAMSys.STSTLSConfig.Enabled
|
||||||
case config.IdentityPluginSubSys:
|
case config.IdentityPluginSubSys:
|
||||||
off = !idplugin.Enabled(item.Config)
|
off = !idplugin.Enabled(item.Config)
|
||||||
}
|
}
|
||||||
@@ -518,3 +537,18 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
writeSuccessResponseJSON(w, econfigData)
|
writeSuccessResponseJSON(w, econfigData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setLoggerWebhookSubnetProxy - Sets the logger webhook's subnet proxy value to
|
||||||
|
// one being set for subnet proxy
|
||||||
|
func setLoggerWebhookSubnetProxy(subSys string, cfg config.Config) bool {
|
||||||
|
if subSys == config.SubnetSubSys {
|
||||||
|
subnetWebhookCfg := cfg[config.LoggerWebhookSubSys][subnet.LoggerWebhookName]
|
||||||
|
loggerWebhookSubnetProxy := subnetWebhookCfg.Get(logger.Proxy)
|
||||||
|
subnetProxy := cfg[config.SubnetSubSys][config.Default].Get(logger.Proxy)
|
||||||
|
if loggerWebhookSubnetProxy != subnetProxy {
|
||||||
|
subnetWebhookCfg.Set(logger.Proxy, subnetProxy)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func (a adminAPIHandlers) addOrUpdateIDPHandler(ctx context.Context, w http.Resp
|
|||||||
if idpCfgType == madmin.LDAPIDPCfg && cfgName != madmin.Default {
|
if idpCfgType == madmin.LDAPIDPCfg && cfgName != madmin.Default {
|
||||||
// LDAP does not support multiple configurations. So cfgName must be
|
// LDAP does not support multiple configurations. So cfgName must be
|
||||||
// empty or `madmin.Default`.
|
// empty or `madmin.Default`.
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPNonDefaultConfigName), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,9 +178,9 @@ func handleCreateUpdateValidation(s config.Config, subSys, cfgTarget string, isU
|
|||||||
var cfgInfos []madmin.IDPCfgInfo
|
var cfgInfos []madmin.IDPCfgInfo
|
||||||
switch subSys {
|
switch subSys {
|
||||||
case madmin.IdentityOpenIDSubSys:
|
case madmin.IdentityOpenIDSubSys:
|
||||||
cfgInfos, _ = globalOpenIDConfig.GetConfigInfo(s, cfgTarget)
|
cfgInfos, _ = globalIAMSys.OpenIDConfig.GetConfigInfo(s, cfgTarget)
|
||||||
case madmin.IdentityLDAPSubSys:
|
case madmin.IdentityLDAPSubSys:
|
||||||
cfgInfos, _ = globalLDAPConfig.GetConfigInfo(s, cfgTarget)
|
cfgInfos, _ = globalIAMSys.LDAPConfig.GetConfigInfo(s, cfgTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfgInfos) > 0 && !isUpdate {
|
if len(cfgInfos) > 0 && !isUpdate {
|
||||||
@@ -240,10 +240,10 @@ func (a adminAPIHandlers) ListIdentityProviderCfg(w http.ResponseWriter, r *http
|
|||||||
switch idpCfgType {
|
switch idpCfgType {
|
||||||
case madmin.OpenidIDPCfg:
|
case madmin.OpenidIDPCfg:
|
||||||
cfg := globalServerConfig.Clone()
|
cfg := globalServerConfig.Clone()
|
||||||
cfgList, err = globalOpenIDConfig.GetConfigList(cfg)
|
cfgList, err = globalIAMSys.OpenIDConfig.GetConfigList(cfg)
|
||||||
case madmin.LDAPIDPCfg:
|
case madmin.LDAPIDPCfg:
|
||||||
cfg := globalServerConfig.Clone()
|
cfg := globalServerConfig.Clone()
|
||||||
cfgList, err = globalLDAPConfig.GetConfigList(cfg)
|
cfgList, err = globalIAMSys.LDAPConfig.GetConfigList(cfg)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
@@ -296,9 +296,9 @@ func (a adminAPIHandlers) GetIdentityProviderCfg(w http.ResponseWriter, r *http.
|
|||||||
var err error
|
var err error
|
||||||
switch idpCfgType {
|
switch idpCfgType {
|
||||||
case madmin.OpenidIDPCfg:
|
case madmin.OpenidIDPCfg:
|
||||||
cfgInfos, err = globalOpenIDConfig.GetConfigInfo(cfg, cfgName)
|
cfgInfos, err = globalIAMSys.OpenIDConfig.GetConfigInfo(cfg, cfgName)
|
||||||
case madmin.LDAPIDPCfg:
|
case madmin.LDAPIDPCfg:
|
||||||
cfgInfos, err = globalLDAPConfig.GetConfigInfo(cfg, cfgName)
|
cfgInfos, err = globalIAMSys.LDAPConfig.GetConfigInfo(cfg, cfgName)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, openid.ErrProviderConfigNotFound) || errors.Is(err, cfgldap.ErrProviderConfigNotFound) {
|
if errors.Is(err, openid.ErrProviderConfigNotFound) || errors.Is(err, cfgldap.ErrProviderConfigNotFound) {
|
||||||
@@ -355,7 +355,7 @@ func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *ht
|
|||||||
switch idpCfgType {
|
switch idpCfgType {
|
||||||
case madmin.OpenidIDPCfg:
|
case madmin.OpenidIDPCfg:
|
||||||
subSys = config.IdentityOpenIDSubSys
|
subSys = config.IdentityOpenIDSubSys
|
||||||
cfgInfos, err := globalOpenIDConfig.GetConfigInfo(cfgCopy, cfgName)
|
cfgInfos, err := globalIAMSys.OpenIDConfig.GetConfigInfo(cfgCopy, cfgName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, openid.ErrProviderConfigNotFound) {
|
if errors.Is(err, openid.ErrProviderConfigNotFound) {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
||||||
@@ -380,7 +380,7 @@ func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *ht
|
|||||||
}
|
}
|
||||||
case madmin.LDAPIDPCfg:
|
case madmin.LDAPIDPCfg:
|
||||||
subSys = config.IdentityLDAPSubSys
|
subSys = config.IdentityLDAPSubSys
|
||||||
cfgInfos, err := globalLDAPConfig.GetConfigInfo(cfgCopy, cfgName)
|
cfgInfos, err := globalIAMSys.LDAPConfig.GetConfigInfo(cfgCopy, cfgName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, openid.ErrProviderConfigNotFound) {
|
if errors.Is(err, openid.ErrProviderConfigNotFound) {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
||||||
@@ -423,6 +423,16 @@ func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = validateConfig(cfg, subSys); err != nil {
|
if err = validateConfig(cfg, subSys); err != nil {
|
||||||
|
|
||||||
|
var validationErr ldap.Validation
|
||||||
|
if errors.As(err, &validationErr) {
|
||||||
|
// If we got an LDAP validation error, we need to send appropriate
|
||||||
|
// error message back to client (likely mc).
|
||||||
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPValidation),
|
||||||
|
validationErr.FormatError(), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
minio "github.com/minio/minio-go/v7"
|
minio "github.com/minio/minio-go/v7"
|
||||||
|
"github.com/minio/pkg/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
func runAllIAMConcurrencyTests(suite *TestSuiteIAM, c *check) {
|
func runAllIAMConcurrencyTests(suite *TestSuiteIAM, c *check) {
|
||||||
@@ -129,18 +129,21 @@ func (s *TestSuiteIAM) TestDeleteUserRace(c *check) {
|
|||||||
secretKeys[i] = secretKey
|
secretKeys[i] = secretKey
|
||||||
}
|
}
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
g := errgroup.Group{}
|
||||||
for i := 0; i < userCount; i++ {
|
for i := 0; i < userCount; i++ {
|
||||||
wg.Add(1)
|
g.Go(func(i int) func() error {
|
||||||
go func(i int) {
|
return func() error {
|
||||||
defer wg.Done()
|
|
||||||
uClient := s.getUserClient(c, accessKeys[i], secretKeys[i], "")
|
uClient := s.getUserClient(c, accessKeys[i], secretKeys[i], "")
|
||||||
err := s.adm.RemoveUser(ctx, accessKeys[i])
|
err := s.adm.RemoveUser(ctx, accessKeys[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("unable to remove user: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
c.mustNotListObjects(ctx, uClient, bucket)
|
c.mustNotListObjects(ctx, uClient, bucket)
|
||||||
}(i)
|
return nil
|
||||||
|
}
|
||||||
|
}(i), i)
|
||||||
|
}
|
||||||
|
if errs := g.Wait(); len(errs) > 0 {
|
||||||
|
c.Fatalf("unable to remove users: %v", errs)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
|
||||||
}
|
}
|
||||||
|
|||||||
+165
-85
@@ -44,7 +44,7 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DeleteUserAdminAction)
|
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.DeleteUserAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,13 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the user is root credential you are not allowed to
|
||||||
|
// remove the root user. Also you cannot delete yourself.
|
||||||
|
if accessKey == globalActiveCred.AccessKey || accessKey == cred.AccessKey {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil {
|
if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -175,7 +182,7 @@ func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -192,9 +199,9 @@ func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.GetUserAdminAction,
|
Action: iampolicy.GetUserAdminAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
DenyOnly: checkDenyOnly,
|
DenyOnly: checkDenyOnly,
|
||||||
}) {
|
}) {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
@@ -239,6 +246,26 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ
|
|||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reject if the group add and remove are temporary credentials, or root credential.
|
||||||
|
for _, member := range updReq.Members {
|
||||||
|
ok, _, err := globalIAMSys.IsTempUser(member)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// When the user is root credential you are not allowed to
|
||||||
|
// add policies for root user.
|
||||||
|
if member == globalActiveCred.AccessKey {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var updatedAt time.Time
|
var updatedAt time.Time
|
||||||
if updReq.IsRemove {
|
if updReq.IsRemove {
|
||||||
updatedAt, err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
|
updatedAt, err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
|
||||||
@@ -374,7 +401,7 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction)
|
objectAPI, creds := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -383,9 +410,9 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
|||||||
accessKey := vars["accessKey"]
|
accessKey := vars["accessKey"]
|
||||||
status := vars["status"]
|
status := vars["status"]
|
||||||
|
|
||||||
// This API is not allowed to lookup master access key user status
|
// you cannot enable or disable yourself.
|
||||||
if accessKey == globalActiveCred.AccessKey {
|
if accessKey == creds.AccessKey {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,14 +451,14 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not allowed to add a user with same access key as root credential
|
// Not allowed to add a user with same access key as root credential
|
||||||
if owner && accessKey == cred.AccessKey {
|
if accessKey == globalActiveCred.AccessKey {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -468,9 +495,9 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.CreateUserAdminAction,
|
Action: iampolicy.CreateUserAdminAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
DenyOnly: checkDenyOnly,
|
DenyOnly: checkDenyOnly,
|
||||||
}) {
|
}) {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
@@ -528,7 +555,7 @@ func (a adminAPIHandlers) TemporaryAccountInfo(w http.ResponseWriter, r *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -542,10 +569,11 @@ func (a adminAPIHandlers) TemporaryAccountInfo(w http.ResponseWriter, r *http.Re
|
|||||||
|
|
||||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.ListTemporaryAccountsAdminAction,
|
Action: iampolicy.ListTemporaryAccountsAdminAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
}) {
|
}) {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
return
|
return
|
||||||
@@ -581,6 +609,7 @@ func (a adminAPIHandlers) TemporaryAccountInfo(w http.ResponseWriter, r *http.Re
|
|||||||
AccountStatus: stsAccount.Status,
|
AccountStatus: stsAccount.Status,
|
||||||
ImpliedPolicy: policy == nil,
|
ImpliedPolicy: policy == nil,
|
||||||
Policy: string(policyJSON),
|
Policy: string(policyJSON),
|
||||||
|
Expiration: &stsAccount.Expiration,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(infoResp)
|
data, err := json.Marshal(infoResp)
|
||||||
@@ -611,7 +640,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -636,6 +665,13 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := createReq.Validate(); err != nil {
|
||||||
|
// Since this validation would happen client side as well, we only send
|
||||||
|
// a generic error message here.
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
targetUser string
|
targetUser string
|
||||||
targetGroups []string
|
targetGroups []string
|
||||||
@@ -648,10 +684,16 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
targetUser = cred.AccessKey
|
targetUser = cred.AccessKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
description := createReq.Description
|
||||||
|
if description == "" {
|
||||||
|
description = createReq.Comment
|
||||||
|
}
|
||||||
opts := newServiceAccountOpts{
|
opts := newServiceAccountOpts{
|
||||||
accessKey: createReq.AccessKey,
|
accessKey: createReq.AccessKey,
|
||||||
secretKey: createReq.SecretKey,
|
secretKey: createReq.SecretKey,
|
||||||
comment: createReq.Comment,
|
name: createReq.Name,
|
||||||
|
description: description,
|
||||||
|
expiration: createReq.Expiration,
|
||||||
claims: make(map[string]interface{}),
|
claims: make(map[string]interface{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,8 +713,9 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
// It could be a regular user account or the root account.
|
// It could be a regular user account or the root account.
|
||||||
_, isRegularUser := globalIAMSys.GetUser(ctx, targetUser)
|
_, isRegularUser := globalIAMSys.GetUser(ctx, targetUser)
|
||||||
if !isRegularUser && targetUser != globalActiveCred.AccessKey {
|
if !isRegularUser && targetUser != globalActiveCred.AccessKey {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx,
|
apiErr := toAdminAPIErr(ctx, errNoSuchUser)
|
||||||
fmt.Errorf("parent user %s does not exist. Cannot create service account", targetUser)), r.URL)
|
apiErr.Description = fmt.Sprintf("Specified target user %s does not exist", targetUser)
|
||||||
|
writeErrorResponseJSON(ctx, w, apiErr, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -695,9 +738,9 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
AccountName: requestorUser,
|
AccountName: requestorUser,
|
||||||
Groups: requestorGroups,
|
Groups: requestorGroups,
|
||||||
Action: iampolicy.CreateServiceAccountAdminAction,
|
Action: iampolicy.CreateServiceAccountAdminAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
DenyOnly: true,
|
DenyOnly: true,
|
||||||
}) {
|
}) {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
@@ -729,9 +772,9 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
AccountName: requestorUser,
|
AccountName: requestorUser,
|
||||||
Groups: requestorGroups,
|
Groups: requestorGroups,
|
||||||
Action: iampolicy.CreateServiceAccountAdminAction,
|
Action: iampolicy.CreateServiceAccountAdminAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
}) {
|
}) {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
return
|
return
|
||||||
@@ -739,9 +782,9 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
// In case of LDAP we need to resolve the targetUser to a DN and
|
// In case of LDAP we need to resolve the targetUser to a DN and
|
||||||
// query their groups:
|
// query their groups:
|
||||||
if globalLDAPConfig.Enabled() {
|
if globalIAMSys.LDAPConfig.Enabled() {
|
||||||
opts.claims[ldapUserN] = targetUser // simple username
|
opts.claims[ldapUserN] = targetUser // simple username
|
||||||
targetUser, targetGroups, err = globalLDAPConfig.LookupUserDN(targetUser)
|
targetUser, targetGroups, err = globalIAMSys.LDAPConfig.LookupUserDN(targetUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -775,6 +818,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
Credentials: madmin.Credentials{
|
Credentials: madmin.Credentials{
|
||||||
AccessKey: newCred.AccessKey,
|
AccessKey: newCred.AccessKey,
|
||||||
SecretKey: newCred.SecretKey,
|
SecretKey: newCred.SecretKey,
|
||||||
|
Expiration: newCred.Expiration,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,10 +847,12 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
AccessKey: newCred.AccessKey,
|
AccessKey: newCred.AccessKey,
|
||||||
SecretKey: newCred.SecretKey,
|
SecretKey: newCred.SecretKey,
|
||||||
Groups: newCred.Groups,
|
Groups: newCred.Groups,
|
||||||
Comment: newCred.Comment,
|
Name: newCred.Name,
|
||||||
|
Description: newCred.Description,
|
||||||
Claims: opts.claims,
|
Claims: opts.claims,
|
||||||
SessionPolicy: createReq.Policy,
|
SessionPolicy: createReq.Policy,
|
||||||
Status: auth.AccountOn,
|
Status: auth.AccountOn,
|
||||||
|
Expiration: createReq.Expiration,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
UpdatedAt: updatedAt,
|
UpdatedAt: updatedAt,
|
||||||
@@ -827,7 +873,7 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -847,10 +893,11 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
|
|||||||
|
|
||||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.UpdateServiceAccountAdminAction,
|
Action: iampolicy.UpdateServiceAccountAdminAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
}) {
|
}) {
|
||||||
requestUser := cred.AccessKey
|
requestUser := cred.AccessKey
|
||||||
if cred.ParentUser != "" {
|
if cred.ParentUser != "" {
|
||||||
@@ -876,6 +923,13 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := updateReq.Validate(); err != nil {
|
||||||
|
// Since this validation would happen client side as well, we only send
|
||||||
|
// a generic error message here.
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var sp *iampolicy.Policy
|
var sp *iampolicy.Policy
|
||||||
if len(updateReq.NewPolicy) > 0 {
|
if len(updateReq.NewPolicy) > 0 {
|
||||||
sp, err = iampolicy.ParseConfig(bytes.NewReader(updateReq.NewPolicy))
|
sp, err = iampolicy.ParseConfig(bytes.NewReader(updateReq.NewPolicy))
|
||||||
@@ -887,7 +941,9 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
|
|||||||
opts := updateServiceAccountOpts{
|
opts := updateServiceAccountOpts{
|
||||||
secretKey: updateReq.NewSecretKey,
|
secretKey: updateReq.NewSecretKey,
|
||||||
status: updateReq.NewStatus,
|
status: updateReq.NewStatus,
|
||||||
comment: updateReq.NewComment,
|
name: updateReq.NewName,
|
||||||
|
description: updateReq.NewDescription,
|
||||||
|
expiration: updateReq.NewExpiration,
|
||||||
sessionPolicy: sp,
|
sessionPolicy: sp,
|
||||||
}
|
}
|
||||||
updatedAt, err := globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts)
|
updatedAt, err := globalIAMSys.UpdateServiceAccount(ctx, accessKey, opts)
|
||||||
@@ -905,8 +961,10 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
|
|||||||
AccessKey: accessKey,
|
AccessKey: accessKey,
|
||||||
SecretKey: opts.secretKey,
|
SecretKey: opts.secretKey,
|
||||||
Status: opts.status,
|
Status: opts.status,
|
||||||
Comment: opts.comment,
|
Name: opts.name,
|
||||||
|
Description: opts.description,
|
||||||
SessionPolicy: updateReq.NewPolicy,
|
SessionPolicy: updateReq.NewPolicy,
|
||||||
|
Expiration: updateReq.NewExpiration,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
UpdatedAt: updatedAt,
|
UpdatedAt: updatedAt,
|
||||||
@@ -929,7 +987,7 @@ func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -949,10 +1007,11 @@ func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.ListServiceAccountsAdminAction,
|
Action: iampolicy.ListServiceAccountsAdminAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
}) {
|
}) {
|
||||||
requestUser := cred.AccessKey
|
requestUser := cred.AccessKey
|
||||||
if cred.ParentUser != "" {
|
if cred.ParentUser != "" {
|
||||||
@@ -984,12 +1043,19 @@ func (a adminAPIHandlers) InfoServiceAccount(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var expiration *time.Time
|
||||||
|
if !svcAccount.Expiration.IsZero() && !svcAccount.Expiration.Equal(timeSentinel) {
|
||||||
|
expiration = &svcAccount.Expiration
|
||||||
|
}
|
||||||
|
|
||||||
infoResp := madmin.InfoServiceAccountResp{
|
infoResp := madmin.InfoServiceAccountResp{
|
||||||
ParentUser: svcAccount.ParentUser,
|
ParentUser: svcAccount.ParentUser,
|
||||||
Comment: svcAccount.Comment,
|
Name: svcAccount.Name,
|
||||||
|
Description: svcAccount.Description,
|
||||||
AccountStatus: svcAccount.Status,
|
AccountStatus: svcAccount.Status,
|
||||||
ImpliedPolicy: policy == nil,
|
ImpliedPolicy: policy == nil,
|
||||||
Policy: string(policyJSON),
|
Policy: string(policyJSON),
|
||||||
|
Expiration: expiration,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(infoResp)
|
data, err := json.Marshal(infoResp)
|
||||||
@@ -1020,7 +1086,7 @@ func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1034,10 +1100,11 @@ func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Req
|
|||||||
if user != "" && user != cred.AccessKey {
|
if user != "" && user != cred.AccessKey {
|
||||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.ListServiceAccountsAdminAction,
|
Action: iampolicy.ListServiceAccountsAdminAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
}) {
|
}) {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1094,7 +1161,7 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1112,10 +1179,11 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
|
|||||||
|
|
||||||
adminPrivilege := globalIAMSys.IsAllowed(iampolicy.Args{
|
adminPrivilege := globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.RemoveServiceAccountAdminAction,
|
Action: iampolicy.RemoveServiceAccountAdminAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
})
|
})
|
||||||
|
|
||||||
if !adminPrivilege {
|
if !adminPrivilege {
|
||||||
@@ -1166,7 +1234,7 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1187,10 +1255,10 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.ListBucketAction,
|
Action: iampolicy.ListBucketAction,
|
||||||
BucketName: bucketName,
|
BucketName: bucketName,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
ObjectName: "",
|
ObjectName: "",
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
}) {
|
}) {
|
||||||
rd = true
|
rd = true
|
||||||
}
|
}
|
||||||
@@ -1200,10 +1268,10 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.GetBucketLocationAction,
|
Action: iampolicy.GetBucketLocationAction,
|
||||||
BucketName: bucketName,
|
BucketName: bucketName,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
ObjectName: "",
|
ObjectName: "",
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
}) {
|
}) {
|
||||||
rd = true
|
rd = true
|
||||||
}
|
}
|
||||||
@@ -1213,10 +1281,10 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.PutObjectAction,
|
Action: iampolicy.PutObjectAction,
|
||||||
BucketName: bucketName,
|
BucketName: bucketName,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
ObjectName: "",
|
ObjectName: "",
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
}) {
|
}) {
|
||||||
wr = true
|
wr = true
|
||||||
}
|
}
|
||||||
@@ -1261,8 +1329,8 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
accountName = cred.ParentUser
|
accountName = cred.ParentUser
|
||||||
}
|
}
|
||||||
|
|
||||||
roleArn := iampolicy.Args{Claims: claims}.GetRoleArn()
|
roleArn := iampolicy.Args{Claims: cred.Claims}.GetRoleArn()
|
||||||
policySetFromClaims, hasPolicyClaim := iampolicy.GetPoliciesFromClaims(claims, iamPolicyClaimNameOpenID())
|
policySetFromClaims, hasPolicyClaim := iampolicy.GetPoliciesFromClaims(cred.Claims, iamPolicyClaimNameOpenID())
|
||||||
var effectivePolicy iampolicy.Policy
|
var effectivePolicy iampolicy.Policy
|
||||||
|
|
||||||
var buf []byte
|
var buf []byte
|
||||||
@@ -1315,11 +1383,12 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
// Fetch the data usage of the current bucket
|
// Fetch the data usage of the current bucket
|
||||||
var size uint64
|
var size uint64
|
||||||
var objectsCount uint64
|
var objectsCount uint64
|
||||||
var objectsHist map[string]uint64
|
var objectsHist, versionsHist map[string]uint64
|
||||||
if !dataUsageInfo.LastUpdate.IsZero() {
|
if !dataUsageInfo.LastUpdate.IsZero() {
|
||||||
size = dataUsageInfo.BucketsUsage[bucket.Name].Size
|
size = dataUsageInfo.BucketsUsage[bucket.Name].Size
|
||||||
objectsCount = dataUsageInfo.BucketsUsage[bucket.Name].ObjectsCount
|
objectsCount = dataUsageInfo.BucketsUsage[bucket.Name].ObjectsCount
|
||||||
objectsHist = dataUsageInfo.BucketsUsage[bucket.Name].ObjectSizesHistogram
|
objectsHist = dataUsageInfo.BucketsUsage[bucket.Name].ObjectSizesHistogram
|
||||||
|
versionsHist = dataUsageInfo.BucketsUsage[bucket.Name].ObjectVersionsHistogram
|
||||||
}
|
}
|
||||||
// Fetch the prefix usage of the current bucket
|
// Fetch the prefix usage of the current bucket
|
||||||
var prefixUsage map[string]uint64
|
var prefixUsage map[string]uint64
|
||||||
@@ -1338,6 +1407,7 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
Size: size,
|
Size: size,
|
||||||
Objects: objectsCount,
|
Objects: objectsCount,
|
||||||
ObjectSizesHistogram: objectsHist,
|
ObjectSizesHistogram: objectsHist,
|
||||||
|
ObjectVersionsHistogram: versionsHist,
|
||||||
PrefixUsage: prefixUsage,
|
PrefixUsage: prefixUsage,
|
||||||
Details: &madmin.BucketDetails{
|
Details: &madmin.BucketDetails{
|
||||||
Versioning: globalBucketVersioningSys.Enabled(bucket.Name),
|
Versioning: globalBucketVersioningSys.Enabled(bucket.Name),
|
||||||
@@ -1607,6 +1677,12 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
|
|||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// When the user is root credential you are not allowed to
|
||||||
|
// add policies for root user.
|
||||||
|
if entityName == globalActiveCred.AccessKey {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that user or group exists.
|
// Validate that user or group exists.
|
||||||
@@ -1702,7 +1778,7 @@ func (a adminAPIHandlers) AttachPolicyBuiltin(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, _, _, s3Err := validateAdminSignature(ctx, r, "")
|
cred, _, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1751,6 +1827,13 @@ func (a adminAPIHandlers) AttachPolicyBuiltin(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the user is root credential you are not allowed to
|
||||||
|
// add policies for root user.
|
||||||
|
if userOrGroup == globalActiveCred.AccessKey {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Validate that user exists.
|
// Validate that user exists.
|
||||||
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType {
|
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType {
|
||||||
_, ok := globalIAMSys.GetUser(ctx, userOrGroup)
|
_, ok := globalIAMSys.GetUser(ctx, userOrGroup)
|
||||||
@@ -1826,7 +1909,7 @@ func (a adminAPIHandlers) DetachPolicyBuiltin(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cred, _, _, s3Err := validateAdminSignature(ctx, r, "")
|
cred, _, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1937,7 +2020,7 @@ func (a adminAPIHandlers) DetachPolicyBuiltin(w http.ResponseWriter, r *http.Req
|
|||||||
UserOrGroup: userOrGroup,
|
UserOrGroup: userOrGroup,
|
||||||
UserType: int(userType),
|
UserType: int(userType),
|
||||||
IsGroup: isGroup,
|
IsGroup: isGroup,
|
||||||
Policy: strings.Join(policiesToDetach, ","),
|
Policy: newPolicies,
|
||||||
},
|
},
|
||||||
UpdatedAt: updatedAt,
|
UpdatedAt: updatedAt,
|
||||||
}))
|
}))
|
||||||
@@ -2029,22 +2112,22 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
case allUsersFile:
|
case allUsersFile:
|
||||||
userIdentities := make(map[string]UserIdentity)
|
userIdentities := make(map[string]UserIdentity)
|
||||||
globalIAMSys.store.rlock()
|
|
||||||
err := globalIAMSys.store.loadUsers(ctx, regUser, userIdentities)
|
err := globalIAMSys.store.loadUsers(ctx, regUser, userIdentities)
|
||||||
globalIAMSys.store.runlock()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userAccounts := make(map[string]madmin.AddOrUpdateUserReq)
|
userAccounts := make(map[string]madmin.AddOrUpdateUserReq)
|
||||||
for u, uid := range userIdentities {
|
for u, uid := range userIdentities {
|
||||||
status := madmin.AccountDisabled
|
|
||||||
if uid.Credentials.IsValid() {
|
|
||||||
status = madmin.AccountEnabled
|
|
||||||
}
|
|
||||||
userAccounts[u] = madmin.AddOrUpdateUserReq{
|
userAccounts[u] = madmin.AddOrUpdateUserReq{
|
||||||
SecretKey: uid.Credentials.SecretKey,
|
SecretKey: uid.Credentials.SecretKey,
|
||||||
Status: status,
|
Status: func() madmin.AccountStatus {
|
||||||
|
// Export current credential status
|
||||||
|
if uid.Credentials.Status == auth.AccountOff {
|
||||||
|
return madmin.AccountDisabled
|
||||||
|
}
|
||||||
|
return madmin.AccountEnabled
|
||||||
|
}(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userData, err := json.Marshal(userAccounts)
|
userData, err := json.Marshal(userAccounts)
|
||||||
@@ -2059,9 +2142,7 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
case allGroupsFile:
|
case allGroupsFile:
|
||||||
groups := make(map[string]GroupInfo)
|
groups := make(map[string]GroupInfo)
|
||||||
globalIAMSys.store.rlock()
|
|
||||||
err := globalIAMSys.store.loadGroups(ctx, groups)
|
err := globalIAMSys.store.loadGroups(ctx, groups)
|
||||||
globalIAMSys.store.runlock()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||||
return
|
return
|
||||||
@@ -2078,15 +2159,17 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
case allSvcAcctsFile:
|
case allSvcAcctsFile:
|
||||||
serviceAccounts := make(map[string]UserIdentity)
|
serviceAccounts := make(map[string]UserIdentity)
|
||||||
globalIAMSys.store.rlock()
|
|
||||||
err := globalIAMSys.store.loadUsers(ctx, svcUser, serviceAccounts)
|
err := globalIAMSys.store.loadUsers(ctx, svcUser, serviceAccounts)
|
||||||
globalIAMSys.store.runlock()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
svcAccts := make(map[string]madmin.SRSvcAccCreate)
|
svcAccts := make(map[string]madmin.SRSvcAccCreate)
|
||||||
for user, acc := range serviceAccounts {
|
for user, acc := range serviceAccounts {
|
||||||
|
if user == siteReplicatorSvcAcc {
|
||||||
|
// skip site-replication service account.
|
||||||
|
continue
|
||||||
|
}
|
||||||
claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey)
|
claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||||
@@ -2129,9 +2212,7 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
case userPolicyMappingsFile:
|
case userPolicyMappingsFile:
|
||||||
userPolicyMap := make(map[string]MappedPolicy)
|
userPolicyMap := make(map[string]MappedPolicy)
|
||||||
globalIAMSys.store.rlock()
|
|
||||||
err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, false, userPolicyMap)
|
err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, false, userPolicyMap)
|
||||||
globalIAMSys.store.runlock()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||||
return
|
return
|
||||||
@@ -2148,9 +2229,7 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
case groupPolicyMappingsFile:
|
case groupPolicyMappingsFile:
|
||||||
groupPolicyMap := make(map[string]MappedPolicy)
|
groupPolicyMap := make(map[string]MappedPolicy)
|
||||||
globalIAMSys.store.rlock()
|
|
||||||
err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, true, groupPolicyMap)
|
err := globalIAMSys.store.loadMappedPolicies(ctx, regUser, true, groupPolicyMap)
|
||||||
globalIAMSys.store.runlock()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||||
return
|
return
|
||||||
@@ -2167,9 +2246,7 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
case stsUserPolicyMappingsFile:
|
case stsUserPolicyMappingsFile:
|
||||||
userPolicyMap := make(map[string]MappedPolicy)
|
userPolicyMap := make(map[string]MappedPolicy)
|
||||||
globalIAMSys.store.rlock()
|
|
||||||
err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, false, userPolicyMap)
|
err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, false, userPolicyMap)
|
||||||
globalIAMSys.store.runlock()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||||
return
|
return
|
||||||
@@ -2185,9 +2262,7 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
case stsGroupPolicyMappingsFile:
|
case stsGroupPolicyMappingsFile:
|
||||||
groupPolicyMap := make(map[string]MappedPolicy)
|
groupPolicyMap := make(map[string]MappedPolicy)
|
||||||
globalIAMSys.store.rlock()
|
|
||||||
err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, true, groupPolicyMap)
|
err := globalIAMSys.store.loadMappedPolicies(ctx, stsUser, true, groupPolicyMap)
|
||||||
globalIAMSys.store.runlock()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||||
return
|
return
|
||||||
@@ -2217,7 +2292,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -2292,7 +2367,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
for accessKey, ureq := range userAccts {
|
for accessKey, ureq := range userAccts {
|
||||||
// Not allowed to add a user with same access key as root credential
|
// Not allowed to add a user with same access key as root credential
|
||||||
if owner && accessKey == cred.AccessKey {
|
if accessKey == globalActiveCred.AccessKey {
|
||||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2329,9 +2404,9 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.CreateUserAdminAction,
|
Action: iampolicy.CreateUserAdminAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
DenyOnly: checkDenyOnly,
|
DenyOnly: checkDenyOnly,
|
||||||
}) {
|
}) {
|
||||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allUsersFile, accessKey), r.URL)
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allUsersFile, accessKey), r.URL)
|
||||||
@@ -2420,12 +2495,12 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
AccountName: svcAcctReq.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
Groups: svcAcctReq.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.CreateServiceAccountAdminAction,
|
Action: iampolicy.CreateServiceAccountAdminAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
}) {
|
}) {
|
||||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allSvcAcctsFile, user), r.URL)
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allSvcAcctsFile, user), r.URL)
|
||||||
return
|
return
|
||||||
@@ -2443,7 +2518,9 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
opts := updateServiceAccountOpts{
|
opts := updateServiceAccountOpts{
|
||||||
secretKey: svcAcctReq.SecretKey,
|
secretKey: svcAcctReq.SecretKey,
|
||||||
status: svcAcctReq.Status,
|
status: svcAcctReq.Status,
|
||||||
comment: svcAcctReq.Comment,
|
name: svcAcctReq.Name,
|
||||||
|
description: svcAcctReq.Description,
|
||||||
|
expiration: svcAcctReq.Expiration,
|
||||||
sessionPolicy: sp,
|
sessionPolicy: sp,
|
||||||
}
|
}
|
||||||
_, err = globalIAMSys.UpdateServiceAccount(ctx, svcAcctReq.AccessKey, opts)
|
_, err = globalIAMSys.UpdateServiceAccount(ctx, svcAcctReq.AccessKey, opts)
|
||||||
@@ -2458,14 +2535,17 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
secretKey: svcAcctReq.SecretKey,
|
secretKey: svcAcctReq.SecretKey,
|
||||||
sessionPolicy: sp,
|
sessionPolicy: sp,
|
||||||
claims: svcAcctReq.Claims,
|
claims: svcAcctReq.Claims,
|
||||||
comment: svcAcctReq.Comment,
|
name: svcAcctReq.Name,
|
||||||
|
description: svcAcctReq.Description,
|
||||||
|
expiration: svcAcctReq.Expiration,
|
||||||
|
allowSiteReplicatorAccount: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// In case of LDAP we need to resolve the targetUser to a DN and
|
// In case of LDAP we need to resolve the targetUser to a DN and
|
||||||
// query their groups:
|
// query their groups:
|
||||||
if globalLDAPConfig.Enabled() {
|
if globalIAMSys.LDAPConfig.Enabled() {
|
||||||
opts.claims[ldapUserN] = svcAcctReq.AccessKey // simple username
|
opts.claims[ldapUserN] = svcAcctReq.AccessKey // simple username
|
||||||
targetUser, _, err := globalLDAPConfig.LookupUserDN(svcAcctReq.AccessKey)
|
targetUser, _, err := globalIAMSys.LDAPConfig.LookupUserDN(svcAcctReq.AccessKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
|
writeErrorResponseJSON(ctx, w, importError(ctx, err, allSvcAcctsFile, user), r.URL)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -825,7 +825,7 @@ func (s *TestSuiteIAM) TestGroupAddRemove(c *check) {
|
|||||||
if set.CreateStringSet(groups...).Contains(group) {
|
if set.CreateStringSet(groups...).Contains(group) {
|
||||||
c.Fatalf("created group still present!")
|
c.Fatalf("created group still present!")
|
||||||
}
|
}
|
||||||
groupInfo, err = s.adm.GetGroupDescription(ctx, group)
|
_, err = s.adm.GetGroupDescription(ctx, group)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.Fatalf("group appears to exist")
|
c.Fatalf("group appears to exist")
|
||||||
}
|
}
|
||||||
@@ -1261,6 +1261,52 @@ func (c *check) mustListBuckets(ctx context.Context, client *minio.Client) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *check) mustNotDelete(ctx context.Context, client *minio.Client, bucket string, vid string) {
|
||||||
|
c.Helper()
|
||||||
|
|
||||||
|
err := client.RemoveObject(ctx, bucket, "some-object", minio.RemoveObjectOptions{VersionID: vid})
|
||||||
|
if err == nil {
|
||||||
|
c.Fatalf("user must not be allowed to delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.RemoveObject(ctx, bucket, "some-object", minio.RemoveObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
c.Fatal("user must be able to create delete marker")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *check) mustDownload(ctx context.Context, client *minio.Client, bucket string) {
|
||||||
|
c.Helper()
|
||||||
|
rd, err := client.GetObject(ctx, bucket, "some-object", minio.GetObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("download did not succeed got %#v", err)
|
||||||
|
}
|
||||||
|
if _, err = io.Copy(io.Discard, rd); err != nil {
|
||||||
|
c.Fatalf("download did not succeed got %#v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *check) mustUploadReturnVersions(ctx context.Context, client *minio.Client, bucket string) []string {
|
||||||
|
c.Helper()
|
||||||
|
versions := []string{}
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
ui, err := client.PutObject(ctx, bucket, "some-object", bytes.NewBuffer([]byte("stuff")), 5, minio.PutObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("upload did not succeed got %#v", err)
|
||||||
|
}
|
||||||
|
versions = append(versions, ui.VersionID)
|
||||||
|
}
|
||||||
|
return versions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *check) mustUpload(ctx context.Context, client *minio.Client, bucket string) {
|
||||||
|
c.Helper()
|
||||||
|
_, err := client.PutObject(ctx, bucket, "some-object", bytes.NewBuffer([]byte("stuff")), 5, minio.PutObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("upload did not succeed got %#v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *check) mustNotUpload(ctx context.Context, client *minio.Client, bucket string) {
|
func (c *check) mustNotUpload(ctx context.Context, client *minio.Client, bucket string) {
|
||||||
c.Helper()
|
c.Helper()
|
||||||
_, err := client.PutObject(ctx, bucket, "some-object", bytes.NewBuffer([]byte("stuff")), 5, minio.PutObjectOptions{})
|
_, err := client.PutObject(ctx, bucket, "some-object", bytes.NewBuffer([]byte("stuff")), 5, minio.PutObjectOptions{})
|
||||||
|
|||||||
+76
-24
@@ -53,9 +53,9 @@ import (
|
|||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
"github.com/minio/minio/internal/kms"
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/minio/internal/logger/message/log"
|
|
||||||
"github.com/minio/mux"
|
"github.com/minio/mux"
|
||||||
iampolicy "github.com/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
|
"github.com/minio/pkg/logger/message/log"
|
||||||
xnet "github.com/minio/pkg/net"
|
xnet "github.com/minio/pkg/net"
|
||||||
"github.com/secure-io/sio-go"
|
"github.com/secure-io/sio-go"
|
||||||
)
|
)
|
||||||
@@ -533,16 +533,19 @@ func lriToLockEntry(l lockRequesterInfo, now time.Time, resource, server string)
|
|||||||
func topLockEntries(peerLocks []*PeerLocks, stale bool) madmin.LockEntries {
|
func topLockEntries(peerLocks []*PeerLocks, stale bool) madmin.LockEntries {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
entryMap := make(map[string]*madmin.LockEntry)
|
entryMap := make(map[string]*madmin.LockEntry)
|
||||||
|
toEntry := func(lri lockRequesterInfo) string {
|
||||||
|
return fmt.Sprintf("%s/%s", lri.Name, lri.UID)
|
||||||
|
}
|
||||||
for _, peerLock := range peerLocks {
|
for _, peerLock := range peerLocks {
|
||||||
if peerLock == nil {
|
if peerLock == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for k, v := range peerLock.Locks {
|
for k, v := range peerLock.Locks {
|
||||||
for _, lockReqInfo := range v {
|
for _, lockReqInfo := range v {
|
||||||
if val, ok := entryMap[lockReqInfo.Name]; ok {
|
if val, ok := entryMap[toEntry(lockReqInfo)]; ok {
|
||||||
val.ServerList = append(val.ServerList, peerLock.Addr)
|
val.ServerList = append(val.ServerList, peerLock.Addr)
|
||||||
} else {
|
} else {
|
||||||
entryMap[lockReqInfo.Name] = lriToLockEntry(lockReqInfo, now, k, peerLock.Addr)
|
entryMap[toEntry(lockReqInfo)] = lriToLockEntry(lockReqInfo, now, k, peerLock.Addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -767,7 +770,7 @@ func (a adminAPIHandlers) ProfileHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
var err error
|
var err error
|
||||||
duration, err = time.ParseDuration(dstr)
|
duration, err = time.ParseDuration(dstr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -803,6 +806,8 @@ func (a adminAPIHandlers) ProfileHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
globalProfilerMu.Lock()
|
||||||
|
defer globalProfilerMu.Unlock()
|
||||||
for k, v := range globalProfiler {
|
for k, v := range globalProfiler {
|
||||||
v.Stop()
|
v.Stop()
|
||||||
delete(globalProfiler, k)
|
delete(globalProfiler, k)
|
||||||
@@ -1013,7 +1018,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if hr.errBody == "" {
|
if hr.errBody == "" {
|
||||||
errorRespJSON = encodeResponseJSON(getAPIErrorResponse(ctx, hr.apiErr,
|
errorRespJSON = encodeResponseJSON(getAPIErrorResponse(ctx, hr.apiErr,
|
||||||
r.URL.Path, w.Header().Get(xhttp.AmzRequestID),
|
r.URL.Path, w.Header().Get(xhttp.AmzRequestID),
|
||||||
globalDeploymentID))
|
w.Header().Get(xhttp.AmzRequestHostID)))
|
||||||
} else {
|
} else {
|
||||||
errorRespJSON = encodeResponseJSON(APIErrorResponse{
|
errorRespJSON = encodeResponseJSON(APIErrorResponse{
|
||||||
Code: hr.apiErr.Code,
|
Code: hr.apiErr.Code,
|
||||||
@@ -1217,6 +1222,7 @@ func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.
|
|||||||
storageClass := strings.TrimSpace(r.Form.Get(peerRESTStorageClass))
|
storageClass := strings.TrimSpace(r.Form.Get(peerRESTStorageClass))
|
||||||
customBucket := strings.TrimSpace(r.Form.Get(peerRESTBucket))
|
customBucket := strings.TrimSpace(r.Form.Get(peerRESTBucket))
|
||||||
autotune := r.Form.Get("autotune") == "true"
|
autotune := r.Form.Get("autotune") == "true"
|
||||||
|
noClear := r.Form.Get("noclear") == "true"
|
||||||
|
|
||||||
size, err := strconv.Atoi(sizeStr)
|
size, err := strconv.Atoi(sizeStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1258,14 +1264,16 @@ func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bucketExists {
|
if !noClear && !bucketExists {
|
||||||
defer deleteObjectPerfBucket(objectAPI)
|
defer deleteObjectPerfBucket(objectAPI)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !noClear {
|
||||||
defer objectAPI.DeleteObject(ctx, customBucket, speedTest+SlashSeparator, ObjectOptions{
|
defer objectAPI.DeleteObject(ctx, customBucket, speedTest+SlashSeparator, ObjectOptions{
|
||||||
DeletePrefix: true,
|
DeletePrefix: true,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Freeze all incoming S3 API calls before running speedtest.
|
// Freeze all incoming S3 API calls before running speedtest.
|
||||||
globalNotificationSys.ServiceFreeze(ctx, true)
|
globalNotificationSys.ServiceFreeze(ctx, true)
|
||||||
@@ -1521,6 +1529,11 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Publish bootstrap events that have already occurred before client could subscribe.
|
||||||
|
if traceOpts.TraceTypes().Contains(madmin.TraceBootstrap) {
|
||||||
|
go globalBootstrapTracer.Publish(ctx, globalTrace)
|
||||||
|
}
|
||||||
|
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
if peer == nil {
|
if peer == nil {
|
||||||
continue
|
continue
|
||||||
@@ -1776,12 +1789,48 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
|
|||||||
writeSuccessResponseJSON(w, resp)
|
writeSuccessResponseJSON(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServerInfo(ctx context.Context, r *http.Request) madmin.InfoMessage {
|
func getPoolsInfo(ctx context.Context, allDisks []madmin.Disk) (map[int]map[int]madmin.ErasureSetInfo, error) {
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
return nil, errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
z, _ := objectAPI.(*erasureServerPools)
|
||||||
|
|
||||||
|
poolsInfo := make(map[int]map[int]madmin.ErasureSetInfo)
|
||||||
|
for _, d := range allDisks {
|
||||||
|
poolInfo, ok := poolsInfo[d.PoolIndex]
|
||||||
|
if !ok {
|
||||||
|
poolInfo = make(map[int]madmin.ErasureSetInfo)
|
||||||
|
}
|
||||||
|
erasureSet, ok := poolInfo[d.SetIndex]
|
||||||
|
if !ok {
|
||||||
|
erasureSet.ID = d.SetIndex
|
||||||
|
cache := dataUsageCache{}
|
||||||
|
if err := cache.load(ctx, z.serverPools[d.PoolIndex].sets[d.SetIndex], dataUsageCacheName); err == nil {
|
||||||
|
dataUsageInfo := cache.dui(dataUsageRoot, nil)
|
||||||
|
erasureSet.ObjectsCount = dataUsageInfo.ObjectsTotalCount
|
||||||
|
erasureSet.VersionsCount = dataUsageInfo.VersionsTotalCount
|
||||||
|
erasureSet.Usage = dataUsageInfo.ObjectsTotalSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
erasureSet.RawCapacity += d.TotalSpace
|
||||||
|
erasureSet.RawUsage += d.UsedSpace
|
||||||
|
if d.Healing {
|
||||||
|
erasureSet.HealDisks = 1
|
||||||
|
}
|
||||||
|
poolInfo[d.SetIndex] = erasureSet
|
||||||
|
poolsInfo[d.PoolIndex] = poolInfo
|
||||||
|
}
|
||||||
|
return poolsInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerInfo(ctx context.Context, poolsInfoEnabled bool, r *http.Request) madmin.InfoMessage {
|
||||||
kmsStat := fetchKMSStatus()
|
kmsStat := fetchKMSStatus()
|
||||||
|
|
||||||
ldap := madmin.LDAP{}
|
ldap := madmin.LDAP{}
|
||||||
if globalLDAPConfig.Enabled() {
|
if globalIAMSys.LDAPConfig.Enabled() {
|
||||||
ldapConn, err := globalLDAPConfig.LDAP.Connect()
|
ldapConn, err := globalIAMSys.LDAPConfig.LDAP.Connect()
|
||||||
//nolint:gocritic
|
//nolint:gocritic
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ldap.Status = string(madmin.ItemOffline)
|
ldap.Status = string(madmin.ItemOffline)
|
||||||
@@ -1805,7 +1854,9 @@ func getServerInfo(ctx context.Context, r *http.Request) madmin.InfoMessage {
|
|||||||
|
|
||||||
assignPoolNumbers(servers)
|
assignPoolNumbers(servers)
|
||||||
|
|
||||||
var backend interface{}
|
var poolsInfo map[int]map[int]madmin.ErasureSetInfo
|
||||||
|
var backend madmin.ErasureBackend
|
||||||
|
|
||||||
mode := madmin.ItemInitializing
|
mode := madmin.ItemInitializing
|
||||||
|
|
||||||
buckets := madmin.Buckets{}
|
buckets := madmin.Buckets{}
|
||||||
@@ -1832,7 +1883,6 @@ func getServerInfo(ctx context.Context, r *http.Request) madmin.InfoMessage {
|
|||||||
|
|
||||||
// Fetching the backend information
|
// Fetching the backend information
|
||||||
backendInfo := objectAPI.BackendInfo()
|
backendInfo := objectAPI.BackendInfo()
|
||||||
if backendInfo.Type == madmin.Erasure {
|
|
||||||
// Calculate the number of online/offline disks of all nodes
|
// Calculate the number of online/offline disks of all nodes
|
||||||
var allDisks []madmin.Disk
|
var allDisks []madmin.Disk
|
||||||
for _, s := range servers {
|
for _, s := range servers {
|
||||||
@@ -1846,11 +1896,12 @@ func getServerInfo(ctx context.Context, r *http.Request) madmin.InfoMessage {
|
|||||||
OfflineDisks: offlineDisks.Sum(),
|
OfflineDisks: offlineDisks.Sum(),
|
||||||
StandardSCParity: backendInfo.StandardSCParity,
|
StandardSCParity: backendInfo.StandardSCParity,
|
||||||
RRSCParity: backendInfo.RRSCParity,
|
RRSCParity: backendInfo.RRSCParity,
|
||||||
|
TotalSets: backendInfo.TotalSets,
|
||||||
|
DrivesPerSet: backendInfo.DrivesPerSet,
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
backend = madmin.FSBackend{
|
if poolsInfoEnabled {
|
||||||
Type: madmin.FsType,
|
poolsInfo, _ = getPoolsInfo(ctx, allDisks)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1876,6 +1927,7 @@ func getServerInfo(ctx context.Context, r *http.Request) madmin.InfoMessage {
|
|||||||
Services: services,
|
Services: services,
|
||||||
Backend: backend,
|
Backend: backend,
|
||||||
Servers: servers,
|
Servers: servers,
|
||||||
|
Pools: poolsInfo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1901,7 +1953,7 @@ func getKubernetesInfo(dctx context.Context) madmin.KubernetesInfo {
|
|||||||
ki.Error = err.Error()
|
ki.Error = err.Error()
|
||||||
return ki
|
return ki
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
decoder := json.NewDecoder(resp.Body)
|
decoder := json.NewDecoder(resp.Body)
|
||||||
if err := decoder.Decode(&ki); err != nil {
|
if err := decoder.Decode(&ki); err != nil {
|
||||||
ki.Error = err.Error()
|
ki.Error = err.Error()
|
||||||
@@ -2078,8 +2130,8 @@ func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *ur
|
|||||||
// No ellipses pattern. Anonymize host name from every pool arg
|
// No ellipses pattern. Anonymize host name from every pool arg
|
||||||
pools := strings.Fields(poolsArgs)
|
pools := strings.Fields(poolsArgs)
|
||||||
anonPools = make([]string, len(pools))
|
anonPools = make([]string, len(pools))
|
||||||
for _, arg := range pools {
|
for index, arg := range pools {
|
||||||
anonPools = append(anonPools, anonAddr(arg))
|
anonPools[index] = anonAddr(arg)
|
||||||
}
|
}
|
||||||
return cmdLineWithoutPools + strings.Join(anonPools, " ")
|
return cmdLineWithoutPools + strings.Join(anonPools, " ")
|
||||||
}
|
}
|
||||||
@@ -2182,7 +2234,7 @@ func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *ur
|
|||||||
getAndWriteSysConfig()
|
getAndWriteSysConfig()
|
||||||
|
|
||||||
if query.Get("minioinfo") == "true" {
|
if query.Get("minioinfo") == "true" {
|
||||||
infoMessage := getServerInfo(healthCtx, nil)
|
infoMessage := getServerInfo(healthCtx, false, nil)
|
||||||
servers := make([]madmin.ServerInfo, 0, len(infoMessage.Servers))
|
servers := make([]madmin.ServerInfo, 0, len(infoMessage.Servers))
|
||||||
for _, server := range infoMessage.Servers {
|
for _, server := range infoMessage.Servers {
|
||||||
anonEndpoint := anonAddr(server.Endpoint)
|
anonEndpoint := anonAddr(server.Endpoint)
|
||||||
@@ -2262,7 +2314,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
errResp := func(err error) {
|
errResp := func(err error) {
|
||||||
errorResponse := getAPIErrorResponse(ctx, toAdminAPIErr(ctx, err), r.URL.String(),
|
errorResponse := getAPIErrorResponse(ctx, toAdminAPIErr(ctx, err), r.URL.String(),
|
||||||
w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
|
w.Header().Get(xhttp.AmzRequestID), w.Header().Get(xhttp.AmzRequestHostID))
|
||||||
encodedErrorResponse := encodeResponse(errorResponse)
|
encodedErrorResponse := encodeResponse(errorResponse)
|
||||||
healthInfo.Error = string(encodedErrorResponse)
|
healthInfo.Error = string(encodedErrorResponse)
|
||||||
logger.LogIf(ctx, enc.Encode(healthInfo))
|
logger.LogIf(ctx, enc.Encode(healthInfo))
|
||||||
@@ -2357,7 +2409,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Marshal API response
|
// Marshal API response
|
||||||
jsonBytes, err := json.Marshal(getServerInfo(ctx, r))
|
jsonBytes, err := json.Marshal(getServerInfo(ctx, true, r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -2387,7 +2439,7 @@ func assignPoolNumbers(servers []madmin.ServerProperties) {
|
|||||||
func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus {
|
func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus {
|
||||||
lambdaMap := make(map[string][]madmin.TargetIDStatus)
|
lambdaMap := make(map[string][]madmin.TargetIDStatus)
|
||||||
|
|
||||||
for _, tgt := range globalConfigTargetList.Targets() {
|
for _, tgt := range globalNotifyTargetList.Targets() {
|
||||||
targetIDStatus := make(map[string]madmin.Status)
|
targetIDStatus := make(map[string]madmin.Status)
|
||||||
active, _ := tgt.IsActive()
|
active, _ := tgt.IsActive()
|
||||||
targetID := tgt.ID()
|
targetID := tgt.ID()
|
||||||
@@ -2459,13 +2511,13 @@ func fetchLoggerInfo() ([]madmin.Logger, []madmin.Audit) {
|
|||||||
var auditloggerInfo []madmin.Audit
|
var auditloggerInfo []madmin.Audit
|
||||||
for _, tgt := range logger.SystemTargets() {
|
for _, tgt := range logger.SystemTargets() {
|
||||||
if tgt.Endpoint() != "" {
|
if tgt.Endpoint() != "" {
|
||||||
loggerInfo = append(loggerInfo, madmin.Logger{tgt.String(): logger.TargetStatus(tgt)})
|
loggerInfo = append(loggerInfo, madmin.Logger{tgt.String(): logger.TargetStatus(GlobalContext, tgt)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tgt := range logger.AuditTargets() {
|
for _, tgt := range logger.AuditTargets() {
|
||||||
if tgt.Endpoint() != "" {
|
if tgt.Endpoint() != "" {
|
||||||
auditloggerInfo = append(auditloggerInfo, madmin.Audit{tgt.String(): logger.TargetStatus(tgt)})
|
auditloggerInfo = append(auditloggerInfo, madmin.Audit{tgt.String(): logger.TargetStatus(GlobalContext, tgt)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -380,3 +382,146 @@ func TestExtractHealInitParams(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type byResourceUID struct{ madmin.LockEntries }
|
||||||
|
|
||||||
|
func (b byResourceUID) Less(i, j int) bool {
|
||||||
|
toUniqLock := func(entry madmin.LockEntry) string {
|
||||||
|
return fmt.Sprintf("%s/%s", entry.Resource, entry.ID)
|
||||||
|
}
|
||||||
|
return toUniqLock(b.LockEntries[i]) < toUniqLock(b.LockEntries[j])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTopLockEntries(t *testing.T) {
|
||||||
|
locksHeld := make(map[string][]lockRequesterInfo)
|
||||||
|
var owners []string
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
owners = append(owners, fmt.Sprintf("node-%d", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate DeleteObjects of 10 objects in a single request. i.e same lock
|
||||||
|
// request UID, but 10 different resource names associated with it.
|
||||||
|
var lris []lockRequesterInfo
|
||||||
|
uuid := mustGetUUID()
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
resource := fmt.Sprintf("bucket/delete-object-%d", i)
|
||||||
|
lri := lockRequesterInfo{
|
||||||
|
Name: resource,
|
||||||
|
Writer: true,
|
||||||
|
UID: uuid,
|
||||||
|
Owner: owners[i%len(owners)],
|
||||||
|
Group: true,
|
||||||
|
Quorum: 3,
|
||||||
|
}
|
||||||
|
lris = append(lris, lri)
|
||||||
|
locksHeld[resource] = []lockRequesterInfo{lri}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a few concurrent read locks to the mix
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
resource := fmt.Sprintf("bucket/get-object-%d", i)
|
||||||
|
lri := lockRequesterInfo{
|
||||||
|
Name: resource,
|
||||||
|
UID: mustGetUUID(),
|
||||||
|
Owner: owners[i%len(owners)],
|
||||||
|
Quorum: 2,
|
||||||
|
}
|
||||||
|
lris = append(lris, lri)
|
||||||
|
locksHeld[resource] = append(locksHeld[resource], lri)
|
||||||
|
// concurrent read lock, same resource different uid
|
||||||
|
lri.UID = mustGetUUID()
|
||||||
|
lris = append(lris, lri)
|
||||||
|
locksHeld[resource] = append(locksHeld[resource], lri)
|
||||||
|
}
|
||||||
|
|
||||||
|
var peerLocks []*PeerLocks
|
||||||
|
for _, owner := range owners {
|
||||||
|
peerLocks = append(peerLocks, &PeerLocks{
|
||||||
|
Addr: owner,
|
||||||
|
Locks: locksHeld,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var exp madmin.LockEntries
|
||||||
|
for _, lri := range lris {
|
||||||
|
lockType := func(lri lockRequesterInfo) string {
|
||||||
|
if lri.Writer {
|
||||||
|
return "WRITE"
|
||||||
|
}
|
||||||
|
return "READ"
|
||||||
|
}
|
||||||
|
exp = append(exp, madmin.LockEntry{
|
||||||
|
Resource: lri.Name,
|
||||||
|
Type: lockType(lri),
|
||||||
|
ServerList: owners,
|
||||||
|
Owner: lri.Owner,
|
||||||
|
ID: lri.UID,
|
||||||
|
Quorum: lri.Quorum,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
peerLocks []*PeerLocks
|
||||||
|
expected madmin.LockEntries
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
peerLocks: peerLocks,
|
||||||
|
expected: exp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// printEntries := func(entries madmin.LockEntries) {
|
||||||
|
// for i, entry := range entries {
|
||||||
|
// fmt.Printf("%d: %s %s %s %s %v %d\n", i, entry.Resource, entry.ID, entry.Owner, entry.Type, entry.ServerList, entry.Elapsed)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
check := func(exp, got madmin.LockEntries) (int, bool) {
|
||||||
|
if len(exp) != len(got) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
sort.Slice(exp, byResourceUID{exp}.Less)
|
||||||
|
sort.Slice(got, byResourceUID{got}.Less)
|
||||||
|
// printEntries(exp)
|
||||||
|
// printEntries(got)
|
||||||
|
for i, e := range exp {
|
||||||
|
if !e.Timestamp.Equal(got[i].Timestamp) {
|
||||||
|
return i, false
|
||||||
|
}
|
||||||
|
// Skip checking elapsed since it's time sensitive.
|
||||||
|
// if e.Elapsed != got[i].Elapsed {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
if e.Resource != got[i].Resource {
|
||||||
|
return i, false
|
||||||
|
}
|
||||||
|
if e.Type != got[i].Type {
|
||||||
|
return i, false
|
||||||
|
}
|
||||||
|
if e.Source != got[i].Source {
|
||||||
|
return i, false
|
||||||
|
}
|
||||||
|
if e.Owner != got[i].Owner {
|
||||||
|
return i, false
|
||||||
|
}
|
||||||
|
if e.ID != got[i].ID {
|
||||||
|
return i, false
|
||||||
|
}
|
||||||
|
if len(e.ServerList) != len(got[i].ServerList) {
|
||||||
|
return i, false
|
||||||
|
}
|
||||||
|
for j := range e.ServerList {
|
||||||
|
if e.ServerList[j] != got[i].ServerList[j] {
|
||||||
|
return i, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, true
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range testCases {
|
||||||
|
got := topLockEntries(tc.peerLocks, false)
|
||||||
|
if idx, ok := check(tc.expected, got); !ok {
|
||||||
|
t.Fatalf("%d: mismatch at %d \n expected %#v but got %#v", i, idx, tc.expected[idx], got[idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+15
-17
@@ -407,9 +407,6 @@ type healSequence struct {
|
|||||||
// bucket, and object on which heal seq. was initiated
|
// bucket, and object on which heal seq. was initiated
|
||||||
bucket, object string
|
bucket, object string
|
||||||
|
|
||||||
// A channel of entities with heal result
|
|
||||||
respCh chan healResult
|
|
||||||
|
|
||||||
// Report healing progress
|
// Report healing progress
|
||||||
reportProgress bool
|
reportProgress bool
|
||||||
|
|
||||||
@@ -472,7 +469,6 @@ func newHealSequence(ctx context.Context, bucket, objPrefix, clientAddr string,
|
|||||||
clientToken := mustGetUUID()
|
clientToken := mustGetUUID()
|
||||||
|
|
||||||
return &healSequence{
|
return &healSequence{
|
||||||
respCh: make(chan healResult),
|
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
object: objPrefix,
|
object: objPrefix,
|
||||||
reportProgress: true,
|
reportProgress: true,
|
||||||
@@ -719,15 +715,18 @@ func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItem
|
|||||||
if serverDebugLog {
|
if serverDebugLog {
|
||||||
logger.Info("Task in the queue: %#v", task)
|
logger.Info("Task in the queue: %#v", task)
|
||||||
}
|
}
|
||||||
case <-h.ctx.Done():
|
|
||||||
return nil
|
|
||||||
default:
|
default:
|
||||||
// task queue is full, no more workers, we shall move on and heal later.
|
// task queue is full, no more workers, we shall move on and heal later.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
} else {
|
// Don't wait for result
|
||||||
// respCh must be set for guaranteed result
|
return nil
|
||||||
task.respCh = h.respCh
|
}
|
||||||
|
|
||||||
|
// respCh must be set to wait for result.
|
||||||
|
// We make it size 1, so a result can always be written
|
||||||
|
// even if we aren't listening.
|
||||||
|
task.respCh = make(chan healResult, 1)
|
||||||
select {
|
select {
|
||||||
case globalBackgroundHealRoutine.tasks <- task:
|
case globalBackgroundHealRoutine.tasks <- task:
|
||||||
if serverDebugLog {
|
if serverDebugLog {
|
||||||
@@ -736,11 +735,10 @@ func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItem
|
|||||||
case <-h.ctx.Done():
|
case <-h.ctx.Done():
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// task queued, now wait for the response.
|
// task queued, now wait for the response.
|
||||||
select {
|
select {
|
||||||
case res := <-h.respCh:
|
case res := <-task.respCh:
|
||||||
if !h.reportProgress {
|
if !h.reportProgress {
|
||||||
if errors.Is(res.err, errSkipFile) { // this is only sent usually by nopHeal
|
if errors.Is(res.err, errSkipFile) { // this is only sent usually by nopHeal
|
||||||
return nil
|
return nil
|
||||||
@@ -786,6 +784,11 @@ func (h *healSequence) healDiskMeta(objAPI ObjectLayer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *healSequence) healItems(objAPI ObjectLayer, bucketsOnly bool) error {
|
func (h *healSequence) healItems(objAPI ObjectLayer, bucketsOnly bool) error {
|
||||||
|
if h.clientToken == bgHealingUUID {
|
||||||
|
// For background heal do nothing.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.healDiskMeta(objAPI); err != nil {
|
if err := h.healDiskMeta(objAPI); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -871,15 +874,10 @@ func (h *healSequence) healBucket(objAPI ObjectLayer, bucket string, bucketsOnly
|
|||||||
|
|
||||||
if !h.settings.Recursive {
|
if !h.settings.Recursive {
|
||||||
if h.object != "" {
|
if h.object != "" {
|
||||||
// Check if an object named as the objPrefix exists,
|
if err := h.healObject(bucket, h.object, ""); err != nil {
|
||||||
// and if so heal it.
|
|
||||||
oi, err := objAPI.GetObjectInfo(h.ctx, bucket, h.object, ObjectOptions{})
|
|
||||||
if err == nil {
|
|
||||||
if err = h.healObject(bucket, h.object, oi.VersionID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,6 +237,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/describe-job").HandlerFunc(
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/describe-job").HandlerFunc(
|
||||||
gz(httpTraceHdrs(adminAPI.DescribeBatchJob)))
|
gz(httpTraceHdrs(adminAPI.DescribeBatchJob)))
|
||||||
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/cancel-job").HandlerFunc(
|
||||||
|
gz(httpTraceHdrs(adminAPI.CancelBatchJob)))
|
||||||
|
|
||||||
// Bucket migration operations
|
// Bucket migration operations
|
||||||
// ExportBucketMetaHandler
|
// ExportBucketMetaHandler
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
|
|
||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
"github.com/minio/minio/internal/config"
|
"github.com/minio/minio/internal/config"
|
||||||
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,7 +87,6 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
props := madmin.ServerProperties{
|
props := madmin.ServerProperties{
|
||||||
State: string(madmin.ItemInitializing),
|
|
||||||
Endpoint: addr,
|
Endpoint: addr,
|
||||||
Uptime: UTCNow().Unix() - globalBootTime.Unix(),
|
Uptime: UTCNow().Unix() - globalBootTime.Unix(),
|
||||||
Version: Version,
|
Version: Version,
|
||||||
@@ -118,7 +118,7 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
|
|||||||
config.EnvRootUser: {},
|
config.EnvRootUser: {},
|
||||||
config.EnvRootPassword: {},
|
config.EnvRootPassword: {},
|
||||||
config.EnvMinIOSubnetAPIKey: {},
|
config.EnvMinIOSubnetAPIKey: {},
|
||||||
config.EnvKMSSecretKey: {},
|
kms.EnvKMSSecretKey: {},
|
||||||
}
|
}
|
||||||
for _, v := range os.Environ() {
|
for _, v := range os.Environ() {
|
||||||
if !strings.HasPrefix(v, "MINIO") && !strings.HasPrefix(v, "_MINIO") {
|
if !strings.HasPrefix(v, "MINIO") && !strings.HasPrefix(v, "_MINIO") {
|
||||||
@@ -145,7 +145,8 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
|
|||||||
props.State = string(madmin.ItemOnline)
|
props.State = string(madmin.ItemOnline)
|
||||||
props.Disks = storageInfo.Disks
|
props.Disks = storageInfo.Disks
|
||||||
} else {
|
} else {
|
||||||
props.State = string(madmin.ItemOffline)
|
props.State = string(madmin.ItemInitializing)
|
||||||
|
props.Disks = getOfflineDisks("", globalEndpoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
return props
|
return props
|
||||||
|
|||||||
+91
-60
@@ -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
|
// This file is part of MinIO Object Storage stack
|
||||||
//
|
//
|
||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||||
|
"github.com/minio/minio/internal/ioutil"
|
||||||
"google.golang.org/api/googleapi"
|
"google.golang.org/api/googleapi"
|
||||||
|
|
||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
@@ -38,10 +39,12 @@ import (
|
|||||||
"github.com/minio/minio/internal/bucket/replication"
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
"github.com/minio/minio/internal/config/dns"
|
"github.com/minio/minio/internal/config/dns"
|
||||||
"github.com/minio/minio/internal/crypto"
|
"github.com/minio/minio/internal/crypto"
|
||||||
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
|
|
||||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||||
"github.com/minio/minio/internal/bucket/versioning"
|
"github.com/minio/minio/internal/bucket/versioning"
|
||||||
|
levent "github.com/minio/minio/internal/config/lambda/event"
|
||||||
"github.com/minio/minio/internal/event"
|
"github.com/minio/minio/internal/event"
|
||||||
"github.com/minio/minio/internal/hash"
|
"github.com/minio/minio/internal/hash"
|
||||||
"github.com/minio/pkg/bucket/policy"
|
"github.com/minio/pkg/bucket/policy"
|
||||||
@@ -132,6 +135,7 @@ const (
|
|||||||
ErrReplicationNeedsVersioningError
|
ErrReplicationNeedsVersioningError
|
||||||
ErrReplicationBucketNeedsVersioningError
|
ErrReplicationBucketNeedsVersioningError
|
||||||
ErrReplicationDenyEditError
|
ErrReplicationDenyEditError
|
||||||
|
ErrRemoteTargetDenyAddError
|
||||||
ErrReplicationNoExistingObjects
|
ErrReplicationNoExistingObjects
|
||||||
ErrObjectRestoreAlreadyInProgress
|
ErrObjectRestoreAlreadyInProgress
|
||||||
ErrNoSuchKey
|
ErrNoSuchKey
|
||||||
@@ -164,7 +168,6 @@ const (
|
|||||||
ErrMalformedDate
|
ErrMalformedDate
|
||||||
ErrMalformedPresignedDate
|
ErrMalformedPresignedDate
|
||||||
ErrMalformedCredentialDate
|
ErrMalformedCredentialDate
|
||||||
ErrMalformedCredentialRegion
|
|
||||||
ErrMalformedExpires
|
ErrMalformedExpires
|
||||||
ErrNegativeExpires
|
ErrNegativeExpires
|
||||||
ErrAuthHeaderEmpty
|
ErrAuthHeaderEmpty
|
||||||
@@ -197,6 +200,7 @@ const (
|
|||||||
ErrInvalidTagDirective
|
ErrInvalidTagDirective
|
||||||
ErrPolicyAlreadyAttached
|
ErrPolicyAlreadyAttached
|
||||||
ErrPolicyNotAttached
|
ErrPolicyNotAttached
|
||||||
|
ErrExcessData
|
||||||
// Add new error codes here.
|
// Add new error codes here.
|
||||||
|
|
||||||
// SSE-S3/SSE-KMS related API errors
|
// SSE-S3/SSE-KMS related API errors
|
||||||
@@ -219,6 +223,7 @@ const (
|
|||||||
ErrIncompatibleEncryptionMethod
|
ErrIncompatibleEncryptionMethod
|
||||||
ErrKMSNotConfigured
|
ErrKMSNotConfigured
|
||||||
ErrKMSKeyNotFoundException
|
ErrKMSKeyNotFoundException
|
||||||
|
ErrKMSDefaultKeyAlreadyConfigured
|
||||||
|
|
||||||
ErrNoAccessKey
|
ErrNoAccessKey
|
||||||
ErrInvalidToken
|
ErrInvalidToken
|
||||||
@@ -242,8 +247,6 @@ const (
|
|||||||
// Add new extended error codes here.
|
// Add new extended error codes here.
|
||||||
|
|
||||||
// MinIO extended errors.
|
// MinIO extended errors.
|
||||||
ErrReadQuorum
|
|
||||||
ErrWriteQuorum
|
|
||||||
ErrStorageFull
|
ErrStorageFull
|
||||||
ErrRequestBodyParse
|
ErrRequestBodyParse
|
||||||
ErrObjectExistsAsDirectory
|
ErrObjectExistsAsDirectory
|
||||||
@@ -267,6 +270,7 @@ const (
|
|||||||
ErrAdminNoSuchUser
|
ErrAdminNoSuchUser
|
||||||
ErrAdminNoSuchGroup
|
ErrAdminNoSuchGroup
|
||||||
ErrAdminGroupNotEmpty
|
ErrAdminGroupNotEmpty
|
||||||
|
ErrAdminGroupDisabled
|
||||||
ErrAdminNoSuchJob
|
ErrAdminNoSuchJob
|
||||||
ErrAdminNoSuchPolicy
|
ErrAdminNoSuchPolicy
|
||||||
ErrAdminPolicyChangeAlreadyApplied
|
ErrAdminPolicyChangeAlreadyApplied
|
||||||
@@ -280,6 +284,7 @@ const (
|
|||||||
ErrAdminConfigEnvOverridden
|
ErrAdminConfigEnvOverridden
|
||||||
ErrAdminConfigDuplicateKeys
|
ErrAdminConfigDuplicateKeys
|
||||||
ErrAdminConfigInvalidIDPType
|
ErrAdminConfigInvalidIDPType
|
||||||
|
ErrAdminConfigLDAPNonDefaultConfigName
|
||||||
ErrAdminConfigLDAPValidation
|
ErrAdminConfigLDAPValidation
|
||||||
ErrAdminConfigIDPCfgNameAlreadyExists
|
ErrAdminConfigIDPCfgNameAlreadyExists
|
||||||
ErrAdminConfigIDPCfgNameDoesNotExist
|
ErrAdminConfigIDPCfgNameDoesNotExist
|
||||||
@@ -410,6 +415,12 @@ const (
|
|||||||
ErrPostPolicyConditionInvalidFormat
|
ErrPostPolicyConditionInvalidFormat
|
||||||
|
|
||||||
ErrInvalidChecksum
|
ErrInvalidChecksum
|
||||||
|
|
||||||
|
// Lambda functions
|
||||||
|
ErrLambdaARNInvalid
|
||||||
|
ErrLambdaARNNotFound
|
||||||
|
|
||||||
|
apiErrCodeEnd // This is used only for the testing code
|
||||||
)
|
)
|
||||||
|
|
||||||
type errorCodeMap map[APIErrorCode]APIError
|
type errorCodeMap map[APIErrorCode]APIError
|
||||||
@@ -423,8 +434,7 @@ func (e errorCodeMap) ToAPIErrWithErr(errCode APIErrorCode, err error) APIError
|
|||||||
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
|
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
|
||||||
}
|
}
|
||||||
if globalSite.Region != "" {
|
if globalSite.Region != "" {
|
||||||
switch errCode {
|
if errCode == ErrAuthorizationHeaderMalformed {
|
||||||
case ErrAuthorizationHeaderMalformed:
|
|
||||||
apiErr.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalSite.Region)
|
apiErr.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalSite.Region)
|
||||||
return apiErr
|
return apiErr
|
||||||
}
|
}
|
||||||
@@ -519,6 +529,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Your proposed upload exceeds the maximum allowed object size.",
|
Description: "Your proposed upload exceeds the maximum allowed object size.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrExcessData: {
|
||||||
|
Code: "ExcessData",
|
||||||
|
Description: "More data provided than indicated content length",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrPolicyTooLarge: {
|
ErrPolicyTooLarge: {
|
||||||
Code: "PolicyTooLarge",
|
Code: "PolicyTooLarge",
|
||||||
Description: "Policy exceeds the maximum allowed document size.",
|
Description: "Policy exceeds the maximum allowed document size.",
|
||||||
@@ -919,6 +934,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "No matching ExistingsObjects rule enabled",
|
Description: "No matching ExistingsObjects rule enabled",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrRemoteTargetDenyAddError: {
|
||||||
|
Code: "XMinioAdminRemoteTargetDenyAdd",
|
||||||
|
Description: "Cannot add remote target endpoint since this server is in a cluster replication setup",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrReplicationDenyEditError: {
|
ErrReplicationDenyEditError: {
|
||||||
Code: "XMinioReplicationDenyEdit",
|
Code: "XMinioReplicationDenyEdit",
|
||||||
Description: "Cannot alter local replication config since this server is in a cluster replication setup",
|
Description: "Cannot alter local replication config since this server is in a cluster replication setup",
|
||||||
@@ -1166,6 +1186,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Invalid keyId",
|
Description: "Invalid keyId",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrKMSDefaultKeyAlreadyConfigured: {
|
||||||
|
Code: "KMS.DefaultKeyAlreadyConfiguredException",
|
||||||
|
Description: "A default encryption already exists and cannot be changed on KMS",
|
||||||
|
HTTPStatusCode: http.StatusConflict,
|
||||||
|
},
|
||||||
ErrNoAccessKey: {
|
ErrNoAccessKey: {
|
||||||
Code: "AccessDenied",
|
Code: "AccessDenied",
|
||||||
Description: "No AWSAccessKey was presented",
|
Description: "No AWSAccessKey was presented",
|
||||||
@@ -1250,6 +1275,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "The specified group is not empty - cannot remove it.",
|
Description: "The specified group is not empty - cannot remove it.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrAdminGroupDisabled: {
|
||||||
|
Code: "XMinioAdminGroupDisabled",
|
||||||
|
Description: "The specified group is disabled.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrAdminNoSuchPolicy: {
|
ErrAdminNoSuchPolicy: {
|
||||||
Code: "XMinioAdminNoSuchPolicy",
|
Code: "XMinioAdminNoSuchPolicy",
|
||||||
Description: "The canned policy does not exist.",
|
Description: "The canned policy does not exist.",
|
||||||
@@ -1312,6 +1342,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: fmt.Sprintf("Invalid IDP configuration type - must be one of %v", madmin.ValidIDPConfigTypes),
|
Description: fmt.Sprintf("Invalid IDP configuration type - must be one of %v", madmin.ValidIDPConfigTypes),
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrAdminConfigLDAPNonDefaultConfigName: {
|
||||||
|
Code: "XMinioAdminConfigLDAPNonDefaultConfigName",
|
||||||
|
Description: "Only a single LDAP configuration is supported - config name must be empty or `_`",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrAdminConfigLDAPValidation: {
|
ErrAdminConfigLDAPValidation: {
|
||||||
Code: "XMinioAdminConfigLDAPValidation",
|
Code: "XMinioAdminConfigLDAPValidation",
|
||||||
Description: "LDAP Configuration validation failed",
|
Description: "LDAP Configuration validation failed",
|
||||||
@@ -1509,7 +1544,7 @@ var errorCodes = errorCodeMap{
|
|||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrBusy: {
|
ErrBusy: {
|
||||||
Code: "Busy",
|
Code: "ServerBusy",
|
||||||
Description: "The service is unavailable. Please retry.",
|
Description: "The service is unavailable. Please retry.",
|
||||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
},
|
},
|
||||||
@@ -1948,6 +1983,16 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Invalid checksum provided.",
|
Description: "Invalid checksum provided.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrLambdaARNInvalid: {
|
||||||
|
Code: "LambdaARNInvalid",
|
||||||
|
Description: "The specified lambda ARN is invalid",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrLambdaARNNotFound: {
|
||||||
|
Code: "LambdaARNNotFound",
|
||||||
|
Description: "The specified lambda ARN does not exist",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
ErrPolicyAlreadyAttached: {
|
ErrPolicyAlreadyAttached: {
|
||||||
Code: "XMinioPolicyAlreadyAttached",
|
Code: "XMinioPolicyAlreadyAttached",
|
||||||
Description: "The specified policy is already attached.",
|
Description: "The specified policy is already attached.",
|
||||||
@@ -1971,15 +2016,15 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
|
|
||||||
// Only return ErrClientDisconnected if the provided context is actually canceled.
|
// Only return ErrClientDisconnected if the provided context is actually canceled.
|
||||||
// This way downstream context.Canceled will still report ErrOperationTimedOut
|
// This way downstream context.Canceled will still report ErrOperationTimedOut
|
||||||
if contextCanceled(ctx) {
|
if contextCanceled(ctx) && errors.Is(ctx.Err(), context.Canceled) {
|
||||||
if ctx.Err() == context.Canceled {
|
|
||||||
return ErrClientDisconnected
|
return ErrClientDisconnected
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
switch err {
|
switch err {
|
||||||
case errInvalidArgument:
|
case errInvalidArgument:
|
||||||
apiErr = ErrAdminInvalidArgument
|
apiErr = ErrAdminInvalidArgument
|
||||||
|
case errNoSuchPolicy:
|
||||||
|
apiErr = ErrAdminNoSuchPolicy
|
||||||
case errNoSuchUser:
|
case errNoSuchUser:
|
||||||
apiErr = ErrAdminNoSuchUser
|
apiErr = ErrAdminNoSuchUser
|
||||||
case errNoSuchServiceAccount:
|
case errNoSuchServiceAccount:
|
||||||
@@ -1990,8 +2035,6 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrAdminGroupNotEmpty
|
apiErr = ErrAdminGroupNotEmpty
|
||||||
case errNoSuchJob:
|
case errNoSuchJob:
|
||||||
apiErr = ErrAdminNoSuchJob
|
apiErr = ErrAdminNoSuchJob
|
||||||
case errNoSuchPolicy:
|
|
||||||
apiErr = ErrAdminNoSuchPolicy
|
|
||||||
case errNoPolicyToAttachOrDetach:
|
case errNoPolicyToAttachOrDetach:
|
||||||
apiErr = ErrAdminPolicyChangeAlreadyApplied
|
apiErr = ErrAdminPolicyChangeAlreadyApplied
|
||||||
case errSignatureMismatch:
|
case errSignatureMismatch:
|
||||||
@@ -2010,6 +2053,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrAdminInvalidSecretKey
|
apiErr = ErrAdminInvalidSecretKey
|
||||||
case errInvalidStorageClass:
|
case errInvalidStorageClass:
|
||||||
apiErr = ErrInvalidStorageClass
|
apiErr = ErrInvalidStorageClass
|
||||||
|
case errErasureReadQuorum:
|
||||||
|
apiErr = ErrSlowDown
|
||||||
|
case errErasureWriteQuorum:
|
||||||
|
apiErr = ErrSlowDown
|
||||||
// SSE errors
|
// SSE errors
|
||||||
case errInvalidEncryptionParameters:
|
case errInvalidEncryptionParameters:
|
||||||
apiErr = ErrInvalidEncryptionParameters
|
apiErr = ErrInvalidEncryptionParameters
|
||||||
@@ -2041,7 +2088,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrKMSNotConfigured
|
apiErr = ErrKMSNotConfigured
|
||||||
case errKMSKeyNotFound:
|
case errKMSKeyNotFound:
|
||||||
apiErr = ErrKMSKeyNotFoundException
|
apiErr = ErrKMSKeyNotFoundException
|
||||||
|
case errKMSDefaultKeyAlreadyConfigured:
|
||||||
|
apiErr = ErrKMSDefaultKeyAlreadyConfigured
|
||||||
case context.Canceled, context.DeadlineExceeded:
|
case context.Canceled, context.DeadlineExceeded:
|
||||||
apiErr = ErrOperationTimedOut
|
apiErr = ErrOperationTimedOut
|
||||||
case errDiskNotFound:
|
case errDiskNotFound:
|
||||||
@@ -2056,11 +2104,14 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrObjectLockInvalidHeaders
|
apiErr = ErrObjectLockInvalidHeaders
|
||||||
case objectlock.ErrMalformedXML:
|
case objectlock.ErrMalformedXML:
|
||||||
apiErr = ErrMalformedXML
|
apiErr = ErrMalformedXML
|
||||||
|
case errInvalidMaxParts:
|
||||||
|
apiErr = ErrInvalidMaxParts
|
||||||
|
case ioutil.ErrOverread:
|
||||||
|
apiErr = ErrExcessData
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compression errors
|
// Compression errors
|
||||||
switch err {
|
if err == errInvalidDecompressedSize {
|
||||||
case errInvalidDecompressedSize:
|
|
||||||
apiErr = ErrInvalidDecompressedSize
|
apiErr = ErrInvalidDecompressedSize
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2071,7 +2122,7 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
|
|
||||||
// etcd specific errors, a key is always a bucket for us return
|
// etcd specific errors, a key is always a bucket for us return
|
||||||
// ErrNoSuchBucket in such a case.
|
// ErrNoSuchBucket in such a case.
|
||||||
if err == dns.ErrNoEntriesFound {
|
if errors.Is(err, dns.ErrNoEntriesFound) {
|
||||||
return ErrNoSuchBucket
|
return ErrNoSuchBucket
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2138,10 +2189,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrContentSHA256Mismatch
|
apiErr = ErrContentSHA256Mismatch
|
||||||
case hash.ChecksumMismatch:
|
case hash.ChecksumMismatch:
|
||||||
apiErr = ErrContentChecksumMismatch
|
apiErr = ErrContentChecksumMismatch
|
||||||
case ObjectTooLarge:
|
case hash.SizeTooSmall:
|
||||||
apiErr = ErrEntityTooLarge
|
|
||||||
case ObjectTooSmall:
|
|
||||||
apiErr = ErrEntityTooSmall
|
apiErr = ErrEntityTooSmall
|
||||||
|
case hash.SizeTooLarge:
|
||||||
|
apiErr = ErrEntityTooLarge
|
||||||
case NotImplemented:
|
case NotImplemented:
|
||||||
apiErr = ErrNotImplemented
|
apiErr = ErrNotImplemented
|
||||||
case PartTooBig:
|
case PartTooBig:
|
||||||
@@ -2186,7 +2237,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrTransitionStorageClassNotFoundError
|
apiErr = ErrTransitionStorageClassNotFoundError
|
||||||
case InvalidObjectState:
|
case InvalidObjectState:
|
||||||
apiErr = ErrInvalidObjectState
|
apiErr = ErrInvalidObjectState
|
||||||
|
case PreConditionFailed:
|
||||||
|
apiErr = ErrPreconditionFailed
|
||||||
case BucketQuotaExceeded:
|
case BucketQuotaExceeded:
|
||||||
apiErr = ErrAdminBucketQuotaExceeded
|
apiErr = ErrAdminBucketQuotaExceeded
|
||||||
case *event.ErrInvalidEventName:
|
case *event.ErrInvalidEventName:
|
||||||
@@ -2195,6 +2247,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrARNNotification
|
apiErr = ErrARNNotification
|
||||||
case *event.ErrARNNotFound:
|
case *event.ErrARNNotFound:
|
||||||
apiErr = ErrARNNotification
|
apiErr = ErrARNNotification
|
||||||
|
case *levent.ErrInvalidARN:
|
||||||
|
apiErr = ErrLambdaARNInvalid
|
||||||
|
case *levent.ErrARNNotFound:
|
||||||
|
apiErr = ErrLambdaARNNotFound
|
||||||
case *event.ErrUnknownRegion:
|
case *event.ErrUnknownRegion:
|
||||||
apiErr = ErrRegionNotification
|
apiErr = ErrRegionNotification
|
||||||
case *event.ErrInvalidFilterName:
|
case *event.ErrInvalidFilterName:
|
||||||
@@ -2258,38 +2314,27 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apiErr := errorCodes.ToAPIErr(toAPIErrorCode(ctx, err))
|
apiErr := errorCodes.ToAPIErr(toAPIErrorCode(ctx, err))
|
||||||
e, ok := err.(dns.ErrInvalidBucketName)
|
switch apiErr.Code {
|
||||||
if ok {
|
case "NotImplemented":
|
||||||
code := toAPIErrorCode(ctx, e)
|
desc := fmt.Sprintf("%s (%v)", apiErr.Description, err)
|
||||||
apiErr = errorCodes.ToAPIErrWithErr(code, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if apiErr.Code == "NotImplemented" {
|
|
||||||
switch e := err.(type) {
|
|
||||||
case NotImplemented:
|
|
||||||
desc := e.Error()
|
|
||||||
if desc == "" {
|
|
||||||
desc = apiErr.Description
|
|
||||||
}
|
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: apiErr.Code,
|
Code: apiErr.Code,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
HTTPStatusCode: apiErr.HTTPStatusCode,
|
HTTPStatusCode: apiErr.HTTPStatusCode,
|
||||||
}
|
}
|
||||||
return apiErr
|
case "XMinioBackendDown":
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if apiErr.Code == "XMinioBackendDown" {
|
|
||||||
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
|
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
|
||||||
return apiErr
|
case "InternalError":
|
||||||
}
|
|
||||||
|
|
||||||
if apiErr.Code == "InternalError" {
|
|
||||||
// If we see an internal error try to interpret
|
// If we see an internal error try to interpret
|
||||||
// any underlying errors if possible depending on
|
// any underlying errors if possible depending on
|
||||||
// their internal error types.
|
// their internal error types.
|
||||||
switch e := err.(type) {
|
switch e := err.(type) {
|
||||||
|
case kms.Error:
|
||||||
|
apiErr = APIError{
|
||||||
|
Description: e.Err.Error(),
|
||||||
|
Code: e.APICode,
|
||||||
|
HTTPStatusCode: e.HTTPStatusCode,
|
||||||
|
}
|
||||||
case batchReplicationJobError:
|
case batchReplicationJobError:
|
||||||
apiErr = APIError(e)
|
apiErr = APIError(e)
|
||||||
case InvalidArgument:
|
case InvalidArgument:
|
||||||
@@ -2301,21 +2346,19 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
case *xml.SyntaxError:
|
case *xml.SyntaxError:
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "MalformedXML",
|
Code: "MalformedXML",
|
||||||
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrMalformedXML].Description,
|
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrMalformedXML].Description, e),
|
||||||
e.Error()),
|
|
||||||
HTTPStatusCode: errorCodes[ErrMalformedXML].HTTPStatusCode,
|
HTTPStatusCode: errorCodes[ErrMalformedXML].HTTPStatusCode,
|
||||||
}
|
}
|
||||||
case url.EscapeError:
|
case url.EscapeError:
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "XMinioInvalidObjectName",
|
Code: "XMinioInvalidObjectName",
|
||||||
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrInvalidObjectName].Description,
|
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrInvalidObjectName].Description, e),
|
||||||
e.Error()),
|
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
}
|
}
|
||||||
case versioning.Error:
|
case versioning.Error:
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "IllegalVersioningConfigurationException",
|
Code: "IllegalVersioningConfigurationException",
|
||||||
Description: fmt.Sprintf("Versioning configuration specified in the request is invalid. (%s)", e.Error()),
|
Description: fmt.Sprintf("Versioning configuration specified in the request is invalid. (%s)", e),
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
}
|
}
|
||||||
case lifecycle.Error:
|
case lifecycle.Error:
|
||||||
@@ -2381,19 +2424,7 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
// Add more other SDK related errors here if any in future.
|
// Add more other SDK related errors here if any in future.
|
||||||
default:
|
default:
|
||||||
//nolint:gocritic
|
//nolint:gocritic
|
||||||
if errors.Is(err, errMalformedEncoding) {
|
if errors.Is(err, errMalformedEncoding) || errors.Is(err, errChunkTooBig) || errors.Is(err, strconv.ErrRange) {
|
||||||
apiErr = APIError{
|
|
||||||
Code: "BadRequest",
|
|
||||||
Description: err.Error(),
|
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
|
||||||
}
|
|
||||||
} else if errors.Is(err, errChunkTooBig) {
|
|
||||||
apiErr = APIError{
|
|
||||||
Code: "BadRequest",
|
|
||||||
Description: err.Error(),
|
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
|
||||||
}
|
|
||||||
} else if errors.Is(err, strconv.ErrRange) {
|
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "BadRequest",
|
Code: "BadRequest",
|
||||||
Description: err.Error(),
|
Description: err.Error(),
|
||||||
|
|||||||
@@ -80,3 +80,19 @@ func TestAPIErrCode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if an API error is properly defined
|
||||||
|
func TestAPIErrCodeDefinition(t *testing.T) {
|
||||||
|
for errAPI := ErrNone + 1; errAPI < apiErrCodeEnd; errAPI++ {
|
||||||
|
errCode, ok := errorCodes[errAPI]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal(errAPI, "error code is not defined in the API error code table")
|
||||||
|
}
|
||||||
|
if errCode.Code == "" {
|
||||||
|
t.Fatal(errAPI, "error code has an empty XML code")
|
||||||
|
}
|
||||||
|
if errCode.HTTPStatusCode == 0 {
|
||||||
|
t.Fatal(errAPI, "error code has a zero HTTP status code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+49
-6
@@ -35,6 +35,7 @@ import (
|
|||||||
"github.com/minio/minio/internal/hash"
|
"github.com/minio/minio/internal/hash"
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
|
"github.com/minio/pkg/bucket/policy"
|
||||||
xxml "github.com/minio/xxml"
|
xxml "github.com/minio/xxml"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -359,6 +360,7 @@ type Object struct {
|
|||||||
|
|
||||||
// UserMetadata user-defined metadata
|
// UserMetadata user-defined metadata
|
||||||
UserMetadata *Metadata `xml:"UserMetadata,omitempty"`
|
UserMetadata *Metadata `xml:"UserMetadata,omitempty"`
|
||||||
|
UserTags string `xml:"UserTags,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyObjectResponse container returns ETag and LastModified of the successfully copied object
|
// CopyObjectResponse container returns ETag and LastModified of the successfully copied object
|
||||||
@@ -492,7 +494,7 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generates an ListBucketVersions response for the said bucket with other enumerated options.
|
// generates an ListBucketVersions response for the said bucket with other enumerated options.
|
||||||
func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType string, maxKeys int, resp ListObjectVersionsInfo) ListVersionsResponse {
|
func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType string, maxKeys int, resp ListObjectVersionsInfo, metadata metaCheckFn) ListVersionsResponse {
|
||||||
versions := make([]ObjectVersion, 0, len(resp.Objects))
|
versions := make([]ObjectVersion, 0, len(resp.Objects))
|
||||||
|
|
||||||
owner := Owner{
|
owner := Owner{
|
||||||
@@ -500,11 +502,19 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
|
|||||||
DisplayName: "minio",
|
DisplayName: "minio",
|
||||||
}
|
}
|
||||||
data := ListVersionsResponse{}
|
data := ListVersionsResponse{}
|
||||||
|
var lastObjMetaName string
|
||||||
|
var tagErr, metaErr APIErrorCode = -1, -1
|
||||||
|
|
||||||
for _, object := range resp.Objects {
|
for _, object := range resp.Objects {
|
||||||
if object.Name == "" {
|
if object.Name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Cache checks for the same object
|
||||||
|
if metadata != nil && lastObjMetaName != object.Name {
|
||||||
|
tagErr = metadata(object.Name, policy.GetObjectTaggingAction)
|
||||||
|
metaErr = metadata(object.Name, policy.GetObjectAction)
|
||||||
|
lastObjMetaName = object.Name
|
||||||
|
}
|
||||||
content := ObjectVersion{}
|
content := ObjectVersion{}
|
||||||
content.Key = s3EncodeName(object.Name, encodingType)
|
content.Key = s3EncodeName(object.Name, encodingType)
|
||||||
content.LastModified = amztime.ISO8601Format(object.ModTime.UTC())
|
content.LastModified = amztime.ISO8601Format(object.ModTime.UTC())
|
||||||
@@ -517,6 +527,32 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
|
|||||||
} else {
|
} else {
|
||||||
content.StorageClass = globalMinioDefaultStorageClass
|
content.StorageClass = globalMinioDefaultStorageClass
|
||||||
}
|
}
|
||||||
|
if tagErr == ErrNone {
|
||||||
|
content.UserTags = object.UserTags
|
||||||
|
}
|
||||||
|
if metaErr == ErrNone {
|
||||||
|
content.UserMetadata = &Metadata{}
|
||||||
|
switch kind, _ := crypto.IsEncrypted(object.UserDefined); kind {
|
||||||
|
case crypto.S3:
|
||||||
|
content.UserMetadata.Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionAES)
|
||||||
|
case crypto.S3KMS:
|
||||||
|
content.UserMetadata.Set(xhttp.AmzServerSideEncryption, xhttp.AmzEncryptionKMS)
|
||||||
|
case crypto.SSEC:
|
||||||
|
content.UserMetadata.Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, xhttp.AmzEncryptionAES)
|
||||||
|
}
|
||||||
|
for k, v := range cleanMinioInternalMetadataKeys(object.UserDefined) {
|
||||||
|
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||||
|
// Do not need to send any internal metadata
|
||||||
|
// values to client.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w
|
||||||
|
if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content.UserMetadata.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
content.Owner = owner
|
content.Owner = owner
|
||||||
content.VersionID = object.VersionID
|
content.VersionID = object.VersionID
|
||||||
if content.VersionID == "" {
|
if content.VersionID == "" {
|
||||||
@@ -600,7 +636,7 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingTy
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generates an ListObjectsV2 response for the said bucket with other enumerated options.
|
// generates an ListObjectsV2 response for the said bucket with other enumerated options.
|
||||||
func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter, encodingType string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string, metadata bool) ListObjectsV2Response {
|
func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter, encodingType string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string, metadata metaCheckFn) ListObjectsV2Response {
|
||||||
contents := make([]Object, 0, len(objects))
|
contents := make([]Object, 0, len(objects))
|
||||||
owner := Owner{
|
owner := Owner{
|
||||||
ID: globalMinioDefaultOwnerID,
|
ID: globalMinioDefaultOwnerID,
|
||||||
@@ -625,7 +661,11 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
|||||||
content.StorageClass = globalMinioDefaultStorageClass
|
content.StorageClass = globalMinioDefaultStorageClass
|
||||||
}
|
}
|
||||||
content.Owner = owner
|
content.Owner = owner
|
||||||
if metadata {
|
if metadata != nil {
|
||||||
|
if metadata(object.Name, policy.GetObjectTaggingAction) == ErrNone {
|
||||||
|
content.UserTags = object.UserTags
|
||||||
|
}
|
||||||
|
if metadata(object.Name, policy.GetObjectAction) == ErrNone {
|
||||||
content.UserMetadata = &Metadata{}
|
content.UserMetadata = &Metadata{}
|
||||||
switch kind, _ := crypto.IsEncrypted(object.UserDefined); kind {
|
switch kind, _ := crypto.IsEncrypted(object.UserDefined); kind {
|
||||||
case crypto.S3:
|
case crypto.S3:
|
||||||
@@ -648,6 +688,7 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
|||||||
content.UserMetadata.Set(k, v)
|
content.UserMetadata.Set(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
contents = append(contents, content)
|
contents = append(contents, content)
|
||||||
}
|
}
|
||||||
data.Name = bucket
|
data.Name = bucket
|
||||||
@@ -673,6 +714,8 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type metaCheckFn = func(name string, action policy.Action) (s3Err APIErrorCode)
|
||||||
|
|
||||||
// generates CopyObjectResponse from etag and lastModified time.
|
// generates CopyObjectResponse from etag and lastModified time.
|
||||||
func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse {
|
func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse {
|
||||||
return CopyObjectResponse{
|
return CopyObjectResponse{
|
||||||
@@ -700,7 +743,7 @@ func generateInitiateMultipartUploadResponse(bucket, key, uploadID string) Initi
|
|||||||
|
|
||||||
// generates CompleteMultipartUploadResponse for given bucket, key, location and ETag.
|
// generates CompleteMultipartUploadResponse for given bucket, key, location and ETag.
|
||||||
func generateCompleteMultpartUploadResponse(bucket, key, location string, oi ObjectInfo) CompleteMultipartUploadResponse {
|
func generateCompleteMultpartUploadResponse(bucket, key, location string, oi ObjectInfo) CompleteMultipartUploadResponse {
|
||||||
cs := oi.decryptChecksums()
|
cs := oi.decryptChecksums(0)
|
||||||
c := CompleteMultipartUploadResponse{
|
c := CompleteMultipartUploadResponse{
|
||||||
Location: location,
|
Location: location,
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
@@ -875,7 +918,7 @@ func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError
|
|||||||
|
|
||||||
// Generate error response.
|
// Generate error response.
|
||||||
errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path,
|
errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path,
|
||||||
w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
|
w.Header().Get(xhttp.AmzRequestID), w.Header().Get(xhttp.AmzRequestHostID))
|
||||||
encodedErrorResponse := encodeResponse(errorResponse)
|
encodedErrorResponse := encodeResponse(errorResponse)
|
||||||
writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeXML)
|
writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeXML)
|
||||||
}
|
}
|
||||||
@@ -893,7 +936,7 @@ func writeErrorResponseString(ctx context.Context, w http.ResponseWriter, err AP
|
|||||||
// useful for admin APIs.
|
// useful for admin APIs.
|
||||||
func writeErrorResponseJSON(ctx context.Context, w http.ResponseWriter, err APIError, reqURL *url.URL) {
|
func writeErrorResponseJSON(ctx context.Context, w http.ResponseWriter, err APIError, reqURL *url.URL) {
|
||||||
// Generate error response.
|
// Generate error response.
|
||||||
errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path, w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
|
errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path, w.Header().Get(xhttp.AmzRequestID), w.Header().Get(xhttp.AmzRequestHostID))
|
||||||
encodedErrorResponse := encodeResponseJSON(errorResponse)
|
encodedErrorResponse := encodeResponseJSON(errorResponse)
|
||||||
writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeJSON)
|
writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeJSON)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-4
@@ -285,7 +285,10 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
// GetObjectLegalHold
|
// GetObjectLegalHold
|
||||||
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||||
collectAPIStats("getobjectlegalhold", maxClients(gz(httpTraceAll(api.GetObjectLegalHoldHandler))))).Queries("legal-hold", "")
|
collectAPIStats("getobjectlegalhold", maxClients(gz(httpTraceAll(api.GetObjectLegalHoldHandler))))).Queries("legal-hold", "")
|
||||||
// GetObject - note gzip compression is *not* added due to Range requests.
|
// GetObject with lambda ARNs
|
||||||
|
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||||
|
collectAPIStats("getobject", maxClients(gz(httpTraceHdrs(api.GetObjectLambdaHandler))))).Queries("lambdaArn", "{lambdaArn:.*}")
|
||||||
|
// GetObject
|
||||||
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||||
collectAPIStats("getobject", maxClients(gz(httpTraceHdrs(api.GetObjectHandler)))))
|
collectAPIStats("getobject", maxClients(gz(httpTraceHdrs(api.GetObjectHandler)))))
|
||||||
// CopyObject
|
// CopyObject
|
||||||
@@ -389,6 +392,9 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
router.Methods(http.MethodGet).HandlerFunc(
|
router.Methods(http.MethodGet).HandlerFunc(
|
||||||
collectAPIStats("listobjectsv2", maxClients(gz(httpTraceAll(api.ListObjectsV2Handler))))).Queries("list-type", "2")
|
collectAPIStats("listobjectsv2", maxClients(gz(httpTraceAll(api.ListObjectsV2Handler))))).Queries("list-type", "2")
|
||||||
// ListObjectVersions
|
// ListObjectVersions
|
||||||
|
router.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
collectAPIStats("listobjectversions", maxClients(gz(httpTraceAll(api.ListObjectVersionsMHandler))))).Queries("versions", "", "metadata", "true")
|
||||||
|
// ListObjectVersions
|
||||||
router.Methods(http.MethodGet).HandlerFunc(
|
router.Methods(http.MethodGet).HandlerFunc(
|
||||||
collectAPIStats("listobjectversions", maxClients(gz(httpTraceAll(api.ListObjectVersionsHandler))))).Queries("versions", "")
|
collectAPIStats("listobjectversions", maxClients(gz(httpTraceAll(api.ListObjectVersionsHandler))))).Queries("versions", "")
|
||||||
// GetBucketPolicyStatus
|
// GetBucketPolicyStatus
|
||||||
@@ -431,8 +437,9 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
router.Methods(http.MethodHead).HandlerFunc(
|
router.Methods(http.MethodHead).HandlerFunc(
|
||||||
collectAPIStats("headbucket", maxClients(gz(httpTraceAll(api.HeadBucketHandler)))))
|
collectAPIStats("headbucket", maxClients(gz(httpTraceAll(api.HeadBucketHandler)))))
|
||||||
// PostPolicy
|
// PostPolicy
|
||||||
router.Methods(http.MethodPost).HeadersRegexp(xhttp.ContentType, "multipart/form-data*").HandlerFunc(
|
router.Methods(http.MethodPost).MatcherFunc(func(r *http.Request, _ *mux.RouteMatch) bool {
|
||||||
collectAPIStats("postpolicybucket", maxClients(gz(httpTraceHdrs(api.PostPolicyBucketHandler)))))
|
return isRequestPostPolicySignatureV4(r)
|
||||||
|
}).HandlerFunc(collectAPIStats("postpolicybucket", maxClients(gz(httpTraceHdrs(api.PostPolicyBucketHandler)))))
|
||||||
// DeleteMultipleObjects
|
// DeleteMultipleObjects
|
||||||
router.Methods(http.MethodPost).HandlerFunc(
|
router.Methods(http.MethodPost).HandlerFunc(
|
||||||
collectAPIStats("deletemultipleobjects", maxClients(gz(httpTraceAll(api.DeleteMultipleObjectsHandler))))).Queries("delete", "")
|
collectAPIStats("deletemultipleobjects", maxClients(gz(httpTraceAll(api.DeleteMultipleObjectsHandler))))).Queries("delete", "")
|
||||||
@@ -516,7 +523,11 @@ func corsHandler(handler http.Handler) http.Handler {
|
|||||||
|
|
||||||
return cors.New(cors.Options{
|
return cors.New(cors.Options{
|
||||||
AllowOriginFunc: func(origin string) bool {
|
AllowOriginFunc: func(origin string) bool {
|
||||||
for _, allowedOrigin := range globalAPIConfig.getCorsAllowOrigins() {
|
allowedOrigins := globalAPIConfig.getCorsAllowOrigins()
|
||||||
|
if len(allowedOrigins) == 0 {
|
||||||
|
allowedOrigins = []string{"*"}
|
||||||
|
}
|
||||||
|
for _, allowedOrigin := range allowedOrigins {
|
||||||
if wildcard.MatchSimple(allowedOrigin, origin) {
|
if wildcard.MatchSimple(allowedOrigin, origin) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-8
@@ -94,14 +94,8 @@ func s3URLEncode(s string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// s3EncodeName encodes string in response when encodingType is specified in AWS S3 requests.
|
// s3EncodeName encodes string in response when encodingType is specified in AWS S3 requests.
|
||||||
func s3EncodeName(name string, encodingType string) (result string) {
|
func s3EncodeName(name, encodingType string) string {
|
||||||
// Quick path to exit
|
if strings.ToLower(encodingType) == "url" {
|
||||||
if encodingType == "" {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
encodingType = strings.ToLower(encodingType)
|
|
||||||
switch encodingType {
|
|
||||||
case "url":
|
|
||||||
return s3URLEncode(name)
|
return s3URLEncode(name)
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
|
|||||||
+204
-199
File diff suppressed because one or more lines are too long
+77
-25
@@ -25,6 +25,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -74,8 +75,11 @@ func isRequestPresignedSignatureV2(r *http.Request) bool {
|
|||||||
|
|
||||||
// Verify if request has AWS Post policy Signature Version '4'.
|
// Verify if request has AWS Post policy Signature Version '4'.
|
||||||
func isRequestPostPolicySignatureV4(r *http.Request) bool {
|
func isRequestPostPolicySignatureV4(r *http.Request) bool {
|
||||||
return strings.Contains(r.Header.Get(xhttp.ContentType), "multipart/form-data") &&
|
mediaType, _, err := mime.ParseMediaType(r.Header.Get(xhttp.ContentType))
|
||||||
r.Method == http.MethodPost
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return mediaType == "multipart/form-data" && r.Method == http.MethodPost
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation.
|
// Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation.
|
||||||
@@ -84,6 +88,18 @@ func isRequestSignStreamingV4(r *http.Request) bool {
|
|||||||
r.Method == http.MethodPut
|
r.Method == http.MethodPut
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation.
|
||||||
|
func isRequestSignStreamingTrailerV4(r *http.Request) bool {
|
||||||
|
return r.Header.Get(xhttp.AmzContentSha256) == streamingContentSHA256Trailer &&
|
||||||
|
r.Method == http.MethodPut
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify if the request has AWS Streaming Signature Version '4', with unsigned content and trailer.
|
||||||
|
func isRequestUnsignedTrailerV4(r *http.Request) bool {
|
||||||
|
return r.Header.Get(xhttp.AmzContentSha256) == unsignedPayloadTrailer &&
|
||||||
|
r.Method == http.MethodPut && strings.Contains(r.Header.Get(xhttp.ContentEncoding), streamingContentEncoding)
|
||||||
|
}
|
||||||
|
|
||||||
// Authorization type.
|
// Authorization type.
|
||||||
//
|
//
|
||||||
//go:generate stringer -type=authType -trimprefix=authType $GOFILE
|
//go:generate stringer -type=authType -trimprefix=authType $GOFILE
|
||||||
@@ -101,10 +117,12 @@ const (
|
|||||||
authTypeSignedV2
|
authTypeSignedV2
|
||||||
authTypeJWT
|
authTypeJWT
|
||||||
authTypeSTS
|
authTypeSTS
|
||||||
|
authTypeStreamingSignedTrailer
|
||||||
|
authTypeStreamingUnsignedTrailer
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get request authentication type.
|
// Get request authentication type.
|
||||||
func getRequestAuthType(r *http.Request) authType {
|
func getRequestAuthType(r *http.Request) (at authType) {
|
||||||
if r.URL != nil {
|
if r.URL != nil {
|
||||||
var err error
|
var err error
|
||||||
r.Form, err = url.ParseQuery(r.URL.RawQuery)
|
r.Form, err = url.ParseQuery(r.URL.RawQuery)
|
||||||
@@ -119,6 +137,10 @@ func getRequestAuthType(r *http.Request) authType {
|
|||||||
return authTypePresignedV2
|
return authTypePresignedV2
|
||||||
} else if isRequestSignStreamingV4(r) {
|
} else if isRequestSignStreamingV4(r) {
|
||||||
return authTypeStreamingSigned
|
return authTypeStreamingSigned
|
||||||
|
} else if isRequestSignStreamingTrailerV4(r) {
|
||||||
|
return authTypeStreamingSignedTrailer
|
||||||
|
} else if isRequestUnsignedTrailerV4(r) {
|
||||||
|
return authTypeStreamingUnsignedTrailer
|
||||||
} else if isRequestSignatureV4(r) {
|
} else if isRequestSignatureV4(r) {
|
||||||
return authTypeSigned
|
return authTypeSigned
|
||||||
} else if isRequestPresignedSignatureV4(r) {
|
} else if isRequestPresignedSignatureV4(r) {
|
||||||
@@ -135,7 +157,7 @@ func getRequestAuthType(r *http.Request) authType {
|
|||||||
return authTypeUnknown
|
return authTypeUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateAdminSignature(ctx context.Context, r *http.Request, region string) (auth.Credentials, map[string]interface{}, bool, APIErrorCode) {
|
func validateAdminSignature(ctx context.Context, r *http.Request, region string) (auth.Credentials, bool, APIErrorCode) {
|
||||||
var cred auth.Credentials
|
var cred auth.Credentials
|
||||||
var owner bool
|
var owner bool
|
||||||
s3Err := ErrAccessDenied
|
s3Err := ErrAccessDenied
|
||||||
@@ -144,24 +166,28 @@ func validateAdminSignature(ctx context.Context, r *http.Request, region string)
|
|||||||
// We only support admin credentials to access admin APIs.
|
// We only support admin credentials to access admin APIs.
|
||||||
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return cred, nil, owner, s3Err
|
return cred, owner, s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
// we only support V4 (no presign) with auth body
|
// we only support V4 (no presign) with auth body
|
||||||
s3Err = isReqAuthenticated(ctx, r, region, serviceS3)
|
s3Err = isReqAuthenticated(ctx, r, region, serviceS3)
|
||||||
}
|
}
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return cred, nil, owner, s3Err
|
return cred, owner, s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
return cred, cred.Claims, owner, ErrNone
|
logger.GetReqInfo(ctx).Cred = cred
|
||||||
|
logger.GetReqInfo(ctx).Owner = owner
|
||||||
|
logger.GetReqInfo(ctx).Region = globalSite.Region
|
||||||
|
|
||||||
|
return cred, owner, ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkAdminRequestAuth checks for authentication and authorization for the incoming
|
// checkAdminRequestAuth checks for authentication and authorization for the incoming
|
||||||
// request. It only accepts V2 and V4 requests. Presigned, JWT and anonymous requests
|
// request. It only accepts V2 and V4 requests. Presigned, JWT and anonymous requests
|
||||||
// are automatically rejected.
|
// are automatically rejected.
|
||||||
func checkAdminRequestAuth(ctx context.Context, r *http.Request, action iampolicy.AdminAction, region string) (auth.Credentials, APIErrorCode) {
|
func checkAdminRequestAuth(ctx context.Context, r *http.Request, action iampolicy.AdminAction, region string) (auth.Credentials, APIErrorCode) {
|
||||||
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, region)
|
cred, owner, s3Err := validateAdminSignature(ctx, r, region)
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return cred, s3Err
|
return cred, s3Err
|
||||||
}
|
}
|
||||||
@@ -169,9 +195,9 @@ func checkAdminRequestAuth(ctx context.Context, r *http.Request, action iampolic
|
|||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.Action(action),
|
Action: iampolicy.Action(action),
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: cred.Claims,
|
||||||
}) {
|
}) {
|
||||||
// Request is allowed return the appropriate access key.
|
// Request is allowed return the appropriate access key.
|
||||||
return cred, ErrNone
|
return cred, ErrNone
|
||||||
@@ -203,7 +229,7 @@ func getClaimsFromTokenWithSecret(token, secret string) (map[string]interface{},
|
|||||||
// that clients cannot decode the token using the temp
|
// that clients cannot decode the token using the temp
|
||||||
// secret keys and generate an entirely new claim by essentially
|
// secret keys and generate an entirely new claim by essentially
|
||||||
// hijacking the policies. We need to make sure that this is
|
// hijacking the policies. We need to make sure that this is
|
||||||
// based an admin credential such that token cannot be decoded
|
// based on admin credential such that token cannot be decoded
|
||||||
// on the client side and is treated like an opaque value.
|
// on the client side and is treated like an opaque value.
|
||||||
claims, err := auth.ExtractClaims(token, secret)
|
claims, err := auth.ExtractClaims(token, secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -253,7 +279,7 @@ func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]in
|
|||||||
return nil, ErrNoAccessKey
|
return nil, ErrNoAccessKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if token == "" && cred.IsTemp() {
|
if token == "" && cred.IsTemp() && !cred.IsServiceAccount() {
|
||||||
// Temporary credentials should always have x-amz-security-token
|
// Temporary credentials should always have x-amz-security-token
|
||||||
return nil, ErrInvalidToken
|
return nil, ErrInvalidToken
|
||||||
}
|
}
|
||||||
@@ -263,7 +289,7 @@ func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]in
|
|||||||
return nil, ErrInvalidToken
|
return nil, ErrInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
if cred.IsTemp() && subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
|
if !cred.IsServiceAccount() && cred.IsTemp() && subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
|
||||||
// validate token for temporary credentials only.
|
// validate token for temporary credentials only.
|
||||||
return nil, ErrInvalidToken
|
return nil, ErrInvalidToken
|
||||||
}
|
}
|
||||||
@@ -300,6 +326,17 @@ func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Ac
|
|||||||
return s3Err
|
return s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkRequestAuthTypeWithVID is similar to checkRequestAuthType
|
||||||
|
// passes versionID additionally.
|
||||||
|
func checkRequestAuthTypeWithVID(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName, versionID string) (s3Err APIErrorCode) {
|
||||||
|
logger.GetReqInfo(ctx).BucketName = bucketName
|
||||||
|
logger.GetReqInfo(ctx).ObjectName = objectName
|
||||||
|
logger.GetReqInfo(ctx).VersionID = versionID
|
||||||
|
|
||||||
|
_, _, s3Err = checkRequestAuthTypeCredential(ctx, r, action)
|
||||||
|
return s3Err
|
||||||
|
}
|
||||||
|
|
||||||
func authenticateRequest(ctx context.Context, r *http.Request, action policy.Action) (s3Err APIErrorCode) {
|
func authenticateRequest(ctx context.Context, r *http.Request, action policy.Action) (s3Err APIErrorCode) {
|
||||||
if logger.GetReqInfo(ctx) == nil {
|
if logger.GetReqInfo(ctx) == nil {
|
||||||
logger.LogIf(ctx, errors.New("unexpected context.Context does not have a logger.ReqInfo"), logger.Minio)
|
logger.LogIf(ctx, errors.New("unexpected context.Context does not have a logger.ReqInfo"), logger.Minio)
|
||||||
@@ -333,6 +370,7 @@ func authenticateRequest(ctx context.Context, r *http.Request, action policy.Act
|
|||||||
|
|
||||||
logger.GetReqInfo(ctx).Cred = cred
|
logger.GetReqInfo(ctx).Cred = cred
|
||||||
logger.GetReqInfo(ctx).Owner = owner
|
logger.GetReqInfo(ctx).Owner = owner
|
||||||
|
logger.GetReqInfo(ctx).Region = globalSite.Region
|
||||||
|
|
||||||
// region is valid only for CreateBucketAction.
|
// region is valid only for CreateBucketAction.
|
||||||
var region string
|
var region string
|
||||||
@@ -377,9 +415,10 @@ func authorizeRequest(ctx context.Context, r *http.Request, action policy.Action
|
|||||||
// Anonymous checks are not meant for ListAllBuckets action
|
// Anonymous checks are not meant for ListAllBuckets action
|
||||||
if globalPolicySys.IsAllowed(policy.Args{
|
if globalPolicySys.IsAllowed(policy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
Action: action,
|
Action: action,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
ConditionValues: getConditionValues(r, region, "", nil),
|
ConditionValues: getConditionValues(r, region, auth.AnonymousCredentials),
|
||||||
IsOwner: false,
|
IsOwner: false,
|
||||||
ObjectName: object,
|
ObjectName: object,
|
||||||
}) {
|
}) {
|
||||||
@@ -392,9 +431,10 @@ func authorizeRequest(ctx context.Context, r *http.Request, action policy.Action
|
|||||||
// verify as a fallback.
|
// verify as a fallback.
|
||||||
if globalPolicySys.IsAllowed(policy.Args{
|
if globalPolicySys.IsAllowed(policy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
Action: policy.ListBucketAction,
|
Action: policy.ListBucketAction,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
ConditionValues: getConditionValues(r, region, "", nil),
|
ConditionValues: getConditionValues(r, region, auth.AnonymousCredentials),
|
||||||
IsOwner: false,
|
IsOwner: false,
|
||||||
ObjectName: object,
|
ObjectName: object,
|
||||||
}) {
|
}) {
|
||||||
@@ -411,7 +451,7 @@ func authorizeRequest(ctx context.Context, r *http.Request, action policy.Action
|
|||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.Action(policy.DeleteObjectVersionAction),
|
Action: iampolicy.Action(policy.DeleteObjectVersionAction),
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
ObjectName: object,
|
ObjectName: object,
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: cred.Claims,
|
Claims: cred.Claims,
|
||||||
@@ -425,7 +465,7 @@ func authorizeRequest(ctx context.Context, r *http.Request, action policy.Action
|
|||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.Action(action),
|
Action: iampolicy.Action(action),
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
ObjectName: object,
|
ObjectName: object,
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: cred.Claims,
|
Claims: cred.Claims,
|
||||||
@@ -442,7 +482,7 @@ func authorizeRequest(ctx context.Context, r *http.Request, action policy.Action
|
|||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.ListBucketAction,
|
Action: iampolicy.ListBucketAction,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
ObjectName: object,
|
ObjectName: object,
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: cred.Claims,
|
Claims: cred.Claims,
|
||||||
@@ -545,6 +585,8 @@ var supportedS3AuthTypes = map[authType]struct{}{
|
|||||||
authTypeSignedV2: {},
|
authTypeSignedV2: {},
|
||||||
authTypePostPolicy: {},
|
authTypePostPolicy: {},
|
||||||
authTypeStreamingSigned: {},
|
authTypeStreamingSigned: {},
|
||||||
|
authTypeStreamingSignedTrailer: {},
|
||||||
|
authTypeStreamingUnsignedTrailer: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate if the authType is valid and supported.
|
// Validate if the authType is valid and supported.
|
||||||
@@ -560,7 +602,8 @@ func setAuthHandler(h http.Handler) http.Handler {
|
|||||||
tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
|
tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
|
||||||
|
|
||||||
aType := getRequestAuthType(r)
|
aType := getRequestAuthType(r)
|
||||||
if aType == authTypeSigned || aType == authTypeSignedV2 || aType == authTypeStreamingSigned {
|
switch aType {
|
||||||
|
case authTypeSigned, authTypeSignedV2, authTypeStreamingSigned, authTypeStreamingSignedTrailer:
|
||||||
// Verify if date headers are set, if not reject the request
|
// Verify if date headers are set, if not reject the request
|
||||||
amzDate, errCode := parseAmzDateHeader(r)
|
amzDate, errCode := parseAmzDateHeader(r)
|
||||||
if errCode != ErrNone {
|
if errCode != ErrNone {
|
||||||
@@ -572,6 +615,7 @@ func setAuthHandler(h http.Handler) http.Handler {
|
|||||||
// All our internal APIs are sensitive towards Date
|
// All our internal APIs are sensitive towards Date
|
||||||
// header, for all requests where Date header is not
|
// header, for all requests where Date header is not
|
||||||
// present we will reject such clients.
|
// present we will reject such clients.
|
||||||
|
defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
|
||||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(errCode), r.URL)
|
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(errCode), r.URL)
|
||||||
atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1)
|
atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1)
|
||||||
return
|
return
|
||||||
@@ -585,14 +629,21 @@ func setAuthHandler(h http.Handler) http.Handler {
|
|||||||
tc.ResponseRecorder.LogErrBody = true
|
tc.ResponseRecorder.LogErrBody = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
|
||||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrRequestTimeTooSkewed), r.URL)
|
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrRequestTimeTooSkewed), r.URL)
|
||||||
atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1)
|
atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if isSupportedS3AuthType(aType) || aType == authTypeJWT || aType == authTypeSTS {
|
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
|
case authTypeJWT, authTypeSTS:
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
if isSupportedS3AuthType(aType) {
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
@@ -600,6 +651,7 @@ func setAuthHandler(h http.Handler) http.Handler {
|
|||||||
tc.ResponseRecorder.LogErrBody = true
|
tc.ResponseRecorder.LogErrBody = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
|
||||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL)
|
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL)
|
||||||
atomic.AddUint64(&globalHTTPStats.rejectedRequestsAuth, 1)
|
atomic.AddUint64(&globalHTTPStats.rejectedRequestsAuth, 1)
|
||||||
})
|
})
|
||||||
@@ -637,7 +689,7 @@ func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate t
|
|||||||
return ErrAccessDenied
|
return ErrAccessDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
conditions := getConditionValues(r, "", cred.AccessKey, cred.Claims)
|
conditions := getConditionValues(r, "", cred)
|
||||||
conditions["object-lock-mode"] = []string{string(retMode)}
|
conditions["object-lock-mode"] = []string{string(retMode)}
|
||||||
conditions["object-lock-retain-until-date"] = []string{retDate.UTC().Format(time.RFC3339)}
|
conditions["object-lock-retain-until-date"] = []string{retDate.UTC().Format(time.RFC3339)}
|
||||||
if retDays > 0 {
|
if retDays > 0 {
|
||||||
@@ -685,7 +737,7 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN
|
|||||||
return ErrSignatureVersionNotSupported
|
return ErrSignatureVersionNotSupported
|
||||||
case authTypeSignedV2, authTypePresignedV2:
|
case authTypeSignedV2, authTypePresignedV2:
|
||||||
cred, owner, s3Err = getReqAccessKeyV2(r)
|
cred, owner, s3Err = getReqAccessKeyV2(r)
|
||||||
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
|
case authTypeStreamingSigned, authTypePresigned, authTypeSigned, authTypeStreamingSignedTrailer, authTypeStreamingUnsignedTrailer:
|
||||||
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
}
|
}
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
@@ -711,7 +763,7 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN
|
|||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: policy.Action(action),
|
Action: policy.Action(action),
|
||||||
BucketName: bucketName,
|
BucketName: bucketName,
|
||||||
ConditionValues: getConditionValues(r, "", "", nil),
|
ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
|
||||||
IsOwner: false,
|
IsOwner: false,
|
||||||
ObjectName: objectName,
|
ObjectName: objectName,
|
||||||
}) {
|
}) {
|
||||||
@@ -725,7 +777,7 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN
|
|||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: action,
|
Action: action,
|
||||||
BucketName: bucketName,
|
BucketName: bucketName,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
ObjectName: objectName,
|
ObjectName: objectName,
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: cred.Claims,
|
Claims: cred.Claims,
|
||||||
|
|||||||
@@ -375,8 +375,6 @@ func TestIsReqAuthenticated(t *testing.T) {
|
|||||||
|
|
||||||
initConfigSubsystem(ctx, objLayer)
|
initConfigSubsystem(ctx, objLayer)
|
||||||
|
|
||||||
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
|
||||||
|
|
||||||
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable create credential, %s", err)
|
t.Fatalf("unable create credential, %s", err)
|
||||||
@@ -384,6 +382,8 @@ func TestIsReqAuthenticated(t *testing.T) {
|
|||||||
|
|
||||||
globalActiveCred = creds
|
globalActiveCred = creds
|
||||||
|
|
||||||
|
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||||
|
|
||||||
// List of test cases for validating http request authentication.
|
// List of test cases for validating http request authentication.
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
req *http.Request
|
req *http.Request
|
||||||
@@ -464,9 +464,8 @@ func TestValidateAdminSignature(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initAllSubsystems(ctx)
|
initAllSubsystems(ctx)
|
||||||
initConfigSubsystem(ctx, objLayer)
|
|
||||||
|
|
||||||
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
initConfigSubsystem(ctx, objLayer)
|
||||||
|
|
||||||
creds, err := auth.CreateCredentials("admin", "mypassword")
|
creds, err := auth.CreateCredentials("admin", "mypassword")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -474,6 +473,8 @@ func TestValidateAdminSignature(t *testing.T) {
|
|||||||
}
|
}
|
||||||
globalActiveCred = creds
|
globalActiveCred = creds
|
||||||
|
|
||||||
|
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
AccessKey string
|
AccessKey string
|
||||||
SecretKey string
|
SecretKey string
|
||||||
@@ -492,7 +493,7 @@ func TestValidateAdminSignature(t *testing.T) {
|
|||||||
if err := signRequestV4(req, testCase.AccessKey, testCase.SecretKey); err != nil {
|
if err := signRequestV4(req, testCase.AccessKey, testCase.SecretKey); err != nil {
|
||||||
t.Fatalf("Unable to inititalized new signed http request %s", err)
|
t.Fatalf("Unable to inititalized new signed http request %s", err)
|
||||||
}
|
}
|
||||||
_, _, _, s3Error := validateAdminSignature(ctx, req, globalMinioDefaultRegion)
|
_, _, s3Error := validateAdminSignature(ctx, req, globalMinioDefaultRegion)
|
||||||
if s3Error != testCase.ErrCode {
|
if s3Error != testCase.ErrCode {
|
||||||
t.Errorf("Test %d: Unexpected s3error returned wanted %d, got %d", i+1, testCase.ErrCode, s3Error)
|
t.Errorf("Test %d: Unexpected s3error returned wanted %d, got %d", i+1, testCase.ErrCode, s3Error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ func _() {
|
|||||||
_ = x[authTypeSignedV2-7]
|
_ = x[authTypeSignedV2-7]
|
||||||
_ = x[authTypeJWT-8]
|
_ = x[authTypeJWT-8]
|
||||||
_ = x[authTypeSTS-9]
|
_ = x[authTypeSTS-9]
|
||||||
|
_ = x[authTypeStreamingSignedTrailer-10]
|
||||||
|
_ = x[authTypeStreamingUnsignedTrailer-11]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _authType_name = "UnknownAnonymousPresignedPresignedV2PostPolicyStreamingSignedSignedSignedV2JWTSTS"
|
const _authType_name = "UnknownAnonymousPresignedPresignedV2PostPolicyStreamingSignedSignedSignedV2JWTSTSStreamingSignedTrailerStreamingUnsignedTrailer"
|
||||||
|
|
||||||
var _authType_index = [...]uint8{0, 7, 16, 25, 36, 46, 61, 67, 75, 78, 81}
|
var _authType_index = [...]uint8{0, 7, 16, 25, 36, 46, 61, 67, 75, 78, 81, 103, 127}
|
||||||
|
|
||||||
func (i authType) String() string {
|
func (i authType) String() string {
|
||||||
if i < 0 || i >= authType(len(_authType_index)-1) {
|
if i < 0 || i >= authType(len(_authType_index)-1) {
|
||||||
|
|||||||
@@ -19,9 +19,14 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
|
"github.com/minio/minio/internal/logger"
|
||||||
|
"github.com/minio/pkg/env"
|
||||||
)
|
)
|
||||||
|
|
||||||
// healTask represents what to heal along with options
|
// healTask represents what to heal along with options
|
||||||
@@ -56,13 +61,43 @@ func activeListeners() int {
|
|||||||
return int(globalHTTPListen.Subscribers()) + int(globalTrace.Subscribers())
|
return int(globalHTTPListen.Subscribers()) + int(globalTrace.Subscribers())
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForLowHTTPReq() {
|
func waitForLowIO(maxIO int, maxWait time.Duration, currentIO func() int) {
|
||||||
var currentIO func() int
|
// No need to wait run at full speed.
|
||||||
if httpServer := newHTTPServerFn(); httpServer != nil {
|
if maxIO <= 0 {
|
||||||
currentIO = httpServer.GetRequestCount
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
globalHealConfig.Wait(currentIO, activeListeners)
|
const waitTick = 100 * time.Millisecond
|
||||||
|
|
||||||
|
tmpMaxWait := maxWait
|
||||||
|
|
||||||
|
for currentIO() >= maxIO {
|
||||||
|
if tmpMaxWait > 0 {
|
||||||
|
if tmpMaxWait < waitTick {
|
||||||
|
time.Sleep(tmpMaxWait)
|
||||||
|
} else {
|
||||||
|
time.Sleep(waitTick)
|
||||||
|
}
|
||||||
|
tmpMaxWait -= waitTick
|
||||||
|
}
|
||||||
|
if tmpMaxWait <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentHTTPIO() int {
|
||||||
|
httpServer := newHTTPServerFn()
|
||||||
|
if httpServer == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpServer.GetRequestCount() - activeListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForLowHTTPReq() {
|
||||||
|
maxIO, maxWait, _ := globalHealConfig.Clone()
|
||||||
|
waitForLowIO(maxIO, maxWait, currentHTTPIO)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
|
func initBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
|
||||||
@@ -111,9 +146,19 @@ func (h *healRoutine) AddWorker(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
|
|
||||||
func newHealRoutine() *healRoutine {
|
func newHealRoutine() *healRoutine {
|
||||||
workers := runtime.GOMAXPROCS(0) / 2
|
workers := runtime.GOMAXPROCS(0) / 2
|
||||||
|
|
||||||
|
if envHealWorkers := env.Get("_MINIO_HEAL_WORKERS", ""); envHealWorkers != "" {
|
||||||
|
if numHealers, err := strconv.Atoi(envHealWorkers); err != nil {
|
||||||
|
logger.LogIf(context.Background(), fmt.Errorf("invalid _MINIO_HEAL_WORKERS value: %w", err))
|
||||||
|
} else {
|
||||||
|
workers = numHealers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if workers == 0 {
|
if workers == 0 {
|
||||||
workers = 4
|
workers = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
return &healRoutine{
|
return &healRoutine{
|
||||||
tasks: make(chan healTask),
|
tasks: make(chan healTask),
|
||||||
workers: workers,
|
workers: workers,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
@@ -44,6 +45,7 @@ const (
|
|||||||
// healingTracker is used to persist healing information during a heal.
|
// healingTracker is used to persist healing information during a heal.
|
||||||
type healingTracker struct {
|
type healingTracker struct {
|
||||||
disk StorageAPI `msg:"-"`
|
disk StorageAPI `msg:"-"`
|
||||||
|
mu *sync.RWMutex `msg:"-"`
|
||||||
|
|
||||||
ID string
|
ID string
|
||||||
PoolIndex int
|
PoolIndex int
|
||||||
@@ -112,22 +114,76 @@ func loadHealingTracker(ctx context.Context, disk StorageAPI) (*healingTracker,
|
|||||||
}
|
}
|
||||||
h.disk = disk
|
h.disk = disk
|
||||||
h.ID = diskID
|
h.ID = diskID
|
||||||
|
h.mu = &sync.RWMutex{}
|
||||||
return &h, nil
|
return &h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newHealingTracker will create a new healing tracker for the disk.
|
// newHealingTracker will create a new healing tracker for the disk.
|
||||||
func newHealingTracker(disk StorageAPI, healID string) *healingTracker {
|
func newHealingTracker() *healingTracker {
|
||||||
diskID, _ := disk.GetDiskID()
|
return &healingTracker{
|
||||||
h := healingTracker{
|
mu: &sync.RWMutex{},
|
||||||
disk: disk,
|
|
||||||
ID: diskID,
|
|
||||||
HealID: healID,
|
|
||||||
Path: disk.String(),
|
|
||||||
Endpoint: disk.Endpoint().String(),
|
|
||||||
Started: time.Now().UTC(),
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initHealingTracker(disk StorageAPI, healID string) *healingTracker {
|
||||||
|
h := newHealingTracker()
|
||||||
|
diskID, _ := disk.GetDiskID()
|
||||||
|
h.disk = disk
|
||||||
|
h.ID = diskID
|
||||||
|
h.HealID = healID
|
||||||
|
h.Path = disk.String()
|
||||||
|
h.Endpoint = disk.Endpoint().String()
|
||||||
|
h.Started = time.Now().UTC()
|
||||||
h.PoolIndex, h.SetIndex, h.DiskIndex = disk.GetDiskLoc()
|
h.PoolIndex, h.SetIndex, h.DiskIndex = disk.GetDiskLoc()
|
||||||
return &h
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h healingTracker) getLastUpdate() time.Time {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
|
||||||
|
return h.LastUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h healingTracker) getBucket() string {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
|
||||||
|
return h.Bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *healingTracker) setBucket(bucket string) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
h.Bucket = bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h healingTracker) getObject() string {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
|
||||||
|
return h.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *healingTracker) setObject(object string) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
h.Object = object
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *healingTracker) updateProgress(success bool, bytes uint64) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
if success {
|
||||||
|
h.ItemsHealed++
|
||||||
|
h.BytesDone += bytes
|
||||||
|
} else {
|
||||||
|
h.ItemsFailed++
|
||||||
|
h.BytesFailed += bytes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update will update the tracker on the disk.
|
// update will update the tracker on the disk.
|
||||||
@@ -136,15 +192,18 @@ func (h *healingTracker) update(ctx context.Context) error {
|
|||||||
if h.disk.Healing() == nil {
|
if h.disk.Healing() == nil {
|
||||||
return fmt.Errorf("healingTracker: drive %q is not marked as healing", h.ID)
|
return fmt.Errorf("healingTracker: drive %q is not marked as healing", h.ID)
|
||||||
}
|
}
|
||||||
|
h.mu.Lock()
|
||||||
if h.ID == "" || h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
|
if h.ID == "" || h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
|
||||||
h.ID, _ = h.disk.GetDiskID()
|
h.ID, _ = h.disk.GetDiskID()
|
||||||
h.PoolIndex, h.SetIndex, h.DiskIndex = h.disk.GetDiskLoc()
|
h.PoolIndex, h.SetIndex, h.DiskIndex = h.disk.GetDiskLoc()
|
||||||
}
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
return h.save(ctx)
|
return h.save(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// save will unconditionally save the tracker and will be created if not existing.
|
// save will unconditionally save the tracker and will be created if not existing.
|
||||||
func (h *healingTracker) save(ctx context.Context) error {
|
func (h *healingTracker) save(ctx context.Context) error {
|
||||||
|
h.mu.Lock()
|
||||||
if h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
|
if h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
|
||||||
// Attempt to get location.
|
// Attempt to get location.
|
||||||
if api := newObjectLayerFn(); api != nil {
|
if api := newObjectLayerFn(); api != nil {
|
||||||
@@ -155,6 +214,7 @@ func (h *healingTracker) save(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
h.LastUpdate = time.Now().UTC()
|
h.LastUpdate = time.Now().UTC()
|
||||||
htrackerBytes, err := h.MarshalMsg(nil)
|
htrackerBytes, err := h.MarshalMsg(nil)
|
||||||
|
h.mu.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -176,6 +236,8 @@ func (h *healingTracker) delete(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *healingTracker) isHealed(bucket string) bool {
|
func (h *healingTracker) isHealed(bucket string) bool {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
for _, v := range h.HealedBuckets {
|
for _, v := range h.HealedBuckets {
|
||||||
if v == bucket {
|
if v == bucket {
|
||||||
return true
|
return true
|
||||||
@@ -186,6 +248,9 @@ func (h *healingTracker) isHealed(bucket string) bool {
|
|||||||
|
|
||||||
// resume will reset progress to the numbers at the start of the bucket.
|
// resume will reset progress to the numbers at the start of the bucket.
|
||||||
func (h *healingTracker) resume() {
|
func (h *healingTracker) resume() {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
h.ItemsHealed = h.ResumeItemsHealed
|
h.ItemsHealed = h.ResumeItemsHealed
|
||||||
h.ItemsFailed = h.ResumeItemsFailed
|
h.ItemsFailed = h.ResumeItemsFailed
|
||||||
h.BytesDone = h.ResumeBytesDone
|
h.BytesDone = h.ResumeBytesDone
|
||||||
@@ -195,6 +260,9 @@ func (h *healingTracker) resume() {
|
|||||||
// bucketDone should be called when a bucket is done healing.
|
// bucketDone should be called when a bucket is done healing.
|
||||||
// Adds the bucket to the list of healed buckets and updates resume numbers.
|
// Adds the bucket to the list of healed buckets and updates resume numbers.
|
||||||
func (h *healingTracker) bucketDone(bucket string) {
|
func (h *healingTracker) bucketDone(bucket string) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
h.ResumeItemsHealed = h.ItemsHealed
|
h.ResumeItemsHealed = h.ItemsHealed
|
||||||
h.ResumeItemsFailed = h.ItemsFailed
|
h.ResumeItemsFailed = h.ItemsFailed
|
||||||
h.ResumeBytesDone = h.BytesDone
|
h.ResumeBytesDone = h.BytesDone
|
||||||
@@ -211,6 +279,9 @@ func (h *healingTracker) bucketDone(bucket string) {
|
|||||||
// setQueuedBuckets will add buckets, but exclude any that is already in h.HealedBuckets.
|
// setQueuedBuckets will add buckets, but exclude any that is already in h.HealedBuckets.
|
||||||
// Order is preserved.
|
// Order is preserved.
|
||||||
func (h *healingTracker) setQueuedBuckets(buckets []BucketInfo) {
|
func (h *healingTracker) setQueuedBuckets(buckets []BucketInfo) {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
s := set.CreateStringSet(h.HealedBuckets...)
|
s := set.CreateStringSet(h.HealedBuckets...)
|
||||||
h.QueuedBuckets = make([]string, 0, len(buckets))
|
h.QueuedBuckets = make([]string, 0, len(buckets))
|
||||||
for _, b := range buckets {
|
for _, b := range buckets {
|
||||||
@@ -221,15 +292,22 @@ func (h *healingTracker) setQueuedBuckets(buckets []BucketInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *healingTracker) printTo(writer io.Writer) {
|
func (h *healingTracker) printTo(writer io.Writer) {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
|
||||||
b, err := json.MarshalIndent(h, "", " ")
|
b, err := json.MarshalIndent(h, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writer.Write([]byte(err.Error()))
|
writer.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
writer.Write(b)
|
writer.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// toHealingDisk converts the information to madmin.HealingDisk
|
// toHealingDisk converts the information to madmin.HealingDisk
|
||||||
func (h *healingTracker) toHealingDisk() madmin.HealingDisk {
|
func (h *healingTracker) toHealingDisk() madmin.HealingDisk {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
|
||||||
return madmin.HealingDisk{
|
return madmin.HealingDisk{
|
||||||
ID: h.ID,
|
ID: h.ID,
|
||||||
HealID: h.HealID,
|
HealID: h.HealID,
|
||||||
@@ -271,6 +349,9 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getLocalDisksToHeal() (disksToHeal Endpoints) {
|
func getLocalDisksToHeal() (disksToHeal Endpoints) {
|
||||||
|
globalLocalDrivesMu.RLock()
|
||||||
|
globalLocalDrives := globalLocalDrives
|
||||||
|
globalLocalDrivesMu.RUnlock()
|
||||||
for _, disk := range globalLocalDrives {
|
for _, disk := range globalLocalDrives {
|
||||||
_, err := disk.GetDiskID()
|
_, err := disk.GetDiskID()
|
||||||
if errors.Is(err, errUnformattedDisk) {
|
if errors.Is(err, errUnformattedDisk) {
|
||||||
@@ -299,7 +380,7 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
|||||||
defer disk.Close()
|
defer disk.Close()
|
||||||
poolIdx := globalEndpoints.GetLocalPoolIdx(disk.Endpoint())
|
poolIdx := globalEndpoints.GetLocalPoolIdx(disk.Endpoint())
|
||||||
if poolIdx < 0 {
|
if poolIdx < 0 {
|
||||||
return fmt.Errorf("unexpected pool index (%d) found in %s", poolIdx, disk.Endpoint())
|
return fmt.Errorf("unexpected pool index (%d) found for %s", poolIdx, disk.Endpoint())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the set index where the current endpoint belongs
|
// Calculate the set index where the current endpoint belongs
|
||||||
@@ -310,14 +391,15 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if setIdx < 0 {
|
if setIdx < 0 {
|
||||||
return fmt.Errorf("unexpected set index (%d) found in %s", setIdx, disk.Endpoint())
|
return fmt.Errorf("unexpected set index (%d) found for %s", setIdx, disk.Endpoint())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent parallel erasure set healing
|
// Prevent parallel erasure set healing
|
||||||
locker := z.NewNSLock(minioMetaBucket, fmt.Sprintf("new-drive-healing/%d/%d", poolIdx, setIdx))
|
locker := z.NewNSLock(minioMetaBucket, fmt.Sprintf("new-drive-healing/%d/%d", poolIdx, setIdx))
|
||||||
lkctx, err := locker.GetLock(ctx, newDiskHealingTimeout)
|
lkctx, err := locker.GetLock(ctx, newDiskHealingTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("Healing of drive '%v' on %s pool, belonging to %s erasure set already in progress: %w",
|
||||||
|
disk, humanize.Ordinal(poolIdx+1), humanize.Ordinal(setIdx+1), err)
|
||||||
}
|
}
|
||||||
ctx = lkctx.Context()
|
ctx = lkctx.Context()
|
||||||
defer locker.Unlock(lkctx)
|
defer locker.Unlock(lkctx)
|
||||||
@@ -325,19 +407,20 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
|||||||
// Load healing tracker in this disk
|
// Load healing tracker in this disk
|
||||||
tracker, err := loadHealingTracker(ctx, disk)
|
tracker, err := loadHealingTracker(ctx, disk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// A healing track can be not found when another disk in the same
|
// A healing tracker may be deleted if another disk in the
|
||||||
// erasure set and same healing-id successfully finished healing.
|
// same erasure set with same healing-id successfully finished
|
||||||
if err == errFileNotFound {
|
// healing.
|
||||||
|
if errors.Is(err, errFileNotFound) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to load a healing tracker on '%s': %w", disk, err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to load healing tracker on '%s': %w, re-initializing..", disk, err))
|
||||||
tracker = newHealingTracker(disk, mustGetUUID())
|
tracker = initHealingTracker(disk, mustGetUUID())
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(fmt.Sprintf("Proceeding to heal '%s' - 'mc admin heal alias/ --verbose' to check the status.", endpoint))
|
logger.Info(fmt.Sprintf("Healing drive '%s' - 'mc admin heal alias/ --verbose' to check the current status.", endpoint))
|
||||||
|
|
||||||
buckets, _ := z.ListBuckets(ctx, BucketOptions{})
|
buckets, _ := z.ListBuckets(ctx, BucketOptions{})
|
||||||
// Buckets data are dispersed in multiple zones/sets, make
|
// Buckets data are dispersed in multiple pools/sets, make
|
||||||
// sure to heal all bucket metadata configuration.
|
// sure to heal all bucket metadata configuration.
|
||||||
buckets = append(buckets, BucketInfo{
|
buckets = append(buckets, BucketInfo{
|
||||||
Name: pathJoin(minioMetaBucket, minioConfigPrefix),
|
Name: pathJoin(minioMetaBucket, minioConfigPrefix),
|
||||||
@@ -355,7 +438,7 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
|||||||
})
|
})
|
||||||
|
|
||||||
if serverDebugLog {
|
if serverDebugLog {
|
||||||
logger.Info("Healing drive '%v' on %s pool", disk, humanize.Ordinal(poolIdx+1))
|
logger.Info("Healing drive '%v' on %s pool, belonging to %s erasure set", disk, humanize.Ordinal(poolIdx+1), humanize.Ordinal(setIdx+1))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load bucket totals
|
// Load bucket totals
|
||||||
@@ -378,9 +461,13 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tracker.ItemsFailed > 0 {
|
if tracker.ItemsFailed > 0 {
|
||||||
logger.Info("Healing drive '%s' failed (healed: %d, failed: %d).", disk, tracker.ItemsHealed, tracker.ItemsFailed)
|
logger.Info("Healing of drive '%s' failed (healed: %d, failed: %d).", disk, tracker.ItemsHealed, tracker.ItemsFailed)
|
||||||
} else {
|
} else {
|
||||||
logger.Info("Healing drive '%s' complete (healed: %d, failed: %d).", disk, tracker.ItemsHealed, tracker.ItemsFailed)
|
logger.Info("Healing of drive '%s' complete (healed: %d, failed: %d).", disk, tracker.ItemsHealed, tracker.ItemsFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tracker.QueuedBuckets) > 0 {
|
||||||
|
return fmt.Errorf("not all buckets were healed: %v", tracker.QueuedBuckets)
|
||||||
}
|
}
|
||||||
|
|
||||||
if serverDebugLog {
|
if serverDebugLog {
|
||||||
@@ -388,7 +475,7 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
|||||||
logger.Info("\n")
|
logger.Info("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if tracker.HealID == "" { // HealID is empty only before Feb 2023
|
if tracker.HealID == "" { // HealID was empty only before Feb 2023
|
||||||
logger.LogIf(ctx, tracker.delete(ctx))
|
logger.LogIf(ctx, tracker.delete(ctx))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -397,7 +484,7 @@ func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint
|
|||||||
for _, disk := range z.serverPools[poolIdx].sets[setIdx].getDisks() {
|
for _, disk := range z.serverPools[poolIdx].sets[setIdx].getDisks() {
|
||||||
t, err := loadHealingTracker(ctx, disk)
|
t, err := loadHealingTracker(ctx, disk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != errFileNotFound {
|
if !errors.Is(err, errFileNotFound) {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -442,10 +529,12 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools) {
|
|||||||
for _, disk := range healDisks {
|
for _, disk := range healDisks {
|
||||||
go func(disk Endpoint) {
|
go func(disk Endpoint) {
|
||||||
globalBackgroundHealState.setDiskHealingStatus(disk, true)
|
globalBackgroundHealState.setDiskHealingStatus(disk, true)
|
||||||
err := healFreshDisk(ctx, z, disk)
|
if err := healFreshDisk(ctx, z, disk); err != nil {
|
||||||
if err != nil {
|
|
||||||
globalBackgroundHealState.setDiskHealingStatus(disk, false)
|
globalBackgroundHealState.setDiskHealingStatus(disk, false)
|
||||||
|
timedout := OperationTimedOut{}
|
||||||
|
if !errors.Is(err, context.Canceled) && !errors.As(err, &timedout) {
|
||||||
printEndpointError(disk, err, false)
|
printEndpointError(disk, err, false)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Only upon success pop the healed disk.
|
// Only upon success pop the healed disk.
|
||||||
|
|||||||
+727
-79
File diff suppressed because it is too large
Load Diff
@@ -1538,6 +1538,24 @@ func (z *BatchJobRequest) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "KeyRotate":
|
||||||
|
if dc.IsNil() {
|
||||||
|
err = dc.ReadNil()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "KeyRotate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
z.KeyRotate = nil
|
||||||
|
} else {
|
||||||
|
if z.KeyRotate == nil {
|
||||||
|
z.KeyRotate = new(BatchJobKeyRotateV1)
|
||||||
|
}
|
||||||
|
err = z.KeyRotate.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "KeyRotate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err = dc.Skip()
|
err = dc.Skip()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1551,9 +1569,9 @@ func (z *BatchJobRequest) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
|
|
||||||
// EncodeMsg implements msgp.Encodable
|
// EncodeMsg implements msgp.Encodable
|
||||||
func (z *BatchJobRequest) EncodeMsg(en *msgp.Writer) (err error) {
|
func (z *BatchJobRequest) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
// map header, size 5
|
// map header, size 6
|
||||||
// write "ID"
|
// write "ID"
|
||||||
err = en.Append(0x85, 0xa2, 0x49, 0x44)
|
err = en.Append(0x86, 0xa2, 0x49, 0x44)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1609,15 +1627,32 @@ func (z *BatchJobRequest) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// write "KeyRotate"
|
||||||
|
err = en.Append(0xa9, 0x4b, 0x65, 0x79, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if z.KeyRotate == nil {
|
||||||
|
err = en.WriteNil()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = z.KeyRotate.EncodeMsg(en)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "KeyRotate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalMsg implements msgp.Marshaler
|
// MarshalMsg implements msgp.Marshaler
|
||||||
func (z *BatchJobRequest) MarshalMsg(b []byte) (o []byte, err error) {
|
func (z *BatchJobRequest) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
o = msgp.Require(b, z.Msgsize())
|
o = msgp.Require(b, z.Msgsize())
|
||||||
// map header, size 5
|
// map header, size 6
|
||||||
// string "ID"
|
// string "ID"
|
||||||
o = append(o, 0x85, 0xa2, 0x49, 0x44)
|
o = append(o, 0x86, 0xa2, 0x49, 0x44)
|
||||||
o = msgp.AppendString(o, z.ID)
|
o = msgp.AppendString(o, z.ID)
|
||||||
// string "User"
|
// string "User"
|
||||||
o = append(o, 0xa4, 0x55, 0x73, 0x65, 0x72)
|
o = append(o, 0xa4, 0x55, 0x73, 0x65, 0x72)
|
||||||
@@ -1639,6 +1674,17 @@ func (z *BatchJobRequest) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// string "KeyRotate"
|
||||||
|
o = append(o, 0xa9, 0x4b, 0x65, 0x79, 0x52, 0x6f, 0x74, 0x61, 0x74, 0x65)
|
||||||
|
if z.KeyRotate == nil {
|
||||||
|
o = msgp.AppendNil(o)
|
||||||
|
} else {
|
||||||
|
o, err = z.KeyRotate.MarshalMsg(o)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "KeyRotate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1701,6 +1747,23 @@ func (z *BatchJobRequest) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "KeyRotate":
|
||||||
|
if msgp.IsNil(bts) {
|
||||||
|
bts, err = msgp.ReadNilBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
z.KeyRotate = nil
|
||||||
|
} else {
|
||||||
|
if z.KeyRotate == nil {
|
||||||
|
z.KeyRotate = new(BatchJobKeyRotateV1)
|
||||||
|
}
|
||||||
|
bts, err = z.KeyRotate.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "KeyRotate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
bts, err = msgp.Skip(bts)
|
bts, err = msgp.Skip(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1721,6 +1784,12 @@ func (z *BatchJobRequest) Msgsize() (s int) {
|
|||||||
} else {
|
} else {
|
||||||
s += z.Replicate.Msgsize()
|
s += z.Replicate.Msgsize()
|
||||||
}
|
}
|
||||||
|
s += 10
|
||||||
|
if z.KeyRotate == nil {
|
||||||
|
s += msgp.NilSize
|
||||||
|
} else {
|
||||||
|
s += z.KeyRotate.Msgsize()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,564 @@
|
|||||||
|
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
|
"github.com/minio/minio/internal/crypto"
|
||||||
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
|
"github.com/minio/minio/internal/kms"
|
||||||
|
"github.com/minio/minio/internal/logger"
|
||||||
|
"github.com/minio/pkg/env"
|
||||||
|
"github.com/minio/pkg/wildcard"
|
||||||
|
"github.com/minio/pkg/workers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// keyrotate:
|
||||||
|
// apiVersion: v1
|
||||||
|
// bucket: BUCKET
|
||||||
|
// prefix: PREFIX
|
||||||
|
// encryption:
|
||||||
|
// type: sse-s3 # valid values are sse-s3 and sse-kms
|
||||||
|
// key: <new-kms-key> # valid only for sse-kms
|
||||||
|
// context: <new-kms-key-context> # valid only for sse-kms
|
||||||
|
// # optional flags based filtering criteria
|
||||||
|
// # for all objects
|
||||||
|
// flags:
|
||||||
|
// filter:
|
||||||
|
// newerThan: "7d" # match objects newer than this value (e.g. 7d10h31s)
|
||||||
|
// olderThan: "7d" # match objects older than this value (e.g. 7d10h31s)
|
||||||
|
// createdAfter: "date" # match objects created after "date"
|
||||||
|
// createdBefore: "date" # match objects created before "date"
|
||||||
|
// tags:
|
||||||
|
// - key: "name"
|
||||||
|
// value: "pick*" # match objects with tag 'name', with all values starting with 'pick'
|
||||||
|
// metadata:
|
||||||
|
// - key: "content-type"
|
||||||
|
// value: "image/*" # match objects with 'content-type', with all values starting with 'image/'
|
||||||
|
// kmskey: "key-id" # match objects with KMS key-id (applicable only for sse-kms)
|
||||||
|
// notify:
|
||||||
|
// endpoint: "https://notify.endpoint" # notification endpoint to receive job status events
|
||||||
|
// token: "Bearer xxxxx" # optional authentication token for the notification endpoint
|
||||||
|
|
||||||
|
// retry:
|
||||||
|
// attempts: 10 # number of retries for the job before giving up
|
||||||
|
// delay: "500ms" # least amount of delay between each retry
|
||||||
|
|
||||||
|
//go:generate msgp -file $GOFILE -unexported
|
||||||
|
|
||||||
|
// BatchKeyRotateKV is a datatype that holds key and values for filtering of objects
|
||||||
|
// used by metadata filter as well as tags based filtering.
|
||||||
|
type BatchKeyRotateKV struct {
|
||||||
|
Key string `yaml:"key" json:"key"`
|
||||||
|
Value string `yaml:"value" json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate returns an error if key is empty
|
||||||
|
func (kv BatchKeyRotateKV) Validate() error {
|
||||||
|
if kv.Key == "" {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty indicates if kv is not set
|
||||||
|
func (kv BatchKeyRotateKV) Empty() bool {
|
||||||
|
return kv.Key == "" && kv.Value == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match matches input kv with kv, value will be wildcard matched depending on the user input
|
||||||
|
func (kv BatchKeyRotateKV) Match(ikv BatchKeyRotateKV) bool {
|
||||||
|
if kv.Empty() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.EqualFold(kv.Key, ikv.Key) {
|
||||||
|
return wildcard.Match(kv.Value, ikv.Value)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchKeyRotateRetry datatype represents total retry attempts and delay between each retries.
|
||||||
|
type BatchKeyRotateRetry struct {
|
||||||
|
Attempts int `yaml:"attempts" json:"attempts"` // number of retry attempts
|
||||||
|
Delay time.Duration `yaml:"delay" json:"delay"` // delay between each retries
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates input replicate retries.
|
||||||
|
func (r BatchKeyRotateRetry) Validate() error {
|
||||||
|
if r.Attempts < 0 {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Delay < 0 {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchKeyRotationType defines key rotation type
|
||||||
|
type BatchKeyRotationType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
sses3 BatchKeyRotationType = "sse-s3"
|
||||||
|
ssekms BatchKeyRotationType = "sse-kms"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BatchJobKeyRotateEncryption defines key rotation encryption options passed
|
||||||
|
type BatchJobKeyRotateEncryption struct {
|
||||||
|
Type BatchKeyRotationType `yaml:"type" json:"type"`
|
||||||
|
Key string `yaml:"key" json:"key"`
|
||||||
|
Context string `yaml:"context" json:"context"`
|
||||||
|
kmsContext kms.Context `msg:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates input key rotation encryption options.
|
||||||
|
func (e BatchJobKeyRotateEncryption) Validate() error {
|
||||||
|
if e.Type != sses3 && e.Type != ssekms {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
spaces := strings.HasPrefix(e.Key, " ") || strings.HasSuffix(e.Key, " ")
|
||||||
|
if e.Type == ssekms && spaces {
|
||||||
|
return crypto.ErrInvalidEncryptionKeyID
|
||||||
|
}
|
||||||
|
if e.Type == ssekms && GlobalKMS != nil {
|
||||||
|
ctx := kms.Context{}
|
||||||
|
if e.Context != "" {
|
||||||
|
b, err := base64.StdEncoding.DecodeString(e.Context)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
if err := json.Unmarshal(b, &ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.kmsContext = kms.Context{}
|
||||||
|
for k, v := range ctx {
|
||||||
|
e.kmsContext[k] = v
|
||||||
|
}
|
||||||
|
ctx["MinIO batch API"] = "batchrotate" // Context for a test key operation
|
||||||
|
if _, err := GlobalKMS.GenerateKey(GlobalContext, e.Key, ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchKeyRotateFilter holds all the filters currently supported for batch replication
|
||||||
|
type BatchKeyRotateFilter struct {
|
||||||
|
NewerThan time.Duration `yaml:"newerThan,omitempty" json:"newerThan"`
|
||||||
|
OlderThan time.Duration `yaml:"olderThan,omitempty" json:"olderThan"`
|
||||||
|
CreatedAfter time.Time `yaml:"createdAfter,omitempty" json:"createdAfter"`
|
||||||
|
CreatedBefore time.Time `yaml:"createdBefore,omitempty" json:"createdBefore"`
|
||||||
|
Tags []BatchKeyRotateKV `yaml:"tags,omitempty" json:"tags"`
|
||||||
|
Metadata []BatchKeyRotateKV `yaml:"metadata,omitempty" json:"metadata"`
|
||||||
|
KMSKeyID string `yaml:"kmskeyid" json:"kmskey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchKeyRotateNotification success or failure notification endpoint for each job attempts
|
||||||
|
type BatchKeyRotateNotification struct {
|
||||||
|
Endpoint string `yaml:"endpoint" json:"endpoint"`
|
||||||
|
Token string `yaml:"token" json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchJobKeyRotateFlags various configurations for replication job definition currently includes
|
||||||
|
// - filter
|
||||||
|
// - notify
|
||||||
|
// - retry
|
||||||
|
type BatchJobKeyRotateFlags struct {
|
||||||
|
Filter BatchKeyRotateFilter `yaml:"filter" json:"filter"`
|
||||||
|
Notify BatchKeyRotateNotification `yaml:"notify" json:"notify"`
|
||||||
|
Retry BatchKeyRotateRetry `yaml:"retry" json:"retry"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchJobKeyRotateV1 v1 of batch key rotation job
|
||||||
|
type BatchJobKeyRotateV1 struct {
|
||||||
|
APIVersion string `yaml:"apiVersion" json:"apiVersion"`
|
||||||
|
Flags BatchJobKeyRotateFlags `yaml:"flags" json:"flags"`
|
||||||
|
Bucket string `yaml:"bucket" json:"bucket"`
|
||||||
|
Prefix string `yaml:"prefix" json:"prefix"`
|
||||||
|
Endpoint string `yaml:"endpoint" json:"endpoint"`
|
||||||
|
Encryption BatchJobKeyRotateEncryption `yaml:"encryption" json:"encryption"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify notifies notification endpoint if configured regarding job failure or success.
|
||||||
|
func (r BatchJobKeyRotateV1) Notify(ctx context.Context, body io.Reader) error {
|
||||||
|
if r.Flags.Notify.Endpoint == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.Flags.Notify.Endpoint, body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Flags.Notify.Token != "" {
|
||||||
|
req.Header.Set("Authorization", r.Flags.Notify.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
clnt := http.Client{Transport: getRemoteInstanceTransport}
|
||||||
|
resp, err := clnt.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
xhttp.DrainBody(resp.Body)
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return errors.New(resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyRotate rotates encryption key of an object
|
||||||
|
func (r *BatchJobKeyRotateV1) KeyRotate(ctx context.Context, api ObjectLayer, objInfo ObjectInfo) error {
|
||||||
|
srcBucket := r.Bucket
|
||||||
|
srcObject := objInfo.Name
|
||||||
|
|
||||||
|
if objInfo.DeleteMarker || !objInfo.VersionPurgeStatus.Empty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sseKMS := crypto.S3KMS.IsEncrypted(objInfo.UserDefined)
|
||||||
|
sseS3 := crypto.S3.IsEncrypted(objInfo.UserDefined)
|
||||||
|
if !sseKMS && !sseS3 { // neither sse-s3 nor sse-kms disallowed
|
||||||
|
return errInvalidEncryptionParameters
|
||||||
|
}
|
||||||
|
if sseKMS && r.Encryption.Type == sses3 { // previously encrypted with sse-kms, now sse-s3 disallowed
|
||||||
|
return errInvalidEncryptionParameters
|
||||||
|
}
|
||||||
|
versioned := globalBucketVersioningSys.PrefixEnabled(srcBucket, srcObject)
|
||||||
|
versionSuspended := globalBucketVersioningSys.PrefixSuspended(srcBucket, srcObject)
|
||||||
|
|
||||||
|
lock := api.NewNSLock(r.Bucket, objInfo.Name)
|
||||||
|
lkctx, err := lock.GetLock(ctx, globalOperationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer lock.Unlock(lkctx)
|
||||||
|
|
||||||
|
opts := ObjectOptions{
|
||||||
|
VersionID: objInfo.VersionID,
|
||||||
|
Versioned: versioned,
|
||||||
|
VersionSuspended: versionSuspended,
|
||||||
|
NoLock: true,
|
||||||
|
}
|
||||||
|
obj, err := api.GetObjectInfo(ctx, r.Bucket, objInfo.Name, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
oi := obj.Clone()
|
||||||
|
var (
|
||||||
|
newKeyID string
|
||||||
|
newKeyContext kms.Context
|
||||||
|
)
|
||||||
|
encMetadata := make(map[string]string)
|
||||||
|
for k, v := range oi.UserDefined {
|
||||||
|
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||||
|
encMetadata[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sseKMS || sseS3) && r.Encryption.Type == ssekms {
|
||||||
|
if err = r.Encryption.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newKeyID = strings.TrimPrefix(r.Encryption.Key, crypto.ARNPrefix)
|
||||||
|
newKeyContext = r.Encryption.kmsContext
|
||||||
|
}
|
||||||
|
if err = rotateKey(ctx, []byte{}, newKeyID, []byte{}, r.Bucket, oi.Name, encMetadata, newKeyContext); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we are rotating the keys, make sure to update the metadata.
|
||||||
|
oi.metadataOnly = true
|
||||||
|
oi.keyRotation = true
|
||||||
|
for k, v := range encMetadata {
|
||||||
|
oi.UserDefined[k] = v
|
||||||
|
}
|
||||||
|
if _, err := api.CopyObject(ctx, r.Bucket, oi.Name, r.Bucket, oi.Name, oi, ObjectOptions{
|
||||||
|
VersionID: oi.VersionID,
|
||||||
|
}, ObjectOptions{
|
||||||
|
VersionID: oi.VersionID,
|
||||||
|
NoLock: true,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
batchKeyRotationName = "batch-rotate.bin"
|
||||||
|
batchKeyRotationFormat = 1
|
||||||
|
batchKeyRotateVersionV1 = 1
|
||||||
|
batchKeyRotateVersion = batchKeyRotateVersionV1
|
||||||
|
batchKeyRotateAPIVersion = "v1"
|
||||||
|
batchKeyRotateJobDefaultRetries = 3
|
||||||
|
batchKeyRotateJobDefaultRetryDelay = 250 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start the batch key rottion job, resumes if there was a pending job via "job.ID"
|
||||||
|
func (r *BatchJobKeyRotateV1) Start(ctx context.Context, api ObjectLayer, job BatchJobRequest) error {
|
||||||
|
ri := &batchJobInfo{
|
||||||
|
JobID: job.ID,
|
||||||
|
JobType: string(job.Type()),
|
||||||
|
StartTime: job.Started,
|
||||||
|
}
|
||||||
|
if err := ri.load(ctx, api, job); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
globalBatchJobsMetrics.save(job.ID, ri)
|
||||||
|
lastObject := ri.Object
|
||||||
|
|
||||||
|
delay := job.KeyRotate.Flags.Retry.Delay
|
||||||
|
if delay == 0 {
|
||||||
|
delay = batchKeyRotateJobDefaultRetryDelay
|
||||||
|
}
|
||||||
|
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
skip := func(info FileInfo) (ok bool) {
|
||||||
|
if r.Flags.Filter.OlderThan > 0 && time.Since(info.ModTime) < r.Flags.Filter.OlderThan {
|
||||||
|
// skip all objects that are newer than specified older duration
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Flags.Filter.NewerThan > 0 && time.Since(info.ModTime) >= r.Flags.Filter.NewerThan {
|
||||||
|
// skip all objects that are older than specified newer duration
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.Before(info.ModTime) {
|
||||||
|
// skip all objects that are created before the specified time.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.After(info.ModTime) {
|
||||||
|
// skip all objects that are created after the specified time.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.Flags.Filter.Tags) > 0 {
|
||||||
|
// Only parse object tags if tags filter is specified.
|
||||||
|
tagMap := map[string]string{}
|
||||||
|
tagStr := info.Metadata[xhttp.AmzObjectTagging]
|
||||||
|
if len(tagStr) != 0 {
|
||||||
|
t, err := tags.ParseObjectTags(tagStr)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tagMap = t.ToMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kv := range r.Flags.Filter.Tags {
|
||||||
|
for t, v := range tagMap {
|
||||||
|
if kv.Match(BatchKeyRotateKV{Key: t, Value: v}) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// None of the provided tags filter match skip the object
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.Flags.Filter.Metadata) > 0 {
|
||||||
|
for _, kv := range r.Flags.Filter.Metadata {
|
||||||
|
for k, v := range info.Metadata {
|
||||||
|
if !strings.HasPrefix(strings.ToLower(k), "x-amz-meta-") && !isStandardHeader(k) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// We only need to match x-amz-meta or standardHeaders
|
||||||
|
if kv.Match(BatchKeyRotateKV{Key: k, Value: v}) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// None of the provided metadata filters match skip the object.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r.Flags.Filter.KMSKeyID != "" {
|
||||||
|
if v, ok := info.Metadata[xhttp.AmzServerSideEncryptionKmsID]; ok && strings.TrimPrefix(v, crypto.ARNPrefix) != r.Flags.Filter.KMSKeyID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_KEYROTATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wk, err := workers.New(workerSize)
|
||||||
|
if err != nil {
|
||||||
|
// invalid worker size.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
retryAttempts := ri.RetryAttempts
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
results := make(chan ObjectInfo, 100)
|
||||||
|
if err := api.Walk(ctx, r.Bucket, r.Prefix, results, ObjectOptions{
|
||||||
|
WalkMarker: lastObject,
|
||||||
|
WalkFilter: skip,
|
||||||
|
}); err != nil {
|
||||||
|
cancel()
|
||||||
|
// Do not need to retry if we can't list objects on source.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for result := range results {
|
||||||
|
result := result
|
||||||
|
sseKMS := crypto.S3KMS.IsEncrypted(result.UserDefined)
|
||||||
|
sseS3 := crypto.S3.IsEncrypted(result.UserDefined)
|
||||||
|
if !sseKMS && !sseS3 { // neither sse-s3 nor sse-kms disallowed
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wk.Take()
|
||||||
|
go func() {
|
||||||
|
defer wk.Give()
|
||||||
|
for attempts := 1; attempts <= retryAttempts; attempts++ {
|
||||||
|
attempts := attempts
|
||||||
|
stopFn := globalBatchJobsMetrics.trace(batchKeyRotationMetricObject, job.ID, attempts, result)
|
||||||
|
success := true
|
||||||
|
if err := r.KeyRotate(ctx, api, result); err != nil {
|
||||||
|
stopFn(err)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
success = false
|
||||||
|
} else {
|
||||||
|
stopFn(nil)
|
||||||
|
}
|
||||||
|
ri.trackCurrentBucketObject(r.Bucket, result, success)
|
||||||
|
ri.RetryAttempts = attempts
|
||||||
|
globalBatchJobsMetrics.save(job.ID, ri)
|
||||||
|
// persist in-memory state to disk after every 10secs.
|
||||||
|
logger.LogIf(ctx, ri.updateAfter(ctx, api, 10*time.Second, job))
|
||||||
|
if success {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wk.Wait()
|
||||||
|
|
||||||
|
ri.Complete = ri.ObjectsFailed == 0
|
||||||
|
ri.Failed = ri.ObjectsFailed > 0
|
||||||
|
globalBatchJobsMetrics.save(job.ID, ri)
|
||||||
|
// persist in-memory state to disk.
|
||||||
|
logger.LogIf(ctx, ri.updateAfter(ctx, api, 0, job))
|
||||||
|
|
||||||
|
buf, _ := json.Marshal(ri)
|
||||||
|
if err := r.Notify(ctx, bytes.NewReader(buf)); err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("unable to notify %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
if ri.Failed {
|
||||||
|
ri.ObjectsFailed = 0
|
||||||
|
ri.Bucket = ""
|
||||||
|
ri.Object = ""
|
||||||
|
ri.Objects = 0
|
||||||
|
time.Sleep(delay + time.Duration(rnd.Float64()*float64(delay)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//msgp:ignore batchKeyRotationJobError
|
||||||
|
type batchKeyRotationJobError struct {
|
||||||
|
Code string
|
||||||
|
Description string
|
||||||
|
HTTPStatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e batchKeyRotationJobError) Error() string {
|
||||||
|
return e.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the job definition input
|
||||||
|
func (r *BatchJobKeyRotateV1) Validate(ctx context.Context, job BatchJobRequest, o ObjectLayer) error {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.APIVersion != batchKeyRotateAPIVersion {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Bucket == "" {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := o.GetBucketInfo(ctx, r.Bucket, BucketOptions{}); err != nil {
|
||||||
|
if isErrBucketNotFound(err) {
|
||||||
|
return batchKeyRotationJobError{
|
||||||
|
Code: "NoSuchSourceBucket",
|
||||||
|
Description: "The specified source bucket does not exist",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if GlobalKMS == nil {
|
||||||
|
return errKMSNotConfigured
|
||||||
|
}
|
||||||
|
if err := r.Encryption.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range r.Flags.Filter.Tags {
|
||||||
|
if err := tag.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, meta := range r.Flags.Filter.Metadata {
|
||||||
|
if err := meta.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Flags.Retry.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,801 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tinylib/msgp/msgp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarshalUnmarshalBatchJobKeyRotateEncryption(t *testing.T) {
|
||||||
|
v := BatchJobKeyRotateEncryption{}
|
||||||
|
bts, err := v.MarshalMsg(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
left, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
|
||||||
|
left, err = msgp.Skip(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMarshalMsgBatchJobKeyRotateEncryption(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateEncryption{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgBatchJobKeyRotateEncryption(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateEncryption{}
|
||||||
|
bts := make([]byte, 0, v.Msgsize())
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnmarshalBatchJobKeyRotateEncryption(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateEncryption{}
|
||||||
|
bts, _ := v.MarshalMsg(nil)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecodeBatchJobKeyRotateEncryption(t *testing.T) {
|
||||||
|
v := BatchJobKeyRotateEncryption{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodeBatchJobKeyRotateEncryption Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := BatchJobKeyRotateEncryption{}
|
||||||
|
err := msgp.Decode(&buf, &vn)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
err = msgp.NewReader(&buf).Skip()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEncodeBatchJobKeyRotateEncryption(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateEncryption{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
en := msgp.NewWriter(msgp.Nowhere)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.EncodeMsg(en)
|
||||||
|
}
|
||||||
|
en.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecodeBatchJobKeyRotateEncryption(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateEncryption{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||||
|
dc := msgp.NewReader(rd)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := v.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalUnmarshalBatchJobKeyRotateFlags(t *testing.T) {
|
||||||
|
v := BatchJobKeyRotateFlags{}
|
||||||
|
bts, err := v.MarshalMsg(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
left, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
|
||||||
|
left, err = msgp.Skip(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMarshalMsgBatchJobKeyRotateFlags(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateFlags{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgBatchJobKeyRotateFlags(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateFlags{}
|
||||||
|
bts := make([]byte, 0, v.Msgsize())
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnmarshalBatchJobKeyRotateFlags(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateFlags{}
|
||||||
|
bts, _ := v.MarshalMsg(nil)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecodeBatchJobKeyRotateFlags(t *testing.T) {
|
||||||
|
v := BatchJobKeyRotateFlags{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodeBatchJobKeyRotateFlags Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := BatchJobKeyRotateFlags{}
|
||||||
|
err := msgp.Decode(&buf, &vn)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
err = msgp.NewReader(&buf).Skip()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEncodeBatchJobKeyRotateFlags(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateFlags{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
en := msgp.NewWriter(msgp.Nowhere)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.EncodeMsg(en)
|
||||||
|
}
|
||||||
|
en.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecodeBatchJobKeyRotateFlags(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateFlags{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||||
|
dc := msgp.NewReader(rd)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := v.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalUnmarshalBatchJobKeyRotateV1(t *testing.T) {
|
||||||
|
v := BatchJobKeyRotateV1{}
|
||||||
|
bts, err := v.MarshalMsg(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
left, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
|
||||||
|
left, err = msgp.Skip(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMarshalMsgBatchJobKeyRotateV1(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateV1{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgBatchJobKeyRotateV1(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateV1{}
|
||||||
|
bts := make([]byte, 0, v.Msgsize())
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnmarshalBatchJobKeyRotateV1(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateV1{}
|
||||||
|
bts, _ := v.MarshalMsg(nil)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecodeBatchJobKeyRotateV1(t *testing.T) {
|
||||||
|
v := BatchJobKeyRotateV1{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodeBatchJobKeyRotateV1 Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := BatchJobKeyRotateV1{}
|
||||||
|
err := msgp.Decode(&buf, &vn)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
err = msgp.NewReader(&buf).Skip()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEncodeBatchJobKeyRotateV1(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateV1{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
en := msgp.NewWriter(msgp.Nowhere)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.EncodeMsg(en)
|
||||||
|
}
|
||||||
|
en.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecodeBatchJobKeyRotateV1(b *testing.B) {
|
||||||
|
v := BatchJobKeyRotateV1{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||||
|
dc := msgp.NewReader(rd)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := v.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalUnmarshalBatchKeyRotateFilter(t *testing.T) {
|
||||||
|
v := BatchKeyRotateFilter{}
|
||||||
|
bts, err := v.MarshalMsg(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
left, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
|
||||||
|
left, err = msgp.Skip(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMarshalMsgBatchKeyRotateFilter(b *testing.B) {
|
||||||
|
v := BatchKeyRotateFilter{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgBatchKeyRotateFilter(b *testing.B) {
|
||||||
|
v := BatchKeyRotateFilter{}
|
||||||
|
bts := make([]byte, 0, v.Msgsize())
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnmarshalBatchKeyRotateFilter(b *testing.B) {
|
||||||
|
v := BatchKeyRotateFilter{}
|
||||||
|
bts, _ := v.MarshalMsg(nil)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecodeBatchKeyRotateFilter(t *testing.T) {
|
||||||
|
v := BatchKeyRotateFilter{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodeBatchKeyRotateFilter Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := BatchKeyRotateFilter{}
|
||||||
|
err := msgp.Decode(&buf, &vn)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
err = msgp.NewReader(&buf).Skip()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEncodeBatchKeyRotateFilter(b *testing.B) {
|
||||||
|
v := BatchKeyRotateFilter{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
en := msgp.NewWriter(msgp.Nowhere)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.EncodeMsg(en)
|
||||||
|
}
|
||||||
|
en.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecodeBatchKeyRotateFilter(b *testing.B) {
|
||||||
|
v := BatchKeyRotateFilter{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||||
|
dc := msgp.NewReader(rd)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := v.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalUnmarshalBatchKeyRotateKV(t *testing.T) {
|
||||||
|
v := BatchKeyRotateKV{}
|
||||||
|
bts, err := v.MarshalMsg(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
left, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
|
||||||
|
left, err = msgp.Skip(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMarshalMsgBatchKeyRotateKV(b *testing.B) {
|
||||||
|
v := BatchKeyRotateKV{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgBatchKeyRotateKV(b *testing.B) {
|
||||||
|
v := BatchKeyRotateKV{}
|
||||||
|
bts := make([]byte, 0, v.Msgsize())
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnmarshalBatchKeyRotateKV(b *testing.B) {
|
||||||
|
v := BatchKeyRotateKV{}
|
||||||
|
bts, _ := v.MarshalMsg(nil)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecodeBatchKeyRotateKV(t *testing.T) {
|
||||||
|
v := BatchKeyRotateKV{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodeBatchKeyRotateKV Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := BatchKeyRotateKV{}
|
||||||
|
err := msgp.Decode(&buf, &vn)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
err = msgp.NewReader(&buf).Skip()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEncodeBatchKeyRotateKV(b *testing.B) {
|
||||||
|
v := BatchKeyRotateKV{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
en := msgp.NewWriter(msgp.Nowhere)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.EncodeMsg(en)
|
||||||
|
}
|
||||||
|
en.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecodeBatchKeyRotateKV(b *testing.B) {
|
||||||
|
v := BatchKeyRotateKV{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||||
|
dc := msgp.NewReader(rd)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := v.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalUnmarshalBatchKeyRotateNotification(t *testing.T) {
|
||||||
|
v := BatchKeyRotateNotification{}
|
||||||
|
bts, err := v.MarshalMsg(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
left, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
|
||||||
|
left, err = msgp.Skip(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMarshalMsgBatchKeyRotateNotification(b *testing.B) {
|
||||||
|
v := BatchKeyRotateNotification{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgBatchKeyRotateNotification(b *testing.B) {
|
||||||
|
v := BatchKeyRotateNotification{}
|
||||||
|
bts := make([]byte, 0, v.Msgsize())
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnmarshalBatchKeyRotateNotification(b *testing.B) {
|
||||||
|
v := BatchKeyRotateNotification{}
|
||||||
|
bts, _ := v.MarshalMsg(nil)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecodeBatchKeyRotateNotification(t *testing.T) {
|
||||||
|
v := BatchKeyRotateNotification{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodeBatchKeyRotateNotification Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := BatchKeyRotateNotification{}
|
||||||
|
err := msgp.Decode(&buf, &vn)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
err = msgp.NewReader(&buf).Skip()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEncodeBatchKeyRotateNotification(b *testing.B) {
|
||||||
|
v := BatchKeyRotateNotification{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
en := msgp.NewWriter(msgp.Nowhere)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.EncodeMsg(en)
|
||||||
|
}
|
||||||
|
en.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecodeBatchKeyRotateNotification(b *testing.B) {
|
||||||
|
v := BatchKeyRotateNotification{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||||
|
dc := msgp.NewReader(rd)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := v.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalUnmarshalBatchKeyRotateRetry(t *testing.T) {
|
||||||
|
v := BatchKeyRotateRetry{}
|
||||||
|
bts, err := v.MarshalMsg(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
left, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
|
||||||
|
left, err = msgp.Skip(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMarshalMsgBatchKeyRotateRetry(b *testing.B) {
|
||||||
|
v := BatchKeyRotateRetry{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgBatchKeyRotateRetry(b *testing.B) {
|
||||||
|
v := BatchKeyRotateRetry{}
|
||||||
|
bts := make([]byte, 0, v.Msgsize())
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnmarshalBatchKeyRotateRetry(b *testing.B) {
|
||||||
|
v := BatchKeyRotateRetry{}
|
||||||
|
bts, _ := v.MarshalMsg(nil)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecodeBatchKeyRotateRetry(t *testing.T) {
|
||||||
|
v := BatchKeyRotateRetry{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodeBatchKeyRotateRetry Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := BatchKeyRotateRetry{}
|
||||||
|
err := msgp.Decode(&buf, &vn)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
err = msgp.NewReader(&buf).Skip()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEncodeBatchKeyRotateRetry(b *testing.B) {
|
||||||
|
v := BatchKeyRotateRetry{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
en := msgp.NewWriter(msgp.Nowhere)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.EncodeMsg(en)
|
||||||
|
}
|
||||||
|
en.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecodeBatchKeyRotateRetry(b *testing.B) {
|
||||||
|
v := BatchKeyRotateRetry{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||||
|
dc := msgp.NewReader(rd)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := v.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by "stringer -type=batchJobMetric -trimprefix=batchJobMetric batch-handlers.go"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[batchReplicationMetricObject-0]
|
||||||
|
_ = x[batchKeyRotationMetricObject-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const _batchJobMetric_name = "batchReplicationMetricObjectbatchKeyRotationMetricObject"
|
||||||
|
|
||||||
|
var _batchJobMetric_index = [...]uint8{0, 28, 56}
|
||||||
|
|
||||||
|
func (i batchJobMetric) String() string {
|
||||||
|
if i >= batchJobMetric(len(_batchJobMetric_index)-1) {
|
||||||
|
return "batchJobMetric(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
return _batchJobMetric_name[_batchJobMetric_index[i]:_batchJobMetric_index[i+1]]
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
// Code generated by "stringer -type=batchReplicationMetric -trimprefix=batchReplicationMetric batch-handlers.go"; DO NOT EDIT.
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import "strconv"
|
|
||||||
|
|
||||||
func _() {
|
|
||||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
|
||||||
// Re-run the stringer command to generate them again.
|
|
||||||
var x [1]struct{}
|
|
||||||
_ = x[batchReplicationMetricObject-0]
|
|
||||||
}
|
|
||||||
|
|
||||||
const _batchReplicationMetric_name = "Object"
|
|
||||||
|
|
||||||
var _batchReplicationMetric_index = [...]uint8{0, 6}
|
|
||||||
|
|
||||||
func (i batchReplicationMetric) String() string {
|
|
||||||
if i >= batchReplicationMetric(len(_batchReplicationMetric_index)-1) {
|
|
||||||
return "batchReplicationMetric(" + strconv.FormatInt(int64(i), 10) + ")"
|
|
||||||
}
|
|
||||||
return _batchReplicationMetric_name[_batchReplicationMetric_index[i]:_batchReplicationMetric_index[i+1]]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/madmin-go/v2"
|
||||||
|
"github.com/minio/minio/internal/pubsub"
|
||||||
|
)
|
||||||
|
|
||||||
|
const bootstrapMsgsLimit = 4 << 10
|
||||||
|
|
||||||
|
type bootstrapInfo struct {
|
||||||
|
msg string
|
||||||
|
ts time.Time
|
||||||
|
source string
|
||||||
|
}
|
||||||
|
type bootstrapTracer struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
idx int
|
||||||
|
info [bootstrapMsgsLimit]bootstrapInfo
|
||||||
|
lastUpdate time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalBootstrapTracer = &bootstrapTracer{}
|
||||||
|
|
||||||
|
func (bs *bootstrapTracer) DropEvents() {
|
||||||
|
bs.mu.Lock()
|
||||||
|
defer bs.mu.Unlock()
|
||||||
|
|
||||||
|
if time.Now().UTC().Sub(bs.lastUpdate) > 24*time.Hour {
|
||||||
|
bs.info = [4096]bootstrapInfo{}
|
||||||
|
bs.idx = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *bootstrapTracer) Empty() bool {
|
||||||
|
var empty bool
|
||||||
|
bs.mu.RLock()
|
||||||
|
empty = bs.info[0].msg == ""
|
||||||
|
bs.mu.RUnlock()
|
||||||
|
|
||||||
|
return empty
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *bootstrapTracer) Record(msg string, skip int) {
|
||||||
|
source := getSource(skip + 1)
|
||||||
|
bs.mu.Lock()
|
||||||
|
now := time.Now().UTC()
|
||||||
|
bs.info[bs.idx] = bootstrapInfo{
|
||||||
|
msg: msg,
|
||||||
|
ts: now,
|
||||||
|
source: source,
|
||||||
|
}
|
||||||
|
bs.lastUpdate = now
|
||||||
|
bs.idx = (bs.idx + 1) % bootstrapMsgsLimit
|
||||||
|
bs.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *bootstrapTracer) Events() []madmin.TraceInfo {
|
||||||
|
traceInfo := make([]madmin.TraceInfo, 0, bootstrapMsgsLimit)
|
||||||
|
|
||||||
|
// Add all messages in order
|
||||||
|
addAll := func(info []bootstrapInfo) {
|
||||||
|
for _, msg := range info {
|
||||||
|
if msg.ts.IsZero() {
|
||||||
|
continue // skip empty events
|
||||||
|
}
|
||||||
|
traceInfo = append(traceInfo, madmin.TraceInfo{
|
||||||
|
TraceType: madmin.TraceBootstrap,
|
||||||
|
Time: msg.ts,
|
||||||
|
NodeName: globalLocalNodeName,
|
||||||
|
FuncName: "BOOTSTRAP",
|
||||||
|
Message: fmt.Sprintf("%s %s", msg.source, msg.msg),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bs.mu.RLock()
|
||||||
|
addAll(bs.info[bs.idx:])
|
||||||
|
addAll(bs.info[:bs.idx])
|
||||||
|
bs.mu.RUnlock()
|
||||||
|
return traceInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *bootstrapTracer) Publish(ctx context.Context, trace *pubsub.PubSub[madmin.TraceInfo, madmin.TraceType]) {
|
||||||
|
if bs.Empty() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, bsEvent := range bs.Events() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
default:
|
||||||
|
trace.Publish(bsEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBootstrap(t *testing.T) {
|
||||||
|
// Bootstrap events exceed bootstrap messages limit
|
||||||
|
bsTracer := &bootstrapTracer{}
|
||||||
|
for i := 0; i < bootstrapMsgsLimit+10; i++ {
|
||||||
|
bsTracer.Record(fmt.Sprintf("msg-%d", i), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
traceInfos := bsTracer.Events()
|
||||||
|
if len(traceInfos) != bootstrapMsgsLimit {
|
||||||
|
t.Fatalf("Expected length of events %d but got %d", bootstrapMsgsLimit, len(traceInfos))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate the case where bootstrap events were updated a day ago
|
||||||
|
bsTracer.lastUpdate = time.Now().UTC().Add(-25 * time.Hour)
|
||||||
|
bsTracer.DropEvents()
|
||||||
|
if !bsTracer.Empty() {
|
||||||
|
t.Fatalf("Expected all bootstrap events to have been dropped, but found %d events", len(bsTracer.Events()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fewer than 4K bootstrap events
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
bsTracer.Record(fmt.Sprintf("msg-%d", i), 1)
|
||||||
|
}
|
||||||
|
events := bsTracer.Events()
|
||||||
|
if len(events) != 10 {
|
||||||
|
t.Fatalf("Expected length of events %d but got %d", 10, len(events))
|
||||||
|
}
|
||||||
|
for i, traceInfo := range bsTracer.Events() {
|
||||||
|
msg := fmt.Sprintf("msg-%d", i)
|
||||||
|
if !strings.HasSuffix(traceInfo.Message, msg) {
|
||||||
|
t.Fatalf("Expected %s but got %s", msg, traceInfo.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,6 +105,10 @@ var skipEnvs = map[string]struct{}{
|
|||||||
"MINIO_CERT_PASSWD": {},
|
"MINIO_CERT_PASSWD": {},
|
||||||
"MINIO_SERVER_DEBUG": {},
|
"MINIO_SERVER_DEBUG": {},
|
||||||
"MINIO_DSYNC_TRACE": {},
|
"MINIO_DSYNC_TRACE": {},
|
||||||
|
"MINIO_ROOT_USER": {},
|
||||||
|
"MINIO_ROOT_PASSWORD": {},
|
||||||
|
"MINIO_ACCESS_KEY": {},
|
||||||
|
"MINIO_SECRET_KEY": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServerSystemCfg() ServerSystemConfig {
|
func getServerSystemCfg() ServerSystemConfig {
|
||||||
@@ -118,7 +122,7 @@ func getServerSystemCfg() ServerSystemConfig {
|
|||||||
if _, ok := skipEnvs[envK]; ok {
|
if _, ok := skipEnvs[envK]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
envValues[envK] = env.Get(envK, "")
|
envValues[envK] = logger.HashString(env.Get(envK, ""))
|
||||||
}
|
}
|
||||||
return ServerSystemConfig{
|
return ServerSystemConfig{
|
||||||
MinioEndpoints: globalEndpoints,
|
MinioEndpoints: globalEndpoints,
|
||||||
@@ -126,11 +130,22 @@ func getServerSystemCfg() ServerSystemConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *bootstrapRESTServer) writeErrorResponse(w http.ResponseWriter, err error) {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
// HealthHandler returns success if request is valid
|
// HealthHandler returns success if request is valid
|
||||||
func (b *bootstrapRESTServer) HealthHandler(w http.ResponseWriter, r *http.Request) {}
|
func (b *bootstrapRESTServer) HealthHandler(w http.ResponseWriter, r *http.Request) {}
|
||||||
|
|
||||||
func (b *bootstrapRESTServer) VerifyHandler(w http.ResponseWriter, r *http.Request) {
|
func (b *bootstrapRESTServer) VerifyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "VerifyHandler")
|
ctx := newContext(r, w, "VerifyHandler")
|
||||||
|
|
||||||
|
if err := storageServerRequestValidate(r); err != nil {
|
||||||
|
b.writeErrorResponse(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cfg := getServerSystemCfg()
|
cfg := getServerSystemCfg()
|
||||||
logger.LogIf(ctx, json.NewEncoder(w).Encode(&cfg))
|
logger.LogIf(ctx, json.NewEncoder(w).Encode(&cfg))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/minio/kes"
|
"github.com/minio/kes-go"
|
||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
"github.com/minio/minio/internal/kms"
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
|
|||||||
+139
-70
@@ -22,8 +22,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -39,6 +41,7 @@ import (
|
|||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
"github.com/minio/minio-go/v7/pkg/set"
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
"github.com/minio/minio-go/v7/pkg/tags"
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
|
"github.com/minio/minio/internal/auth"
|
||||||
sse "github.com/minio/minio/internal/bucket/encryption"
|
sse "github.com/minio/minio/internal/bucket/encryption"
|
||||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||||
"github.com/minio/minio/internal/bucket/replication"
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
@@ -50,9 +53,9 @@ import (
|
|||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
"github.com/minio/minio/internal/kms"
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/minio/internal/sync/errgroup"
|
|
||||||
"github.com/minio/pkg/bucket/policy"
|
"github.com/minio/pkg/bucket/policy"
|
||||||
iampolicy "github.com/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
|
"github.com/minio/pkg/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -359,7 +362,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.ListBucketAction,
|
Action: iampolicy.ListBucketAction,
|
||||||
BucketName: bucketInfo.Name,
|
BucketName: bucketInfo.Name,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
ObjectName: "",
|
ObjectName: "",
|
||||||
Claims: cred.Claims,
|
Claims: cred.Claims,
|
||||||
@@ -371,7 +374,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.GetBucketLocationAction,
|
Action: iampolicy.GetBucketLocationAction,
|
||||||
BucketName: bucketInfo.Name,
|
BucketName: bucketInfo.Name,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
ObjectName: "",
|
ObjectName: "",
|
||||||
Claims: cred.Claims,
|
Claims: cred.Claims,
|
||||||
@@ -494,7 +497,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
vc, _ := globalBucketVersioningSys.Get(bucket)
|
vc, _ := globalBucketVersioningSys.Get(bucket)
|
||||||
oss := make([]*objSweeper, len(deleteObjectsReq.Objects))
|
oss := make([]*objSweeper, len(deleteObjectsReq.Objects))
|
||||||
for index, object := range deleteObjectsReq.Objects {
|
for index, object := range deleteObjectsReq.Objects {
|
||||||
if apiErrCode := checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, object.ObjectName); apiErrCode != ErrNone {
|
if apiErrCode := checkRequestAuthTypeWithVID(ctx, r, policy.DeleteObjectAction, bucket, object.ObjectName, object.VersionID); apiErrCode != ErrNone {
|
||||||
if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID {
|
if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
|
||||||
return
|
return
|
||||||
@@ -539,6 +542,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
oss[index].SetTransitionState(goi.TransitionedObject)
|
oss[index].SetTransitionState(goi.TransitionedObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All deletes on directory objects needs to be for `nullVersionID`
|
||||||
|
if isDirObject(object.ObjectName) && object.VersionID == "" {
|
||||||
|
object.VersionID = nullVersionID
|
||||||
|
}
|
||||||
|
|
||||||
if replicateDeletes {
|
if replicateDeletes {
|
||||||
dsc = checkReplicateDelete(ctx, bucket, ObjectToDelete{
|
dsc = checkReplicateDelete(ctx, bucket, ObjectToDelete{
|
||||||
ObjectV: ObjectV{
|
ObjectV: ObjectV{
|
||||||
@@ -742,7 +750,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: action,
|
Action: action,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: cred.Claims,
|
Claims: cred.Claims,
|
||||||
@@ -754,19 +762,12 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse incoming location constraint.
|
// Parse incoming location constraint.
|
||||||
location, s3Error := parseLocationConstraint(r)
|
_, s3Error = parseLocationConstraint(r)
|
||||||
if s3Error != ErrNone {
|
if s3Error != ErrNone {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate if location sent by the client is valid, reject
|
|
||||||
// requests which do not follow valid region requirements.
|
|
||||||
if !isValidLocation(location) {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRegion), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if client is attempting to create more buckets, complain about it.
|
// check if client is attempting to create more buckets, complain about it.
|
||||||
if currBuckets := globalBucketMetadataSys.Count(); currBuckets+1 > maxBuckets {
|
if currBuckets := globalBucketMetadataSys.Count(); currBuckets+1 > maxBuckets {
|
||||||
logger.LogIf(ctx, fmt.Errorf("An attempt to create %d buckets beyond recommended %d", currBuckets+1, maxBuckets))
|
logger.LogIf(ctx, fmt.Errorf("An attempt to create %d buckets beyond recommended %d", currBuckets+1, maxBuckets))
|
||||||
@@ -802,8 +803,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
||||||
|
|
||||||
// Make sure to add Location information here only for bucket
|
// Make sure to add Location information here only for bucket
|
||||||
w.Header().Set(xhttp.Location,
|
w.Header().Set(xhttp.Location, pathJoin(SlashSeparator, bucket))
|
||||||
getObjectLocation(r, globalDomainNames, bucket, ""))
|
|
||||||
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
|
||||||
@@ -854,9 +854,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
logger.LogIf(ctx, globalSiteReplicationSys.MakeBucketHook(ctx, bucket, opts))
|
logger.LogIf(ctx, globalSiteReplicationSys.MakeBucketHook(ctx, bucket, opts))
|
||||||
|
|
||||||
// Make sure to add Location information here only for bucket
|
// Make sure to add Location information here only for bucket
|
||||||
if cp := pathClean(r.URL.Path); cp != "" {
|
w.Header().Set(xhttp.Location, pathJoin(SlashSeparator, bucket))
|
||||||
w.Header().Set(xhttp.Location, cp) // Clean any trailing slashes.
|
|
||||||
}
|
|
||||||
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
|
||||||
@@ -916,40 +914,110 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
reader, err := r.MultipartReader()
|
reader, err := r.MultipartReader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
||||||
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
|
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
|
||||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read multipart data and save in memory and in the disk if needed
|
var (
|
||||||
form, err := reader.ReadForm(maxFormMemory)
|
fileBody io.ReadCloser
|
||||||
|
fileName string
|
||||||
|
)
|
||||||
|
|
||||||
|
maxParts := 1000
|
||||||
|
// Canonicalize the form values into http.Header.
|
||||||
|
formValues := make(http.Header)
|
||||||
|
for {
|
||||||
|
part, err := reader.NextRawPart()
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
||||||
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
|
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
|
||||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if maxParts <= 0 {
|
||||||
// Remove all tmp files created during multipart upload
|
|
||||||
defer form.RemoveAll()
|
|
||||||
|
|
||||||
// Extract all form fields
|
|
||||||
fileBody, fileName, fileSize, formValues, err := extractPostPolicyFormValues(ctx, form)
|
|
||||||
if err != nil {
|
|
||||||
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
||||||
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
|
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
maxParts--
|
||||||
|
|
||||||
|
name := part.FormName()
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName = part.FileName()
|
||||||
|
|
||||||
|
// Multiple values for the same key (one map entry, longer slice) are cheaper
|
||||||
|
// than the same number of values for different keys (many map entries), but
|
||||||
|
// using a consistent per-value cost for overhead is simpler.
|
||||||
|
const mapEntryOverhead = 200
|
||||||
|
maxMemoryBytes := 2 * int64(10<<20)
|
||||||
|
maxMemoryBytes -= int64(len(name))
|
||||||
|
maxMemoryBytes -= mapEntryOverhead
|
||||||
|
if maxMemoryBytes < 0 {
|
||||||
|
// We can't actually take this path, since nextPart would already have
|
||||||
|
// rejected the MIME headers for being too large. Check anyway.
|
||||||
|
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
||||||
|
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
|
||||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if file is provided, error out otherwise.
|
var b bytes.Buffer
|
||||||
if fileBody == nil {
|
if fileName == "" {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPOSTFileRequired), r.URL)
|
// value, store as string in memory
|
||||||
|
n, err := io.CopyN(&b, part, maxMemoryBytes+1)
|
||||||
|
part.Close()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
||||||
|
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
maxMemoryBytes -= n
|
||||||
|
if maxMemoryBytes < 0 {
|
||||||
|
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
||||||
|
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n > maxFormFieldSize {
|
||||||
|
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
||||||
|
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, multipart.ErrMessageTooLarge)
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formValues[http.CanonicalHeaderKey(name)] = append(formValues[http.CanonicalHeaderKey(name)], b.String())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Close multipart file
|
// In accordance with https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html
|
||||||
defer fileBody.Close()
|
// The file or text content.
|
||||||
|
// The file or text content must be the last field in the form.
|
||||||
|
// You cannot upload more than one file at a time.
|
||||||
|
fileBody = part // we have found the File part of the request breakout
|
||||||
|
defer part.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := formValues["Key"]; !ok {
|
||||||
|
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
||||||
|
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, errors.New("The name of the uploaded key is missing"))
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if fileName == "" {
|
||||||
|
apiErr := errorCodes.ToAPIErr(ErrMalformedPOSTRequest)
|
||||||
|
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, errors.New("The file or text content is missing"))
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
formValues.Set("Bucket", bucket)
|
formValues.Set("Bucket", bucket)
|
||||||
if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") {
|
if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") {
|
||||||
@@ -983,7 +1051,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.PutObjectAction,
|
Action: iampolicy.PutObjectAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
ObjectName: object,
|
ObjectName: object,
|
||||||
IsOwner: globalActiveCred.AccessKey == cred.AccessKey,
|
IsOwner: globalActiveCred.AccessKey == cred.AccessKey,
|
||||||
@@ -999,6 +1067,13 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hashReader, err := hash.NewReader(fileBody, -1, "", "", -1)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Handle policy if it is set.
|
// Handle policy if it is set.
|
||||||
if len(policyBytes) > 0 {
|
if len(policyBytes) > 0 {
|
||||||
postPolicyForm, err := parsePostPolicyForm(bytes.NewReader(policyBytes))
|
postPolicyForm, err := parsePostPolicyForm(bytes.NewReader(policyBytes))
|
||||||
@@ -1019,15 +1094,8 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
// should not exceed the maximum single Put size (5 GiB)
|
// should not exceed the maximum single Put size (5 GiB)
|
||||||
lengthRange := postPolicyForm.Conditions.ContentLengthRange
|
lengthRange := postPolicyForm.Conditions.ContentLengthRange
|
||||||
if lengthRange.Valid {
|
if lengthRange.Valid {
|
||||||
if fileSize < lengthRange.Min {
|
hashReader.SetExpectedMin(lengthRange.Min)
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, errDataTooSmall), r.URL)
|
hashReader.SetExpectedMax(lengthRange.Max)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileSize > lengthRange.Max || isMaxObjectSize(fileSize) {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, errDataTooLarge), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1039,12 +1107,6 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hashReader, err := hash.NewReader(fileBody, fileSize, "", "", fileSize)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rawReader := hashReader
|
rawReader := hashReader
|
||||||
pReader := NewPutObjReader(rawReader)
|
pReader := NewPutObjReader(rawReader)
|
||||||
var objectEncryptionKey crypto.ObjectKey
|
var objectEncryptionKey crypto.ObjectKey
|
||||||
@@ -1099,9 +1161,8 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info := ObjectInfo{Size: fileSize}
|
// do not try to verify encrypted content/
|
||||||
// do not try to verify encrypted content
|
hashReader, err = hash.NewReader(reader, -1, "", "", -1)
|
||||||
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", fileSize)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1129,7 +1190,9 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
|
w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set(xhttp.Location, getObjectLocation(r, globalDomainNames, bucket, object))
|
if obj := getObjectLocation(r, globalDomainNames, bucket, object); obj != "" {
|
||||||
|
w.Header().Set(xhttp.Location, obj)
|
||||||
|
}
|
||||||
|
|
||||||
// Notify object created event.
|
// Notify object created event.
|
||||||
defer sendEvent(eventArgs{
|
defer sendEvent(eventArgs{
|
||||||
@@ -1141,6 +1204,17 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
UserAgent: r.UserAgent(),
|
UserAgent: r.UserAgent(),
|
||||||
Host: handlers.GetSourceIP(r),
|
Host: handlers.GetSourceIP(r),
|
||||||
})
|
})
|
||||||
|
if objInfo.NumVersions > dataScannerExcessiveVersionsThreshold {
|
||||||
|
defer sendEvent(eventArgs{
|
||||||
|
EventName: event.ObjectManyVersions,
|
||||||
|
BucketName: objInfo.Bucket,
|
||||||
|
Object: objInfo,
|
||||||
|
ReqParams: extractReqParams(r),
|
||||||
|
RespElements: extractRespElements(w),
|
||||||
|
UserAgent: r.UserAgent(),
|
||||||
|
Host: handlers.GetSourceIP(r),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if redirectURL != nil { // success_action_redirect is valid and set.
|
if redirectURL != nil { // success_action_redirect is valid and set.
|
||||||
v := redirectURL.Query()
|
v := redirectURL.Query()
|
||||||
@@ -1200,7 +1274,7 @@ func (api objectAPIHandlers) GetBucketPolicyStatusHandler(w http.ResponseWriter,
|
|||||||
readable := globalPolicySys.IsAllowed(policy.Args{
|
readable := globalPolicySys.IsAllowed(policy.Args{
|
||||||
Action: policy.ListBucketAction,
|
Action: policy.ListBucketAction,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
ConditionValues: getConditionValues(r, "", "", nil),
|
ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
|
||||||
IsOwner: false,
|
IsOwner: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1208,7 +1282,7 @@ func (api objectAPIHandlers) GetBucketPolicyStatusHandler(w http.ResponseWriter,
|
|||||||
writable := globalPolicySys.IsAllowed(policy.Args{
|
writable := globalPolicySys.IsAllowed(policy.Args{
|
||||||
Action: policy.PutObjectAction,
|
Action: policy.PutObjectAction,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
ConditionValues: getConditionValues(r, "", "", nil),
|
ConditionValues: getConditionValues(r, "", auth.AnonymousCredentials),
|
||||||
IsOwner: false,
|
IsOwner: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1320,14 +1394,6 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalDNSConfig != nil {
|
|
||||||
if err := globalDNSConfig.Delete(bucket); err != nil {
|
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to delete bucket DNS entry %w, please delete it manually", err))
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteBucket := objectAPI.DeleteBucket
|
deleteBucket := objectAPI.DeleteBucket
|
||||||
|
|
||||||
// Attempt to delete bucket.
|
// Attempt to delete bucket.
|
||||||
@@ -1341,15 +1407,18 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
apiErr.Description = "The bucket you tried to delete is not empty. You must delete all versions in the bucket."
|
apiErr.Description = "The bucket you tried to delete is not empty. You must delete all versions in the bucket."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if globalDNSConfig != nil {
|
|
||||||
if err2 := globalDNSConfig.Put(bucket); err2 != nil {
|
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to restore bucket DNS entry %w, please fix it manually", err2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if globalDNSConfig != nil {
|
||||||
|
if err := globalDNSConfig.Delete(bucket); err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to delete bucket DNS entry %w, please delete it manually, bucket on MinIO no longer exists", err))
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
globalNotificationSys.DeleteBucketMetadata(ctx, bucket)
|
globalNotificationSys.DeleteBucketMetadata(ctx, bucket)
|
||||||
globalReplicationPool.deleteResyncMetadata(ctx, bucket)
|
globalReplicationPool.deleteResyncMetadata(ctx, bucket)
|
||||||
|
|
||||||
|
|||||||
@@ -657,13 +657,14 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
) {
|
) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
contentBytes := []byte("hello")
|
|
||||||
sha256sum := ""
|
sha256sum := ""
|
||||||
var objectNames []string
|
var objectNames []string
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
|
contentBytes := []byte("hello")
|
||||||
objectName := "test-object-" + strconv.Itoa(i)
|
objectName := "test-object-" + strconv.Itoa(i)
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
objectName += "/"
|
objectName += "/"
|
||||||
|
contentBytes = []byte{}
|
||||||
}
|
}
|
||||||
// uploading the object.
|
// uploading the object.
|
||||||
_, err = obj.PutObject(GlobalContext, bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{})
|
_, err = obj.PutObject(GlobalContext, bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{})
|
||||||
@@ -676,6 +677,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
objectNames = append(objectNames, objectName)
|
objectNames = append(objectNames, objectName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contentBytes := []byte("hello")
|
||||||
for _, name := range []string{"private/object", "public/object"} {
|
for _, name := range []string{"private/object", "public/object"} {
|
||||||
// Uploading the object with retention enabled
|
// Uploading the object with retention enabled
|
||||||
_, err = obj.PutObject(GlobalContext, bucketName, name, mustGetPutObjReader(t, bytes.NewReader(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{})
|
_, err = obj.PutObject(GlobalContext, bucketName, name, mustGetPutObjReader(t, bytes.NewReader(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{})
|
||||||
@@ -745,8 +747,13 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
|
|
||||||
deletedObjects := make([]DeletedObject, len(requestList[0].Objects))
|
deletedObjects := make([]DeletedObject, len(requestList[0].Objects))
|
||||||
for i := range requestList[0].Objects {
|
for i := range requestList[0].Objects {
|
||||||
|
var vid string
|
||||||
|
if isDirObject(requestList[0].Objects[i].ObjectName) {
|
||||||
|
vid = nullVersionID
|
||||||
|
}
|
||||||
deletedObjects[i] = DeletedObject{
|
deletedObjects[i] = DeletedObject{
|
||||||
ObjectName: requestList[0].Objects[i].ObjectName,
|
ObjectName: requestList[0].Objects[i].ObjectName,
|
||||||
|
VersionID: vid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -756,9 +763,14 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
successRequest1 := encodeResponse(requestList[1])
|
successRequest1 := encodeResponse(requestList[1])
|
||||||
|
|
||||||
deletedObjects = make([]DeletedObject, len(requestList[1].Objects))
|
deletedObjects = make([]DeletedObject, len(requestList[1].Objects))
|
||||||
for i := range requestList[0].Objects {
|
for i := range requestList[1].Objects {
|
||||||
|
var vid string
|
||||||
|
if isDirObject(requestList[0].Objects[i].ObjectName) {
|
||||||
|
vid = nullVersionID
|
||||||
|
}
|
||||||
deletedObjects[i] = DeletedObject{
|
deletedObjects[i] = DeletedObject{
|
||||||
ObjectName: requestList[1].Objects[i].ObjectName,
|
ObjectName: requestList[1].Objects[i].ObjectName,
|
||||||
|
VersionID: vid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+153
-69
@@ -24,12 +24,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/minio/madmin-go/v2"
|
||||||
"github.com/minio/minio-go/v7/pkg/tags"
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
"github.com/minio/minio/internal/amztime"
|
"github.com/minio/minio/internal/amztime"
|
||||||
sse "github.com/minio/minio/internal/bucket/encryption"
|
sse "github.com/minio/minio/internal/bucket/encryption"
|
||||||
@@ -38,6 +41,8 @@ import (
|
|||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/minio/internal/s3select"
|
"github.com/minio/minio/internal/s3select"
|
||||||
|
"github.com/minio/pkg/env"
|
||||||
|
"github.com/minio/pkg/workers"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -66,10 +71,33 @@ func NewLifecycleSys() *LifecycleSys {
|
|||||||
return &LifecycleSys{}
|
return &LifecycleSys{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event string) madmin.TraceInfo {
|
||||||
|
return madmin.TraceInfo{
|
||||||
|
TraceType: madmin.TraceILM,
|
||||||
|
Time: startTime,
|
||||||
|
NodeName: globalLocalNodeName,
|
||||||
|
FuncName: event,
|
||||||
|
Duration: duration,
|
||||||
|
Path: pathJoin(oi.Bucket, oi.Name),
|
||||||
|
Error: "",
|
||||||
|
Message: getSource(4),
|
||||||
|
Custom: map[string]string{"version-id": oi.VersionID},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sys *LifecycleSys) trace(oi ObjectInfo) func(event string) {
|
||||||
|
startTime := time.Now()
|
||||||
|
return func(event string) {
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
if globalTrace.NumSubscribers(madmin.TraceILM) > 0 {
|
||||||
|
globalTrace.Publish(ilmTrace(startTime, duration, oi, event))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type expiryTask struct {
|
type expiryTask struct {
|
||||||
objInfo ObjectInfo
|
objInfo ObjectInfo
|
||||||
versionExpiry bool
|
event lifecycle.Event
|
||||||
restoredObject bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type expiryState struct {
|
type expiryState struct {
|
||||||
@@ -92,22 +120,22 @@ func (es *expiryState) close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// enqueueByDays enqueues object versions expired by days for expiry.
|
// enqueueByDays enqueues object versions expired by days for expiry.
|
||||||
func (es *expiryState) enqueueByDays(oi ObjectInfo, restoredObject bool, rmVersion bool) {
|
func (es *expiryState) enqueueByDays(oi ObjectInfo, event lifecycle.Event) {
|
||||||
select {
|
select {
|
||||||
case <-GlobalContext.Done():
|
case <-GlobalContext.Done():
|
||||||
es.close()
|
es.close()
|
||||||
case es.byDaysCh <- expiryTask{objInfo: oi, versionExpiry: rmVersion, restoredObject: restoredObject}:
|
case es.byDaysCh <- expiryTask{objInfo: oi, event: event}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// enqueueByNewerNoncurrent enqueues object versions expired by
|
// enqueueByNewerNoncurrent enqueues object versions expired by
|
||||||
// NewerNoncurrentVersions limit for expiry.
|
// NewerNoncurrentVersions limit for expiry.
|
||||||
func (es *expiryState) enqueueByNewerNoncurrent(bucket string, versions []ObjectToDelete) {
|
func (es *expiryState) enqueueByNewerNoncurrent(bucket string, versions []ObjectToDelete, lcEvent lifecycle.Event) {
|
||||||
select {
|
select {
|
||||||
case <-GlobalContext.Done():
|
case <-GlobalContext.Done():
|
||||||
es.close()
|
es.close()
|
||||||
case es.byNewerNoncurrentCh <- newerNoncurrentTask{bucket: bucket, versions: versions}:
|
case es.byNewerNoncurrentCh <- newerNoncurrentTask{bucket: bucket, versions: versions, event: lcEvent}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,26 +144,51 @@ var globalExpiryState *expiryState
|
|||||||
|
|
||||||
func newExpiryState() *expiryState {
|
func newExpiryState() *expiryState {
|
||||||
return &expiryState{
|
return &expiryState{
|
||||||
byDaysCh: make(chan expiryTask, 10000),
|
byDaysCh: make(chan expiryTask, 100000),
|
||||||
byNewerNoncurrentCh: make(chan newerNoncurrentTask, 10000),
|
byNewerNoncurrentCh: make(chan newerNoncurrentTask, 100000),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
|
func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
|
||||||
globalExpiryState = newExpiryState()
|
globalExpiryState = newExpiryState()
|
||||||
|
|
||||||
|
workerSize, _ := strconv.Atoi(env.Get("_MINIO_ILM_EXPIRY_WORKERS", strconv.Itoa((runtime.GOMAXPROCS(0)+1)/2)))
|
||||||
|
if workerSize == 0 {
|
||||||
|
workerSize = 4
|
||||||
|
}
|
||||||
|
ewk, err := workers.New(workerSize)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nwk, err := workers.New(workerSize)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for t := range globalExpiryState.byDaysCh {
|
for t := range globalExpiryState.byDaysCh {
|
||||||
|
ewk.Take()
|
||||||
|
go func(t expiryTask) {
|
||||||
|
defer ewk.Give()
|
||||||
if t.objInfo.TransitionedObject.Status != "" {
|
if t.objInfo.TransitionedObject.Status != "" {
|
||||||
applyExpiryOnTransitionedObject(ctx, objectAPI, t.objInfo, t.restoredObject)
|
applyExpiryOnTransitionedObject(ctx, objectAPI, t.objInfo, t.event)
|
||||||
} else {
|
} else {
|
||||||
applyExpiryOnNonTransitionedObjects(ctx, objectAPI, t.objInfo, t.versionExpiry)
|
applyExpiryOnNonTransitionedObjects(ctx, objectAPI, t.objInfo, t.event)
|
||||||
}
|
}
|
||||||
|
}(t)
|
||||||
}
|
}
|
||||||
|
ewk.Wait()
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
for t := range globalExpiryState.byNewerNoncurrentCh {
|
for t := range globalExpiryState.byNewerNoncurrentCh {
|
||||||
deleteObjectVersions(ctx, objectAPI, t.bucket, t.versions)
|
nwk.Take()
|
||||||
|
go func(t newerNoncurrentTask) {
|
||||||
|
defer nwk.Give()
|
||||||
|
deleteObjectVersions(ctx, objectAPI, t.bucket, t.versions, t.event)
|
||||||
|
}(t)
|
||||||
}
|
}
|
||||||
|
nwk.Wait()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,15 +197,15 @@ func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
|
|||||||
type newerNoncurrentTask struct {
|
type newerNoncurrentTask struct {
|
||||||
bucket string
|
bucket string
|
||||||
versions []ObjectToDelete
|
versions []ObjectToDelete
|
||||||
|
event lifecycle.Event
|
||||||
}
|
}
|
||||||
|
|
||||||
type transitionTask struct {
|
type transitionTask struct {
|
||||||
tier string
|
|
||||||
objInfo ObjectInfo
|
objInfo ObjectInfo
|
||||||
|
event lifecycle.Event
|
||||||
}
|
}
|
||||||
|
|
||||||
type transitionState struct {
|
type transitionState struct {
|
||||||
once sync.Once
|
|
||||||
transitionCh chan transitionTask
|
transitionCh chan transitionTask
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -167,33 +220,42 @@ type transitionState struct {
|
|||||||
lastDayStats map[string]*lastDayTierStats
|
lastDayStats map[string]*lastDayTierStats
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *transitionState) queueTransitionTask(oi ObjectInfo, sc string) {
|
func (t *transitionState) queueTransitionTask(oi ObjectInfo, event lifecycle.Event) {
|
||||||
select {
|
select {
|
||||||
case <-GlobalContext.Done():
|
case <-t.ctx.Done():
|
||||||
t.once.Do(func() {
|
case t.transitionCh <- transitionTask{objInfo: oi, event: event}:
|
||||||
close(t.transitionCh)
|
|
||||||
})
|
|
||||||
case t.transitionCh <- transitionTask{objInfo: oi, tier: sc}:
|
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var globalTransitionState *transitionState
|
var globalTransitionState *transitionState
|
||||||
|
|
||||||
func newTransitionState(ctx context.Context, objAPI ObjectLayer) *transitionState {
|
// newTransitionState returns a transitionState object ready to be initialized
|
||||||
|
// via its Init method.
|
||||||
|
func newTransitionState(ctx context.Context) *transitionState {
|
||||||
return &transitionState{
|
return &transitionState{
|
||||||
transitionCh: make(chan transitionTask, 10000),
|
transitionCh: make(chan transitionTask, 10000),
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
objAPI: objAPI,
|
|
||||||
killCh: make(chan struct{}),
|
killCh: make(chan struct{}),
|
||||||
lastDayStats: make(map[string]*lastDayTierStats),
|
lastDayStats: make(map[string]*lastDayTierStats),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init initializes t with given objAPI and instantiates the configured number
|
||||||
|
// of transition workers.
|
||||||
|
func (t *transitionState) Init(objAPI ObjectLayer) {
|
||||||
|
n := globalAPIConfig.getTransitionWorkers()
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
t.objAPI = objAPI
|
||||||
|
t.updateWorkers(n)
|
||||||
|
}
|
||||||
|
|
||||||
// PendingTasks returns the number of ILM transition tasks waiting for a worker
|
// PendingTasks returns the number of ILM transition tasks waiting for a worker
|
||||||
// goroutine.
|
// goroutine.
|
||||||
func (t *transitionState) PendingTasks() int {
|
func (t *transitionState) PendingTasks() int {
|
||||||
return len(globalTransitionState.transitionCh)
|
return len(t.transitionCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActiveTasks returns the number of active (ongoing) ILM transition tasks.
|
// ActiveTasks returns the number of active (ongoing) ILM transition tasks.
|
||||||
@@ -202,21 +264,21 @@ func (t *transitionState) ActiveTasks() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// worker waits for transition tasks
|
// worker waits for transition tasks
|
||||||
func (t *transitionState) worker(ctx context.Context, objectAPI ObjectLayer) {
|
func (t *transitionState) worker(objectAPI ObjectLayer) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-t.killCh:
|
case <-t.killCh:
|
||||||
return
|
return
|
||||||
case <-ctx.Done():
|
case <-t.ctx.Done():
|
||||||
return
|
return
|
||||||
case task, ok := <-t.transitionCh:
|
case task, ok := <-t.transitionCh:
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
atomic.AddInt32(&t.activeTasks, 1)
|
atomic.AddInt32(&t.activeTasks, 1)
|
||||||
if err := transitionObject(ctx, objectAPI, task.objInfo, task.tier); err != nil {
|
if err := transitionObject(t.ctx, objectAPI, task.objInfo, task.event); err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Transition failed for %s/%s version:%s with %w",
|
logger.LogIf(t.ctx, fmt.Errorf("Transition to %s failed for %s/%s version:%s with %w",
|
||||||
task.objInfo.Bucket, task.objInfo.Name, task.objInfo.VersionID, err))
|
task.event.StorageClass, task.objInfo.Bucket, task.objInfo.Name, task.objInfo.VersionID, err))
|
||||||
} else {
|
} else {
|
||||||
ts := tierStats{
|
ts := tierStats{
|
||||||
TotalSize: uint64(task.objInfo.Size),
|
TotalSize: uint64(task.objInfo.Size),
|
||||||
@@ -225,7 +287,7 @@ func (t *transitionState) worker(ctx context.Context, objectAPI ObjectLayer) {
|
|||||||
if task.objInfo.IsLatest {
|
if task.objInfo.IsLatest {
|
||||||
ts.NumObjects = 1
|
ts.NumObjects = 1
|
||||||
}
|
}
|
||||||
t.addLastDayStats(task.tier, ts)
|
t.addLastDayStats(task.event.StorageClass, ts)
|
||||||
}
|
}
|
||||||
atomic.AddInt32(&t.activeTasks, -1)
|
atomic.AddInt32(&t.activeTasks, -1)
|
||||||
}
|
}
|
||||||
@@ -258,9 +320,15 @@ func (t *transitionState) getDailyAllTierStats() DailyAllTierStats {
|
|||||||
func (t *transitionState) UpdateWorkers(n int) {
|
func (t *transitionState) UpdateWorkers(n int) {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer t.mu.Unlock()
|
defer t.mu.Unlock()
|
||||||
|
if t.objAPI == nil { // Init hasn't been called yet.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.updateWorkers(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *transitionState) updateWorkers(n int) {
|
||||||
for t.numWorkers < n {
|
for t.numWorkers < n {
|
||||||
go t.worker(t.ctx, t.objAPI)
|
go t.worker(t.objAPI)
|
||||||
t.numWorkers++
|
t.numWorkers++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,12 +338,6 @@ func (t *transitionState) UpdateWorkers(n int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initBackgroundTransition(ctx context.Context, objectAPI ObjectLayer) {
|
|
||||||
globalTransitionState = newTransitionState(ctx, objectAPI)
|
|
||||||
n := globalAPIConfig.getTransitionWorkers()
|
|
||||||
globalTransitionState.UpdateWorkers(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
var errInvalidStorageClass = errors.New("invalid storage class")
|
var errInvalidStorageClass = errors.New("invalid storage class")
|
||||||
|
|
||||||
func validateTransitionTier(lc *lifecycle.Lifecycle) error {
|
func validateTransitionTier(lc *lifecycle.Lifecycle) error {
|
||||||
@@ -298,39 +360,38 @@ func validateTransitionTier(lc *lifecycle.Lifecycle) error {
|
|||||||
// This is to be called after a successful upload of an object (version).
|
// This is to be called after a successful upload of an object (version).
|
||||||
func enqueueTransitionImmediate(obj ObjectInfo) {
|
func enqueueTransitionImmediate(obj ObjectInfo) {
|
||||||
if lc, err := globalLifecycleSys.Get(obj.Bucket); err == nil {
|
if lc, err := globalLifecycleSys.Get(obj.Bucket); err == nil {
|
||||||
event := lc.Eval(obj.ToLifecycleOpts())
|
switch event := lc.Eval(obj.ToLifecycleOpts()); event.Action {
|
||||||
switch event.Action {
|
|
||||||
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
|
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
|
||||||
globalTransitionState.queueTransitionTask(obj, event.StorageClass)
|
globalTransitionState.queueTransitionTask(obj, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// expireAction represents different actions to be performed on expiry of a
|
|
||||||
// restored/transitioned object
|
|
||||||
type expireAction int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ignore the zero value
|
|
||||||
_ expireAction = iota
|
|
||||||
// expireObj indicates expiry of 'regular' transitioned objects.
|
|
||||||
expireObj
|
|
||||||
// expireRestoredObj indicates expiry of restored objects.
|
|
||||||
expireRestoredObj
|
|
||||||
)
|
|
||||||
|
|
||||||
// expireTransitionedObject handles expiry of transitioned/restored objects
|
// expireTransitionedObject handles expiry of transitioned/restored objects
|
||||||
// (versions) in one of the following situations:
|
// (versions) in one of the following situations:
|
||||||
//
|
//
|
||||||
// 1. when a restored (via PostRestoreObject API) object expires.
|
// 1. when a restored (via PostRestoreObject API) object expires.
|
||||||
// 2. when a transitioned object expires (based on an ILM rule).
|
// 2. when a transitioned object expires (based on an ILM rule).
|
||||||
func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *ObjectInfo, lcOpts lifecycle.ObjectOpts, action expireAction) error {
|
func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *ObjectInfo, lcOpts lifecycle.ObjectOpts, lcEvent lifecycle.Event) error {
|
||||||
|
traceFn := globalLifecycleSys.trace(*oi)
|
||||||
var opts ObjectOptions
|
var opts ObjectOptions
|
||||||
opts.Versioned = globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name)
|
opts.Versioned = globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name)
|
||||||
opts.VersionID = lcOpts.VersionID
|
opts.VersionID = lcOpts.VersionID
|
||||||
opts.Expiration = ExpirationOptions{Expire: true}
|
opts.Expiration = ExpirationOptions{Expire: true}
|
||||||
switch action {
|
tags := auditLifecycleTags(lcEvent)
|
||||||
case expireObj:
|
if lcEvent.Action.DeleteRestored() {
|
||||||
|
// delete locally restored copy of object or object version
|
||||||
|
// from the source, while leaving metadata behind. The data on
|
||||||
|
// transitioned tier lies untouched and still accessible
|
||||||
|
opts.Transition.ExpireRestored = true
|
||||||
|
_, err := objectAPI.DeleteObject(ctx, oi.Bucket, oi.Name, opts)
|
||||||
|
if err == nil {
|
||||||
|
// TODO consider including expiry of restored object to events we
|
||||||
|
// notify.
|
||||||
|
auditLogLifecycle(ctx, *oi, ILMExpiry, tags, traceFn)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
// When an object is past expiry or when a transitioned object is being
|
// When an object is past expiry or when a transitioned object is being
|
||||||
// deleted, 'mark' the data in the remote tier for delete.
|
// deleted, 'mark' the data in the remote tier for delete.
|
||||||
entry := jentry{
|
entry := jentry{
|
||||||
@@ -350,7 +411,7 @@ func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *Ob
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send audit for the lifecycle delete operation
|
// Send audit for the lifecycle delete operation
|
||||||
auditLogLifecycle(ctx, *oi, ILMExpiry)
|
defer auditLogLifecycle(ctx, *oi, ILMExpiry, tags, traceFn)
|
||||||
|
|
||||||
eventName := event.ObjectRemovedDelete
|
eventName := event.ObjectRemovedDelete
|
||||||
if lcOpts.DeleteMarker {
|
if lcOpts.DeleteMarker {
|
||||||
@@ -366,20 +427,10 @@ func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *Ob
|
|||||||
EventName: eventName,
|
EventName: eventName,
|
||||||
BucketName: oi.Bucket,
|
BucketName: oi.Bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [ILM-EXPIRY]",
|
UserAgent: "Internal: [ILM-Expiry]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
|
|
||||||
case expireRestoredObj:
|
|
||||||
// delete locally restored copy of object or object version
|
|
||||||
// from the source, while leaving metadata behind. The data on
|
|
||||||
// transitioned tier lies untouched and still accessible
|
|
||||||
opts.Transition.ExpireRestored = true
|
|
||||||
_, err := objectAPI.DeleteObject(ctx, oi.Bucket, oi.Name, opts)
|
|
||||||
return err
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Unknown expire action %v", action)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,13 +449,14 @@ func genTransitionObjName(bucket string) (string, error) {
|
|||||||
// storage specified by the transition ARN, the metadata is left behind on source cluster and original content
|
// storage specified by the transition ARN, the metadata is left behind on source cluster and original content
|
||||||
// is moved to the transition tier. Note that in the case of encrypted objects, entire encrypted stream is moved
|
// is moved to the transition tier. Note that in the case of encrypted objects, entire encrypted stream is moved
|
||||||
// to the transition tier without decrypting or re-encrypting.
|
// to the transition tier without decrypting or re-encrypting.
|
||||||
func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo, tier string) error {
|
func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo, lcEvent lifecycle.Event) error {
|
||||||
opts := ObjectOptions{
|
opts := ObjectOptions{
|
||||||
Transition: TransitionOptions{
|
Transition: TransitionOptions{
|
||||||
Status: lifecycle.TransitionPending,
|
Status: lifecycle.TransitionPending,
|
||||||
Tier: tier,
|
Tier: lcEvent.StorageClass,
|
||||||
ETag: oi.ETag,
|
ETag: oi.ETag,
|
||||||
},
|
},
|
||||||
|
LifecycleEvent: lcEvent,
|
||||||
VersionID: oi.VersionID,
|
VersionID: oi.VersionID,
|
||||||
Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
|
Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
|
||||||
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
|
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
|
||||||
@@ -816,3 +868,35 @@ func (oi ObjectInfo) ToLifecycleOpts() lifecycle.ObjectOpts {
|
|||||||
TransitionStatus: oi.TransitionedObject.Status,
|
TransitionStatus: oi.TransitionedObject.Status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func auditLifecycleTags(event lifecycle.Event) map[string]interface{} {
|
||||||
|
const (
|
||||||
|
ilmAction = "ilm-action"
|
||||||
|
ilmDue = "ilm-due"
|
||||||
|
ilmRuleID = "ilm-rule-id"
|
||||||
|
ilmTier = "ilm-tier"
|
||||||
|
ilmNewerNoncurrentVersions = "ilm-newer-noncurrent-versions"
|
||||||
|
ilmNoncurrentDays = "ilm-noncurrent-days"
|
||||||
|
)
|
||||||
|
tags := make(map[string]interface{}, 4)
|
||||||
|
tags[ilmAction] = event.Action.String()
|
||||||
|
tags[ilmRuleID] = event.RuleID
|
||||||
|
|
||||||
|
if !event.Due.IsZero() {
|
||||||
|
tags[ilmDue] = event.Due
|
||||||
|
}
|
||||||
|
|
||||||
|
// rule with Transition/NoncurrentVersionTransition in effect
|
||||||
|
if event.StorageClass != "" {
|
||||||
|
tags[ilmTier] = event.StorageClass
|
||||||
|
}
|
||||||
|
|
||||||
|
// rule with NewernoncurrentVersions in effect
|
||||||
|
if event.NewerNoncurrentVersions > 0 {
|
||||||
|
tags[ilmNewerNoncurrentVersions] = event.NewerNoncurrentVersions
|
||||||
|
}
|
||||||
|
if event.NoncurrentDays > 0 {
|
||||||
|
tags[ilmNoncurrentDays] = event.NoncurrentDays
|
||||||
|
}
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,10 +59,18 @@ func validateListObjectsArgs(prefix, marker, delimiter, encodingType string, max
|
|||||||
return ErrNone
|
return ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListObjectVersions - GET Bucket Object versions
|
func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.listObjectVersionsHandler(w, r, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api objectAPIHandlers) ListObjectVersionsMHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
api.listObjectVersionsHandler(w, r, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListObjectVersionsHandler - GET Bucket Object versions
|
||||||
// You can use the versions subresource to list metadata about all
|
// You can use the versions subresource to list metadata about all
|
||||||
// of the versions of objects in a bucket.
|
// of the versions of objects in a bucket.
|
||||||
func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) listObjectVersionsHandler(w http.ResponseWriter, r *http.Request, metadata bool) {
|
||||||
ctx := newContext(r, w, "ListObjectVersions")
|
ctx := newContext(r, w, "ListObjectVersions")
|
||||||
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
@@ -80,7 +88,12 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
|||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var checkObjMeta metaCheckFn
|
||||||
|
if metadata {
|
||||||
|
checkObjMeta = func(name string, action policy.Action) (s3Err APIErrorCode) {
|
||||||
|
return checkRequestAuthType(ctx, r, action, bucket, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
urlValues := r.Form
|
urlValues := r.Form
|
||||||
|
|
||||||
// Extract all the listBucketVersions query params to their native values.
|
// Extract all the listBucketVersions query params to their native values.
|
||||||
@@ -111,8 +124,7 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
|||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
response := generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType, maxkeys, listObjectVersionsInfo, checkObjMeta)
|
||||||
response := generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType, maxkeys, listObjectVersionsInfo)
|
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessResponseXML(w, encodeResponseList(response))
|
writeSuccessResponseXML(w, encodeResponseList(response))
|
||||||
@@ -128,64 +140,7 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
|||||||
// MinIO continues to support ListObjectsV1 and V2 for supporting legacy tools.
|
// MinIO continues to support ListObjectsV1 and V2 for supporting legacy tools.
|
||||||
func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListObjectsV2M")
|
ctx := newContext(r, w, "ListObjectsV2M")
|
||||||
|
api.listObjectsV2Handler(ctx, w, r, true)
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
|
||||||
if objectAPI == nil {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
urlValues := r.Form
|
|
||||||
|
|
||||||
// Extract all the listObjectsV2 query params to their native values.
|
|
||||||
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, encodingType, errCode := getListObjectsV2Args(urlValues)
|
|
||||||
if errCode != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the query params before beginning to serve the request.
|
|
||||||
// fetch-owner is not validated since it is a boolean
|
|
||||||
if s3Error := validateListObjectsArgs(prefix, token, delimiter, encodingType, maxKeys); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
listObjectsV2 := objectAPI.ListObjectsV2
|
|
||||||
|
|
||||||
// Inititate a list objects operation based on the input params.
|
|
||||||
// On success would return back ListObjectsInfo object to be
|
|
||||||
// marshaled into S3 compatible XML header.
|
|
||||||
listObjectsV2Info, err := listObjectsV2(ctx, bucket, prefix, token, delimiter, maxKeys, fetchOwner, startAfter)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = DecryptETags(ctx, GlobalKMS, listObjectsV2Info.Objects); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// The next continuation token has id@node_index format to optimize paginated listing
|
|
||||||
nextContinuationToken := listObjectsV2Info.NextContinuationToken
|
|
||||||
|
|
||||||
response := generateListObjectsV2Response(bucket, prefix, token, nextContinuationToken, startAfter,
|
|
||||||
delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated,
|
|
||||||
maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes, true)
|
|
||||||
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessResponseXML(w, encodeResponseList(response))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListObjectsV2Handler - GET Bucket (List Objects) Version 2.
|
// ListObjectsV2Handler - GET Bucket (List Objects) Version 2.
|
||||||
@@ -198,7 +153,11 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
|
|||||||
// MinIO continues to support ListObjectsV1 for supporting legacy tools.
|
// MinIO continues to support ListObjectsV1 for supporting legacy tools.
|
||||||
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListObjectsV2")
|
ctx := newContext(r, w, "ListObjectsV2")
|
||||||
|
api.listObjectsV2Handler(ctx, w, r, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// listObjectsV2Handler performs listing either with or without extra metadata.
|
||||||
|
func (api objectAPIHandlers) listObjectsV2Handler(ctx context.Context, w http.ResponseWriter, r *http.Request, metadata bool) {
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
@@ -215,6 +174,12 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var checkObjMeta metaCheckFn
|
||||||
|
if metadata {
|
||||||
|
checkObjMeta = func(name string, action policy.Action) (s3Err APIErrorCode) {
|
||||||
|
return checkRequestAuthType(ctx, r, action, bucket, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
urlValues := r.Form
|
urlValues := r.Form
|
||||||
|
|
||||||
// Extract all the listObjectsV2 query params to their native values.
|
// Extract all the listObjectsV2 query params to their native values.
|
||||||
@@ -257,7 +222,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
|
|
||||||
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsV2Info.NextContinuationToken, startAfter,
|
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsV2Info.NextContinuationToken, startAfter,
|
||||||
delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated,
|
delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated,
|
||||||
maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes, false)
|
maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes, checkObjMeta)
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessResponseXML(w, encodeResponseList(response))
|
writeSuccessResponseXML(w, encodeResponseList(response))
|
||||||
|
|||||||
+156
-41
@@ -21,10 +21,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
"github.com/minio/minio-go/v7/pkg/tags"
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
bucketsse "github.com/minio/minio/internal/bucket/encryption"
|
bucketsse "github.com/minio/minio/internal/bucket/encryption"
|
||||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||||
@@ -34,12 +36,14 @@ import (
|
|||||||
"github.com/minio/minio/internal/event"
|
"github.com/minio/minio/internal/event"
|
||||||
"github.com/minio/minio/internal/kms"
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/minio/internal/sync/errgroup"
|
|
||||||
"github.com/minio/pkg/bucket/policy"
|
"github.com/minio/pkg/bucket/policy"
|
||||||
|
"github.com/minio/pkg/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BucketMetadataSys captures all bucket metadata for a given cluster.
|
// BucketMetadataSys captures all bucket metadata for a given cluster.
|
||||||
type BucketMetadataSys struct {
|
type BucketMetadataSys struct {
|
||||||
|
objAPI ObjectLayer
|
||||||
|
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
metadataMap map[string]BucketMetadata
|
metadataMap map[string]BucketMetadata
|
||||||
}
|
}
|
||||||
@@ -53,13 +57,29 @@ func (sys *BucketMetadataSys) Count() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove bucket metadata from memory.
|
// Remove bucket metadata from memory.
|
||||||
func (sys *BucketMetadataSys) Remove(bucket string) {
|
func (sys *BucketMetadataSys) Remove(buckets ...string) {
|
||||||
sys.Lock()
|
sys.Lock()
|
||||||
|
for _, bucket := range buckets {
|
||||||
delete(sys.metadataMap, bucket)
|
delete(sys.metadataMap, bucket)
|
||||||
globalBucketMonitor.DeleteBucket(bucket)
|
globalBucketMonitor.DeleteBucket(bucket)
|
||||||
|
}
|
||||||
sys.Unlock()
|
sys.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveStaleBuckets removes all stale buckets in memory that are not on disk.
|
||||||
|
func (sys *BucketMetadataSys) RemoveStaleBuckets(diskBuckets set.StringSet) {
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
for bucket := range sys.metadataMap {
|
||||||
|
if diskBuckets.Contains(bucket) {
|
||||||
|
continue
|
||||||
|
} // doesn't exist on disk remove from memory.
|
||||||
|
delete(sys.metadataMap, bucket)
|
||||||
|
globalBucketMonitor.DeleteBucket(bucket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set - sets a new metadata in-memory.
|
// Set - sets a new metadata in-memory.
|
||||||
// Only a shallow copy is saved and fields with references
|
// Only a shallow copy is saved and fields with references
|
||||||
// cannot be modified without causing a race condition,
|
// cannot be modified without causing a race condition,
|
||||||
@@ -182,7 +202,7 @@ func (sys *BucketMetadataSys) Get(bucket string) (BucketMetadata, error) {
|
|||||||
// GetVersioningConfig returns configured versioning config
|
// GetVersioningConfig returns configured versioning config
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetVersioningConfig(bucket string) (*versioning.Versioning, time.Time, error) {
|
func (sys *BucketMetadataSys) GetVersioningConfig(bucket string) (*versioning.Versioning, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
meta, _, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, meta.Created, nil
|
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, meta.Created, nil
|
||||||
@@ -195,7 +215,7 @@ func (sys *BucketMetadataSys) GetVersioningConfig(bucket string) (*versioning.Ve
|
|||||||
// GetTaggingConfig returns configured tagging config
|
// GetTaggingConfig returns configured tagging config
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetTaggingConfig(bucket string) (*tags.Tags, time.Time, error) {
|
func (sys *BucketMetadataSys) GetTaggingConfig(bucket string) (*tags.Tags, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
meta, _, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, time.Time{}, BucketTaggingNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketTaggingNotFound{Bucket: bucket}
|
||||||
@@ -211,7 +231,7 @@ func (sys *BucketMetadataSys) GetTaggingConfig(bucket string) (*tags.Tags, time.
|
|||||||
// GetObjectLockConfig returns configured object lock config
|
// GetObjectLockConfig returns configured object lock config
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetObjectLockConfig(bucket string) (*objectlock.Config, time.Time, error) {
|
func (sys *BucketMetadataSys) GetObjectLockConfig(bucket string) (*objectlock.Config, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
meta, _, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, time.Time{}, BucketObjectLockConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketObjectLockConfigNotFound{Bucket: bucket}
|
||||||
@@ -227,7 +247,7 @@ func (sys *BucketMetadataSys) GetObjectLockConfig(bucket string) (*objectlock.Co
|
|||||||
// GetLifecycleConfig returns configured lifecycle config
|
// GetLifecycleConfig returns configured lifecycle config
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetLifecycleConfig(bucket string) (*lifecycle.Lifecycle, error) {
|
func (sys *BucketMetadataSys) GetLifecycleConfig(bucket string) (*lifecycle.Lifecycle, error) {
|
||||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
meta, _, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, BucketLifecycleNotFound{Bucket: bucket}
|
return nil, BucketLifecycleNotFound{Bucket: bucket}
|
||||||
@@ -243,7 +263,7 @@ func (sys *BucketMetadataSys) GetLifecycleConfig(bucket string) (*lifecycle.Life
|
|||||||
// GetNotificationConfig returns configured notification config
|
// GetNotificationConfig returns configured notification config
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetNotificationConfig(bucket string) (*event.Config, error) {
|
func (sys *BucketMetadataSys) GetNotificationConfig(bucket string) (*event.Config, error) {
|
||||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
meta, _, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -253,7 +273,7 @@ func (sys *BucketMetadataSys) GetNotificationConfig(bucket string) (*event.Confi
|
|||||||
// GetSSEConfig returns configured SSE config
|
// GetSSEConfig returns configured SSE config
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetSSEConfig(bucket string) (*bucketsse.BucketSSEConfig, time.Time, error) {
|
func (sys *BucketMetadataSys) GetSSEConfig(bucket string) (*bucketsse.BucketSSEConfig, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
meta, _, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, time.Time{}, BucketSSEConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketSSEConfigNotFound{Bucket: bucket}
|
||||||
@@ -268,7 +288,7 @@ func (sys *BucketMetadataSys) GetSSEConfig(bucket string) (*bucketsse.BucketSSEC
|
|||||||
|
|
||||||
// CreatedAt returns the time of creation of bucket
|
// CreatedAt returns the time of creation of bucket
|
||||||
func (sys *BucketMetadataSys) CreatedAt(bucket string) (time.Time, error) {
|
func (sys *BucketMetadataSys) CreatedAt(bucket string) (time.Time, error) {
|
||||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
meta, _, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, err
|
return time.Time{}, err
|
||||||
}
|
}
|
||||||
@@ -278,7 +298,7 @@ func (sys *BucketMetadataSys) CreatedAt(bucket string) (time.Time, error) {
|
|||||||
// GetPolicyConfig returns configured bucket policy
|
// GetPolicyConfig returns configured bucket policy
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, time.Time, error) {
|
func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
meta, _, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
|
||||||
@@ -294,7 +314,7 @@ func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, ti
|
|||||||
// GetQuotaConfig returns configured bucket quota
|
// GetQuotaConfig returns configured bucket quota
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetQuotaConfig(ctx context.Context, bucket string) (*madmin.BucketQuota, time.Time, error) {
|
func (sys *BucketMetadataSys) GetQuotaConfig(ctx context.Context, bucket string) (*madmin.BucketQuota, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(ctx, bucket)
|
meta, _, err := sys.GetConfig(ctx, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, time.Time{}, BucketQuotaConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketQuotaConfigNotFound{Bucket: bucket}
|
||||||
@@ -307,7 +327,7 @@ func (sys *BucketMetadataSys) GetQuotaConfig(ctx context.Context, bucket string)
|
|||||||
// GetReplicationConfig returns configured bucket replication config
|
// GetReplicationConfig returns configured bucket replication config
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetReplicationConfig(ctx context.Context, bucket string) (*replication.Config, time.Time, error) {
|
func (sys *BucketMetadataSys) GetReplicationConfig(ctx context.Context, bucket string) (*replication.Config, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(ctx, bucket)
|
meta, reloaded, err := sys.GetConfig(ctx, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, time.Time{}, BucketReplicationConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketReplicationConfigNotFound{Bucket: bucket}
|
||||||
@@ -318,13 +338,18 @@ func (sys *BucketMetadataSys) GetReplicationConfig(ctx context.Context, bucket s
|
|||||||
if meta.replicationConfig == nil {
|
if meta.replicationConfig == nil {
|
||||||
return nil, time.Time{}, BucketReplicationConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketReplicationConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
|
if reloaded {
|
||||||
|
globalBucketTargetSys.set(BucketInfo{
|
||||||
|
Name: bucket,
|
||||||
|
}, meta)
|
||||||
|
}
|
||||||
return meta.replicationConfig, meta.ReplicationConfigUpdatedAt, nil
|
return meta.replicationConfig, meta.ReplicationConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketTargetsConfig returns configured bucket targets for this bucket
|
// GetBucketTargetsConfig returns configured bucket targets for this bucket
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.BucketTargets, error) {
|
func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.BucketTargets, error) {
|
||||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
meta, reloaded, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, BucketRemoteTargetNotFound{Bucket: bucket}
|
return nil, BucketRemoteTargetNotFound{Bucket: bucket}
|
||||||
@@ -334,12 +359,16 @@ func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.Buc
|
|||||||
if meta.bucketTargetConfig == nil {
|
if meta.bucketTargetConfig == nil {
|
||||||
return nil, BucketRemoteTargetNotFound{Bucket: bucket}
|
return nil, BucketRemoteTargetNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
|
if reloaded {
|
||||||
|
globalBucketTargetSys.set(BucketInfo{
|
||||||
|
Name: bucket,
|
||||||
|
}, meta)
|
||||||
|
}
|
||||||
return meta.bucketTargetConfig, nil
|
return meta.bucketTargetConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfig returns a specific configuration from the bucket metadata.
|
// GetConfigFromDisk read bucket metadata config from disk.
|
||||||
// The returned object may not be modified.
|
func (sys *BucketMetadataSys) GetConfigFromDisk(ctx context.Context, bucket string) (BucketMetadata, error) {
|
||||||
func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (BucketMetadata, error) {
|
|
||||||
objAPI := newObjectLayerFn()
|
objAPI := newObjectLayerFn()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return newBucketMetadata(bucket), errServerNotInitialized
|
return newBucketMetadata(bucket), errServerNotInitialized
|
||||||
@@ -349,21 +378,37 @@ func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (Buc
|
|||||||
return newBucketMetadata(bucket), errInvalidArgument
|
return newBucketMetadata(bucket), errInvalidArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return loadBucketMetadata(ctx, objAPI, bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig returns a specific configuration from the bucket metadata.
|
||||||
|
// The returned object may not be modified.
|
||||||
|
// reloaded will be true if metadata refreshed from disk
|
||||||
|
func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (meta BucketMetadata, reloaded bool, err error) {
|
||||||
|
objAPI := newObjectLayerFn()
|
||||||
|
if objAPI == nil {
|
||||||
|
return newBucketMetadata(bucket), reloaded, errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMinioMetaBucketName(bucket) {
|
||||||
|
return newBucketMetadata(bucket), reloaded, errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
sys.RLock()
|
sys.RLock()
|
||||||
meta, ok := sys.metadataMap[bucket]
|
meta, ok := sys.metadataMap[bucket]
|
||||||
sys.RUnlock()
|
sys.RUnlock()
|
||||||
if ok {
|
if ok {
|
||||||
return meta, nil
|
return meta, reloaded, nil
|
||||||
}
|
}
|
||||||
meta, err := loadBucketMetadata(ctx, objAPI, bucket)
|
meta, err = loadBucketMetadata(ctx, objAPI, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return meta, err
|
return meta, reloaded, err
|
||||||
}
|
}
|
||||||
sys.Lock()
|
sys.Lock()
|
||||||
sys.metadataMap[bucket] = meta
|
sys.metadataMap[bucket] = meta
|
||||||
sys.Unlock()
|
sys.Unlock()
|
||||||
|
|
||||||
return meta, nil
|
return meta, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init - initializes bucket metadata system for all buckets.
|
// Init - initializes bucket metadata system for all buckets.
|
||||||
@@ -372,59 +417,129 @@ func (sys *BucketMetadataSys) Init(ctx context.Context, buckets []BucketInfo, ob
|
|||||||
return errServerNotInitialized
|
return errServerNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sys.objAPI = objAPI
|
||||||
|
|
||||||
// Load bucket metadata sys in background
|
// Load bucket metadata sys in background
|
||||||
go sys.load(ctx, buckets, objAPI)
|
go sys.init(ctx, buckets)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sys *BucketMetadataSys) loadBucketMetadata(ctx context.Context, bucket BucketInfo) error {
|
||||||
|
meta, err := loadBucketMetadata(ctx, sys.objAPI, bucket.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Lock()
|
||||||
|
sys.metadataMap[bucket.Name] = meta
|
||||||
|
sys.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// concurrently load bucket metadata to speed up loading bucket metadata.
|
// concurrently load bucket metadata to speed up loading bucket metadata.
|
||||||
func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) {
|
func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []BucketInfo) {
|
||||||
g := errgroup.WithNErrs(len(buckets))
|
g := errgroup.WithNErrs(len(buckets))
|
||||||
|
bucketMetas := make([]BucketMetadata, len(buckets))
|
||||||
for index := range buckets {
|
for index := range buckets {
|
||||||
index := index
|
index := index
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
_, _ = objAPI.HealBucket(ctx, buckets[index].Name, madmin.HealOpts{
|
_, _ = sys.objAPI.HealBucket(ctx, buckets[index].Name, madmin.HealOpts{
|
||||||
// Ensure heal opts for bucket metadata be deep healed all the time.
|
// Ensure heal opts for bucket metadata be deep healed all the time.
|
||||||
ScanMode: madmin.HealDeepScan,
|
ScanMode: madmin.HealDeepScan,
|
||||||
Recreate: true,
|
Recreate: true,
|
||||||
})
|
})
|
||||||
meta, err := loadBucketMetadata(ctx, objAPI, buckets[index].Name)
|
meta, err := loadBucketMetadata(ctx, sys.objAPI, buckets[index].Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !globalIsErasure && !globalIsDistErasure && errors.Is(err, errVolumeNotFound) {
|
|
||||||
meta = newBucketMetadata(buckets[index].Name)
|
|
||||||
} else {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
bucketMetas[index] = meta
|
||||||
sys.Lock()
|
|
||||||
sys.metadataMap[buckets[index].Name] = meta
|
|
||||||
sys.Unlock()
|
|
||||||
|
|
||||||
globalEventNotifier.set(buckets[index], meta) // set notification targets
|
|
||||||
|
|
||||||
globalBucketTargetSys.set(buckets[index], meta) // set remote replication targets
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}, index)
|
}, index)
|
||||||
}
|
}
|
||||||
for _, err := range g.Wait() {
|
|
||||||
|
errs := g.Wait()
|
||||||
|
for _, err := range errs {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hold lock here to update in-memory map at once,
|
||||||
|
// instead of serializing the Go routines.
|
||||||
|
sys.Lock()
|
||||||
|
for i, meta := range bucketMetas {
|
||||||
|
if errs[i] != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sys.metadataMap[buckets[i].Name] = meta
|
||||||
|
}
|
||||||
|
sys.Unlock()
|
||||||
|
|
||||||
|
for i, meta := range bucketMetas {
|
||||||
|
if errs[i] != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
globalEventNotifier.set(buckets[i], meta) // set notification targets
|
||||||
|
globalBucketTargetSys.set(buckets[i], meta) // set remote replication targets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sys *BucketMetadataSys) refreshBucketsMetadataLoop(ctx context.Context) {
|
||||||
|
const bucketMetadataRefresh = 15 * time.Minute
|
||||||
|
|
||||||
|
t := time.NewTimer(bucketMetadataRefresh)
|
||||||
|
defer t.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
buckets, err := sys.objAPI.ListBuckets(ctx, BucketOptions{})
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle if we have some buckets in-memory those are stale.
|
||||||
|
// first delete them and then replace the newer state()
|
||||||
|
// from disk.
|
||||||
|
diskBuckets := set.CreateStringSet()
|
||||||
|
for _, bucket := range buckets {
|
||||||
|
diskBuckets.Add(bucket.Name)
|
||||||
|
}
|
||||||
|
sys.RemoveStaleBuckets(diskBuckets)
|
||||||
|
|
||||||
|
for _, bucket := range buckets {
|
||||||
|
err := sys.loadBucketMetadata(ctx, bucket)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Check if there is a spare procs, wait 100ms instead
|
||||||
|
waitForLowIO(runtime.GOMAXPROCS(0), 100*time.Millisecond, currentHTTPIO)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Reset(bucketMetadataRefresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads bucket metadata for all buckets into BucketMetadataSys.
|
// Loads bucket metadata for all buckets into BucketMetadataSys.
|
||||||
func (sys *BucketMetadataSys) load(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) {
|
func (sys *BucketMetadataSys) init(ctx context.Context, buckets []BucketInfo) {
|
||||||
count := 100 // load 100 bucket metadata at a time.
|
count := 100 // load 100 bucket metadata at a time.
|
||||||
for {
|
for {
|
||||||
if len(buckets) < count {
|
if len(buckets) < count {
|
||||||
sys.concurrentLoad(ctx, buckets, objAPI)
|
sys.concurrentLoad(ctx, buckets)
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
sys.concurrentLoad(ctx, buckets[:count], objAPI)
|
sys.concurrentLoad(ctx, buckets[:count])
|
||||||
buckets = buckets[count:]
|
buckets = buckets[count:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if globalIsDistErasure {
|
||||||
|
go sys.refreshBucketsMetadataLoop(ctx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the state of the BucketMetadataSys.
|
// Reset the state of the BucketMetadataSys.
|
||||||
|
|||||||
@@ -323,8 +323,7 @@ func (b *BucketMetadata) getAllLegacyConfigs(ctx context.Context, objectAPI Obje
|
|||||||
|
|
||||||
configData, err := readConfig(ctx, objectAPI, configFile)
|
configData, err := readConfig(ctx, objectAPI, configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err.(type) {
|
if _, ok := err.(ObjectExistsAsDirectory); ok {
|
||||||
case ObjectExistsAsDirectory:
|
|
||||||
// in FS mode it possible that we have actual
|
// in FS mode it possible that we have actual
|
||||||
// files in this folder with `.minio.sys/buckets/bucket/configFile`
|
// files in this folder with `.minio.sys/buckets/bucket/configFile`
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
|
|||||||
|
|
||||||
opts.VersionID = object.VersionID
|
opts.VersionID = object.VersionID
|
||||||
if gerr != nil { // error from GetObjectInfo
|
if gerr != nil { // error from GetObjectInfo
|
||||||
switch gerr.(type) {
|
if _, ok := gerr.(MethodNotAllowed); ok {
|
||||||
case MethodNotAllowed: // This happens usually for a delete marker
|
// This happens usually for a delete marker
|
||||||
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
|
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
|
||||||
// Delete marker should be present and valid.
|
// Delete marker should be present and valid.
|
||||||
return ErrNone
|
return ErrNone
|
||||||
@@ -156,11 +156,8 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
|
|||||||
return ErrNone
|
return ErrNone
|
||||||
}
|
}
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
|
// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
|
||||||
// If you try to delete objects protected by governance mode and have s3:BypassGovernanceRetention
|
// If you try to delete objects protected by governance mode and have s3:BypassGovernanceRetention, the operation will succeed.
|
||||||
// or s3:GetBucketObjectLockConfiguration permissions, the operation will succeed.
|
if checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object.ObjectName) != ErrNone {
|
||||||
govBypassPerms1 := checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object.ObjectName)
|
|
||||||
govBypassPerms2 := checkRequestAuthType(ctx, r, policy.GetBucketObjectLockConfigurationAction, bucket, object.ObjectName)
|
|
||||||
if govBypassPerms1 != ErrNone && govBypassPerms2 != ErrNone {
|
|
||||||
return ErrAccessDenied
|
return ErrAccessDenied
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,8 +192,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi Objec
|
|||||||
days, objRetention.RetainUntilDate.Time,
|
days, objRetention.RetainUntilDate.Time,
|
||||||
objRetention.Mode, byPassSet, r, cred,
|
objRetention.Mode, byPassSet, r, cred,
|
||||||
owner)
|
owner)
|
||||||
switch apiErr {
|
if apiErr == ErrAccessDenied {
|
||||||
case ErrAccessDenied:
|
|
||||||
return errAuthentication
|
return errAuthentication
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -213,8 +209,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi Objec
|
|||||||
return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID}
|
return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch govPerm {
|
if govPerm == ErrAccessDenied {
|
||||||
case ErrAccessDenied:
|
|
||||||
return errAuthentication
|
return errAuthentication
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -227,8 +222,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi Objec
|
|||||||
apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
|
apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
|
||||||
days, objRetention.RetainUntilDate.Time, objRetention.Mode,
|
days, objRetention.RetainUntilDate.Time, objRetention.Mode,
|
||||||
false, r, cred, owner)
|
false, r, cred, owner)
|
||||||
switch apiErr {
|
if apiErr == ErrAccessDenied {
|
||||||
case ErrAccessDenied:
|
|
||||||
return errAuthentication
|
return errAuthentication
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -239,8 +233,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi Objec
|
|||||||
apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
|
apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
|
||||||
days, objRetention.RetainUntilDate.Time,
|
days, objRetention.RetainUntilDate.Time,
|
||||||
objRetention.Mode, byPassSet, r, cred, owner)
|
objRetention.Mode, byPassSet, r, cred, owner)
|
||||||
switch apiErr {
|
if apiErr == ErrAccessDenied {
|
||||||
case ErrAccessDenied:
|
|
||||||
return errAuthentication
|
return errAuthentication
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -86,7 +86,11 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
|||||||
|
|
||||||
bucketPolicy, err := policy.ParseConfig(bytes.NewReader(bucketPolicyBytes), bucket)
|
bucketPolicy, err := policy.ParseConfig(bytes.NewReader(bucketPolicyBytes), bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, APIError{
|
||||||
|
Code: "MalformedPolicy",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
Description: err.Error(),
|
||||||
|
}, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-10
@@ -28,6 +28,7 @@ import (
|
|||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
miniogopolicy "github.com/minio/minio-go/v7/pkg/policy"
|
miniogopolicy "github.com/minio/minio-go/v7/pkg/policy"
|
||||||
"github.com/minio/minio-go/v7/pkg/tags"
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
|
"github.com/minio/minio/internal/auth"
|
||||||
"github.com/minio/minio/internal/handlers"
|
"github.com/minio/minio/internal/handlers"
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
@@ -65,9 +66,20 @@ func NewPolicySys() *PolicySys {
|
|||||||
return &PolicySys{}
|
return &PolicySys{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConditionValues(r *http.Request, lc string, username string, claims map[string]interface{}) map[string][]string {
|
func getConditionValues(r *http.Request, lc string, cred auth.Credentials) map[string][]string {
|
||||||
currTime := UTCNow()
|
currTime := UTCNow()
|
||||||
|
|
||||||
|
var (
|
||||||
|
username = cred.AccessKey
|
||||||
|
claims = cred.Claims
|
||||||
|
groups = cred.Groups
|
||||||
|
)
|
||||||
|
|
||||||
|
if cred.IsTemp() || cred.IsServiceAccount() {
|
||||||
|
// For derived credentials, check the parent user's permissions.
|
||||||
|
username = cred.ParentUser
|
||||||
|
}
|
||||||
|
|
||||||
principalType := "Anonymous"
|
principalType := "Anonymous"
|
||||||
if username != "" {
|
if username != "" {
|
||||||
principalType = "User"
|
principalType = "User"
|
||||||
@@ -192,17 +204,11 @@ func getConditionValues(r *http.Request, lc string, username string, claims map[
|
|||||||
for k, v := range claims {
|
for k, v := range claims {
|
||||||
vStr, ok := v.(string)
|
vStr, ok := v.(string)
|
||||||
if ok {
|
if ok {
|
||||||
// Special case for AD/LDAP STS users
|
// Trim any LDAP specific prefix
|
||||||
switch k {
|
args[strings.ToLower(strings.TrimPrefix(k, "ldap"))] = []string{vStr}
|
||||||
case ldapUser:
|
|
||||||
args["user"] = []string{vStr}
|
|
||||||
case ldapUserN:
|
|
||||||
args["username"] = []string{vStr}
|
|
||||||
default:
|
|
||||||
args[k] = []string{vStr}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add groups claim which could be a list. This will ensure that the claim
|
// Add groups claim which could be a list. This will ensure that the claim
|
||||||
// `jwt:groups` works.
|
// `jwt:groups` works.
|
||||||
if grpsVal, ok := claims["groups"]; ok {
|
if grpsVal, ok := claims["groups"]; ok {
|
||||||
@@ -219,6 +225,13 @@ func getConditionValues(r *http.Request, lc string, username string, claims map[
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if not claim groups are available use the one with auth.Credentials
|
||||||
|
if _, ok := args["groups"]; !ok {
|
||||||
|
if len(groups) > 0 {
|
||||||
|
args["groups"] = groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -82,7 +82,7 @@ func parseBucketQuota(bucket string, data []byte) (quotaCfg *madmin.BucketQuota,
|
|||||||
if !quotaCfg.IsValid() {
|
if !quotaCfg.IsValid() {
|
||||||
if quotaCfg.Type == "fifo" {
|
if quotaCfg.Type == "fifo" {
|
||||||
logger.LogIf(GlobalContext, errors.New("Detected older 'fifo' quota config, 'fifo' feature is removed and not supported anymore. Please clear your quota configs using 'mc admin bucket quota alias/bucket --clear' and use 'mc ilm add' for expiration of objects"))
|
logger.LogIf(GlobalContext, errors.New("Detected older 'fifo' quota config, 'fifo' feature is removed and not supported anymore. Please clear your quota configs using 'mc admin bucket quota alias/bucket --clear' and use 'mc ilm add' for expiration of objects"))
|
||||||
return quotaCfg, nil
|
return quotaCfg, fmt.Errorf("invalid quota type 'fifo'")
|
||||||
}
|
}
|
||||||
return quotaCfg, fmt.Errorf("Invalid quota config %#v", quotaCfg)
|
return quotaCfg, fmt.Errorf("Invalid quota config %#v", quotaCfg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ func (api objectAPIHandlers) ResetBucketReplicationStartHandler(w http.ResponseW
|
|||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Err: fmt.Errorf("invalid query parameter older-than %s for %s : %w", durationStr, bucket, err),
|
Err: fmt.Errorf("invalid query parameter older-than %s for %s : %w", durationStr, bucket, err),
|
||||||
}), r.URL)
|
}), r.URL)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resetBeforeDate := UTCNow().AddDate(0, 0, -1*int(days/24))
|
resetBeforeDate := UTCNow().AddDate(0, 0, -1*int(days/24))
|
||||||
@@ -347,6 +348,7 @@ func (api objectAPIHandlers) ResetBucketReplicationStartHandler(w http.ResponseW
|
|||||||
default:
|
default:
|
||||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -496,8 +496,7 @@ func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) Repl
|
|||||||
oi.VersionPurgeStatusInternal = fmt.Sprintf("%s=%s;", rcfg.Config.RoleArn, oi.VersionPurgeStatus)
|
oi.VersionPurgeStatusInternal = fmt.Sprintf("%s=%s;", rcfg.Config.RoleArn, oi.VersionPurgeStatus)
|
||||||
}
|
}
|
||||||
for k, v := range oi.UserDefined {
|
for k, v := range oi.UserDefined {
|
||||||
switch {
|
if strings.EqualFold(k, ReservedMetadataPrefixLower+ReplicationReset) {
|
||||||
case strings.EqualFold(k, ReservedMetadataPrefixLower+ReplicationReset):
|
|
||||||
delete(oi.UserDefined, k)
|
delete(oi.UserDefined, k)
|
||||||
oi.UserDefined[targetResetHeader(rcfg.Config.RoleArn)] = v
|
oi.UserDefined[targetResetHeader(rcfg.Config.RoleArn)] = v
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-58
@@ -26,6 +26,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -138,6 +139,13 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
|
|||||||
// validate replication ARN against target endpoint
|
// validate replication ARN against target endpoint
|
||||||
c := globalBucketTargetSys.GetRemoteTargetClient(ctx, arnStr)
|
c := globalBucketTargetSys.GetRemoteTargetClient(ctx, arnStr)
|
||||||
if c != nil {
|
if c != nil {
|
||||||
|
if err := checkRemoteEndpoint(ctx, c.EndpointURL()); err != nil {
|
||||||
|
switch err.(type) {
|
||||||
|
case BucketRemoteIdenticalToSource:
|
||||||
|
return true, errorCodes.ToAPIErrWithErr(ErrBucketRemoteIdenticalToSource, fmt.Errorf("remote target endpoint %s is self referential", c.EndpointURL().String()))
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
if c.EndpointURL().String() == clnt.EndpointURL().String() {
|
if c.EndpointURL().String() == clnt.EndpointURL().String() {
|
||||||
selfTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort)
|
selfTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort)
|
||||||
if !sameTarget {
|
if !sameTarget {
|
||||||
@@ -154,6 +162,45 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
|
|||||||
return sameTarget, toAPIError(ctx, nil)
|
return sameTarget, toAPIError(ctx, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// performs a http request to remote endpoint to check if deployment id of remote endpoint is same as
|
||||||
|
// local cluster deployment id. This is to prevent replication to self, especially in case of a loadbalancer
|
||||||
|
// in front of MinIO.
|
||||||
|
func checkRemoteEndpoint(ctx context.Context, epURL *url.URL) error {
|
||||||
|
reqURL := &url.URL{
|
||||||
|
Scheme: epURL.Scheme,
|
||||||
|
Host: epURL.Host,
|
||||||
|
Path: healthCheckReadinessPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: NewHTTPTransport(),
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
// Drain the connection.
|
||||||
|
xhttp.DrainBody(resp.Body)
|
||||||
|
}
|
||||||
|
if resp != nil {
|
||||||
|
amzid := resp.Header.Get(xhttp.AmzRequestHostID)
|
||||||
|
if _, ok := globalNodeNamesHex[amzid]; ok {
|
||||||
|
return BucketRemoteIdenticalToSource{
|
||||||
|
Endpoint: epURL.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type mustReplicateOptions struct {
|
type mustReplicateOptions struct {
|
||||||
meta map[string]string
|
meta map[string]string
|
||||||
status replication.StatusType
|
status replication.StatusType
|
||||||
@@ -387,7 +434,8 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
|||||||
VersionID: versionID,
|
VersionID: versionID,
|
||||||
DeleteMarker: dobj.DeleteMarker,
|
DeleteMarker: dobj.DeleteMarker,
|
||||||
},
|
},
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -403,7 +451,8 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
|||||||
VersionID: versionID,
|
VersionID: versionID,
|
||||||
DeleteMarker: dobj.DeleteMarker,
|
DeleteMarker: dobj.DeleteMarker,
|
||||||
},
|
},
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -415,7 +464,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
|||||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
globalReplicationPool.queueMRFSave(dobj.ToMRFEntry())
|
globalReplicationPool.queueMRFSave(dobj.ToMRFEntry())
|
||||||
logger.LogIf(ctx, fmt.Errorf("failed to get lock for object: %s bucket:%s arn:%s", dobj.ObjectName, bucket, rcfg.RoleArn))
|
logger.LogIf(ctx, fmt.Errorf("failed to get lock for object: %s bucket:%s arn:%s", dobj.ObjectName, bucket, dobj.TargetArn))
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: ObjectInfo{
|
Object: ObjectInfo{
|
||||||
@@ -424,7 +473,8 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
|||||||
VersionID: versionID,
|
VersionID: versionID,
|
||||||
DeleteMarker: dobj.DeleteMarker,
|
DeleteMarker: dobj.DeleteMarker,
|
||||||
},
|
},
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -449,7 +499,8 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
|||||||
VersionID: versionID,
|
VersionID: versionID,
|
||||||
DeleteMarker: dobj.DeleteMarker,
|
DeleteMarker: dobj.DeleteMarker,
|
||||||
},
|
},
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
@@ -508,7 +559,6 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
|||||||
VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
|
VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
|
||||||
})
|
})
|
||||||
if err != nil && !isErrVersionNotFound(err) { // VersionNotFound would be reported by pool that object version is missing on.
|
if err != nil && !isErrVersionNotFound(err) { // VersionNotFound would be reported by pool that object version is missing on.
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to update replication metadata for %s/%s(%s): %s", bucket, dobj.ObjectName, versionID, err))
|
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: ObjectInfo{
|
Object: ObjectInfo{
|
||||||
@@ -517,14 +567,16 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
|||||||
VersionID: versionID,
|
VersionID: versionID,
|
||||||
DeleteMarker: dobj.DeleteMarker,
|
DeleteMarker: dobj.DeleteMarker,
|
||||||
},
|
},
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
EventName: eventName,
|
EventName: eventName,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: dobjInfo,
|
Object: dobjInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
EventName: eventName,
|
EventName: eventName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -561,7 +613,8 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
|
|||||||
VersionID: dobj.VersionID,
|
VersionID: dobj.VersionID,
|
||||||
DeleteMarker: dobj.DeleteMarker,
|
DeleteMarker: dobj.DeleteMarker,
|
||||||
},
|
},
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
})
|
})
|
||||||
if dobj.VersionID == "" {
|
if dobj.VersionID == "" {
|
||||||
@@ -586,6 +639,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !isErrObjectNotFound(ErrorRespToObjectError(err, dobj.Bucket, dobj.ObjectName)) {
|
||||||
// mark delete marker replication as failed if target cluster not ready to receive
|
// mark delete marker replication as failed if target cluster not ready to receive
|
||||||
// this request yet (object version not replicated yet)
|
// this request yet (object version not replicated yet)
|
||||||
if err != nil && !toi.ReplicationReady {
|
if err != nil && !toi.ReplicationReady {
|
||||||
@@ -593,6 +647,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rmErr := tgt.RemoveObject(ctx, tgt.Bucket, dobj.ObjectName, minio.RemoveObjectOptions{
|
rmErr := tgt.RemoveObject(ctx, tgt.Bucket, dobj.ObjectName, minio.RemoveObjectOptions{
|
||||||
VersionID: versionID,
|
VersionID: versionID,
|
||||||
@@ -802,10 +857,12 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati
|
|||||||
oi1.ModTime.Unix() > oi2.LastModified.Unix() && oi1.VersionID == nullVersionID {
|
oi1.ModTime.Unix() > oi2.LastModified.Unix() && oi1.VersionID == nullVersionID {
|
||||||
return replicateNone
|
return replicateNone
|
||||||
}
|
}
|
||||||
|
sz, _ := oi1.GetActualSize()
|
||||||
|
|
||||||
// needs full replication
|
// needs full replication
|
||||||
if oi1.ETag != oi2.ETag ||
|
if oi1.ETag != oi2.ETag ||
|
||||||
oi1.VersionID != oi2.VersionID ||
|
oi1.VersionID != oi2.VersionID ||
|
||||||
oi1.Size != oi2.Size ||
|
sz != oi2.Size ||
|
||||||
oi1.DeleteMarker != oi2.IsDeleteMarker ||
|
oi1.DeleteMarker != oi2.IsDeleteMarker ||
|
||||||
oi1.ModTime.Unix() != oi2.LastModified.Unix() {
|
oi1.ModTime.Unix() != oi2.LastModified.Unix() {
|
||||||
return replicateAll
|
return replicateAll
|
||||||
@@ -917,7 +974,8 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
|
|||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -935,10 +993,11 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
|
|||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
globalReplicationPool.queueMRFSave(ri.ToMRFEntry())
|
globalReplicationPool.queueMRFSave(ri.ToMRFEntry())
|
||||||
logger.LogIf(ctx, fmt.Errorf("failed to get lock for object: %s bucket:%s arn:%s", object, bucket, cfg.RoleArn))
|
logger.LogIf(ctx, fmt.Errorf("failed to get lock for object: %s bucket:%s arn:%s", object, bucket, ri.TargetArn))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx = lkctx.Context()
|
ctx = lkctx.Context()
|
||||||
@@ -955,7 +1014,8 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
|
|||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -971,11 +1031,13 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
|
|||||||
}(i, tgt)
|
}(i, tgt)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
replicationStatus = rinfos.ReplicationStatus() // used in defer function
|
||||||
// FIXME: add support for missing replication events
|
// FIXME: add support for missing replication events
|
||||||
// - event.ObjectReplicationMissedThreshold
|
// - event.ObjectReplicationMissedThreshold
|
||||||
// - event.ObjectReplicationReplicatedAfterThreshold
|
// - event.ObjectReplicationReplicatedAfterThreshold
|
||||||
eventName := event.ObjectReplicationComplete
|
eventName := event.ObjectReplicationComplete
|
||||||
if rinfos.ReplicationStatus() == replication.Failed {
|
if replicationStatus == replication.Failed {
|
||||||
eventName = event.ObjectReplicationFailed
|
eventName = event.ObjectReplicationFailed
|
||||||
}
|
}
|
||||||
newReplStatusInternal := rinfos.ReplicationStatusInternal()
|
newReplStatusInternal := rinfos.ReplicationStatusInternal()
|
||||||
@@ -1001,10 +1063,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = objectAPI.PutObjectMetadata(ctx, bucket, object, popts); err != nil {
|
_, _ = objectAPI.PutObjectMetadata(ctx, bucket, object, popts)
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to update replication metadata for %s/%s(%s): %w",
|
|
||||||
bucket, objInfo.Name, objInfo.VersionID, err))
|
|
||||||
}
|
|
||||||
opType := replication.MetadataReplicationType
|
opType := replication.MetadataReplicationType
|
||||||
if rinfos.Action() == replicateAll {
|
if rinfos.Action() == replicateAll {
|
||||||
opType = replication.ObjectReplicationType
|
opType = replication.ObjectReplicationType
|
||||||
@@ -1020,7 +1079,8 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
|
|||||||
EventName: eventName,
|
EventName: eventName,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
|
|
||||||
// re-queue failures once more - keep a retry count to avoid flooding the queue if
|
// re-queue failures once more - keep a retry count to avoid flooding the queue if
|
||||||
@@ -1065,7 +1125,8 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1073,7 +1134,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
|
versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
|
||||||
versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
|
versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
|
||||||
|
|
||||||
gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, readLock, ObjectOptions{
|
gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, ObjectOptions{
|
||||||
VersionID: objInfo.VersionID,
|
VersionID: objInfo.VersionID,
|
||||||
Versioned: versioned,
|
Versioned: versioned,
|
||||||
VersionSuspended: versionSuspended,
|
VersionSuspended: versionSuspended,
|
||||||
@@ -1084,7 +1145,8 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to update replicate metadata for %s/%s(%s): %w", bucket, object, objInfo.VersionID, err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to update replicate metadata for %s/%s(%s): %w", bucket, object, objInfo.VersionID, err))
|
||||||
}
|
}
|
||||||
@@ -1101,7 +1163,8 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1112,7 +1175,8 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
return rinfo
|
return rinfo
|
||||||
}
|
}
|
||||||
@@ -1137,7 +1201,8 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1162,14 +1227,14 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
if objInfo.isMultipart() {
|
if objInfo.isMultipart() {
|
||||||
if err := replicateObjectWithMultipart(ctx, c, tgt.Bucket, object,
|
if err := replicateObjectWithMultipart(ctx, c, tgt.Bucket, object,
|
||||||
r, objInfo, putOpts); err != nil {
|
r, objInfo, putOpts); err != nil {
|
||||||
if minio.ToErrorResponse(err).Code != "PreConditionFailed" {
|
if minio.ToErrorResponse(err).Code != "PreconditionFailed" {
|
||||||
rinfo.ReplicationStatus = replication.Failed
|
rinfo.ReplicationStatus = replication.Failed
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if _, err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts); err != nil {
|
if _, err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts); err != nil {
|
||||||
if minio.ToErrorResponse(err).Code != "PreConditionFailed" {
|
if minio.ToErrorResponse(err).Code != "PreconditionFailed" {
|
||||||
rinfo.ReplicationStatus = replication.Failed
|
rinfo.ReplicationStatus = replication.Failed
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
||||||
}
|
}
|
||||||
@@ -1202,19 +1267,14 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
ReplicationAction: rAction,
|
ReplicationAction: rAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ri.ObjectInfo.TargetReplicationStatus(tgt.ARN) == replication.Completed && !ri.ExistingObjResync.Empty() && !ri.ExistingObjResync.mustResyncTarget(tgt.ARN) {
|
|
||||||
rinfo.ReplicationStatus = replication.Completed
|
|
||||||
rinfo.ReplicationResynced = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
|
if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
|
||||||
logger.LogIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", bucket, tgt.ARN))
|
logger.LogIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", bucket, tgt.ARN))
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1222,7 +1282,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
|
versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
|
||||||
versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
|
versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
|
||||||
|
|
||||||
gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, readLock, ObjectOptions{
|
gr, err := objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, ObjectOptions{
|
||||||
VersionID: objInfo.VersionID,
|
VersionID: objInfo.VersionID,
|
||||||
Versioned: versioned,
|
Versioned: versioned,
|
||||||
VersionSuspended: versionSuspended,
|
VersionSuspended: versionSuspended,
|
||||||
@@ -1233,7 +1293,8 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to update replicate metadata for %s/%s(%s): %w", bucket, object, objInfo.VersionID, err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to update replicate metadata for %s/%s(%s): %w", bucket, object, objInfo.VersionID, err))
|
||||||
}
|
}
|
||||||
@@ -1243,6 +1304,13 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
|
|
||||||
objInfo = gr.ObjInfo
|
objInfo = gr.ObjInfo
|
||||||
|
|
||||||
|
// use latest ObjectInfo to check if previous replication attempt succeeded
|
||||||
|
if objInfo.TargetReplicationStatus(tgt.ARN) == replication.Completed && !ri.ExistingObjResync.Empty() && !ri.ExistingObjResync.mustResyncTarget(tgt.ARN) {
|
||||||
|
rinfo.ReplicationStatus = replication.Completed
|
||||||
|
rinfo.ReplicationResynced = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
size, err := objInfo.GetActualSize()
|
size, err := objInfo.GetActualSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
@@ -1250,18 +1318,20 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if tgt.Bucket == "" {
|
if tgt.Bucket == "" {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to replicate object %s(%s), bucket is empty", objInfo.Name, objInfo.VersionID))
|
logger.LogIf(ctx, fmt.Errorf("unable to replicate object %s(%s), bucket is empty", objInfo.Name, objInfo.VersionID))
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
return rinfo
|
return rinfo
|
||||||
}
|
}
|
||||||
@@ -1286,12 +1356,13 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
if rAction == replicateNone {
|
if rAction == replicateNone {
|
||||||
if ri.OpType == replication.ExistingObjectReplicationType &&
|
if ri.OpType == replication.ExistingObjectReplicationType &&
|
||||||
objInfo.ModTime.Unix() > oi.LastModified.Unix() && objInfo.VersionID == nullVersionID {
|
objInfo.ModTime.Unix() > oi.LastModified.Unix() && objInfo.VersionID == nullVersionID {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to replicate %s/%s (null). Newer version exists on target", bucket, object))
|
logger.LogIf(ctx, fmt.Errorf("unable to replicate %s/%s (null). Newer version exists on target", bucket, object))
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// object with same VersionID already exists, replication kicked off by
|
// object with same VersionID already exists, replication kicked off by
|
||||||
@@ -1328,7 +1399,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
}
|
}
|
||||||
if _, err = c.CopyObject(ctx, tgt.Bucket, object, tgt.Bucket, object, getCopyObjMetadata(objInfo, tgt.StorageClass), srcOpts, dstOpts); err != nil {
|
if _, err = c.CopyObject(ctx, tgt.Bucket, object, tgt.Bucket, object, getCopyObjMetadata(objInfo, tgt.StorageClass), srcOpts, dstOpts); err != nil {
|
||||||
rinfo.ReplicationStatus = replication.Failed
|
rinfo.ReplicationStatus = replication.Failed
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to replicate metadata for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
logger.LogIf(ctx, fmt.Errorf("unable to replicate metadata for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var putOpts minio.PutObjectOptions
|
var putOpts minio.PutObjectOptions
|
||||||
@@ -1339,7 +1410,8 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
EventName: event.ObjectReplicationNotTracked,
|
EventName: event.ObjectReplicationNotTracked,
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
Host: "Internal: [Replication]",
|
UserAgent: "Internal: [Replication]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1363,18 +1435,18 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
if objInfo.isMultipart() {
|
if objInfo.isMultipart() {
|
||||||
if err := replicateObjectWithMultipart(ctx, c, tgt.Bucket, object,
|
if err := replicateObjectWithMultipart(ctx, c, tgt.Bucket, object,
|
||||||
r, objInfo, putOpts); err != nil {
|
r, objInfo, putOpts); err != nil {
|
||||||
if minio.ToErrorResponse(err).Code != "PreConditionFailed" {
|
if minio.ToErrorResponse(err).Code != "PreconditionFailed" {
|
||||||
rinfo.ReplicationStatus = replication.Failed
|
rinfo.ReplicationStatus = replication.Failed
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
logger.LogIf(ctx, fmt.Errorf("unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
||||||
} else {
|
} else {
|
||||||
rinfo.ReplicationStatus = replication.Completed
|
rinfo.ReplicationStatus = replication.Completed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if _, err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts); err != nil {
|
if _, err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts); err != nil {
|
||||||
if minio.ToErrorResponse(err).Code != "PreConditionFailed" {
|
if minio.ToErrorResponse(err).Code != "PreconditionFailed" {
|
||||||
rinfo.ReplicationStatus = replication.Failed
|
rinfo.ReplicationStatus = replication.Failed
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
logger.LogIf(ctx, fmt.Errorf("unable to replicate for object %s/%s(%s): %s", bucket, objInfo.Name, objInfo.VersionID, err))
|
||||||
} else {
|
} else {
|
||||||
rinfo.ReplicationStatus = replication.Completed
|
rinfo.ReplicationStatus = replication.Completed
|
||||||
}
|
}
|
||||||
@@ -1415,11 +1487,16 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
|||||||
)
|
)
|
||||||
|
|
||||||
for _, partInfo := range objInfo.Parts {
|
for _, partInfo := range objInfo.Parts {
|
||||||
hr, err = hash.NewReader(r, partInfo.ActualSize, "", "", partInfo.ActualSize)
|
hr, err = hash.NewReader(io.LimitReader(r, partInfo.ActualSize), partInfo.ActualSize, "", "", partInfo.ActualSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pInfo, err = c.PutObjectPart(ctx, bucket, object, uploadID, partInfo.Number, hr, partInfo.ActualSize, "", "", opts.ServerSideEncryption)
|
|
||||||
|
popts := minio.PutObjectPartOptions{
|
||||||
|
SSE: opts.ServerSideEncryption,
|
||||||
|
}
|
||||||
|
|
||||||
|
pInfo, err = c.PutObjectPart(ctx, bucket, object, uploadID, partInfo.Number, hr, partInfo.ActualSize, popts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1535,7 +1612,6 @@ type ReplicationPool struct {
|
|||||||
// workers:
|
// workers:
|
||||||
workers []chan ReplicationWorkerOperation
|
workers []chan ReplicationWorkerOperation
|
||||||
existingWorkers chan ReplicationWorkerOperation
|
existingWorkers chan ReplicationWorkerOperation
|
||||||
workerWg sync.WaitGroup
|
|
||||||
|
|
||||||
// mrf:
|
// mrf:
|
||||||
mrfWorkerKillCh chan struct{}
|
mrfWorkerKillCh chan struct{}
|
||||||
@@ -1544,7 +1620,6 @@ type ReplicationPool struct {
|
|||||||
mrfStopCh chan struct{}
|
mrfStopCh chan struct{}
|
||||||
mrfWorkerSize int
|
mrfWorkerSize int
|
||||||
saveStateCh chan struct{}
|
saveStateCh chan struct{}
|
||||||
mrfWorkerWg sync.WaitGroup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplicationWorkerOperation is a shared interface of replication operations.
|
// ReplicationWorkerOperation is a shared interface of replication operations.
|
||||||
@@ -1607,7 +1682,6 @@ func NewReplicationPool(ctx context.Context, o ObjectLayer, opts replicationPool
|
|||||||
|
|
||||||
pool.ResizeWorkers(workers, 0)
|
pool.ResizeWorkers(workers, 0)
|
||||||
pool.ResizeFailedWorkers(failedWorkers)
|
pool.ResizeFailedWorkers(failedWorkers)
|
||||||
pool.workerWg.Add(1)
|
|
||||||
go pool.AddWorker(pool.existingWorkers, nil)
|
go pool.AddWorker(pool.existingWorkers, nil)
|
||||||
go pool.resyncer.PersistToDisk(ctx, o)
|
go pool.resyncer.PersistToDisk(ctx, o)
|
||||||
go pool.processMRF()
|
go pool.processMRF()
|
||||||
@@ -1619,7 +1693,6 @@ func NewReplicationPool(ctx context.Context, o ObjectLayer, opts replicationPool
|
|||||||
// AddMRFWorker adds a pending/failed replication worker to handle requests that could not be queued
|
// AddMRFWorker adds a pending/failed replication worker to handle requests that could not be queued
|
||||||
// to the other workers
|
// to the other workers
|
||||||
func (p *ReplicationPool) AddMRFWorker() {
|
func (p *ReplicationPool) AddMRFWorker() {
|
||||||
defer p.mrfWorkerWg.Done()
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-p.ctx.Done():
|
case <-p.ctx.Done():
|
||||||
@@ -1646,7 +1719,6 @@ func (p *ReplicationPool) AddMRFWorker() {
|
|||||||
// An optional pointer to a tracker that will be atomically
|
// An optional pointer to a tracker that will be atomically
|
||||||
// incremented when operations are running can be provided.
|
// incremented when operations are running can be provided.
|
||||||
func (p *ReplicationPool) AddWorker(input <-chan ReplicationWorkerOperation, opTracker *int32) {
|
func (p *ReplicationPool) AddWorker(input <-chan ReplicationWorkerOperation, opTracker *int32) {
|
||||||
defer p.workerWg.Done()
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-p.ctx.Done():
|
case <-p.ctx.Done():
|
||||||
@@ -1704,7 +1776,6 @@ func (p *ReplicationPool) ResizeWorkers(n, checkOld int) {
|
|||||||
input := make(chan ReplicationWorkerOperation, 10000)
|
input := make(chan ReplicationWorkerOperation, 10000)
|
||||||
p.workers = append(p.workers, input)
|
p.workers = append(p.workers, input)
|
||||||
|
|
||||||
p.workerWg.Add(1)
|
|
||||||
go p.AddWorker(input, &p.activeWorkers)
|
go p.AddWorker(input, &p.activeWorkers)
|
||||||
}
|
}
|
||||||
for len(p.workers) > n {
|
for len(p.workers) > n {
|
||||||
@@ -1748,7 +1819,6 @@ func (p *ReplicationPool) ResizeFailedWorkers(n int) {
|
|||||||
|
|
||||||
for p.mrfWorkerSize < n {
|
for p.mrfWorkerSize < n {
|
||||||
p.mrfWorkerSize++
|
p.mrfWorkerSize++
|
||||||
p.mrfWorkerWg.Add(1)
|
|
||||||
go p.AddMRFWorker()
|
go p.AddMRFWorker()
|
||||||
}
|
}
|
||||||
for p.mrfWorkerSize > n {
|
for p.mrfWorkerSize > n {
|
||||||
@@ -3030,7 +3100,7 @@ func (p *ReplicationPool) loadStatsFromDisk() (rs map[string]BucketReplicationSt
|
|||||||
|
|
||||||
data, err := readConfig(p.ctx, p.objLayer, getReplicationStatsPath())
|
data, err := readConfig(p.ctx, p.objLayer, getReplicationStatsPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return rs, nil
|
return rs, nil
|
||||||
}
|
}
|
||||||
return rs, err
|
return rs, err
|
||||||
|
|||||||
+16
-7
@@ -208,14 +208,14 @@ func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *m
|
|||||||
}
|
}
|
||||||
clnt, err := sys.getRemoteTargetClient(tgt)
|
clnt, err := sys.getRemoteTargetClient(tgt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket}
|
return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket, Err: err}
|
||||||
}
|
}
|
||||||
// validate if target credentials are ok
|
// validate if target credentials are ok
|
||||||
exists, err := clnt.BucketExists(ctx, tgt.TargetBucket)
|
exists, err := clnt.BucketExists(ctx, tgt.TargetBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch minio.ToErrorResponse(err).Code {
|
switch minio.ToErrorResponse(err).Code {
|
||||||
case "NoSuchBucket":
|
case "NoSuchBucket":
|
||||||
return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket}
|
return BucketRemoteTargetNotFound{Bucket: tgt.TargetBucket, Err: err}
|
||||||
case "AccessDenied":
|
case "AccessDenied":
|
||||||
return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, AccessKey: tgt.Credentials.AccessKey, Err: err}
|
return RemoteTargetConnectionErr{Bucket: tgt.TargetBucket, AccessKey: tgt.Credentials.AccessKey, Err: err}
|
||||||
}
|
}
|
||||||
@@ -453,6 +453,8 @@ func (sys *BucketTargetSys) getRemoteTargetClient(tcfg *madmin.BucketTarget) (*T
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
api.SetAppInfo("minio-replication-target", ReleaseTag+" "+tcfg.Arn)
|
||||||
|
|
||||||
hcDuration := defaultHealthCheckDuration
|
hcDuration := defaultHealthCheckDuration
|
||||||
if tcfg.HealthCheckDuration >= 1 { // require minimum health check duration of 1 sec.
|
if tcfg.HealthCheckDuration >= 1 { // require minimum health check duration of 1 sec.
|
||||||
hcDuration = tcfg.HealthCheckDuration
|
hcDuration = tcfg.HealthCheckDuration
|
||||||
@@ -473,20 +475,23 @@ func (sys *BucketTargetSys) getRemoteTargetClient(tcfg *madmin.BucketTarget) (*T
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getRemoteARN gets existing ARN for an endpoint or generates a new one.
|
// getRemoteARN gets existing ARN for an endpoint or generates a new one.
|
||||||
func (sys *BucketTargetSys) getRemoteARN(bucket string, target *madmin.BucketTarget) (arn string, exists bool) {
|
func (sys *BucketTargetSys) getRemoteARN(bucket string, target *madmin.BucketTarget, deplID string) (arn string, exists bool) {
|
||||||
if target == nil {
|
if target == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tgts := sys.targetsMap[bucket]
|
tgts := sys.targetsMap[bucket]
|
||||||
for _, tgt := range tgts {
|
for _, tgt := range tgts {
|
||||||
if tgt.Type == target.Type && tgt.TargetBucket == target.TargetBucket && target.URL().String() == tgt.URL().String() && tgt.Credentials.AccessKey == target.Credentials.AccessKey {
|
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
|
return tgt.Arn, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !target.Type.IsValid() {
|
if !target.Type.IsValid() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return generateARN(target), false
|
return generateARN(target, deplID), false
|
||||||
}
|
}
|
||||||
|
|
||||||
// getRemoteARNForPeer returns the remote target for a peer site in site replication
|
// getRemoteARNForPeer returns the remote target for a peer site in site replication
|
||||||
@@ -506,10 +511,14 @@ func (sys *BucketTargetSys) getRemoteARNForPeer(bucket string, peer madmin.PeerI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generate ARN that is unique to this target type
|
// generate ARN that is unique to this target type
|
||||||
func generateARN(t *madmin.BucketTarget) string {
|
func generateARN(t *madmin.BucketTarget, deplID string) string {
|
||||||
|
uuid := deplID
|
||||||
|
if uuid == "" {
|
||||||
|
uuid = mustGetUUID()
|
||||||
|
}
|
||||||
arn := madmin.ARN{
|
arn := madmin.ARN{
|
||||||
Type: t.Type,
|
Type: t.Type,
|
||||||
ID: mustGetUUID(),
|
ID: uuid,
|
||||||
Region: t.Region,
|
Region: t.Region,
|
||||||
Bucket: t.TargetBucket,
|
Bucket: t.TargetBucket,
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-41
@@ -51,7 +51,7 @@ import (
|
|||||||
consoleCerts "github.com/minio/console/pkg/certs"
|
consoleCerts "github.com/minio/console/pkg/certs"
|
||||||
"github.com/minio/console/restapi"
|
"github.com/minio/console/restapi"
|
||||||
"github.com/minio/console/restapi/operations"
|
"github.com/minio/console/restapi/operations"
|
||||||
"github.com/minio/kes"
|
"github.com/minio/kes-go"
|
||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
@@ -59,7 +59,6 @@ import (
|
|||||||
"github.com/minio/minio/internal/auth"
|
"github.com/minio/minio/internal/auth"
|
||||||
"github.com/minio/minio/internal/color"
|
"github.com/minio/minio/internal/color"
|
||||||
"github.com/minio/minio/internal/config"
|
"github.com/minio/minio/internal/config"
|
||||||
"github.com/minio/minio/internal/handlers"
|
|
||||||
"github.com/minio/minio/internal/kms"
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/certs"
|
"github.com/minio/pkg/certs"
|
||||||
@@ -133,20 +132,11 @@ func init() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
globalForwarder = handlers.NewForwarder(&handlers.Forwarder{
|
|
||||||
PassHost: true,
|
|
||||||
RoundTripper: NewHTTPTransportWithTimeout(1 * time.Hour),
|
|
||||||
Logger: func(err error) {
|
|
||||||
if err != nil && !errors.Is(err, context.Canceled) {
|
|
||||||
logger.LogIf(GlobalContext, err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
console.SetColor("Debug", fcolor.New())
|
console.SetColor("Debug", fcolor.New())
|
||||||
|
|
||||||
gob.Register(StorageErr(""))
|
gob.Register(StorageErr(""))
|
||||||
gob.Register(madmin.TimeInfo{})
|
gob.Register(madmin.TimeInfo{})
|
||||||
|
gob.Register(madmin.XFSErrorConfigs{})
|
||||||
gob.Register(map[string]interface{}{})
|
gob.Register(map[string]interface{}{})
|
||||||
|
|
||||||
defaultAWSCredProvider = []credentials.Provider{
|
defaultAWSCredProvider = []credentials.Provider{
|
||||||
@@ -187,7 +177,7 @@ func minioConfigToConsoleFeatures() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// pass the console subpath configuration
|
// pass the console subpath configuration
|
||||||
if value := env.Get(config.EnvMinIOBrowserRedirectURL, ""); value != "" {
|
if value := env.Get(config.EnvBrowserRedirectURL, ""); value != "" {
|
||||||
subPath := path.Clean(pathJoin(strings.TrimSpace(globalBrowserRedirectURL.Path), SlashSeparator))
|
subPath := path.Clean(pathJoin(strings.TrimSpace(globalBrowserRedirectURL.Path), SlashSeparator))
|
||||||
if subPath != SlashSeparator {
|
if subPath != SlashSeparator {
|
||||||
os.Setenv("CONSOLE_SUBPATH", subPath)
|
os.Setenv("CONSOLE_SUBPATH", subPath)
|
||||||
@@ -205,9 +195,14 @@ func minioConfigToConsoleFeatures() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Enable if LDAP is enabled.
|
// Enable if LDAP is enabled.
|
||||||
if globalLDAPConfig.Enabled() {
|
if globalIAMSys.LDAPConfig.Enabled() {
|
||||||
os.Setenv("CONSOLE_LDAP_ENABLED", config.EnableOn)
|
os.Setenv("CONSOLE_LDAP_ENABLED", config.EnableOn)
|
||||||
}
|
}
|
||||||
|
// Handle animation in welcome page
|
||||||
|
if value := env.Get(config.EnvBrowserLoginAnimation, "on"); value != "" {
|
||||||
|
os.Setenv("CONSOLE_ANIMATED_LOGIN", value)
|
||||||
|
}
|
||||||
|
|
||||||
os.Setenv("CONSOLE_MINIO_REGION", globalSite.Region)
|
os.Setenv("CONSOLE_MINIO_REGION", globalSite.Region)
|
||||||
os.Setenv("CONSOLE_CERT_PASSWD", env.Get("MINIO_CERT_PASSWD", ""))
|
os.Setenv("CONSOLE_CERT_PASSWD", env.Get("MINIO_CERT_PASSWD", ""))
|
||||||
|
|
||||||
@@ -215,8 +210,9 @@ func minioConfigToConsoleFeatures() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildOpenIDConsoleConfig() consoleoauth2.OpenIDPCfg {
|
func buildOpenIDConsoleConfig() consoleoauth2.OpenIDPCfg {
|
||||||
m := make(map[string]consoleoauth2.ProviderConfig, len(globalOpenIDConfig.ProviderCfgs))
|
pcfgs := globalIAMSys.OpenIDConfig.ProviderCfgs
|
||||||
for name, cfg := range globalOpenIDConfig.ProviderCfgs {
|
m := make(map[string]consoleoauth2.ProviderConfig, len(pcfgs))
|
||||||
|
for name, cfg := range pcfgs {
|
||||||
callback := getConsoleEndpoints()[0] + "/oauth_callback"
|
callback := getConsoleEndpoints()[0] + "/oauth_callback"
|
||||||
if cfg.RedirectURI != "" {
|
if cfg.RedirectURI != "" {
|
||||||
callback = cfg.RedirectURI
|
callback = cfg.RedirectURI
|
||||||
@@ -256,6 +252,9 @@ func initConsoleServer() (*restapi.Server, error) {
|
|||||||
Path: globalCertsCADir.Get(),
|
Path: globalCertsCADir.Get(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set certs before other console initialization
|
||||||
|
restapi.GlobalRootCAs, restapi.GlobalPublicCerts, restapi.GlobalTLSCertsManager = globalRootCAs, globalPublicCerts, globalTLSCerts
|
||||||
|
|
||||||
swaggerSpec, err := loads.Embedded(restapi.SwaggerJSON, restapi.FlatSwaggerJSON)
|
swaggerSpec, err := loads.Embedded(restapi.SwaggerJSON, restapi.FlatSwaggerJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -282,8 +281,6 @@ func initConsoleServer() (*restapi.Server, error) {
|
|||||||
// register all APIs
|
// register all APIs
|
||||||
server.ConfigureAPI()
|
server.ConfigureAPI()
|
||||||
|
|
||||||
restapi.GlobalRootCAs, restapi.GlobalPublicCerts, restapi.GlobalTLSCertsManager = globalRootCAs, globalPublicCerts, globalTLSCerts
|
|
||||||
|
|
||||||
consolePort, _ := strconv.Atoi(globalMinioConsolePort)
|
consolePort, _ := strconv.Atoi(globalMinioConsolePort)
|
||||||
|
|
||||||
server.Host = globalMinioConsoleHost
|
server.Host = globalMinioConsoleHost
|
||||||
@@ -600,13 +597,13 @@ func loadEnvVarsFromFiles() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if env.IsSet(config.EnvKMSSecretKeyFile) {
|
if env.IsSet(kms.EnvKMSSecretKeyFile) {
|
||||||
kmsSecret, err := readFromSecret(env.Get(config.EnvKMSSecretKeyFile, ""))
|
kmsSecret, err := readFromSecret(env.Get(kms.EnvKMSSecretKeyFile, ""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(err, "Unable to read the KMS secret key inherited from secret file")
|
logger.Fatal(err, "Unable to read the KMS secret key inherited from secret file")
|
||||||
}
|
}
|
||||||
if kmsSecret != "" {
|
if kmsSecret != "" {
|
||||||
os.Setenv(config.EnvKMSSecretKey, kmsSecret)
|
os.Setenv(kms.EnvKMSSecretKey, kmsSecret)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,7 +627,7 @@ func handleCommonEnvVars() {
|
|||||||
logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable")
|
logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable")
|
||||||
}
|
}
|
||||||
if globalBrowserEnabled {
|
if globalBrowserEnabled {
|
||||||
if redirectURL := env.Get(config.EnvMinIOBrowserRedirectURL, ""); redirectURL != "" {
|
if redirectURL := env.Get(config.EnvBrowserRedirectURL, ""); redirectURL != "" {
|
||||||
u, err := xnet.ParseHTTPURL(redirectURL)
|
u, err := xnet.ParseHTTPURL(redirectURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable")
|
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable")
|
||||||
@@ -639,7 +636,7 @@ func handleCommonEnvVars() {
|
|||||||
if !((u.Scheme == "http" || u.Scheme == "https") &&
|
if !((u.Scheme == "http" || u.Scheme == "https") &&
|
||||||
u.Opaque == "" &&
|
u.Opaque == "" &&
|
||||||
!u.ForceQuery && u.RawQuery == "" && u.Fragment == "") {
|
!u.ForceQuery && u.RawQuery == "" && u.Fragment == "") {
|
||||||
err := fmt.Errorf("URL contains unexpected resources, expected URL to be of http(s)://minio.example.com format: %v", u)
|
err := fmt.Errorf("URL contains unexpected resources, expected URL to be one of http(s)://console.example.com or as a subpath via API endpoint http(s)://minio.example.com/minio format: %v", u)
|
||||||
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value is environment variable")
|
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value is environment variable")
|
||||||
}
|
}
|
||||||
globalBrowserRedirectURL = u
|
globalBrowserRedirectURL = u
|
||||||
@@ -660,6 +657,7 @@ func handleCommonEnvVars() {
|
|||||||
}
|
}
|
||||||
u.Path = "" // remove any path component such as `/`
|
u.Path = "" // remove any path component such as `/`
|
||||||
globalMinioEndpoint = u.String()
|
globalMinioEndpoint = u.String()
|
||||||
|
globalMinioEndpointURL = u
|
||||||
}
|
}
|
||||||
|
|
||||||
globalFSOSync, err = config.ParseBool(env.Get(config.EnvFSOSync, config.EnableOff))
|
globalFSOSync, err = config.ParseBool(env.Get(config.EnvFSOSync, config.EnableOff))
|
||||||
@@ -774,6 +772,7 @@ func handleCommonEnvVars() {
|
|||||||
logger.Info(color.RedBold(msg))
|
logger.Info(color.RedBold(msg))
|
||||||
}
|
}
|
||||||
globalActiveCred = cred
|
globalActiveCred = cred
|
||||||
|
globalCredViaEnv = true
|
||||||
} else {
|
} else {
|
||||||
globalActiveCred = auth.DefaultCredentials
|
globalActiveCred = auth.DefaultCredentials
|
||||||
}
|
}
|
||||||
@@ -782,21 +781,29 @@ func handleCommonEnvVars() {
|
|||||||
// Initialize KMS global variable after valiadating and loading the configuration.
|
// Initialize KMS global variable after valiadating and loading the configuration.
|
||||||
// It depends on KMS env variables and global cli flags.
|
// It depends on KMS env variables and global cli flags.
|
||||||
func handleKMSConfig() {
|
func handleKMSConfig() {
|
||||||
switch {
|
if env.IsSet(kms.EnvKMSSecretKey) && env.IsSet(kms.EnvKESEndpoint) {
|
||||||
case env.IsSet(config.EnvKMSSecretKey) && env.IsSet(config.EnvKESEndpoint):
|
logger.Fatal(errors.New("ambigious KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKMSSecretKey, kms.EnvKESEndpoint))
|
||||||
logger.Fatal(errors.New("ambigious KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", config.EnvKMSSecretKey, config.EnvKESEndpoint))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if env.IsSet(config.EnvKMSSecretKey) {
|
if env.IsSet(kms.EnvKMSSecretKey) {
|
||||||
KMS, err := kms.Parse(env.Get(config.EnvKMSSecretKey, ""))
|
KMS, err := kms.Parse(env.Get(kms.EnvKMSSecretKey, ""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(err, "Unable to parse the KMS secret key inherited from the shell environment")
|
logger.Fatal(err, "Unable to parse the KMS secret key inherited from the shell environment")
|
||||||
}
|
}
|
||||||
GlobalKMS = KMS
|
GlobalKMS = KMS
|
||||||
}
|
}
|
||||||
if env.IsSet(config.EnvKESEndpoint) {
|
if env.IsSet(kms.EnvKESEndpoint) {
|
||||||
|
if env.IsSet(kms.EnvKESAPIKey) {
|
||||||
|
if env.IsSet(kms.EnvKESClientKey) {
|
||||||
|
logger.Fatal(errors.New("ambigious KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKESAPIKey, kms.EnvKESClientKey))
|
||||||
|
}
|
||||||
|
if env.IsSet(kms.EnvKESClientCert) {
|
||||||
|
logger.Fatal(errors.New("ambigious KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKESAPIKey, kms.EnvKESClientCert))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var endpoints []string
|
var endpoints []string
|
||||||
for _, endpoint := range strings.Split(env.Get(config.EnvKESEndpoint, ""), ",") {
|
for _, endpoint := range strings.Split(env.Get(kms.EnvKESEndpoint, ""), ",") {
|
||||||
if strings.TrimSpace(endpoint) == "" {
|
if strings.TrimSpace(endpoint) == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -812,11 +819,25 @@ func handleKMSConfig() {
|
|||||||
endpoints = append(endpoints, strings.Join(lbls, ""))
|
endpoints = append(endpoints, strings.Join(lbls, ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rootCAs, err := certs.GetRootCAs(env.Get(config.EnvKESServerCA, globalCertsCADir.Get()))
|
rootCAs, err := certs.GetRootCAs(env.Get(kms.EnvKESServerCA, globalCertsCADir.Get()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(err, fmt.Sprintf("Unable to load X.509 root CAs for KES from %q", env.Get(config.EnvKESServerCA, globalCertsCADir.Get())))
|
logger.Fatal(err, fmt.Sprintf("Unable to load X.509 root CAs for KES from %q", env.Get(kms.EnvKESServerCA, globalCertsCADir.Get())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var kmsConf kms.Config
|
||||||
|
if env.IsSet(kms.EnvKESAPIKey) {
|
||||||
|
key, err := kes.ParseAPIKey(env.Get(kms.EnvKESAPIKey, ""))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err, fmt.Sprintf("Failed to parse KES API key from %q", env.Get(kms.EnvKESAPIKey, "")))
|
||||||
|
}
|
||||||
|
kmsConf = kms.Config{
|
||||||
|
Endpoints: endpoints,
|
||||||
|
Enclave: env.Get(kms.EnvKESEnclave, ""),
|
||||||
|
DefaultKeyID: env.Get(kms.EnvKESKeyName, ""),
|
||||||
|
APIKey: key,
|
||||||
|
RootCAs: rootCAs,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
loadX509KeyPair := func(certFile, keyFile string) (tls.Certificate, error) {
|
loadX509KeyPair := func(certFile, keyFile string) (tls.Certificate, error) {
|
||||||
// Manually load the certificate and private key into memory.
|
// Manually load the certificate and private key into memory.
|
||||||
// We need to check whether the private key is encrypted, and
|
// We need to check whether the private key is encrypted, and
|
||||||
@@ -834,7 +855,7 @@ func handleKMSConfig() {
|
|||||||
return tls.Certificate{}, errors.New("Unable to load KES client private key as specified by the shell environment: private key contains additional data")
|
return tls.Certificate{}, errors.New("Unable to load KES client private key as specified by the shell environment: private key contains additional data")
|
||||||
}
|
}
|
||||||
if x509.IsEncryptedPEMBlock(privateKeyPEM) {
|
if x509.IsEncryptedPEMBlock(privateKeyPEM) {
|
||||||
keyBytes, err = x509.DecryptPEMBlock(privateKeyPEM, []byte(env.Get(config.EnvKESClientPassword, "")))
|
keyBytes, err = x509.DecryptPEMBlock(privateKeyPEM, []byte(env.Get(kms.EnvKESClientPassword, "")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tls.Certificate{}, fmt.Errorf("Unable to decrypt KES client private key as specified by the shell environment: %v", err)
|
return tls.Certificate{}, fmt.Errorf("Unable to decrypt KES client private key as specified by the shell environment: %v", err)
|
||||||
}
|
}
|
||||||
@@ -848,31 +869,32 @@ func handleKMSConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reloadCertEvents := make(chan tls.Certificate, 1)
|
reloadCertEvents := make(chan tls.Certificate, 1)
|
||||||
certificate, err := certs.NewCertificate(env.Get(config.EnvKESClientCert, ""), env.Get(config.EnvKESClientKey, ""), loadX509KeyPair)
|
certificate, err := certs.NewCertificate(env.Get(kms.EnvKESClientCert, ""), env.Get(kms.EnvKESClientKey, ""), loadX509KeyPair)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(err, "Failed to load KES client certificate")
|
logger.Fatal(err, "Failed to load KES client certificate")
|
||||||
}
|
}
|
||||||
certificate.Watch(context.Background(), 15*time.Minute, syscall.SIGHUP)
|
certificate.Watch(context.Background(), 15*time.Minute, syscall.SIGHUP)
|
||||||
certificate.Notify(reloadCertEvents)
|
certificate.Notify(reloadCertEvents)
|
||||||
|
|
||||||
defaultKeyID := env.Get(config.EnvKESKeyName, "")
|
kmsConf = kms.Config{
|
||||||
KMS, err := kms.NewWithConfig(kms.Config{
|
|
||||||
Endpoints: endpoints,
|
Endpoints: endpoints,
|
||||||
Enclave: env.Get(config.EnvKESEnclave, ""),
|
Enclave: env.Get(kms.EnvKESEnclave, ""),
|
||||||
DefaultKeyID: defaultKeyID,
|
DefaultKeyID: env.Get(kms.EnvKESKeyName, ""),
|
||||||
Certificate: certificate,
|
Certificate: certificate,
|
||||||
ReloadCertEvents: reloadCertEvents,
|
ReloadCertEvents: reloadCertEvents,
|
||||||
RootCAs: rootCAs,
|
RootCAs: rootCAs,
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KMS, err := kms.NewWithConfig(kmsConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment")
|
logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment")
|
||||||
}
|
}
|
||||||
|
|
||||||
// We check that the default key ID exists or try to create it otherwise.
|
// We check that the default key ID exists or try to create it otherwise.
|
||||||
// This implicitly checks that we can communicate to KES. We don't treat
|
// This implicitly checks that we can communicate to KES. We don't treat
|
||||||
// a policy error as failure condition since MinIO may not have the permission
|
// a policy error as failure condition since MinIO may not have the permission
|
||||||
// to create keys - just to generate/decrypt data encryption keys.
|
// to create keys - just to generate/decrypt data encryption keys.
|
||||||
if err = KMS.CreateKey(context.Background(), defaultKeyID); err != nil && !errors.Is(err, kes.ErrKeyExists) && !errors.Is(err, kes.ErrNotAllowed) {
|
if err = KMS.CreateKey(context.Background(), env.Get(kms.EnvKESKeyName, "")); err != nil && !errors.Is(err, kes.ErrKeyExists) && !errors.Is(err, kes.ErrNotAllowed) {
|
||||||
logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment")
|
logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment")
|
||||||
}
|
}
|
||||||
GlobalKMS = KMS
|
GlobalKMS = KMS
|
||||||
|
|||||||
@@ -30,12 +30,7 @@ import (
|
|||||||
var errConfigNotFound = errors.New("config file not found")
|
var errConfigNotFound = errors.New("config file not found")
|
||||||
|
|
||||||
func readConfigWithMetadata(ctx context.Context, store objectIO, configFile string, opts ObjectOptions) ([]byte, ObjectInfo, error) {
|
func readConfigWithMetadata(ctx context.Context, store objectIO, configFile string, opts ObjectOptions) ([]byte, ObjectInfo, error) {
|
||||||
lockType := readLock
|
r, err := store.GetObjectNInfo(ctx, minioMetaBucket, configFile, nil, http.Header{}, opts)
|
||||||
if opts.NoLock {
|
|
||||||
lockType = noLock // erasureObjects.GetObjectNInfo honors lockType argument but not opts.NoLock.
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := store.GetObjectNInfo(ctx, minioMetaBucket, configFile, nil, http.Header{}, lockType, opts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isErrObjectNotFound(err) {
|
if isErrObjectNotFound(err) {
|
||||||
return nil, ObjectInfo{}, errConfigNotFound
|
return nil, ObjectInfo{}, errConfigNotFound
|
||||||
|
|||||||
+46
-25
@@ -37,6 +37,7 @@ import (
|
|||||||
"github.com/minio/minio/internal/config/identity/openid"
|
"github.com/minio/minio/internal/config/identity/openid"
|
||||||
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
||||||
xtls "github.com/minio/minio/internal/config/identity/tls"
|
xtls "github.com/minio/minio/internal/config/identity/tls"
|
||||||
|
"github.com/minio/minio/internal/config/lambda"
|
||||||
"github.com/minio/minio/internal/config/notify"
|
"github.com/minio/minio/internal/config/notify"
|
||||||
"github.com/minio/minio/internal/config/policy/opa"
|
"github.com/minio/minio/internal/config/policy/opa"
|
||||||
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||||
@@ -74,6 +75,9 @@ func initHelp() {
|
|||||||
for k, v := range notify.DefaultNotificationKVS {
|
for k, v := range notify.DefaultNotificationKVS {
|
||||||
kvs[k] = v
|
kvs[k] = v
|
||||||
}
|
}
|
||||||
|
for k, v := range lambda.DefaultLambdaKVS {
|
||||||
|
kvs[k] = v
|
||||||
|
}
|
||||||
if globalIsErasure {
|
if globalIsErasure {
|
||||||
kvs[config.StorageClassSubSys] = storageclass.DefaultKVS
|
kvs[config.StorageClassSubSys] = storageclass.DefaultKVS
|
||||||
kvs[config.HealSubSys] = heal.DefaultKVS
|
kvs[config.HealSubSys] = heal.DefaultKVS
|
||||||
@@ -196,6 +200,11 @@ func initHelp() {
|
|||||||
Description: "publish bucket notifications to Redis datastores",
|
Description: "publish bucket notifications to Redis datastores",
|
||||||
MultipleTargets: true,
|
MultipleTargets: true,
|
||||||
},
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: config.LambdaWebhookSubSys,
|
||||||
|
Description: "manage remote lambda functions",
|
||||||
|
MultipleTargets: true,
|
||||||
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.EtcdSubSys,
|
Key: config.EtcdSubSys,
|
||||||
Description: "persist IAM assets externally to etcd",
|
Description: "persist IAM assets externally to etcd",
|
||||||
@@ -246,6 +255,7 @@ func initHelp() {
|
|||||||
config.NotifyRedisSubSys: notify.HelpRedis,
|
config.NotifyRedisSubSys: notify.HelpRedis,
|
||||||
config.NotifyWebhookSubSys: notify.HelpWebhook,
|
config.NotifyWebhookSubSys: notify.HelpWebhook,
|
||||||
config.NotifyESSubSys: notify.HelpES,
|
config.NotifyESSubSys: notify.HelpES,
|
||||||
|
config.LambdaWebhookSubSys: lambda.HelpWebhook,
|
||||||
config.SubnetSubSys: subnet.HelpSubnet,
|
config.SubnetSubSys: subnet.HelpSubnet,
|
||||||
config.CallhomeSubSys: callhome.HelpCallhome,
|
config.CallhomeSubSys: callhome.HelpCallhome,
|
||||||
}
|
}
|
||||||
@@ -387,6 +397,13 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.LambdaSubSystems.Contains(subSys) {
|
||||||
|
if err := lambda.TestSubSysLambdaTargets(GlobalContext, s, subSys, NewHTTPTransport()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,6 +440,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
|||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize remote webhook DNS config %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize remote webhook DNS config %w", err))
|
||||||
}
|
}
|
||||||
if err == nil && dnsURL != "" {
|
if err == nil && dnsURL != "" {
|
||||||
|
bootstrapTrace("initialize remote bucket DNS store")
|
||||||
globalDNSConfig, err = dns.NewOperatorDNS(dnsURL,
|
globalDNSConfig, err = dns.NewOperatorDNS(dnsURL,
|
||||||
dns.Authentication(dnsUser, dnsPass),
|
dns.Authentication(dnsUser, dnsPass),
|
||||||
dns.RootCAs(globalRootCAs))
|
dns.RootCAs(globalRootCAs))
|
||||||
@@ -437,6 +455,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if etcdCfg.Enabled {
|
if etcdCfg.Enabled {
|
||||||
|
bootstrapTrace("initialize etcd store")
|
||||||
globalEtcdClient, err = etcd.New(etcdCfg)
|
globalEtcdClient, err = etcd.New(etcdCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize etcd config: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize etcd config: %w", err))
|
||||||
@@ -493,23 +512,20 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
|||||||
logger.Fatal(errors.New("no KMS configured"), "MINIO_KMS_AUTO_ENCRYPTION requires a valid KMS configuration")
|
logger.Fatal(errors.New("no KMS configured"), "MINIO_KMS_AUTO_ENCRYPTION requires a valid KMS configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
globalSTSTLSConfig, err = xtls.Lookup(s[config.IdentityTLSSubSys][config.Default])
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize X.509/TLS STS API: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalSTSTLSConfig.InsecureSkipVerify {
|
|
||||||
logger.LogIf(ctx, fmt.Errorf("CRITICAL: enabling %s is not recommended in a production environment", xtls.EnvIdentityTLSSkipVerify))
|
|
||||||
}
|
|
||||||
|
|
||||||
transport := NewHTTPTransport()
|
transport := NewHTTPTransport()
|
||||||
|
|
||||||
bootstrapTrace("lookup the event notification targets")
|
bootstrapTrace("initialize the event notification targets")
|
||||||
globalConfigTargetList, err = notify.FetchEnabledTargets(GlobalContext, s, transport)
|
globalNotifyTargetList, err = notify.FetchEnabledTargets(GlobalContext, s, transport)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bootstrapTrace("initialize the lambda targets")
|
||||||
|
globalLambdaTargetList, err = lambda.FetchEnabledTargets(GlobalContext, s, transport)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize lambda target(s): %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
bootstrapTrace("applying the dynamic configuration")
|
bootstrapTrace("applying the dynamic configuration")
|
||||||
// Apply dynamic config values
|
// Apply dynamic config values
|
||||||
if err := applyDynamicConfig(ctx, objAPI, s); err != nil {
|
if err := applyDynamicConfig(ctx, objAPI, s); err != nil {
|
||||||
@@ -572,7 +588,7 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
|||||||
}
|
}
|
||||||
loggerCfg.HTTP[n] = l
|
loggerCfg.HTTP[n] = l
|
||||||
}
|
}
|
||||||
if errs := logger.UpdateSystemTargets(loggerCfg); len(errs) > 0 {
|
if errs := logger.UpdateSystemTargets(ctx, loggerCfg); len(errs) > 0 {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to update logger webhook config: %v", errs))
|
logger.LogIf(ctx, fmt.Errorf("Unable to update logger webhook config: %v", errs))
|
||||||
}
|
}
|
||||||
case config.AuditWebhookSubSys:
|
case config.AuditWebhookSubSys:
|
||||||
@@ -590,7 +606,7 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
|||||||
loggerCfg.AuditWebhook[n] = l
|
loggerCfg.AuditWebhook[n] = l
|
||||||
}
|
}
|
||||||
|
|
||||||
if errs := logger.UpdateAuditWebhookTargets(loggerCfg); len(errs) > 0 {
|
if errs := logger.UpdateAuditWebhookTargets(ctx, loggerCfg); len(errs) > 0 {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to update audit webhook targets: %v", errs))
|
logger.LogIf(ctx, fmt.Errorf("Unable to update audit webhook targets: %v", errs))
|
||||||
}
|
}
|
||||||
case config.AuditKafkaSubSys:
|
case config.AuditKafkaSubSys:
|
||||||
@@ -604,7 +620,7 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
|||||||
loggerCfg.AuditKafka[n] = l
|
loggerCfg.AuditKafka[n] = l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errs := logger.UpdateAuditKafkaTargets(loggerCfg); len(errs) > 0 {
|
if errs := logger.UpdateAuditKafkaTargets(ctx, loggerCfg); len(errs) > 0 {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to update audit kafka targets: %v", errs))
|
logger.LogIf(ctx, fmt.Errorf("Unable to update audit kafka targets: %v", errs))
|
||||||
}
|
}
|
||||||
case config.StorageClassSubSys:
|
case config.StorageClassSubSys:
|
||||||
@@ -701,37 +717,42 @@ func GetHelp(subSys, key string, envOnly bool) (Help, error) {
|
|||||||
h = config.HelpKVS{value}
|
h = config.HelpKVS{value}
|
||||||
}
|
}
|
||||||
|
|
||||||
envHelp := config.HelpKVS{}
|
help := config.HelpKVS{}
|
||||||
if envOnly {
|
|
||||||
// Only for multiple targets, make sure
|
// Only for multiple targets, make sure
|
||||||
// to list the ENV, for regular k/v EnableKey is
|
// to list the ENV, for regular k/v EnableKey is
|
||||||
// implicit, for ENVs we cannot make it implicit.
|
// implicit, for ENVs we cannot make it implicit.
|
||||||
if subSysHelp.MultipleTargets {
|
if subSysHelp.MultipleTargets {
|
||||||
envK := config.EnvPrefix + strings.ToTitle(subSys) + config.EnvWordDelimiter + strings.ToTitle(madmin.EnableKey)
|
key := madmin.EnableKey
|
||||||
envHelp = append(envHelp, config.HelpKV{
|
if envOnly {
|
||||||
Key: envK,
|
key = config.EnvPrefix + strings.ToTitle(subSys) + config.EnvWordDelimiter + strings.ToTitle(madmin.EnableKey)
|
||||||
|
}
|
||||||
|
help = append(help, config.HelpKV{
|
||||||
|
Key: key,
|
||||||
Description: fmt.Sprintf("enable %s target, default is 'off'", subSys),
|
Description: fmt.Sprintf("enable %s target, default is 'off'", subSys),
|
||||||
Optional: false,
|
Optional: false,
|
||||||
Type: "on|off",
|
Type: "on|off",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, hkv := range h {
|
for _, hkv := range h {
|
||||||
envK := config.EnvPrefix + strings.ToTitle(subSys) + config.EnvWordDelimiter + strings.ToTitle(hkv.Key)
|
key := hkv.Key
|
||||||
envHelp = append(envHelp, config.HelpKV{
|
if envOnly {
|
||||||
Key: envK,
|
key = config.EnvPrefix + strings.ToTitle(subSys) + config.EnvWordDelimiter + strings.ToTitle(hkv.Key)
|
||||||
|
}
|
||||||
|
help = append(help, config.HelpKV{
|
||||||
|
Key: key,
|
||||||
Description: hkv.Description,
|
Description: hkv.Description,
|
||||||
Optional: hkv.Optional,
|
Optional: hkv.Optional,
|
||||||
Type: hkv.Type,
|
Type: hkv.Type,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
h = envHelp
|
|
||||||
}
|
|
||||||
|
|
||||||
return Help{
|
return Help{
|
||||||
SubSys: subSys,
|
SubSys: subSys,
|
||||||
Description: subSysHelp.Description,
|
Description: subSysHelp.Description,
|
||||||
MultipleTargets: subSysHelp.MultipleTargets,
|
MultipleTargets: subSysHelp.MultipleTargets,
|
||||||
KeysHelp: h,
|
KeysHelp: help,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,215 +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 (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/minio/madmin-go/v2"
|
|
||||||
"github.com/minio/minio/internal/config"
|
|
||||||
"github.com/minio/minio/internal/kms"
|
|
||||||
"github.com/minio/minio/internal/logger"
|
|
||||||
etcd "go.etcd.io/etcd/client/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleEncryptedConfigBackend(objAPI ObjectLayer) error {
|
|
||||||
encrypted, err := checkBackendEncrypted(objAPI)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Unable to encrypt config %w", err)
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
return err == nil && bytes.Equal(data, backendEncryptedMigrationComplete), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
return err == nil && bytes.Equal(data, backendEncryptedMigrationComplete), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrateIAMConfigsEtcdToEncrypted(ctx context.Context, client *etcd.Client) error {
|
|
||||||
encrypted, err := checkBackendEtcdEncrypted(ctx, client)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If backend doesn't have this file means we have already
|
|
||||||
// attempted then migration
|
|
||||||
if !encrypted {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrapTrace("encrypt etcd config")
|
|
||||||
|
|
||||||
if GlobalKMS != nil {
|
|
||||||
stat, err := GlobalKMS.Stat(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logger.Info(fmt.Sprintf("Attempting to re-encrypt IAM users and policies on etcd with %q (%s)", stat.DefaultKey, stat.Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
listCtx, cancel := context.WithTimeout(ctx, 1*time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
r, err := client.Get(listCtx, minioConfigPrefix, etcd.WithPrefix(), etcd.WithKeysOnly())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, kv := range r.Kvs {
|
|
||||||
data, err := readKeyEtcd(ctx, client, string(kv.Key))
|
|
||||||
if err == errConfigNotFound { // Perhaps not present or someone deleted it.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utf8.Valid(data) {
|
|
||||||
pdata, err := madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
if GlobalKMS != nil {
|
|
||||||
pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
|
|
||||||
minioMetaBucket: path.Join(minioMetaBucket, string(kv.Key)),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
|
|
||||||
minioMetaBucket: string(kv.Key),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Decrypting IAM config failed %w, possibly credentials are incorrect", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("Decrypting IAM config failed %w, possibly credentials are incorrect", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = pdata
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS != nil {
|
|
||||||
data, err = config.EncryptBytes(GlobalKMS, data, kms.Context{
|
|
||||||
minioMetaBucket: path.Join(minioMetaBucket, string(kv.Key)),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = saveKeyEtcd(ctx, client, string(kv.Key), data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) error {
|
|
||||||
bootstrapTrace("migrating config prefix to encrypted")
|
|
||||||
if GlobalKMS != nil {
|
|
||||||
stat, err := GlobalKMS.Stat(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logger.Info(fmt.Sprintf("Attempting to re-encrypt config, IAM users and policies on MinIO with %q (%s)", stat.DefaultKey, stat.Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
results := make(chan ObjectInfo)
|
|
||||||
if err := objAPI.Walk(GlobalContext, minioMetaBucket, minioConfigPrefix, results, ObjectOptions{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for obj := range results {
|
|
||||||
data, err := readConfig(GlobalContext, objAPI, obj.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utf8.Valid(data) {
|
|
||||||
pdata, err := madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
if GlobalKMS != nil {
|
|
||||||
pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
|
|
||||||
minioMetaBucket: path.Join(minioMetaBucket, obj.Name),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
|
|
||||||
minioMetaBucket: obj.Name,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Decrypting IAM config failed %w, possibly credentials are incorrect", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("Decrypting IAM config failed %w, possibly credentials are incorrect", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = pdata
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS != nil {
|
|
||||||
data, err = config.EncryptBytes(GlobalKMS, data, kms.Context{
|
|
||||||
obj.Bucket: path.Join(obj.Bucket, obj.Name),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = saveConfig(GlobalContext, objAPI, obj.Name, data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS != nil {
|
|
||||||
logger.Info("Migration of encrypted config data completed. All config data is now encrypted.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return deleteConfig(GlobalContext, objAPI, backendEncryptedFile)
|
|
||||||
}
|
|
||||||
+63
-265
@@ -18,16 +18,15 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/minio/madmin-go/v2"
|
|
||||||
"github.com/minio/minio/internal/auth"
|
"github.com/minio/minio/internal/auth"
|
||||||
"github.com/minio/minio/internal/config"
|
"github.com/minio/minio/internal/config"
|
||||||
"github.com/minio/minio/internal/config/cache"
|
"github.com/minio/minio/internal/config/cache"
|
||||||
@@ -39,14 +38,13 @@ import (
|
|||||||
"github.com/minio/minio/internal/config/storageclass"
|
"github.com/minio/minio/internal/config/storageclass"
|
||||||
"github.com/minio/minio/internal/event"
|
"github.com/minio/minio/internal/event"
|
||||||
"github.com/minio/minio/internal/event/target"
|
"github.com/minio/minio/internal/event/target"
|
||||||
"github.com/minio/minio/internal/kms"
|
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/minio/internal/logger/target/http"
|
"github.com/minio/minio/internal/logger/target/http"
|
||||||
xnet "github.com/minio/pkg/net"
|
xnet "github.com/minio/pkg/net"
|
||||||
"github.com/minio/pkg/quick"
|
"github.com/minio/pkg/quick"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DO NOT EDIT following message template, please open a GitHub issue to discuss instead.
|
// Do not edit following message template, please open a GitHub issue to discuss instead.
|
||||||
var configMigrateMSGTemplate = "Configuration file %s migrated from version '%s' to '%s' successfully."
|
var configMigrateMSGTemplate = "Configuration file %s migrated from version '%s' to '%s' successfully."
|
||||||
|
|
||||||
// Save config file to corresponding backend
|
// Save config file to corresponding backend
|
||||||
@@ -2471,291 +2469,97 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
|||||||
return saveServerConfig(GlobalContext, objAPI, config)
|
return saveServerConfig(GlobalContext, objAPI, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrates '.minio.sys/config.json' to v33.
|
func readConfigWithoutMigrate(ctx context.Context, objAPI ObjectLayer) (config.Config, error) {
|
||||||
func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
|
||||||
bootstrapTrace("migrate .minio.sys/config/config.json to latest version")
|
|
||||||
// Construct path to config.json for the given bucket.
|
// Construct path to config.json for the given bucket.
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
|
||||||
// Check if the config version is latest, if not migrate.
|
configFiles := []string{
|
||||||
ok, _, err := checkConfigVersion(objAPI, configFile, "33")
|
getConfigFile(),
|
||||||
|
getConfigFile() + ".deprecated",
|
||||||
|
configFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
newServerCfg := func() (config.Config, error) {
|
||||||
|
// Initialize server config.
|
||||||
|
srvCfg := newServerConfig()
|
||||||
|
|
||||||
|
return srvCfg, saveServerConfig(ctx, objAPI, srvCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
cfg := &serverConfigV33{}
|
||||||
|
for _, cfgFile := range configFiles {
|
||||||
|
if _, err = Load(cfgFile, cfg); err != nil {
|
||||||
|
if !osIsNotExist(err) && !osIsPermission(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, _ = json.Marshal(cfg)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if osIsPermission(err) {
|
||||||
|
logger.Info("Older config found but is not readable %s, proceeding to read config from other places", err)
|
||||||
|
}
|
||||||
|
if osIsNotExist(err) || osIsPermission(err) || len(data) == 0 {
|
||||||
|
data, err = readConfig(GlobalContext, objAPI, configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
// when config.json is not found, then we freshly initialize.
|
||||||
|
if errors.Is(err, errConfigNotFound) {
|
||||||
|
return newServerCfg()
|
||||||
}
|
}
|
||||||
if ok {
|
return nil, err
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := migrateV27ToV28MinioSys(objAPI); err != nil {
|
data, err = decryptData(data, configFile)
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := migrateV28ToV29MinioSys(objAPI); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := migrateV29ToV30MinioSys(objAPI); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := migrateV30ToV31MinioSys(objAPI); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := migrateV31ToV32MinioSys(objAPI); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return migrateV32ToV33MinioSys(objAPI)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkConfigVersion(objAPI ObjectLayer, configFile string, version string) (bool, []byte, error) {
|
|
||||||
data, err := readConfig(GlobalContext, objAPI, configFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !utf8.Valid(data) {
|
newCfg, err := readServerConfig(GlobalContext, objAPI, data)
|
||||||
if GlobalKMS != nil {
|
if err == nil {
|
||||||
data, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
|
return newCfg, nil
|
||||||
minioMetaBucket: path.Join(minioMetaBucket, configFile),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
data, err = madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
if err == madmin.ErrMaliciousData {
|
|
||||||
return false, nil, config.ErrInvalidCredentialsBackendEncrypted(nil)
|
|
||||||
}
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data, err = madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
if err == madmin.ErrMaliciousData {
|
|
||||||
return false, nil, config.ErrInvalidCredentialsBackendEncrypted(nil)
|
|
||||||
}
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if version == "kvs" {
|
// Read older `.minio.sys/config/config.json`, if not
|
||||||
// Check api config values are present.
|
// possible just fail.
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
vcfg := &versionConfig
|
|
||||||
if err = json.Unmarshal(data, vcfg); err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
return vcfg.Version == version, data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrateV27ToV28MinioSys(objAPI ObjectLayer) error {
|
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
|
||||||
ok, data, err := checkConfigVersion(objAPI, configFile, "27")
|
|
||||||
if err == errConfigNotFound {
|
|
||||||
return nil
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("Unable to load config file. %w", err)
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &serverConfigV28{}
|
|
||||||
if err = json.Unmarshal(data, cfg); err != nil {
|
if err = json.Unmarshal(data, cfg); err != nil {
|
||||||
return err
|
// Unable to parse old JSON simply re-initialize a new one.
|
||||||
|
return newServerCfg()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Version = "28"
|
if !globalCredViaEnv && cfg.Credential.IsValid() {
|
||||||
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
|
// Preserve older credential if we do not have
|
||||||
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %w", err)
|
// root credentials set via environment variable.
|
||||||
|
globalActiveCred = cfg.Credential
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrateV28ToV29MinioSys(objAPI ObjectLayer) error {
|
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
|
||||||
|
|
||||||
ok, data, err := checkConfigVersion(objAPI, configFile, "28")
|
|
||||||
if err == errConfigNotFound {
|
|
||||||
return nil
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("Unable to load config file. %w", err)
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &serverConfigV29{}
|
|
||||||
if err = json.Unmarshal(data, cfg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Version = "29"
|
|
||||||
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
|
|
||||||
return fmt.Errorf("Failed to migrate config from ‘28’ to ‘29’. %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info(configMigrateMSGTemplate, configFile, "28", "29")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrateV29ToV30MinioSys(objAPI ObjectLayer) error {
|
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
|
||||||
|
|
||||||
ok, data, err := checkConfigVersion(objAPI, configFile, "29")
|
|
||||||
if err == errConfigNotFound {
|
|
||||||
return nil
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("Unable to load config file. %w", err)
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &serverConfigV30{}
|
|
||||||
if err = json.Unmarshal(data, cfg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Version = "30"
|
|
||||||
// Init compression config. For future migration, Compression config needs to be copied over from previous version.
|
// Init compression config. For future migration, Compression config needs to be copied over from previous version.
|
||||||
|
switch cfg.Version {
|
||||||
|
case "29":
|
||||||
|
// V29 -> V30
|
||||||
cfg.Compression.Enabled = false
|
cfg.Compression.Enabled = false
|
||||||
cfg.Compression.Extensions = strings.Split(compress.DefaultExtensions, config.ValueSeparator)
|
cfg.Compression.Extensions = strings.Split(compress.DefaultExtensions, config.ValueSeparator)
|
||||||
cfg.Compression.MimeTypes = strings.Split(compress.DefaultMimeTypes, config.ValueSeparator)
|
cfg.Compression.MimeTypes = strings.Split(compress.DefaultMimeTypes, config.ValueSeparator)
|
||||||
|
case "30":
|
||||||
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
|
// V30 -> V31
|
||||||
return fmt.Errorf("Failed to migrate config from ‘29’ to ‘30’. %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info(configMigrateMSGTemplate, configFile, "29", "30")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrateV30ToV31MinioSys(objAPI ObjectLayer) error {
|
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
|
||||||
|
|
||||||
ok, data, err := checkConfigVersion(objAPI, configFile, "30")
|
|
||||||
if err == errConfigNotFound {
|
|
||||||
return nil
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("Unable to load config file. %w", err)
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &serverConfigV31{}
|
|
||||||
if err = json.Unmarshal(data, cfg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Version = "31"
|
|
||||||
cfg.OpenID = openid.Config{}
|
cfg.OpenID = openid.Config{}
|
||||||
|
|
||||||
cfg.Policy.OPA = opa.Args{
|
cfg.Policy.OPA = opa.Args{
|
||||||
URL: &xnet.URL{},
|
URL: &xnet.URL{},
|
||||||
AuthToken: "",
|
AuthToken: "",
|
||||||
}
|
}
|
||||||
|
case "31":
|
||||||
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
|
// V31 -> V32
|
||||||
return fmt.Errorf("Failed to migrate config from ‘30’ to ‘31’. %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info(configMigrateMSGTemplate, configFile, "30", "31")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrateV31ToV32MinioSys(objAPI ObjectLayer) error {
|
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
|
||||||
|
|
||||||
ok, data, err := checkConfigVersion(objAPI, configFile, "31")
|
|
||||||
if err == errConfigNotFound {
|
|
||||||
return nil
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("Unable to load config file. %w", err)
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &serverConfigV32{}
|
|
||||||
if err = json.Unmarshal(data, cfg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Version = "32"
|
|
||||||
cfg.Notify.NSQ = make(map[string]target.NSQArgs)
|
cfg.Notify.NSQ = make(map[string]target.NSQArgs)
|
||||||
cfg.Notify.NSQ["1"] = target.NSQArgs{}
|
cfg.Notify.NSQ["1"] = target.NSQArgs{}
|
||||||
|
|
||||||
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
|
|
||||||
return fmt.Errorf("Failed to migrate config from ‘31’ to ‘32’. %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info(configMigrateMSGTemplate, configFile, "31", "32")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrateV32ToV33MinioSys(objAPI ObjectLayer) error {
|
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
|
||||||
|
|
||||||
ok, data, err := checkConfigVersion(objAPI, configFile, "32")
|
|
||||||
if err == errConfigNotFound {
|
|
||||||
return nil
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("Unable to load config file. %w", err)
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &serverConfigV33{}
|
|
||||||
if err = json.Unmarshal(data, cfg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move to latest.
|
||||||
cfg.Version = "33"
|
cfg.Version = "33"
|
||||||
|
|
||||||
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
|
|
||||||
return fmt.Errorf("Failed to migrate config from '32' to '33' . %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info(configMigrateMSGTemplate, configFile, "32", "33")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
|
||||||
ok, data, err := checkConfigVersion(objAPI, configFile, "33")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &serverConfigV33{}
|
|
||||||
if err = json.Unmarshal(data, cfg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
newCfg := newServerConfig()
|
newCfg := newServerConfig()
|
||||||
|
|
||||||
config.SetRegion(newCfg, cfg.Region)
|
config.SetRegion(newCfg, cfg.Region)
|
||||||
@@ -2804,11 +2608,5 @@ func migrateMinioSysConfigToKV(objAPI ObjectLayer) error {
|
|||||||
notify.SetNotifyWebhook(newCfg, k, args)
|
notify.SetNotifyWebhook(newCfg, k, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = saveServerConfig(GlobalContext, objAPI, newCfg); err != nil {
|
return newCfg, nil
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Configuration file %s migrated from version '%s' to new KV format successfully.",
|
|
||||||
configFile, "33")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,11 +196,12 @@ func TestServerConfigMigrateV2toV33(t *testing.T) {
|
|||||||
t.Fatal("Unexpected error: ", err)
|
t.Fatal("Unexpected error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := migrateMinioSysConfig(objLayer); err != nil {
|
srvCfg, err := readConfigWithoutMigrate(context.Background(), objLayer)
|
||||||
|
if err != nil {
|
||||||
t.Fatal("Unexpected error: ", err)
|
t.Fatal("Unexpected error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := migrateMinioSysConfigToKV(objLayer); err != nil {
|
if err = saveServerConfig(GlobalContext, objLayer, srvCfg); err != nil {
|
||||||
t.Fatal("Unexpected error: ", err)
|
t.Fatal("Unexpected error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -717,107 +717,6 @@ type serverConfigV28 struct {
|
|||||||
Logger logger.Config `json:"logger"`
|
Logger logger.Config `json:"logger"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV29 is just like version '28'.
|
|
||||||
type serverConfigV29 serverConfigV28
|
|
||||||
|
|
||||||
// serverConfigV30 is just like version '29', stores additionally
|
|
||||||
// extensions and mimetypes fields for compression.
|
|
||||||
type serverConfigV30 struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
|
|
||||||
// S3 API configuration.
|
|
||||||
Credential auth.Credentials `json:"credential"`
|
|
||||||
Region string `json:"region"`
|
|
||||||
Worm config.BoolFlag `json:"worm"`
|
|
||||||
|
|
||||||
// Storage class configuration
|
|
||||||
StorageClass storageclass.Config `json:"storageclass"`
|
|
||||||
|
|
||||||
// Cache configuration
|
|
||||||
Cache cache.Config `json:"cache"`
|
|
||||||
|
|
||||||
// Notification queue configuration.
|
|
||||||
Notify notifierV3 `json:"notify"`
|
|
||||||
|
|
||||||
// Logger configuration
|
|
||||||
Logger logger.Config `json:"logger"`
|
|
||||||
|
|
||||||
// Compression configuration
|
|
||||||
Compression compress.Config `json:"compress"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// serverConfigV31 is just like version '30', with OPA and OpenID configuration.
|
|
||||||
type serverConfigV31 struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
|
|
||||||
// S3 API configuration.
|
|
||||||
Credential auth.Credentials `json:"credential"`
|
|
||||||
Region string `json:"region"`
|
|
||||||
Worm config.BoolFlag `json:"worm"`
|
|
||||||
|
|
||||||
// Storage class configuration
|
|
||||||
StorageClass storageclass.Config `json:"storageclass"`
|
|
||||||
|
|
||||||
// Cache configuration
|
|
||||||
Cache cache.Config `json:"cache"`
|
|
||||||
|
|
||||||
// Notification queue configuration.
|
|
||||||
Notify notifierV3 `json:"notify"`
|
|
||||||
|
|
||||||
// Logger configuration
|
|
||||||
Logger logger.Config `json:"logger"`
|
|
||||||
|
|
||||||
// Compression configuration
|
|
||||||
Compression compress.Config `json:"compress"`
|
|
||||||
|
|
||||||
// OpenID configuration
|
|
||||||
OpenID openid.Config `json:"openid"`
|
|
||||||
|
|
||||||
// External policy enforcements.
|
|
||||||
Policy struct {
|
|
||||||
// OPA configuration.
|
|
||||||
OPA opa.Args `json:"opa"`
|
|
||||||
|
|
||||||
// Add new external policy enforcements here.
|
|
||||||
} `json:"policy"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// serverConfigV32 is just like version '31' with added nsq notifer.
|
|
||||||
type serverConfigV32 struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
|
|
||||||
// S3 API configuration.
|
|
||||||
Credential auth.Credentials `json:"credential"`
|
|
||||||
Region string `json:"region"`
|
|
||||||
Worm config.BoolFlag `json:"worm"`
|
|
||||||
|
|
||||||
// Storage class configuration
|
|
||||||
StorageClass storageclass.Config `json:"storageclass"`
|
|
||||||
|
|
||||||
// Cache configuration
|
|
||||||
Cache cache.Config `json:"cache"`
|
|
||||||
|
|
||||||
// Notification queue configuration.
|
|
||||||
Notify notify.Config `json:"notify"`
|
|
||||||
|
|
||||||
// Logger configuration
|
|
||||||
Logger logger.Config `json:"logger"`
|
|
||||||
|
|
||||||
// Compression configuration
|
|
||||||
Compression compress.Config `json:"compress"`
|
|
||||||
|
|
||||||
// OpenID configuration
|
|
||||||
OpenID openid.Config `json:"openid"`
|
|
||||||
|
|
||||||
// External policy enforcements.
|
|
||||||
Policy struct {
|
|
||||||
// OPA configuration.
|
|
||||||
OPA opa.Args `json:"opa"`
|
|
||||||
|
|
||||||
// Add new external policy enforcements here.
|
|
||||||
} `json:"policy"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// serverConfigV33 is just like version '32', removes clientID from NATS and MQTT, and adds queueDir, queueLimit in all notification targets.
|
// serverConfigV33 is just like version '32', removes clientID from NATS and MQTT, and adds queueDir, queueLimit in all notification targets.
|
||||||
type serverConfigV33 struct {
|
type serverConfigV33 struct {
|
||||||
quick.Config `json:"-"` // ignore interfaces
|
quick.Config `json:"-"` // ignore interfaces
|
||||||
|
|||||||
+25
-50
@@ -25,7 +25,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
@@ -65,16 +64,16 @@ func listServerConfigHistory(ctx context.Context, objAPI ObjectLayer, withData b
|
|||||||
if withData {
|
if withData {
|
||||||
data, err := readConfig(ctx, objAPI, obj.Name)
|
data, err := readConfig(ctx, objAPI, obj.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// ignore history file if not readable.
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if GlobalKMS != nil {
|
|
||||||
data, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
|
data, err = decryptData(data, obj.Name)
|
||||||
obj.Bucket: path.Join(obj.Bucket, obj.Name),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// ignore history file that cannot be loaded.
|
||||||
}
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgEntry.Data = string(data)
|
cfgEntry.Data = string(data)
|
||||||
}
|
}
|
||||||
configHistory = append(configHistory, cfgEntry)
|
configHistory = append(configHistory, cfgEntry)
|
||||||
@@ -110,12 +109,7 @@ func readServerConfigHistory(ctx context.Context, objAPI ObjectLayer, uuidKV str
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if GlobalKMS != nil {
|
return decryptData(data, historyFile)
|
||||||
data, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
|
|
||||||
minioMetaBucket: path.Join(minioMetaBucket, historyFile),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return data, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveServerConfigHistory(ctx context.Context, objAPI ObjectLayer, kv []byte) error {
|
func saveServerConfigHistory(ctx context.Context, objAPI ObjectLayer, kv []byte) error {
|
||||||
@@ -167,16 +161,12 @@ func readServerConfig(ctx context.Context, objAPI ObjectLayer, data []byte) (con
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if GlobalKMS != nil && !utf8.Valid(data) {
|
data, err = decryptData(data, configFile)
|
||||||
data, err = config.DecryptBytes(GlobalKMS, data, kms.Context{
|
|
||||||
minioMetaBucket: path.Join(minioMetaBucket, configFile),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lookupConfigs(srvCfg, objAPI)
|
lookupConfigs(srvCfg, objAPI)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
if err := json.Unmarshal(data, &srvCfg); err != nil {
|
if err := json.Unmarshal(data, &srvCfg); err != nil {
|
||||||
@@ -205,47 +195,32 @@ func NewConfigSys() *ConfigSys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize and load config from remote etcd or local config directory
|
// Initialize and load config from remote etcd or local config directory
|
||||||
func initConfig(objAPI ObjectLayer) error {
|
func initConfig(objAPI ObjectLayer) (err error) {
|
||||||
bootstrapTrace("load the configuration")
|
bootstrapTrace("load the configuration")
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
bootstrapTrace(fmt.Sprintf("loading configuration failed: %v", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return errServerNotInitialized
|
return errServerNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
if isFile(getConfigFile()) {
|
srvCfg, err := readConfigWithoutMigrate(GlobalContext, objAPI)
|
||||||
if err := migrateConfig(); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the config version is latest (kvs), if not migrate.
|
bootstrapTrace("lookup the configuration")
|
||||||
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
|
// Override any values from ENVs.
|
||||||
// to '<export_path>/.minio.sys/config/config.json'
|
lookupConfigs(srvCfg, objAPI)
|
||||||
// ignore if the file doesn't exist.
|
|
||||||
// If etcd is set then migrates /config/config.json
|
|
||||||
// to '<export_path>/.minio.sys/config/config.json'
|
|
||||||
if err := migrateConfigToMinioSys(objAPI); err != nil {
|
|
||||||
return fmt.Errorf("migrateConfigToMinioSys: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrates backend '<export_path>/.minio.sys/config/config.json' to latest version.
|
// hold the mutex lock before a new config is assigned.
|
||||||
if err := migrateMinioSysConfig(objAPI); err != nil {
|
globalServerConfigMu.Lock()
|
||||||
return fmt.Errorf("migrateMinioSysConfig: %w", err)
|
globalServerConfig = srvCfg
|
||||||
}
|
globalServerConfigMu.Unlock()
|
||||||
|
|
||||||
// Migrates backend '<export_path>/.minio.sys/config/config.json' to
|
return nil
|
||||||
// latest config format.
|
|
||||||
if err := migrateMinioSysConfigToKV(objAPI); err != nil {
|
|
||||||
return fmt.Errorf("migrateMinioSysConfigToKV: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return loadConfig(objAPI, nil)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ import (
|
|||||||
|
|
||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/minio/internal/logger/message/log"
|
|
||||||
"github.com/minio/minio/internal/logger/target/console"
|
"github.com/minio/minio/internal/logger/target/console"
|
||||||
"github.com/minio/minio/internal/logger/target/types"
|
"github.com/minio/minio/internal/logger/target/types"
|
||||||
"github.com/minio/minio/internal/pubsub"
|
"github.com/minio/minio/internal/pubsub"
|
||||||
|
"github.com/minio/pkg/logger/message/log"
|
||||||
xnet "github.com/minio/pkg/net"
|
xnet "github.com/minio/pkg/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ func NewConsoleLogger(ctx context.Context) *HTTPConsoleLoggerSys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsOnline always true in case of console logger
|
// IsOnline always true in case of console logger
|
||||||
func (sys *HTTPConsoleLoggerSys) IsOnline() bool {
|
func (sys *HTTPConsoleLoggerSys) IsOnline(_ context.Context) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ func (sys *HTTPConsoleLoggerSys) HasLogListeners() bool {
|
|||||||
func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan log.Info, doneCh <-chan struct{}, node string, last int, logKind madmin.LogMask, filter func(entry log.Info) bool) error {
|
func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan log.Info, doneCh <-chan struct{}, node string, last int, logKind madmin.LogMask, filter func(entry log.Info) bool) error {
|
||||||
// Enable console logging for remote client.
|
// Enable console logging for remote client.
|
||||||
if !sys.HasLogListeners() {
|
if !sys.HasLogListeners() {
|
||||||
logger.AddSystemTarget(sys)
|
logger.AddSystemTarget(GlobalContext, sys)
|
||||||
}
|
}
|
||||||
|
|
||||||
cnt := 0
|
cnt := 0
|
||||||
@@ -128,7 +128,7 @@ func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan log.Info, doneCh <-chan st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init if HTTPConsoleLoggerSys is valid, always returns nil right now
|
// Init if HTTPConsoleLoggerSys is valid, always returns nil right now
|
||||||
func (sys *HTTPConsoleLoggerSys) Init() error {
|
func (sys *HTTPConsoleLoggerSys) Init(_ context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ func (sys *HTTPConsoleLoggerSys) Type() types.TargetType {
|
|||||||
|
|
||||||
// Send log message 'e' to console and publish to console
|
// Send log message 'e' to console and publish to console
|
||||||
// log pubsub system
|
// log pubsub system
|
||||||
func (sys *HTTPConsoleLoggerSys) Send(entry interface{}) error {
|
func (sys *HTTPConsoleLoggerSys) Send(ctx context.Context, entry interface{}) error {
|
||||||
var lg log.Info
|
var lg log.Info
|
||||||
switch e := entry.(type) {
|
switch e := entry.(type) {
|
||||||
case log.Entry:
|
case log.Entry:
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ const crossDomainXMLEntity = "/crossdomain.xml"
|
|||||||
func setCrossDomainPolicy(h http.Handler) http.Handler {
|
func setCrossDomainPolicy(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Look for 'crossdomain.xml' in the incoming request.
|
// Look for 'crossdomain.xml' in the incoming request.
|
||||||
switch r.URL.Path {
|
if r.URL.Path == crossDomainXMLEntity {
|
||||||
case crossDomainXMLEntity:
|
|
||||||
// Write the standard cross domain policy xml.
|
// Write the standard cross domain policy xml.
|
||||||
w.Write([]byte(crossDomainXML))
|
w.Write([]byte(crossDomainXML))
|
||||||
// Request completed, no need to serve to other handlers.
|
// Request completed, no need to serve to other handlers.
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ const (
|
|||||||
|
|
||||||
// log scanner action.
|
// log scanner action.
|
||||||
// Use for s > scannerMetricStartTrace
|
// Use for s > scannerMetricStartTrace
|
||||||
func (p *scannerMetrics) log(s scannerMetric, paths ...string) func() {
|
func (p *scannerMetrics) log(s scannerMetric, paths ...string) func(custom map[string]string) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
return func() {
|
return func(custom map[string]string) {
|
||||||
duration := time.Since(startTime)
|
duration := time.Since(startTime)
|
||||||
|
|
||||||
atomic.AddUint64(&p.operations[s], 1)
|
atomic.AddUint64(&p.operations[s], 1)
|
||||||
@@ -80,7 +80,7 @@ func (p *scannerMetrics) log(s scannerMetric, paths ...string) func() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s > scannerMetricStartTrace && globalTrace.NumSubscribers(madmin.TraceScanner) > 0 {
|
if s > scannerMetricStartTrace && globalTrace.NumSubscribers(madmin.TraceScanner) > 0 {
|
||||||
globalTrace.Publish(scannerTrace(s, startTime, duration, strings.Join(paths, " ")))
|
globalTrace.Publish(scannerTrace(s, startTime, duration, strings.Join(paths, " "), custom))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+116
-106
@@ -18,7 +18,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -29,11 +28,11 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bits-and-blooms/bloom/v3"
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/minio/madmin-go/v2"
|
"github.com/minio/madmin-go/v2"
|
||||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||||
@@ -59,6 +58,9 @@ const (
|
|||||||
healDeleteDangling = true
|
healDeleteDangling = true
|
||||||
healFolderIncludeProb = 32 // Include a clean folder one in n cycles.
|
healFolderIncludeProb = 32 // Include a clean folder one in n cycles.
|
||||||
healObjectSelectProb = 512 // Overall probability of a file being scanned; one in n.
|
healObjectSelectProb = 512 // Overall probability of a file being scanned; one in n.
|
||||||
|
|
||||||
|
dataScannerExcessiveVersionsThreshold = 1000 // Issue a warning when a single object has more versions than this
|
||||||
|
dataScannerExcessiveFoldersThreshold = 50000 // Issue a warning when a folder has more subfolders than this in a *set*
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -158,7 +160,6 @@ func runDataScanner(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
|
|
||||||
// Load current bloom cycle
|
// Load current bloom cycle
|
||||||
var cycleInfo currentScannerCycle
|
var cycleInfo currentScannerCycle
|
||||||
cycleInfo.next = intDataUpdateTracker.current() + 1
|
|
||||||
|
|
||||||
buf, _ := readConfig(ctx, objAPI, dataUsageBloomNamePath)
|
buf, _ := readConfig(ctx, objAPI, dataUsageBloomNamePath)
|
||||||
if len(buf) == 8 {
|
if len(buf) == 8 {
|
||||||
@@ -203,11 +204,13 @@ func runDataScanner(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
// Wait before starting next cycle and wait on startup.
|
// Wait before starting next cycle and wait on startup.
|
||||||
results := make(chan DataUsageInfo, 1)
|
results := make(chan DataUsageInfo, 1)
|
||||||
go storeDataUsageInBackend(ctx, objAPI, results)
|
go storeDataUsageInBackend(ctx, objAPI, results)
|
||||||
bf, err := globalNotificationSys.updateBloomFilter(ctx, cycleInfo.current)
|
err := objAPI.NSScanner(ctx, results, uint32(cycleInfo.current), scanMode)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
err = objAPI.NSScanner(ctx, bf, results, uint32(cycleInfo.current), scanMode)
|
res := map[string]string{"cycle": fmt.Sprint(cycleInfo.current)}
|
||||||
logger.LogIf(ctx, err)
|
if err != nil {
|
||||||
stopFn()
|
res["error"] = err.Error()
|
||||||
|
}
|
||||||
|
stopFn(res)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Store new cycle...
|
// Store new cycle...
|
||||||
cycleInfo.next++
|
cycleInfo.next++
|
||||||
@@ -240,10 +243,8 @@ type folderScanner struct {
|
|||||||
oldCache dataUsageCache
|
oldCache dataUsageCache
|
||||||
newCache dataUsageCache
|
newCache dataUsageCache
|
||||||
updateCache dataUsageCache
|
updateCache dataUsageCache
|
||||||
withFilter *bloomFilter
|
|
||||||
|
|
||||||
dataUsageScannerDebug bool
|
dataUsageScannerDebug bool
|
||||||
healFolderInclude uint32 // Include a clean folder one in n cycles.
|
|
||||||
healObjectSelect uint32 // Do a heal check on an object once every n cycles. Must divide into healFolderInclude
|
healObjectSelect uint32 // Do a heal check on an object once every n cycles. Must divide into healFolderInclude
|
||||||
scanMode madmin.HealScanMode
|
scanMode madmin.HealScanMode
|
||||||
|
|
||||||
@@ -302,8 +303,6 @@ type folderScanner struct {
|
|||||||
// Before each operation sleepDuration is called which can be used to temporarily halt the scanner.
|
// Before each operation sleepDuration is called which can be used to temporarily halt the scanner.
|
||||||
// If the supplied context is canceled the function will return at the first chance.
|
// If the supplied context is canceled the function will return at the first chance.
|
||||||
func scanDataFolder(ctx context.Context, poolIdx, setIdx int, basePath string, cache dataUsageCache, getSize getSizeFn, scanMode madmin.HealScanMode) (dataUsageCache, error) {
|
func scanDataFolder(ctx context.Context, poolIdx, setIdx int, basePath string, cache dataUsageCache, getSize getSizeFn, scanMode madmin.HealScanMode) (dataUsageCache, error) {
|
||||||
logPrefix := color.Green("data-usage: ")
|
|
||||||
|
|
||||||
switch cache.Info.Name {
|
switch cache.Info.Name {
|
||||||
case "", dataUsageRoot:
|
case "", dataUsageRoot:
|
||||||
return cache, errors.New("internal error: root scan attempted")
|
return cache, errors.New("internal error: root scan attempted")
|
||||||
@@ -317,8 +316,7 @@ func scanDataFolder(ctx context.Context, poolIdx, setIdx int, basePath string, c
|
|||||||
oldCache: cache,
|
oldCache: cache,
|
||||||
newCache: dataUsageCache{Info: cache.Info},
|
newCache: dataUsageCache{Info: cache.Info},
|
||||||
updateCache: dataUsageCache{Info: cache.Info},
|
updateCache: dataUsageCache{Info: cache.Info},
|
||||||
dataUsageScannerDebug: intDataUpdateTracker.debug,
|
dataUsageScannerDebug: false,
|
||||||
healFolderInclude: 0,
|
|
||||||
healObjectSelect: 0,
|
healObjectSelect: 0,
|
||||||
scanMode: scanMode,
|
scanMode: scanMode,
|
||||||
updates: cache.Info.updates,
|
updates: cache.Info.updates,
|
||||||
@@ -341,19 +339,9 @@ func scanDataFolder(ctx context.Context, poolIdx, setIdx int, basePath string, c
|
|||||||
|
|
||||||
// Enable healing in XL mode.
|
// Enable healing in XL mode.
|
||||||
if globalIsErasure && !cache.Info.SkipHealing {
|
if globalIsErasure && !cache.Info.SkipHealing {
|
||||||
// Include a clean folder one in n cycles.
|
|
||||||
s.healFolderInclude = healFolderIncludeProb
|
|
||||||
// Do a heal check on an object once every n cycles. Must divide into healFolderInclude
|
// Do a heal check on an object once every n cycles. Must divide into healFolderInclude
|
||||||
s.healObjectSelect = healObjectSelectProb
|
s.healObjectSelect = healObjectSelectProb
|
||||||
}
|
}
|
||||||
if len(cache.Info.BloomFilter) > 0 {
|
|
||||||
s.withFilter = &bloomFilter{BloomFilter: &bloom.BloomFilter{}}
|
|
||||||
_, err := s.withFilter.ReadFrom(bytes.NewReader(cache.Info.BloomFilter))
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err, logPrefix+"Error reading bloom filter")
|
|
||||||
s.withFilter = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
done := ctx.Done()
|
done := ctx.Done()
|
||||||
|
|
||||||
@@ -410,14 +398,12 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
existing, ok := f.oldCache.Cache[thisHash.Key()]
|
|
||||||
var abandonedChildren dataUsageHashMap
|
var abandonedChildren dataUsageHashMap
|
||||||
if !into.Compacted {
|
if !into.Compacted {
|
||||||
abandonedChildren = f.oldCache.findChildrenCopy(thisHash)
|
abandonedChildren = f.oldCache.findChildrenCopy(thisHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are lifecycle rules for the prefix, remove the filter.
|
// If there are lifecycle rules for the prefix, remove the filter.
|
||||||
filter := f.withFilter
|
|
||||||
_, prefix := path2BucketObjectWithBasePath(f.root, folder.name)
|
_, prefix := path2BucketObjectWithBasePath(f.root, folder.name)
|
||||||
var activeLifeCycle *lifecycle.Lifecycle
|
var activeLifeCycle *lifecycle.Lifecycle
|
||||||
if f.oldCache.Info.lifeCycle != nil && f.oldCache.Info.lifeCycle.HasActiveRules(prefix) {
|
if f.oldCache.Info.lifeCycle != nil && f.oldCache.Info.lifeCycle.HasActiveRules(prefix) {
|
||||||
@@ -425,33 +411,13 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
console.Debugf(scannerLogPrefix+" Prefix %q has active rules\n", prefix)
|
console.Debugf(scannerLogPrefix+" Prefix %q has active rules\n", prefix)
|
||||||
}
|
}
|
||||||
activeLifeCycle = f.oldCache.Info.lifeCycle
|
activeLifeCycle = f.oldCache.Info.lifeCycle
|
||||||
filter = nil
|
|
||||||
}
|
}
|
||||||
// If there are replication rules for the prefix, remove the filter.
|
// If there are replication rules for the prefix, remove the filter.
|
||||||
var replicationCfg replicationConfig
|
var replicationCfg replicationConfig
|
||||||
if !f.oldCache.Info.replication.Empty() && f.oldCache.Info.replication.Config.HasActiveRules(prefix, true) {
|
if !f.oldCache.Info.replication.Empty() && f.oldCache.Info.replication.Config.HasActiveRules(prefix, true) {
|
||||||
replicationCfg = f.oldCache.Info.replication
|
replicationCfg = f.oldCache.Info.replication
|
||||||
filter = nil
|
|
||||||
}
|
}
|
||||||
// Check if we can skip it due to bloom filter...
|
// Check if we can skip it due to bloom filter...
|
||||||
if filter != nil && ok && existing.Compacted {
|
|
||||||
// If folder isn't in filter and we have data, skip it completely.
|
|
||||||
if folder.name != dataUsageRoot && !filter.containsDir(folder.name) {
|
|
||||||
if f.healObjectSelect == 0 || !thisHash.modAlt(f.oldCache.Info.NextCycle/folder.objectHealProbDiv, f.healFolderInclude/folder.objectHealProbDiv) {
|
|
||||||
f.newCache.copyWithChildren(&f.oldCache, thisHash, folder.parent)
|
|
||||||
f.updateCache.copyWithChildren(&f.oldCache, thisHash, folder.parent)
|
|
||||||
if f.dataUsageScannerDebug {
|
|
||||||
console.Debugf(scannerLogPrefix+" Skipping non-updated folder: %v\n", folder.name)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if f.dataUsageScannerDebug {
|
|
||||||
console.Debugf(scannerLogPrefix+" Adding non-updated folder to heal check: %v\n", folder.name)
|
|
||||||
}
|
|
||||||
// If probability was already scannerHealFolderInclude, keep it.
|
|
||||||
folder.objectHealProbDiv = f.healFolderInclude
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scannerSleeper.Sleep(ctx, dataScannerSleepPerFolder)
|
scannerSleeper.Sleep(ctx, dataScannerSleepPerFolder)
|
||||||
|
|
||||||
var existingFolders, newFolders []cachedFolder
|
var existingFolders, newFolders []cachedFolder
|
||||||
@@ -566,6 +532,19 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
len(existingFolders)+len(newFolders) >= dataScannerCompactAtFolders ||
|
len(existingFolders)+len(newFolders) >= dataScannerCompactAtFolders ||
|
||||||
len(existingFolders)+len(newFolders) >= dataScannerForceCompactAtFolders
|
len(existingFolders)+len(newFolders) >= dataScannerForceCompactAtFolders
|
||||||
|
|
||||||
|
if len(existingFolders)+len(newFolders) > dataScannerExcessiveFoldersThreshold {
|
||||||
|
// Notify object accessed via a GET request.
|
||||||
|
sendEvent(eventArgs{
|
||||||
|
EventName: event.PrefixManyFolders,
|
||||||
|
BucketName: f.root,
|
||||||
|
Object: ObjectInfo{
|
||||||
|
Name: strings.TrimSuffix(folder.name, "/") + "/",
|
||||||
|
Size: int64(len(existingFolders) + len(newFolders)),
|
||||||
|
},
|
||||||
|
UserAgent: "scanner",
|
||||||
|
Host: globalMinioHost,
|
||||||
|
})
|
||||||
|
}
|
||||||
if !into.Compacted && shouldCompact {
|
if !into.Compacted && shouldCompact {
|
||||||
into.Compacted = true
|
into.Compacted = true
|
||||||
newFolders = append(newFolders, existingFolders...)
|
newFolders = append(newFolders, existingFolders...)
|
||||||
@@ -632,7 +611,8 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
f.updateCurrentPath(folder.name)
|
f.updateCurrentPath(folder.name)
|
||||||
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
|
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
|
||||||
scanFolder(folder)
|
scanFolder(folder)
|
||||||
stopFn()
|
stopFn(map[string]string{"type": "new"})
|
||||||
|
|
||||||
// Add new folders if this is new and we don't have existing.
|
// Add new folders if this is new and we don't have existing.
|
||||||
if !into.Compacted {
|
if !into.Compacted {
|
||||||
parent := f.updateCache.find(thisHash.Key())
|
parent := f.updateCache.find(thisHash.Key())
|
||||||
@@ -651,19 +631,16 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
// and the entry itself is compacted.
|
// and the entry itself is compacted.
|
||||||
if !into.Compacted && f.oldCache.isCompacted(h) {
|
if !into.Compacted && f.oldCache.isCompacted(h) {
|
||||||
if !h.mod(f.oldCache.Info.NextCycle, dataUsageUpdateDirCycles) {
|
if !h.mod(f.oldCache.Info.NextCycle, dataUsageUpdateDirCycles) {
|
||||||
if f.healObjectSelect == 0 || !h.modAlt(f.oldCache.Info.NextCycle/folder.objectHealProbDiv, f.healFolderInclude/folder.objectHealProbDiv) {
|
|
||||||
// Transfer and add as child...
|
// Transfer and add as child...
|
||||||
f.newCache.copyWithChildren(&f.oldCache, h, folder.parent)
|
f.newCache.copyWithChildren(&f.oldCache, h, folder.parent)
|
||||||
into.addChild(h)
|
into.addChild(h)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
folder.objectHealProbDiv = f.healFolderInclude
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
f.updateCurrentPath(folder.name)
|
f.updateCurrentPath(folder.name)
|
||||||
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name, "EXISTING")
|
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
|
||||||
scanFolder(folder)
|
scanFolder(folder)
|
||||||
stopFn()
|
stopFn(map[string]string{"type": "existing"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan for healing
|
// Scan for healing
|
||||||
@@ -803,7 +780,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
this := cachedFolder{name: k, parent: &thisHash, objectHealProbDiv: 1}
|
this := cachedFolder{name: k, parent: &thisHash, objectHealProbDiv: 1}
|
||||||
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, this.name, "HEALED")
|
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, this.name, "HEALED")
|
||||||
scanFolder(this)
|
scanFolder(this)
|
||||||
stopFn()
|
stopFn(map[string]string{"type": "healed"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -817,9 +794,6 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
flat.Compacted = true
|
flat.Compacted = true
|
||||||
var compact bool
|
var compact bool
|
||||||
if flat.Objects < dataScannerCompactLeastObject {
|
if flat.Objects < dataScannerCompactLeastObject {
|
||||||
if flat.Objects > 1 {
|
|
||||||
globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
|
|
||||||
}
|
|
||||||
compact = true
|
compact = true
|
||||||
} else {
|
} else {
|
||||||
// Compact if we only have objects as children...
|
// Compact if we only have objects as children...
|
||||||
@@ -832,13 +806,20 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if compact {
|
|
||||||
globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if compact {
|
if compact {
|
||||||
|
stop := globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
|
||||||
f.newCache.deleteRecursive(thisHash)
|
f.newCache.deleteRecursive(thisHash)
|
||||||
f.newCache.replaceHashed(thisHash, folder.parent, *flat)
|
f.newCache.replaceHashed(thisHash, folder.parent, *flat)
|
||||||
|
total := map[string]string{
|
||||||
|
"objects": fmt.Sprint(flat.Objects),
|
||||||
|
"size": fmt.Sprint(flat.Size),
|
||||||
|
}
|
||||||
|
if flat.Versions > 0 {
|
||||||
|
total["versions"] = fmt.Sprint(flat.Versions)
|
||||||
|
}
|
||||||
|
stop(total)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -964,9 +945,9 @@ func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, oi Obje
|
|||||||
|
|
||||||
switch lcEvt.Action {
|
switch lcEvt.Action {
|
||||||
case lifecycle.DeleteAction, lifecycle.DeleteVersionAction, lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
case lifecycle.DeleteAction, lifecycle.DeleteVersionAction, lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||||
return applyLifecycleAction(lcEvt.Action, oi, ""), 0
|
return applyLifecycleAction(lcEvt, oi), 0
|
||||||
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
|
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
|
||||||
return applyLifecycleAction(lcEvt.Action, oi, lcEvt.StorageClass), size
|
return applyLifecycleAction(lcEvt, oi), size
|
||||||
default:
|
default:
|
||||||
// No action.
|
// No action.
|
||||||
return false, size
|
return false, size
|
||||||
@@ -976,6 +957,7 @@ func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, oi Obje
|
|||||||
// applyTierObjSweep removes remote object pending deletion and the free-version
|
// applyTierObjSweep removes remote object pending deletion and the free-version
|
||||||
// tracking this information.
|
// tracking this information.
|
||||||
func (i *scannerItem) applyTierObjSweep(ctx context.Context, o ObjectLayer, oi ObjectInfo) {
|
func (i *scannerItem) applyTierObjSweep(ctx context.Context, o ObjectLayer, oi ObjectInfo) {
|
||||||
|
traceFn := globalLifecycleSys.trace(oi)
|
||||||
if !oi.TransitionedObject.FreeVersion {
|
if !oi.TransitionedObject.FreeVersion {
|
||||||
// nothing to be done
|
// nothing to be done
|
||||||
return
|
return
|
||||||
@@ -1001,7 +983,7 @@ func (i *scannerItem) applyTierObjSweep(ctx context.Context, o ObjectLayer, oi O
|
|||||||
InclFreeVersions: true,
|
InclFreeVersions: true,
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
auditLogLifecycle(ctx, oi, ILMFreeVersionDelete)
|
auditLogLifecycle(ctx, oi, ILMFreeVersionDelete, nil, traceFn)
|
||||||
}
|
}
|
||||||
if ignoreNotFoundErr(err) != nil {
|
if ignoreNotFoundErr(err) != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
@@ -1010,27 +992,36 @@ func (i *scannerItem) applyTierObjSweep(ctx context.Context, o ObjectLayer, oi O
|
|||||||
|
|
||||||
// applyNewerNoncurrentVersionLimit removes noncurrent versions older than the most recent NewerNoncurrentVersions configured.
|
// applyNewerNoncurrentVersionLimit removes noncurrent versions older than the most recent NewerNoncurrentVersions configured.
|
||||||
// Note: This function doesn't update sizeSummary since it always removes versions that it doesn't return.
|
// Note: This function doesn't update sizeSummary since it always removes versions that it doesn't return.
|
||||||
func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ ObjectLayer, fivs []FileInfo) ([]FileInfo, error) {
|
func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ ObjectLayer, fivs []FileInfo) ([]ObjectInfo, error) {
|
||||||
if i.lifeCycle == nil {
|
|
||||||
return fivs, nil
|
|
||||||
}
|
|
||||||
done := globalScannerMetrics.time(scannerMetricApplyNonCurrent)
|
done := globalScannerMetrics.time(scannerMetricApplyNonCurrent)
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
_, days, lim := i.lifeCycle.NoncurrentVersionsExpirationLimit(lifecycle.ObjectOpts{Name: i.objectPath()})
|
|
||||||
if lim == 0 || len(fivs) <= lim+1 { // fewer than lim _noncurrent_ versions
|
|
||||||
return fivs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
overflowVersions := fivs[lim+1:]
|
|
||||||
// current version + most recent lim noncurrent versions
|
|
||||||
fivs = fivs[:lim+1]
|
|
||||||
|
|
||||||
rcfg, _ := globalBucketObjectLockSys.Get(i.bucket)
|
rcfg, _ := globalBucketObjectLockSys.Get(i.bucket)
|
||||||
vcfg, _ := globalBucketVersioningSys.Get(i.bucket)
|
vcfg, _ := globalBucketVersioningSys.Get(i.bucket)
|
||||||
|
|
||||||
versioned := vcfg != nil && vcfg.Versioned(i.objectPath())
|
versioned := vcfg != nil && vcfg.Versioned(i.objectPath())
|
||||||
|
|
||||||
|
// current version + most recent lim noncurrent versions
|
||||||
|
objectInfos := make([]ObjectInfo, 0, len(fivs))
|
||||||
|
|
||||||
|
if i.lifeCycle == nil {
|
||||||
|
for _, fi := range fivs {
|
||||||
|
objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned))
|
||||||
|
}
|
||||||
|
return objectInfos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
event := i.lifeCycle.NoncurrentVersionsExpirationLimit(lifecycle.ObjectOpts{Name: i.objectPath()})
|
||||||
|
lim := event.NewerNoncurrentVersions
|
||||||
|
if lim == 0 || len(fivs) <= lim+1 { // fewer than lim _noncurrent_ versions
|
||||||
|
for _, fi := range fivs {
|
||||||
|
objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned))
|
||||||
|
}
|
||||||
|
return objectInfos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
overflowVersions := fivs[lim+1:]
|
||||||
|
|
||||||
toDel := make([]ObjectToDelete, 0, len(overflowVersions))
|
toDel := make([]ObjectToDelete, 0, len(overflowVersions))
|
||||||
for _, fi := range overflowVersions {
|
for _, fi := range overflowVersions {
|
||||||
obj := fi.ToObjectInfo(i.bucket, i.objectPath(), versioned)
|
obj := fi.ToObjectInfo(i.bucket, i.objectPath(), versioned)
|
||||||
@@ -1045,12 +1036,12 @@ func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ Ob
|
|||||||
}
|
}
|
||||||
// add this version back to remaining versions for
|
// add this version back to remaining versions for
|
||||||
// subsequent lifecycle policy applications
|
// subsequent lifecycle policy applications
|
||||||
fivs = append(fivs, fi)
|
objectInfos = append(objectInfos, obj)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoncurrentDays not passed yet.
|
// NoncurrentDays not passed yet.
|
||||||
if time.Now().UTC().Before(lifecycle.ExpectedExpiryTime(obj.SuccessorModTime, days)) {
|
if time.Now().UTC().Before(lifecycle.ExpectedExpiryTime(obj.SuccessorModTime, event.NoncurrentDays)) {
|
||||||
// add this version back to remaining versions for
|
// add this version back to remaining versions for
|
||||||
// subsequent lifecycle policy applications
|
// subsequent lifecycle policy applications
|
||||||
fivs = append(fivs, fi)
|
fivs = append(fivs, fi)
|
||||||
@@ -1059,19 +1050,19 @@ func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ Ob
|
|||||||
|
|
||||||
toDel = append(toDel, ObjectToDelete{
|
toDel = append(toDel, ObjectToDelete{
|
||||||
ObjectV: ObjectV{
|
ObjectV: ObjectV{
|
||||||
ObjectName: fi.Name,
|
ObjectName: obj.Name,
|
||||||
VersionID: fi.VersionID,
|
VersionID: obj.VersionID,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
globalExpiryState.enqueueByNewerNoncurrent(i.bucket, toDel)
|
globalExpiryState.enqueueByNewerNoncurrent(i.bucket, toDel, event)
|
||||||
return fivs, nil
|
return objectInfos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyVersionActions will apply lifecycle checks on all versions of a scanned item. Returns versions that remain
|
// applyVersionActions will apply lifecycle checks on all versions of a scanned item. Returns versions that remain
|
||||||
// after applying lifecycle checks configured.
|
// after applying lifecycle checks configured.
|
||||||
func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fivs []FileInfo) ([]FileInfo, error) {
|
func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fivs []FileInfo) ([]ObjectInfo, error) {
|
||||||
if i.heal.enabled {
|
if i.heal.enabled {
|
||||||
if healDeleteDangling {
|
if healDeleteDangling {
|
||||||
done := globalScannerMetrics.time(scannerMetricCleanAbandoned)
|
done := globalScannerMetrics.time(scannerMetricCleanAbandoned)
|
||||||
@@ -1083,7 +1074,27 @@ func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return i.applyNewerNoncurrentVersionLimit(ctx, o, fivs)
|
objInfos, err := i.applyNewerNoncurrentVersionLimit(ctx, o, fivs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have many versions after applyNewerNoncurrentVersionLimit.
|
||||||
|
if len(objInfos) > dataScannerExcessiveVersionsThreshold {
|
||||||
|
// Notify object accessed via a GET request.
|
||||||
|
sendEvent(eventArgs{
|
||||||
|
EventName: event.ObjectManyVersions,
|
||||||
|
BucketName: i.bucket,
|
||||||
|
Object: ObjectInfo{
|
||||||
|
Name: i.objectPath(),
|
||||||
|
},
|
||||||
|
UserAgent: "Internal: [Scanner]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
|
RespElements: map[string]string{"x-minio-versions": strconv.Itoa(len(fivs))},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return objInfos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyActions will apply lifecycle checks on to a scanned item.
|
// applyActions will apply lifecycle checks on to a scanned item.
|
||||||
@@ -1143,20 +1154,16 @@ func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr loc
|
|||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyTransitionRule(obj ObjectInfo, storageClass string) bool {
|
func applyTransitionRule(event lifecycle.Event, obj ObjectInfo) bool {
|
||||||
if obj.DeleteMarker {
|
if obj.DeleteMarker {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
globalTransitionState.queueTransitionTask(obj, storageClass)
|
globalTransitionState.queueTransitionTask(obj, event)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, restoredObject bool) bool {
|
func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, lcEvent lifecycle.Event) bool {
|
||||||
action := expireObj
|
if err := expireTransitionedObject(ctx, objLayer, &obj, obj.ToLifecycleOpts(), lcEvent); err != nil {
|
||||||
if restoredObject {
|
|
||||||
action = expireRestoredObj
|
|
||||||
}
|
|
||||||
if err := expireTransitionedObject(ctx, objLayer, &obj, obj.ToLifecycleOpts(), action); err != nil {
|
|
||||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -1167,12 +1174,13 @@ func applyExpiryOnTransitionedObject(ctx context.Context, objLayer ObjectLayer,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, applyOnVersion bool) bool {
|
func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLayer, obj ObjectInfo, lcEvent lifecycle.Event) bool {
|
||||||
|
traceFn := globalLifecycleSys.trace(obj)
|
||||||
opts := ObjectOptions{
|
opts := ObjectOptions{
|
||||||
Expiration: ExpirationOptions{Expire: true},
|
Expiration: ExpirationOptions{Expire: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
if applyOnVersion {
|
if lcEvent.Action.DeleteVersioned() {
|
||||||
opts.VersionID = obj.VersionID
|
opts.VersionID = obj.VersionID
|
||||||
}
|
}
|
||||||
if opts.VersionID == "" {
|
if opts.VersionID == "" {
|
||||||
@@ -1190,8 +1198,9 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tags := auditLifecycleTags(lcEvent)
|
||||||
// Send audit for the lifecycle delete operation
|
// Send audit for the lifecycle delete operation
|
||||||
auditLogLifecycle(ctx, obj, ILMExpiry)
|
auditLogLifecycle(ctx, obj, ILMExpiry, tags, traceFn)
|
||||||
|
|
||||||
eventName := event.ObjectRemovedDelete
|
eventName := event.ObjectRemovedDelete
|
||||||
if obj.DeleteMarker {
|
if obj.DeleteMarker {
|
||||||
@@ -1203,27 +1212,27 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
|
|||||||
EventName: eventName,
|
EventName: eventName,
|
||||||
BucketName: obj.Bucket,
|
BucketName: obj.Bucket,
|
||||||
Object: obj,
|
Object: obj,
|
||||||
Host: "Internal: [ILM-EXPIRY]",
|
UserAgent: "Internal: [ILM-Expiry]",
|
||||||
|
Host: globalLocalNodeName,
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply object, object version, restored object or restored object version action on the given object
|
// Apply object, object version, restored object or restored object version action on the given object
|
||||||
func applyExpiryRule(obj ObjectInfo, restoredObject, applyOnVersion bool) bool {
|
func applyExpiryRule(event lifecycle.Event, obj ObjectInfo) bool {
|
||||||
globalExpiryState.enqueueByDays(obj, restoredObject, applyOnVersion)
|
globalExpiryState.enqueueByDays(obj, event)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform actions (removal or transitioning of objects), return true the action is successfully performed
|
// Perform actions (removal or transitioning of objects), return true the action is successfully performed
|
||||||
func applyLifecycleAction(action lifecycle.Action, obj ObjectInfo, storageClass string) (success bool) {
|
func applyLifecycleAction(event lifecycle.Event, obj ObjectInfo) (success bool) {
|
||||||
switch action {
|
switch action := event.Action; action {
|
||||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction,
|
||||||
success = applyExpiryRule(obj, false, action == lifecycle.DeleteVersionAction)
|
lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||||
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
success = applyExpiryRule(event, obj)
|
||||||
success = applyExpiryRule(obj, true, action == lifecycle.DeleteRestoredVersionAction)
|
|
||||||
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
|
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
|
||||||
success = applyTransitionRule(obj, storageClass)
|
success = applyTransitionRule(event, obj)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1273,8 +1282,7 @@ func (i *scannerItem) healReplication(ctx context.Context, o ObjectLayer, oi Obj
|
|||||||
sizeS.replTargetStats[arn] = tgtSizeS
|
sizeS.replTargetStats[arn] = tgtSizeS
|
||||||
}
|
}
|
||||||
|
|
||||||
switch oi.ReplicationStatus {
|
if oi.ReplicationStatus == replication.Replica {
|
||||||
case replication.Replica:
|
|
||||||
sizeS.replicaSize += oi.Size
|
sizeS.replicaSize += oi.Size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1434,7 +1442,7 @@ const (
|
|||||||
ILMTransition = " ilm:transition"
|
ILMTransition = " ilm:transition"
|
||||||
)
|
)
|
||||||
|
|
||||||
func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string) {
|
func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string, tags map[string]interface{}, traceFn func(event string)) {
|
||||||
var apiName string
|
var apiName string
|
||||||
switch event {
|
switch event {
|
||||||
case ILMExpiry:
|
case ILMExpiry:
|
||||||
@@ -1450,5 +1458,7 @@ func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string) {
|
|||||||
Bucket: oi.Bucket,
|
Bucket: oi.Bucket,
|
||||||
Object: oi.Name,
|
Object: oi.Name,
|
||||||
VersionID: oi.VersionID,
|
VersionID: oi.VersionID,
|
||||||
|
Tags: tags,
|
||||||
})
|
})
|
||||||
|
traceFn(event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,682 +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 (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bits-and-blooms/bloom/v3"
|
|
||||||
"github.com/minio/minio/internal/color"
|
|
||||||
"github.com/minio/minio/internal/logger"
|
|
||||||
"github.com/minio/pkg/console"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Estimate bloom filter size. With this many items
|
|
||||||
dataUpdateTrackerEstItems = 200000
|
|
||||||
// ... we want this false positive rate:
|
|
||||||
dataUpdateTrackerFP = 0.1
|
|
||||||
dataUpdateTrackerQueueSize = 0
|
|
||||||
|
|
||||||
dataUpdateTrackerFilename = dataUsageBucket + SlashSeparator + ".tracker.bin"
|
|
||||||
dataUpdateTrackerVersion = 7
|
|
||||||
dataUpdateTrackerSaveInterval = 5 * time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
var intDataUpdateTracker *dataUpdateTracker
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
intDataUpdateTracker = newDataUpdateTracker()
|
|
||||||
}
|
|
||||||
|
|
||||||
type dataUpdateTracker struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
input chan string
|
|
||||||
save chan struct{}
|
|
||||||
debug bool
|
|
||||||
saveExited chan struct{}
|
|
||||||
dirty bool
|
|
||||||
|
|
||||||
Current dataUpdateFilter
|
|
||||||
History dataUpdateTrackerHistory
|
|
||||||
Saved time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// newDataUpdateTracker returns a dataUpdateTracker with default settings.
|
|
||||||
func newDataUpdateTracker() *dataUpdateTracker {
|
|
||||||
d := &dataUpdateTracker{
|
|
||||||
Current: dataUpdateFilter{
|
|
||||||
idx: 1,
|
|
||||||
},
|
|
||||||
debug: serverDebugLog,
|
|
||||||
input: make(chan string, dataUpdateTrackerQueueSize),
|
|
||||||
save: make(chan struct{}, 1),
|
|
||||||
saveExited: make(chan struct{}),
|
|
||||||
}
|
|
||||||
d.Current.bf = d.newBloomFilter()
|
|
||||||
d.dirty = true
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
type dataUpdateTrackerHistory []dataUpdateFilter
|
|
||||||
|
|
||||||
type dataUpdateFilter struct {
|
|
||||||
idx uint64
|
|
||||||
bf bloomFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
type bloomFilter struct {
|
|
||||||
*bloom.BloomFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
// emptyBloomFilter returns an empty bloom filter.
|
|
||||||
func emptyBloomFilter() bloomFilter {
|
|
||||||
return bloomFilter{BloomFilter: &bloom.BloomFilter{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// containsDir returns whether the bloom filter contains a directory.
|
|
||||||
// Note that objects in XL mode are also considered directories.
|
|
||||||
func (b bloomFilter) containsDir(in string) bool {
|
|
||||||
split := splitPathDeterministic(path.Clean(in))
|
|
||||||
|
|
||||||
if len(split) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return b.TestString(hashPath(path.Join(split...)).String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// bytes returns the bloom filter serialized as a byte slice.
|
|
||||||
func (b *bloomFilter) bytes() []byte {
|
|
||||||
if b == nil || b.BloomFilter == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
_, err := b.WriteTo(&buf)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(GlobalContext, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort the dataUpdateTrackerHistory, newest first.
|
|
||||||
// Returns whether the history is complete.
|
|
||||||
func (d dataUpdateTrackerHistory) sort() bool {
|
|
||||||
if len(d) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
sort.Slice(d, func(i, j int) bool {
|
|
||||||
return d[i].idx > d[j].idx
|
|
||||||
})
|
|
||||||
return d[0].idx-d[len(d)-1].idx == uint64(len(d))
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeOlderThan will remove entries older than index 'n'.
|
|
||||||
func (d *dataUpdateTrackerHistory) removeOlderThan(n uint64) {
|
|
||||||
d.sort()
|
|
||||||
dd := *d
|
|
||||||
end := len(dd)
|
|
||||||
for i := end - 1; i >= 0; i-- {
|
|
||||||
if dd[i].idx < n {
|
|
||||||
end = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dd = dd[:end]
|
|
||||||
*d = dd
|
|
||||||
}
|
|
||||||
|
|
||||||
// newBloomFilter returns a new bloom filter with default settings.
|
|
||||||
func (d *dataUpdateTracker) newBloomFilter() bloomFilter {
|
|
||||||
return bloomFilter{bloom.NewWithEstimates(dataUpdateTrackerEstItems, dataUpdateTrackerFP)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// current returns the current index.
|
|
||||||
func (d *dataUpdateTracker) current() uint64 {
|
|
||||||
d.mu.Lock()
|
|
||||||
defer d.mu.Unlock()
|
|
||||||
return d.Current.idx
|
|
||||||
}
|
|
||||||
|
|
||||||
// latestWithDir returns the highest index that contains the directory.
|
|
||||||
// This means that any cycle higher than this does NOT contain the entry.
|
|
||||||
func (d *dataUpdateTracker) latestWithDir(dir string) uint64 {
|
|
||||||
dateUpdateTrackerLogPrefix := color.Green("dataUpdateTracker:")
|
|
||||||
bucket, _ := path2BucketObjectWithBasePath("", dir)
|
|
||||||
if bucket == "" {
|
|
||||||
if d.debug && len(dir) > 0 {
|
|
||||||
console.Debugf(dateUpdateTrackerLogPrefix+" no bucket (%s)\n", dir)
|
|
||||||
}
|
|
||||||
return d.current()
|
|
||||||
}
|
|
||||||
if isReservedOrInvalidBucket(bucket, false) {
|
|
||||||
return d.current()
|
|
||||||
}
|
|
||||||
|
|
||||||
d.mu.Lock()
|
|
||||||
defer d.mu.Unlock()
|
|
||||||
if d.Current.bf.containsDir(dir) || d.Current.idx == 0 {
|
|
||||||
return d.Current.idx
|
|
||||||
}
|
|
||||||
if d.debug {
|
|
||||||
console.Debugf(dateUpdateTrackerLogPrefix+" current bloom does NOT contains dir %s\n", dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
idx := d.Current.idx - 1
|
|
||||||
for {
|
|
||||||
f := d.History.find(idx)
|
|
||||||
if f == nil || f.bf.containsDir(dir) || idx == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
idx--
|
|
||||||
}
|
|
||||||
return idx
|
|
||||||
}
|
|
||||||
|
|
||||||
// start will load the current data from the drives start collecting information and
|
|
||||||
// start a saver goroutine.
|
|
||||||
// All of these will exit when the context is canceled.
|
|
||||||
func (d *dataUpdateTracker) start(ctx context.Context, drives ...string) {
|
|
||||||
if len(drives) == 0 {
|
|
||||||
logger.LogIf(ctx, errors.New("dataUpdateTracker.start: No local drives specified"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.load(ctx, drives...)
|
|
||||||
go d.startCollector(ctx)
|
|
||||||
// startSaver will unlock.
|
|
||||||
d.mu.Lock()
|
|
||||||
go d.startSaver(ctx, dataUpdateTrackerSaveInterval, drives...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// load will attempt to load data tracking information from the supplied drives.
|
|
||||||
// The data will only be loaded if d.Saved is older than the one found on disk.
|
|
||||||
// The newest working cache will be kept in d.
|
|
||||||
// If no valid data usage tracker can be found d will remain unchanged.
|
|
||||||
// If object is shared the caller should lock it.
|
|
||||||
func (d *dataUpdateTracker) load(ctx context.Context, drives ...string) {
|
|
||||||
if len(drives) == 0 {
|
|
||||||
logger.LogIf(ctx, errors.New("dataUpdateTracker.load: No local drives specified"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, drive := range drives {
|
|
||||||
|
|
||||||
cacheFormatPath := pathJoin(drive, dataUpdateTrackerFilename)
|
|
||||||
f, err := OpenFile(cacheFormatPath, readMode, 0o666)
|
|
||||||
if err != nil {
|
|
||||||
if osIsNotExist(err) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err = d.deserialize(f, d.Saved)
|
|
||||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
f.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// startSaver will start a saver that will write d to all supplied drives at specific intervals.
|
|
||||||
// 'd' must be write locked when started and will be unlocked.
|
|
||||||
// The saver will save and exit when supplied context is closed.
|
|
||||||
func (d *dataUpdateTracker) startSaver(ctx context.Context, interval time.Duration, drives ...string) {
|
|
||||||
if len(drives) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
saveNow := d.save
|
|
||||||
exited := make(chan struct{})
|
|
||||||
d.saveExited = exited
|
|
||||||
d.mu.Unlock()
|
|
||||||
t := time.NewTicker(interval)
|
|
||||||
defer t.Stop()
|
|
||||||
defer close(exited)
|
|
||||||
var buf bytes.Buffer
|
|
||||||
for {
|
|
||||||
var exit bool
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
exit = true
|
|
||||||
case <-t.C:
|
|
||||||
case <-saveNow:
|
|
||||||
}
|
|
||||||
buf.Reset()
|
|
||||||
d.mu.Lock()
|
|
||||||
if !d.dirty {
|
|
||||||
d.mu.Unlock()
|
|
||||||
if exit {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
d.Saved = UTCNow()
|
|
||||||
err := d.serialize(&buf)
|
|
||||||
if d.debug {
|
|
||||||
console.Debugf(color.Green("dataUpdateTracker:")+" Saving: %v bytes, Current idx: %v\n", buf.Len(), d.Current.idx)
|
|
||||||
}
|
|
||||||
d.dirty = false
|
|
||||||
d.mu.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err, "Error serializing usage tracker data")
|
|
||||||
if exit {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if buf.Len() == 0 {
|
|
||||||
logger.LogIf(ctx, errors.New("zero sized output, skipping save"))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, drive := range drives {
|
|
||||||
cacheFormatPath := pathJoin(drive, dataUpdateTrackerFilename)
|
|
||||||
err := os.WriteFile(cacheFormatPath, buf.Bytes(), os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
if osIsNotExist(err) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if exit {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// serialize all data in d to dst.
|
|
||||||
// Caller should hold lock if d is expected to be shared.
|
|
||||||
// If an error is returned, there will likely be partial data written to dst.
|
|
||||||
func (d *dataUpdateTracker) serialize(dst io.Writer) (err error) {
|
|
||||||
ctx := GlobalContext
|
|
||||||
var tmp [8]byte
|
|
||||||
o := bufio.NewWriter(dst)
|
|
||||||
defer func() {
|
|
||||||
if err == nil {
|
|
||||||
err = o.Flush()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Version
|
|
||||||
if err := o.WriteByte(dataUpdateTrackerVersion); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Timestamp.
|
|
||||||
binary.LittleEndian.PutUint64(tmp[:], uint64(d.Saved.Unix()))
|
|
||||||
if _, err := o.Write(tmp[:]); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current
|
|
||||||
binary.LittleEndian.PutUint64(tmp[:], d.Current.idx)
|
|
||||||
if _, err := o.Write(tmp[:]); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := d.Current.bf.WriteTo(o); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// History
|
|
||||||
binary.LittleEndian.PutUint64(tmp[:], uint64(len(d.History)))
|
|
||||||
if _, err := o.Write(tmp[:]); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, bf := range d.History {
|
|
||||||
// Current
|
|
||||||
binary.LittleEndian.PutUint64(tmp[:], bf.idx)
|
|
||||||
if _, err := o.Write(tmp[:]); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := bf.bf.WriteTo(o); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deserialize will deserialize the supplied input if the input is newer than the supplied time.
|
|
||||||
func (d *dataUpdateTracker) deserialize(src io.Reader, newerThan time.Time) error {
|
|
||||||
ctx := GlobalContext
|
|
||||||
var dst dataUpdateTracker
|
|
||||||
var tmp [8]byte
|
|
||||||
|
|
||||||
// Version
|
|
||||||
if _, err := io.ReadFull(src, tmp[:1]); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
if err != io.EOF {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch tmp[0] {
|
|
||||||
case 1, 2, 3, 4, 5, 6:
|
|
||||||
if intDataUpdateTracker.debug {
|
|
||||||
console.Debugln(color.Green("dataUpdateTracker: ") + "deprecated data version, updating.")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case dataUpdateTrackerVersion:
|
|
||||||
default:
|
|
||||||
return errors.New("dataUpdateTracker: Unknown data version")
|
|
||||||
}
|
|
||||||
// Timestamp.
|
|
||||||
if _, err := io.ReadFull(src, tmp[:8]); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t := time.Unix(int64(binary.LittleEndian.Uint64(tmp[:])), 0)
|
|
||||||
if !t.After(newerThan) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current
|
|
||||||
if _, err := io.ReadFull(src, tmp[:8]); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dst.Current.idx = binary.LittleEndian.Uint64(tmp[:])
|
|
||||||
dst.Current.bf = emptyBloomFilter()
|
|
||||||
if _, err := dst.Current.bf.ReadFrom(src); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// History
|
|
||||||
if _, err := io.ReadFull(src, tmp[:8]); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n := binary.LittleEndian.Uint64(tmp[:])
|
|
||||||
dst.History = make(dataUpdateTrackerHistory, int(n))
|
|
||||||
for i, e := range dst.History {
|
|
||||||
if _, err := io.ReadFull(src, tmp[:8]); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
e.idx = binary.LittleEndian.Uint64(tmp[:])
|
|
||||||
e.bf = emptyBloomFilter()
|
|
||||||
if _, err := e.bf.ReadFrom(src); err != nil {
|
|
||||||
if d.debug {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dst.History[i] = e
|
|
||||||
}
|
|
||||||
// Ignore what remains on the stream.
|
|
||||||
// Update d:
|
|
||||||
d.mu.Lock()
|
|
||||||
defer d.mu.Unlock()
|
|
||||||
d.Current = dst.Current
|
|
||||||
d.History = dst.History
|
|
||||||
d.Saved = dst.Saved
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// start a collector that picks up entries from objectUpdatedCh
|
|
||||||
// and adds them to the current bloom filter.
|
|
||||||
func (d *dataUpdateTracker) startCollector(ctx context.Context) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case in := <-d.input:
|
|
||||||
bucket, _ := path2BucketObjectWithBasePath("", in)
|
|
||||||
if bucket == "" {
|
|
||||||
if d.debug && len(in) > 0 {
|
|
||||||
console.Debugf(color.Green("dataUpdateTracker:")+" no bucket (%s)\n", in)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if isReservedOrInvalidBucket(bucket, false) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
split := splitPathDeterministic(in)
|
|
||||||
|
|
||||||
// Add all paths until done.
|
|
||||||
d.mu.Lock()
|
|
||||||
for i := range split {
|
|
||||||
d.Current.bf.AddString(hashPath(path.Join(split[:i+1]...)).String())
|
|
||||||
}
|
|
||||||
d.dirty = d.dirty || len(split) > 0
|
|
||||||
d.mu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// markDirty adds the supplied path to the current bloom filter.
|
|
||||||
func (d *dataUpdateTracker) markDirty(bucket, prefix string) {
|
|
||||||
dateUpdateTrackerLogPrefix := color.Green("dataUpdateTracker:")
|
|
||||||
if bucket == "" && d.debug {
|
|
||||||
console.Debugf(dateUpdateTrackerLogPrefix + " no bucket specified\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if isReservedOrInvalidBucket(bucket, false) && d.debug {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
split := splitPathDeterministic(pathJoin(bucket, prefix))
|
|
||||||
|
|
||||||
// Add all paths until done.
|
|
||||||
d.mu.Lock()
|
|
||||||
for i := range split {
|
|
||||||
d.Current.bf.AddString(hashPath(path.Join(split[:i+1]...)).String())
|
|
||||||
}
|
|
||||||
d.dirty = d.dirty || len(split) > 0
|
|
||||||
d.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// find entry with specified index.
|
|
||||||
// Returns nil if not found.
|
|
||||||
func (d dataUpdateTrackerHistory) find(idx uint64) *dataUpdateFilter {
|
|
||||||
for _, f := range d {
|
|
||||||
if f.idx == idx {
|
|
||||||
return &f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterFrom will return a combined bloom filter.
|
|
||||||
func (d *dataUpdateTracker) filterFrom(ctx context.Context, oldest, newest uint64) *bloomFilterResponse {
|
|
||||||
bf := d.newBloomFilter()
|
|
||||||
bfr := bloomFilterResponse{
|
|
||||||
OldestIdx: oldest,
|
|
||||||
CurrentIdx: d.Current.idx,
|
|
||||||
Complete: true,
|
|
||||||
}
|
|
||||||
// Loop through each index requested.
|
|
||||||
for idx := oldest; idx <= newest; idx++ {
|
|
||||||
v := d.History.find(idx)
|
|
||||||
if v == nil {
|
|
||||||
if d.Current.idx == idx {
|
|
||||||
// Merge current.
|
|
||||||
err := bf.Merge(d.Current.bf.BloomFilter)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
if err != nil {
|
|
||||||
bfr.Complete = false
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
bfr.Complete = false
|
|
||||||
bfr.OldestIdx = idx + 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err := bf.Merge(v.bf.BloomFilter)
|
|
||||||
if err != nil {
|
|
||||||
bfr.Complete = false
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
bfr.NewestIdx = idx
|
|
||||||
}
|
|
||||||
var dst bytes.Buffer
|
|
||||||
_, err := bf.WriteTo(&dst)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
bfr.Filter = dst.Bytes()
|
|
||||||
|
|
||||||
return &bfr
|
|
||||||
}
|
|
||||||
|
|
||||||
// cycleFilter will cycle the bloom filter to start recording to index y if not already.
|
|
||||||
// The response will contain a bloom filter starting at index x up to, but not including index y.
|
|
||||||
// If y is 0, the response will not update y, but return the currently recorded information
|
|
||||||
// from the oldest (unless 0, then it will be all) until and including current y.
|
|
||||||
func (d *dataUpdateTracker) cycleFilter(ctx context.Context, req bloomFilterRequest) (*bloomFilterResponse, error) {
|
|
||||||
if req.OldestClean != "" {
|
|
||||||
return &bloomFilterResponse{OldestIdx: d.latestWithDir(req.OldestClean)}, nil
|
|
||||||
}
|
|
||||||
current := req.Current
|
|
||||||
oldest := req.Oldest
|
|
||||||
d.mu.Lock()
|
|
||||||
defer d.mu.Unlock()
|
|
||||||
if current == 0 {
|
|
||||||
if len(d.History) == 0 {
|
|
||||||
return d.filterFrom(ctx, d.Current.idx, d.Current.idx), nil
|
|
||||||
}
|
|
||||||
d.History.sort()
|
|
||||||
if oldest == 0 {
|
|
||||||
oldest = d.History[len(d.History)-1].idx
|
|
||||||
}
|
|
||||||
return d.filterFrom(ctx, oldest, d.Current.idx), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move current to history if new one requested
|
|
||||||
if d.Current.idx != current {
|
|
||||||
d.dirty = true
|
|
||||||
if d.debug {
|
|
||||||
console.Debugf(color.Green("dataUpdateTracker:")+" cycle bloom filter: %v -> %v\n", d.Current.idx, current)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.History = append(d.History, d.Current)
|
|
||||||
d.Current.idx = current
|
|
||||||
d.Current.bf = d.newBloomFilter()
|
|
||||||
select {
|
|
||||||
case d.save <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.History.removeOlderThan(oldest)
|
|
||||||
return d.filterFrom(ctx, oldest, current), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitPathDeterministic will split the provided relative path
|
|
||||||
// deterministically and return up to the first 3 elements of the path.
|
|
||||||
// slash and dot prefixes are removed.
|
|
||||||
// Trailing slashes are removed.
|
|
||||||
// Returns 0 length if no parts are found after trimming.
|
|
||||||
func splitPathDeterministic(in string) []string {
|
|
||||||
split := strings.Split(decodeDirObject(in), SlashSeparator)
|
|
||||||
|
|
||||||
// Trim empty start/end
|
|
||||||
for len(split) > 0 {
|
|
||||||
if len(split[0]) > 0 && split[0] != "." {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
split = split[1:]
|
|
||||||
}
|
|
||||||
for len(split) > 0 {
|
|
||||||
if len(split[len(split)-1]) > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
split = split[:len(split)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return split
|
|
||||||
}
|
|
||||||
|
|
||||||
// bloomFilterRequest request bloom filters.
|
|
||||||
// Current index will be updated to current and entries back to Oldest is returned.
|
|
||||||
type bloomFilterRequest struct {
|
|
||||||
Oldest uint64
|
|
||||||
Current uint64
|
|
||||||
// If set the oldest clean version will be returned in OldestIdx
|
|
||||||
// and the rest of the request will be ignored.
|
|
||||||
OldestClean string
|
|
||||||
}
|
|
||||||
|
|
||||||
type bloomFilterResponse struct {
|
|
||||||
// Current index being written to.
|
|
||||||
CurrentIdx uint64
|
|
||||||
// Oldest index in the returned bloom filter.
|
|
||||||
OldestIdx uint64
|
|
||||||
// Newest Index in the returned bloom filter.
|
|
||||||
NewestIdx uint64
|
|
||||||
// Are all indexes between oldest and newest filled?
|
|
||||||
Complete bool
|
|
||||||
// Binary data of the bloom filter.
|
|
||||||
Filter []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// NSUpdated indicates namespace has been updated.
|
|
||||||
// The function will block until the entry has been picked up.
|
|
||||||
func NSUpdated(bucket, prefix string) {
|
|
||||||
if intDataUpdateTracker != nil {
|
|
||||||
intDataUpdateTracker.markDirty(bucket, prefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,299 +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 (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/minio/minio/internal/logger"
|
|
||||||
"github.com/minio/minio/internal/logger/message/log"
|
|
||||||
"github.com/minio/minio/internal/logger/target/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testLoggerI interface {
|
|
||||||
Helper()
|
|
||||||
Log(args ...interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
type testingLogger struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
t testLoggerI
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testingLogger) Endpoint() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testingLogger) String() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testingLogger) Init() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testingLogger) Cancel() {
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
if t.t == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
e, ok := entry.(log.Entry)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unexpected log entry structure %#v", entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.t.Helper()
|
|
||||||
t.t.Log(e.Level, ":", e.Message)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addTestingLogging(t testLoggerI) func() {
|
|
||||||
tl := &testingLogger{t: t}
|
|
||||||
logger.AddSystemTarget(tl)
|
|
||||||
return func() {
|
|
||||||
tl.mu.Lock()
|
|
||||||
defer tl.mu.Unlock()
|
|
||||||
tl.t = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDataUpdateTracker(t *testing.T) {
|
|
||||||
dut := newDataUpdateTracker()
|
|
||||||
// Change some defaults.
|
|
||||||
dut.debug = testing.Verbose()
|
|
||||||
dut.input = make(chan string)
|
|
||||||
dut.save = make(chan struct{})
|
|
||||||
|
|
||||||
defer addTestingLogging(t)()
|
|
||||||
|
|
||||||
dut.Current.bf = dut.newBloomFilter()
|
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
err := os.MkdirAll(filepath.Dir(filepath.Join(tmpDir, dataUpdateTrackerFilename)), os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
dut.start(ctx, tmpDir)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
in string
|
|
||||||
check []string // if not empty, check against these instead.
|
|
||||||
exist bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
in: "bucket/directory/file.txt",
|
|
||||||
check: []string{"bucket", "bucket/", "/bucket", "bucket/directory", "bucket/directory/", "bucket/directory/file.txt", "/bucket/directory/file.txt"},
|
|
||||||
exist: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// System bucket
|
|
||||||
in: ".minio.sys/ignoreme/pls",
|
|
||||||
exist: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Not a valid bucket
|
|
||||||
in: "./bucket/okfile.txt",
|
|
||||||
check: []string{"./bucket/okfile.txt", "/bucket/okfile.txt", "bucket/okfile.txt"},
|
|
||||||
exist: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Not a valid bucket
|
|
||||||
in: "æ/okfile.txt",
|
|
||||||
check: []string{"æ/okfile.txt", "æ/okfile.txt", "æ"},
|
|
||||||
exist: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: "/bucket2/okfile2.txt",
|
|
||||||
check: []string{"./bucket2/okfile2.txt", "/bucket2/okfile2.txt", "bucket2/okfile2.txt", "bucket2"},
|
|
||||||
exist: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
in: "/bucket3/prefix/okfile2.txt",
|
|
||||||
check: []string{"./bucket3/prefix/okfile2.txt", "/bucket3/prefix/okfile2.txt", "bucket3/prefix/okfile2.txt", "bucket3/prefix", "bucket3"},
|
|
||||||
exist: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.in, func(t *testing.T) {
|
|
||||||
dut.input <- tt.in
|
|
||||||
dut.input <- "" // Sending empty string ensures the previous is added to filter.
|
|
||||||
dut.mu.Lock()
|
|
||||||
defer dut.mu.Unlock()
|
|
||||||
if len(tt.check) == 0 {
|
|
||||||
got := dut.Current.bf.containsDir(tt.in)
|
|
||||||
if got != tt.exist {
|
|
||||||
// For unlimited tests this could lead to false positives,
|
|
||||||
// but it should be deterministic.
|
|
||||||
t.Errorf("entry %q, got: %v, want %v", tt.in, got, tt.exist)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, check := range tt.check {
|
|
||||||
got := dut.Current.bf.containsDir(check)
|
|
||||||
if got != tt.exist {
|
|
||||||
// For unlimited tests this could lead to false positives,
|
|
||||||
// but it should be deterministic.
|
|
||||||
t.Errorf("entry %q, check: %q, got: %v, want %v", tt.in, check, got, tt.exist)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Cycle to history
|
|
||||||
req := bloomFilterRequest{
|
|
||||||
Oldest: 1,
|
|
||||||
Current: 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = dut.cycleFilter(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
dut.input <- "cycle2/file.txt"
|
|
||||||
dut.input <- "" // Sending empty string ensures the previous is added to filter.
|
|
||||||
|
|
||||||
tests = append(tests, struct {
|
|
||||||
in string
|
|
||||||
check []string
|
|
||||||
exist bool
|
|
||||||
}{in: "cycle2/file.txt", exist: true})
|
|
||||||
|
|
||||||
// Shut down
|
|
||||||
cancel()
|
|
||||||
<-dut.saveExited
|
|
||||||
|
|
||||||
if dut.current() != 2 {
|
|
||||||
t.Fatal("wrong current idx after save. want 2, got:", dut.current())
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel = context.WithCancel(context.Background())
|
|
||||||
// Reload...
|
|
||||||
dut = newDataUpdateTracker()
|
|
||||||
dut.start(ctx, tmpDir)
|
|
||||||
defer func() {
|
|
||||||
cancel()
|
|
||||||
<-dut.saveExited
|
|
||||||
}()
|
|
||||||
|
|
||||||
if dut.current() != 2 {
|
|
||||||
t.Fatal("current idx after load not preserved. want 2, got:", dut.current())
|
|
||||||
}
|
|
||||||
req = bloomFilterRequest{
|
|
||||||
Oldest: 1,
|
|
||||||
Current: 3,
|
|
||||||
}
|
|
||||||
bfr2, err := dut.cycleFilter(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bfr2.Complete {
|
|
||||||
t.Fatal("Wanted complete, didn't get it")
|
|
||||||
}
|
|
||||||
if bfr2.CurrentIdx != 3 {
|
|
||||||
t.Fatal("wanted index 3, got", bfr2.CurrentIdx)
|
|
||||||
}
|
|
||||||
if bfr2.OldestIdx != 1 {
|
|
||||||
t.Fatal("wanted oldest index 3, got", bfr2.OldestIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Size of filter %d bytes, M: %d, K:%d", len(bfr2.Filter), dut.Current.bf.Cap(), dut.Current.bf.K())
|
|
||||||
// Rerun test with returned bfr2
|
|
||||||
bf := dut.newBloomFilter()
|
|
||||||
_, err = bf.ReadFrom(bytes.NewReader(bfr2.Filter))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.in+"-reloaded", func(t *testing.T) {
|
|
||||||
if len(tt.check) == 0 {
|
|
||||||
got := bf.containsDir(tt.in)
|
|
||||||
if got != tt.exist {
|
|
||||||
// For unlimited tests this could lead to false positives,
|
|
||||||
// but it should be deterministic.
|
|
||||||
t.Errorf("entry %q, got: %v, want %v", tt.in, got, tt.exist)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, check := range tt.check {
|
|
||||||
got := bf.containsDir(check)
|
|
||||||
if got != tt.exist {
|
|
||||||
// For unlimited tests this could lead to false positives,
|
|
||||||
// but it should be deterministic.
|
|
||||||
t.Errorf("entry %q, check: %q, got: %v, want %v", tt.in, check, got, tt.exist)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkDataUpdateTracker(b *testing.B) {
|
|
||||||
dut := newDataUpdateTracker()
|
|
||||||
// Change some defaults.
|
|
||||||
dut.debug = false
|
|
||||||
dut.input = make(chan string)
|
|
||||||
dut.save = make(chan struct{})
|
|
||||||
|
|
||||||
defer addTestingLogging(b)()
|
|
||||||
|
|
||||||
dut.Current.bf = dut.newBloomFilter()
|
|
||||||
// We do this unbuffered. This will very significantly reduce throughput, so this is a worst case.
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
go dut.startCollector(ctx)
|
|
||||||
input := make([]string, 1000)
|
|
||||||
rng := rand.New(rand.NewSource(0xabad1dea))
|
|
||||||
tmp := []string{"bucket", "aprefix", "nextprefixlevel", "maybeobjname", "evendeeper", "ok-one-morelevel", "final.object"}
|
|
||||||
for i := range input {
|
|
||||||
tmp := tmp[:1+rng.Intn(cap(tmp)-1)]
|
|
||||||
input[i] = path.Join(tmp...)
|
|
||||||
}
|
|
||||||
b.SetBytes(1)
|
|
||||||
b.ResetTimer()
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
dut.input <- input[rng.Intn(len(input))]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+61
-16
@@ -23,6 +23,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -47,6 +48,9 @@ type dataUsageHash string
|
|||||||
// sizeHistogram is a size histogram.
|
// sizeHistogram is a size histogram.
|
||||||
type sizeHistogram [dataUsageBucketLen]uint64
|
type sizeHistogram [dataUsageBucketLen]uint64
|
||||||
|
|
||||||
|
// versionsHistogram is a histogram of number of versions in an object.
|
||||||
|
type versionsHistogram [dataUsageVersionLen]uint64
|
||||||
|
|
||||||
type dataUsageEntry struct {
|
type dataUsageEntry struct {
|
||||||
Children dataUsageHashMap `msg:"ch"`
|
Children dataUsageHashMap `msg:"ch"`
|
||||||
// These fields do no include any children.
|
// These fields do no include any children.
|
||||||
@@ -54,6 +58,7 @@ type dataUsageEntry struct {
|
|||||||
Objects uint64 `msg:"os"`
|
Objects uint64 `msg:"os"`
|
||||||
Versions uint64 `msg:"vs"` // Versions that are not delete markers.
|
Versions uint64 `msg:"vs"` // Versions that are not delete markers.
|
||||||
ObjSizes sizeHistogram `msg:"szs"`
|
ObjSizes sizeHistogram `msg:"szs"`
|
||||||
|
ObjVersions versionsHistogram `msg:"vh"`
|
||||||
ReplicationStats *replicationAllStats `msg:"rs,omitempty"`
|
ReplicationStats *replicationAllStats `msg:"rs,omitempty"`
|
||||||
AllTierStats *allTierStats `msg:"ats,omitempty"`
|
AllTierStats *allTierStats `msg:"ats,omitempty"`
|
||||||
Compacted bool `msg:"c"`
|
Compacted bool `msg:"c"`
|
||||||
@@ -276,7 +281,6 @@ type dataUsageCacheInfo struct {
|
|||||||
// indicates if the disk is being healed and scanner
|
// indicates if the disk is being healed and scanner
|
||||||
// should skip healing the disk
|
// should skip healing the disk
|
||||||
SkipHealing bool
|
SkipHealing bool
|
||||||
BloomFilter []byte `msg:"BloomFilter,omitempty"`
|
|
||||||
|
|
||||||
// Active lifecycle, if any on the bucket
|
// Active lifecycle, if any on the bucket
|
||||||
lifeCycle *lifecycle.Lifecycle `msg:"-"`
|
lifeCycle *lifecycle.Lifecycle `msg:"-"`
|
||||||
@@ -292,6 +296,7 @@ func (e *dataUsageEntry) addSizes(summary sizeSummary) {
|
|||||||
e.Size += summary.totalSize
|
e.Size += summary.totalSize
|
||||||
e.Versions += summary.versions
|
e.Versions += summary.versions
|
||||||
e.ObjSizes.add(summary.totalSize)
|
e.ObjSizes.add(summary.totalSize)
|
||||||
|
e.ObjVersions.add(summary.versions)
|
||||||
|
|
||||||
if e.ReplicationStats == nil {
|
if e.ReplicationStats == nil {
|
||||||
e.ReplicationStats = &replicationAllStats{
|
e.ReplicationStats = &replicationAllStats{
|
||||||
@@ -352,6 +357,10 @@ func (e *dataUsageEntry) merge(other dataUsageEntry) {
|
|||||||
e.ObjSizes[i] += v
|
e.ObjSizes[i] += v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i, v := range other.ObjVersions[:] {
|
||||||
|
e.ObjVersions[i] += v
|
||||||
|
}
|
||||||
|
|
||||||
if other.AllTierStats != nil {
|
if other.AllTierStats != nil {
|
||||||
if e.AllTierStats == nil {
|
if e.AllTierStats == nil {
|
||||||
e.AllTierStats = newAllTierStats()
|
e.AllTierStats = newAllTierStats()
|
||||||
@@ -649,10 +658,7 @@ func (d *dataUsageCache) reduceChildrenOf(path dataUsageHash, limit int, compact
|
|||||||
// StringAll returns a detailed string representation of all entries in the cache.
|
// StringAll returns a detailed string representation of all entries in the cache.
|
||||||
func (d *dataUsageCache) StringAll() string {
|
func (d *dataUsageCache) StringAll() string {
|
||||||
// Remove bloom filter from print.
|
// Remove bloom filter from print.
|
||||||
bf := d.Info.BloomFilter
|
|
||||||
d.Info.BloomFilter = nil
|
|
||||||
s := fmt.Sprintf("info:%+v\n", d.Info)
|
s := fmt.Sprintf("info:%+v\n", d.Info)
|
||||||
d.Info.BloomFilter = bf
|
|
||||||
for k, v := range d.Cache {
|
for k, v := range d.Cache {
|
||||||
s += fmt.Sprintf("\t%v: %+v\n", k, v)
|
s += fmt.Sprintf("\t%v: %+v\n", k, v)
|
||||||
}
|
}
|
||||||
@@ -698,7 +704,7 @@ func (d *dataUsageCache) flatten(root dataUsageEntry) dataUsageEntry {
|
|||||||
func (h *sizeHistogram) add(size int64) {
|
func (h *sizeHistogram) add(size int64) {
|
||||||
// Fetch the histogram interval corresponding
|
// Fetch the histogram interval corresponding
|
||||||
// to the passed object size.
|
// to the passed object size.
|
||||||
for i, interval := range ObjectsHistogramIntervals {
|
for i, interval := range ObjectsHistogramIntervals[:] {
|
||||||
if size >= interval.start && size <= interval.end {
|
if size >= interval.start && size <= interval.end {
|
||||||
h[i]++
|
h[i]++
|
||||||
break
|
break
|
||||||
@@ -715,6 +721,27 @@ func (h *sizeHistogram) toMap() map[string]uint64 {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add a version count to the histogram.
|
||||||
|
func (h *versionsHistogram) add(versions uint64) {
|
||||||
|
// Fetch the histogram interval corresponding
|
||||||
|
// to the passed object size.
|
||||||
|
for i, interval := range ObjectsVersionCountIntervals[:] {
|
||||||
|
if versions >= uint64(interval.start) && versions <= uint64(interval.end) {
|
||||||
|
h[i]++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toMap returns the map to a map[string]uint64.
|
||||||
|
func (h *versionsHistogram) toMap() map[string]uint64 {
|
||||||
|
res := make(map[string]uint64, dataUsageVersionLen)
|
||||||
|
for i, count := range h {
|
||||||
|
res[ObjectsVersionCountIntervals[i].name] = count
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
func (d *dataUsageCache) tiersUsageInfo(buckets []BucketInfo) *allTierStats {
|
func (d *dataUsageCache) tiersUsageInfo(buckets []BucketInfo) *allTierStats {
|
||||||
dst := newAllTierStats()
|
dst := newAllTierStats()
|
||||||
for _, bucket := range buckets {
|
for _, bucket := range buckets {
|
||||||
@@ -749,6 +776,7 @@ func (d *dataUsageCache) bucketsUsageInfo(buckets []BucketInfo) map[string]Bucke
|
|||||||
VersionsCount: flat.Versions,
|
VersionsCount: flat.Versions,
|
||||||
ObjectsCount: flat.Objects,
|
ObjectsCount: flat.Objects,
|
||||||
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
||||||
|
ObjectVersionsHistogram: flat.ObjVersions.toMap(),
|
||||||
}
|
}
|
||||||
if flat.ReplicationStats != nil {
|
if flat.ReplicationStats != nil {
|
||||||
bui.ReplicaSize = flat.ReplicationStats.ReplicaSize
|
bui.ReplicaSize = flat.ReplicationStats.ReplicaSize
|
||||||
@@ -845,43 +873,60 @@ func (d *dataUsageCache) merge(other dataUsageCache) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type objectIO interface {
|
type objectIO interface {
|
||||||
GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (reader *GetObjectReader, err error)
|
GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (reader *GetObjectReader, err error)
|
||||||
PutObject(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
PutObject(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// load the cache content with name from minioMetaBackgroundOpsBucket.
|
// load the cache content with name from minioMetaBackgroundOpsBucket.
|
||||||
// Only backend errors are returned as errors.
|
// Only backend errors are returned as errors.
|
||||||
|
// The loader is optimistic and has no locking, but tries 5 times before giving up.
|
||||||
// If the object is not found or unable to deserialize d is cleared and nil error is returned.
|
// If the object is not found or unable to deserialize d is cleared and nil error is returned.
|
||||||
func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string) error {
|
func (d *dataUsageCache) load(ctx context.Context, store objectIO, name string) error {
|
||||||
// Abandon if more than 5 minutes, so we don't hold up scanner.
|
// Abandon if more than 5 minutes, so we don't hold up scanner.
|
||||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
r, err := store.GetObjectNInfo(ctx, dataUsageBucket, name, nil, http.Header{}, readLock, ObjectOptions{})
|
// Caches are read+written without locks,
|
||||||
|
retries := 0
|
||||||
|
for retries < 5 {
|
||||||
|
r, err := store.GetObjectNInfo(ctx, dataUsageBucket, name, nil, http.Header{}, ObjectOptions{NoLock: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case ObjectNotFound:
|
case ObjectNotFound, BucketNotFound:
|
||||||
case BucketNotFound:
|
case InsufficientReadQuorum, StorageErr:
|
||||||
case InsufficientReadQuorum:
|
retries++
|
||||||
case StorageErr:
|
time.Sleep(time.Duration(rand.Int63n(int64(time.Second))))
|
||||||
|
continue
|
||||||
default:
|
default:
|
||||||
return toObjectErr(err, dataUsageBucket, name)
|
return toObjectErr(err, dataUsageBucket, name)
|
||||||
}
|
}
|
||||||
*d = dataUsageCache{}
|
*d = dataUsageCache{}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
defer r.Close()
|
|
||||||
if err := d.deserialize(r); err != nil {
|
if err := d.deserialize(r); err != nil {
|
||||||
*d = dataUsageCache{}
|
r.Close()
|
||||||
logger.LogOnceIf(ctx, err, err.Error())
|
retries++
|
||||||
|
time.Sleep(time.Duration(rand.Int63n(int64(time.Second))))
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
r.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*d = dataUsageCache{}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Maximum running concurrent saves on server.
|
||||||
|
var maxConcurrentScannerSaves = make(chan struct{}, 4)
|
||||||
|
|
||||||
// save the content of the cache to minioMetaBackgroundOpsBucket with the provided name.
|
// save the content of the cache to minioMetaBackgroundOpsBucket with the provided name.
|
||||||
|
// Note that no locking is done when saving.
|
||||||
func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string) error {
|
func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string) error {
|
||||||
var r io.Reader
|
var r io.Reader
|
||||||
|
maxConcurrentScannerSaves <- struct{}{}
|
||||||
|
defer func() {
|
||||||
|
<-maxConcurrentScannerSaves
|
||||||
|
}()
|
||||||
// If big, do streaming...
|
// If big, do streaming...
|
||||||
size := int64(-1)
|
size := int64(-1)
|
||||||
if len(d.Cache) > 10000 {
|
if len(d.Cache) > 10000 {
|
||||||
@@ -913,7 +958,7 @@ func (d *dataUsageCache) save(ctx context.Context, store objectIO, name string)
|
|||||||
dataUsageBucket,
|
dataUsageBucket,
|
||||||
name,
|
name,
|
||||||
NewPutObjReader(hr),
|
NewPutObjReader(hr),
|
||||||
ObjectOptions{})
|
ObjectOptions{NoLock: true})
|
||||||
if isErrBucketNotFound(err) {
|
if isErrBucketNotFound(err) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+155
-74
@@ -629,12 +629,6 @@ func (z *dataUsageCacheInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
err = msgp.WrapError(err, "SkipHealing")
|
err = msgp.WrapError(err, "SkipHealing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "BloomFilter":
|
|
||||||
z.BloomFilter, err = dc.ReadBytes(z.BloomFilter)
|
|
||||||
if err != nil {
|
|
||||||
err = msgp.WrapError(err, "BloomFilter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
err = dc.Skip()
|
err = dc.Skip()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -648,24 +642,9 @@ func (z *dataUsageCacheInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
|
|
||||||
// EncodeMsg implements msgp.Encodable
|
// EncodeMsg implements msgp.Encodable
|
||||||
func (z *dataUsageCacheInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
func (z *dataUsageCacheInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
// omitempty: check for empty values
|
// map header, size 4
|
||||||
zb0001Len := uint32(5)
|
|
||||||
var zb0001Mask uint8 /* 5 bits */
|
|
||||||
_ = zb0001Mask
|
|
||||||
if z.BloomFilter == nil {
|
|
||||||
zb0001Len--
|
|
||||||
zb0001Mask |= 0x10
|
|
||||||
}
|
|
||||||
// variable map header, size zb0001Len
|
|
||||||
err = en.Append(0x80 | uint8(zb0001Len))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if zb0001Len == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// write "Name"
|
// write "Name"
|
||||||
err = en.Append(0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
err = en.Append(0x84, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -704,39 +683,15 @@ func (z *dataUsageCacheInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
err = msgp.WrapError(err, "SkipHealing")
|
err = msgp.WrapError(err, "SkipHealing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (zb0001Mask & 0x10) == 0 { // if not empty
|
|
||||||
// write "BloomFilter"
|
|
||||||
err = en.Append(0xab, 0x42, 0x6c, 0x6f, 0x6f, 0x6d, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = en.WriteBytes(z.BloomFilter)
|
|
||||||
if err != nil {
|
|
||||||
err = msgp.WrapError(err, "BloomFilter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalMsg implements msgp.Marshaler
|
// MarshalMsg implements msgp.Marshaler
|
||||||
func (z *dataUsageCacheInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
func (z *dataUsageCacheInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
o = msgp.Require(b, z.Msgsize())
|
o = msgp.Require(b, z.Msgsize())
|
||||||
// omitempty: check for empty values
|
// map header, size 4
|
||||||
zb0001Len := uint32(5)
|
|
||||||
var zb0001Mask uint8 /* 5 bits */
|
|
||||||
_ = zb0001Mask
|
|
||||||
if z.BloomFilter == nil {
|
|
||||||
zb0001Len--
|
|
||||||
zb0001Mask |= 0x10
|
|
||||||
}
|
|
||||||
// variable map header, size zb0001Len
|
|
||||||
o = append(o, 0x80|uint8(zb0001Len))
|
|
||||||
if zb0001Len == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// string "Name"
|
// string "Name"
|
||||||
o = append(o, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
o = append(o, 0x84, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||||
o = msgp.AppendString(o, z.Name)
|
o = msgp.AppendString(o, z.Name)
|
||||||
// string "NextCycle"
|
// string "NextCycle"
|
||||||
o = append(o, 0xa9, 0x4e, 0x65, 0x78, 0x74, 0x43, 0x79, 0x63, 0x6c, 0x65)
|
o = append(o, 0xa9, 0x4e, 0x65, 0x78, 0x74, 0x43, 0x79, 0x63, 0x6c, 0x65)
|
||||||
@@ -747,11 +702,6 @@ func (z *dataUsageCacheInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
// string "SkipHealing"
|
// string "SkipHealing"
|
||||||
o = append(o, 0xab, 0x53, 0x6b, 0x69, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x69, 0x6e, 0x67)
|
o = append(o, 0xab, 0x53, 0x6b, 0x69, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x69, 0x6e, 0x67)
|
||||||
o = msgp.AppendBool(o, z.SkipHealing)
|
o = msgp.AppendBool(o, z.SkipHealing)
|
||||||
if (zb0001Mask & 0x10) == 0 { // if not empty
|
|
||||||
// string "BloomFilter"
|
|
||||||
o = append(o, 0xab, 0x42, 0x6c, 0x6f, 0x6f, 0x6d, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
|
|
||||||
o = msgp.AppendBytes(o, z.BloomFilter)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,12 +747,6 @@ func (z *dataUsageCacheInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
err = msgp.WrapError(err, "SkipHealing")
|
err = msgp.WrapError(err, "SkipHealing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "BloomFilter":
|
|
||||||
z.BloomFilter, bts, err = msgp.ReadBytesBytes(bts, z.BloomFilter)
|
|
||||||
if err != nil {
|
|
||||||
err = msgp.WrapError(err, "BloomFilter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
bts, err = msgp.Skip(bts)
|
bts, err = msgp.Skip(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -817,7 +761,7 @@ func (z *dataUsageCacheInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
|
|
||||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||||
func (z *dataUsageCacheInfo) Msgsize() (s int) {
|
func (z *dataUsageCacheInfo) Msgsize() (s int) {
|
||||||
s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 10 + msgp.Uint32Size + 11 + msgp.TimeSize + 12 + msgp.BoolSize + 12 + msgp.BytesPrefixSize + len(z.BloomFilter)
|
s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 10 + msgp.Uint32Size + 11 + msgp.TimeSize + 12 + msgp.BoolSize
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1596,6 +1540,24 @@ func (z *dataUsageEntry) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "vh":
|
||||||
|
var zb0003 uint32
|
||||||
|
zb0003, err = dc.ReadArrayHeader()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjVersions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if zb0003 != uint32(dataUsageVersionLen) {
|
||||||
|
err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0003}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for za0002 := range z.ObjVersions {
|
||||||
|
z.ObjVersions[za0002], err = dc.ReadUint64()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjVersions", za0002)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
case "rs":
|
case "rs":
|
||||||
if dc.IsNil() {
|
if dc.IsNil() {
|
||||||
err = dc.ReadNil()
|
err = dc.ReadNil()
|
||||||
@@ -1652,16 +1614,16 @@ func (z *dataUsageEntry) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
// EncodeMsg implements msgp.Encodable
|
// EncodeMsg implements msgp.Encodable
|
||||||
func (z *dataUsageEntry) EncodeMsg(en *msgp.Writer) (err error) {
|
func (z *dataUsageEntry) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
// omitempty: check for empty values
|
// omitempty: check for empty values
|
||||||
zb0001Len := uint32(8)
|
zb0001Len := uint32(9)
|
||||||
var zb0001Mask uint8 /* 8 bits */
|
var zb0001Mask uint16 /* 9 bits */
|
||||||
_ = zb0001Mask
|
_ = zb0001Mask
|
||||||
if z.ReplicationStats == nil {
|
if z.ReplicationStats == nil {
|
||||||
zb0001Len--
|
zb0001Len--
|
||||||
zb0001Mask |= 0x20
|
zb0001Mask |= 0x40
|
||||||
}
|
}
|
||||||
if z.AllTierStats == nil {
|
if z.AllTierStats == nil {
|
||||||
zb0001Len--
|
zb0001Len--
|
||||||
zb0001Mask |= 0x40
|
zb0001Mask |= 0x80
|
||||||
}
|
}
|
||||||
// variable map header, size zb0001Len
|
// variable map header, size zb0001Len
|
||||||
err = en.Append(0x80 | uint8(zb0001Len))
|
err = en.Append(0x80 | uint8(zb0001Len))
|
||||||
@@ -1728,7 +1690,24 @@ func (z *dataUsageEntry) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (zb0001Mask & 0x20) == 0 { // if not empty
|
// write "vh"
|
||||||
|
err = en.Append(0xa2, 0x76, 0x68)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteArrayHeader(uint32(dataUsageVersionLen))
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjVersions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for za0002 := range z.ObjVersions {
|
||||||
|
err = en.WriteUint64(z.ObjVersions[za0002])
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjVersions", za0002)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (zb0001Mask & 0x40) == 0 { // if not empty
|
||||||
// write "rs"
|
// write "rs"
|
||||||
err = en.Append(0xa2, 0x72, 0x73)
|
err = en.Append(0xa2, 0x72, 0x73)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1747,7 +1726,7 @@ func (z *dataUsageEntry) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (zb0001Mask & 0x40) == 0 { // if not empty
|
if (zb0001Mask & 0x80) == 0 { // if not empty
|
||||||
// write "ats"
|
// write "ats"
|
||||||
err = en.Append(0xa3, 0x61, 0x74, 0x73)
|
err = en.Append(0xa3, 0x61, 0x74, 0x73)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1783,16 +1762,16 @@ func (z *dataUsageEntry) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
func (z *dataUsageEntry) MarshalMsg(b []byte) (o []byte, err error) {
|
func (z *dataUsageEntry) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
o = msgp.Require(b, z.Msgsize())
|
o = msgp.Require(b, z.Msgsize())
|
||||||
// omitempty: check for empty values
|
// omitempty: check for empty values
|
||||||
zb0001Len := uint32(8)
|
zb0001Len := uint32(9)
|
||||||
var zb0001Mask uint8 /* 8 bits */
|
var zb0001Mask uint16 /* 9 bits */
|
||||||
_ = zb0001Mask
|
_ = zb0001Mask
|
||||||
if z.ReplicationStats == nil {
|
if z.ReplicationStats == nil {
|
||||||
zb0001Len--
|
zb0001Len--
|
||||||
zb0001Mask |= 0x20
|
zb0001Mask |= 0x40
|
||||||
}
|
}
|
||||||
if z.AllTierStats == nil {
|
if z.AllTierStats == nil {
|
||||||
zb0001Len--
|
zb0001Len--
|
||||||
zb0001Mask |= 0x40
|
zb0001Mask |= 0x80
|
||||||
}
|
}
|
||||||
// variable map header, size zb0001Len
|
// variable map header, size zb0001Len
|
||||||
o = append(o, 0x80|uint8(zb0001Len))
|
o = append(o, 0x80|uint8(zb0001Len))
|
||||||
@@ -1821,7 +1800,13 @@ func (z *dataUsageEntry) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
for za0001 := range z.ObjSizes {
|
for za0001 := range z.ObjSizes {
|
||||||
o = msgp.AppendUint64(o, z.ObjSizes[za0001])
|
o = msgp.AppendUint64(o, z.ObjSizes[za0001])
|
||||||
}
|
}
|
||||||
if (zb0001Mask & 0x20) == 0 { // if not empty
|
// string "vh"
|
||||||
|
o = append(o, 0xa2, 0x76, 0x68)
|
||||||
|
o = msgp.AppendArrayHeader(o, uint32(dataUsageVersionLen))
|
||||||
|
for za0002 := range z.ObjVersions {
|
||||||
|
o = msgp.AppendUint64(o, z.ObjVersions[za0002])
|
||||||
|
}
|
||||||
|
if (zb0001Mask & 0x40) == 0 { // if not empty
|
||||||
// string "rs"
|
// string "rs"
|
||||||
o = append(o, 0xa2, 0x72, 0x73)
|
o = append(o, 0xa2, 0x72, 0x73)
|
||||||
if z.ReplicationStats == nil {
|
if z.ReplicationStats == nil {
|
||||||
@@ -1834,7 +1819,7 @@ func (z *dataUsageEntry) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (zb0001Mask & 0x40) == 0 { // if not empty
|
if (zb0001Mask & 0x80) == 0 { // if not empty
|
||||||
// string "ats"
|
// string "ats"
|
||||||
o = append(o, 0xa3, 0x61, 0x74, 0x73)
|
o = append(o, 0xa3, 0x61, 0x74, 0x73)
|
||||||
if z.AllTierStats == nil {
|
if z.AllTierStats == nil {
|
||||||
@@ -1913,6 +1898,24 @@ func (z *dataUsageEntry) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "vh":
|
||||||
|
var zb0003 uint32
|
||||||
|
zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjVersions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if zb0003 != uint32(dataUsageVersionLen) {
|
||||||
|
err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0003}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for za0002 := range z.ObjVersions {
|
||||||
|
z.ObjVersions[za0002], bts, err = msgp.ReadUint64Bytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjVersions", za0002)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
case "rs":
|
case "rs":
|
||||||
if msgp.IsNil(bts) {
|
if msgp.IsNil(bts) {
|
||||||
bts, err = msgp.ReadNilBytes(bts)
|
bts, err = msgp.ReadNilBytes(bts)
|
||||||
@@ -1967,7 +1970,7 @@ func (z *dataUsageEntry) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
|
|
||||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||||
func (z *dataUsageEntry) Msgsize() (s int) {
|
func (z *dataUsageEntry) Msgsize() (s int) {
|
||||||
s = 1 + 3 + z.Children.Msgsize() + 3 + msgp.Int64Size + 3 + msgp.Uint64Size + 3 + msgp.Uint64Size + 4 + msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size)) + 3
|
s = 1 + 3 + z.Children.Msgsize() + 3 + msgp.Int64Size + 3 + msgp.Uint64Size + 3 + msgp.Uint64Size + 4 + msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size)) + 3 + msgp.ArrayHeaderSize + (dataUsageVersionLen * (msgp.Uint64Size)) + 3
|
||||||
if z.ReplicationStats == nil {
|
if z.ReplicationStats == nil {
|
||||||
s += msgp.NilSize
|
s += msgp.NilSize
|
||||||
} else {
|
} else {
|
||||||
@@ -3760,3 +3763,81 @@ func (z tierStats) Msgsize() (s int) {
|
|||||||
s = 1 + 3 + msgp.Uint64Size + 3 + msgp.IntSize + 3 + msgp.IntSize
|
s = 1 + 3 + msgp.Uint64Size + 3 + msgp.IntSize + 3 + msgp.IntSize
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DecodeMsg implements msgp.Decodable
|
||||||
|
func (z *versionsHistogram) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||||
|
var zb0001 uint32
|
||||||
|
zb0001, err = dc.ReadArrayHeader()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if zb0001 != uint32(dataUsageVersionLen) {
|
||||||
|
err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0001}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for za0001 := range z {
|
||||||
|
z[za0001], err = dc.ReadUint64()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, za0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeMsg implements msgp.Encodable
|
||||||
|
func (z *versionsHistogram) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
|
err = en.WriteArrayHeader(uint32(dataUsageVersionLen))
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for za0001 := range z {
|
||||||
|
err = en.WriteUint64(z[za0001])
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, za0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalMsg implements msgp.Marshaler
|
||||||
|
func (z *versionsHistogram) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
|
o = msgp.Require(b, z.Msgsize())
|
||||||
|
o = msgp.AppendArrayHeader(o, uint32(dataUsageVersionLen))
|
||||||
|
for za0001 := range z {
|
||||||
|
o = msgp.AppendUint64(o, z[za0001])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalMsg implements msgp.Unmarshaler
|
||||||
|
func (z *versionsHistogram) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||||
|
var zb0001 uint32
|
||||||
|
zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if zb0001 != uint32(dataUsageVersionLen) {
|
||||||
|
err = msgp.ArrayError{Wanted: uint32(dataUsageVersionLen), Got: zb0001}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for za0001 := range z {
|
||||||
|
z[za0001], bts, err = msgp.ReadUint64Bytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, za0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o = bts
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||||
|
func (z *versionsHistogram) Msgsize() (s int) {
|
||||||
|
s = msgp.ArrayHeaderSize + (dataUsageVersionLen * (msgp.Uint64Size))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -1196,3 +1196,116 @@ func BenchmarkDecodetierStats(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMarshalUnmarshalversionsHistogram(t *testing.T) {
|
||||||
|
v := versionsHistogram{}
|
||||||
|
bts, err := v.MarshalMsg(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
left, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
|
||||||
|
left, err = msgp.Skip(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMarshalMsgversionsHistogram(b *testing.B) {
|
||||||
|
v := versionsHistogram{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgversionsHistogram(b *testing.B) {
|
||||||
|
v := versionsHistogram{}
|
||||||
|
bts := make([]byte, 0, v.Msgsize())
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnmarshalversionsHistogram(b *testing.B) {
|
||||||
|
v := versionsHistogram{}
|
||||||
|
bts, _ := v.MarshalMsg(nil)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecodeversionsHistogram(t *testing.T) {
|
||||||
|
v := versionsHistogram{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodeversionsHistogram Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := versionsHistogram{}
|
||||||
|
err := msgp.Decode(&buf, &vn)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
err = msgp.NewReader(&buf).Skip()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEncodeversionsHistogram(b *testing.B) {
|
||||||
|
v := versionsHistogram{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
en := msgp.NewWriter(msgp.Nowhere)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.EncodeMsg(en)
|
||||||
|
}
|
||||||
|
en.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecodeversionsHistogram(b *testing.B) {
|
||||||
|
v := versionsHistogram{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||||
|
dc := msgp.NewReader(rd)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := v.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ type BucketUsageInfo struct {
|
|||||||
|
|
||||||
ObjectsCount uint64 `json:"objectsCount"`
|
ObjectsCount uint64 `json:"objectsCount"`
|
||||||
ObjectSizesHistogram map[string]uint64 `json:"objectsSizesHistogram"`
|
ObjectSizesHistogram map[string]uint64 `json:"objectsSizesHistogram"`
|
||||||
|
ObjectVersionsHistogram map[string]uint64 `json:"objectsVersionsHistogram"`
|
||||||
VersionsCount uint64 `json:"versionsCount"`
|
VersionsCount uint64 `json:"versionsCount"`
|
||||||
ReplicaSize uint64 `json:"objectReplicaTotalSize"`
|
ReplicaSize uint64 `json:"objectReplicaTotalSize"`
|
||||||
ReplicationInfo map[string]BucketTargetUsageInfo `json:"objectsReplicationInfo"`
|
ReplicationInfo map[string]BucketTargetUsageInfo `json:"objectsReplicationInfo"`
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user