mirror of
https://github.com/pgsty/minio.git
synced 2026-11-12 10:35:33 +03:00
Compare commits
337 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd53b287f2 | |||
| 21526efe51 | |||
| 7413045f0e | |||
| d76c508566 | |||
| 8fb46de5e4 | |||
| af1944f28d | |||
| 214ea14f29 | |||
| 5fb420c703 | |||
| 2420f6c000 | |||
| b0d7332a0c | |||
| f71b56a5d0 | |||
| d55efc791f | |||
| f63645546d | |||
| e2dd3e3587 | |||
| 27ab780317 | |||
| e2d4d097e7 | |||
| ac8cb6ba0d | |||
| 4ce81fd07f | |||
| df9eeb7f8f | |||
| 31c4fdbf79 | |||
| 48e367ff7d | |||
| fd02492cb7 | |||
| addfa35d93 | |||
| 5afdc56796 | |||
| fb1c333a83 | |||
| c3e1da8e04 | |||
| 20a753e2e5 | |||
| 3a398775fb | |||
| 61a7434379 | |||
| 09f5e29327 | |||
| 29edb4ccfe | |||
| 197d6fb644 | |||
| d4e565e595 | |||
| 1fce2b180f | |||
| be6ccd129d | |||
| f7cecf0945 | |||
| 7b2198f7e5 | |||
| 52221db7ef | |||
| befbf48563 | |||
| 56a61bab56 | |||
| d480022711 | |||
| f1abb92f0c | |||
| 5792be71fa | |||
| c2630bb3a3 | |||
| 5e3010d455 | |||
| ccbf65c8e8 | |||
| 9d07cde385 | |||
| 464b9d7c80 | |||
| 5c81d0d89a | |||
| 62cd643868 | |||
| c0bf02b8b2 | |||
| 1b7dd70f72 | |||
| 372a08be49 | |||
| fd46a1c3b3 | |||
| 5aae7178ad | |||
| dea8220eee | |||
| a4be0b88f6 | |||
| d8101573be | |||
| 41cdb357bb | |||
| 38caddffe7 | |||
| 80fe166902 | |||
| 0e26f983d6 | |||
| fc08fcab52 | |||
| 77dc99e71d | |||
| 5041bfcb5c | |||
| 5be76856bd | |||
| 2a3f5e1ad1 | |||
| f8650a3493 | |||
| 90a52a29c5 | |||
| 8859c92f80 | |||
| 01e5632949 | |||
| 18a4276e25 | |||
| c06032f35f | |||
| 95a6b2c991 | |||
| ee28f6caaa | |||
| 30c9e50701 | |||
| 9aadd725d2 | |||
| 6cfb1cb6fd | |||
| 2dc8ac1e62 | |||
| e952e2a691 | |||
| 040ac5cad8 | |||
| 05685863e3 | |||
| d324c0a1c3 | |||
| 03f8b25b50 | |||
| b0e2c2da78 | |||
| f28a8eca91 | |||
| ca69e54cb6 | |||
| 4629abd5a2 | |||
| dc99f4a7a3 | |||
| 389ec21d0c | |||
| 9341201132 | |||
| 88dd83a365 | |||
| 74285d50c4 | |||
| 60d0611ac2 | |||
| f939222942 | |||
| c293c2e9a3 | |||
| e34ca9acd1 | |||
| 83071a3459 | |||
| edf364bf21 | |||
| 1e037883b0 | |||
| d909f167ff | |||
| 4592aaa3e2 | |||
| 95d1a12422 | |||
| 62aa42cccf | |||
| 5cffd3780a | |||
| def75ffcfe | |||
| ad8e611098 | |||
| 3ec1844e4a | |||
| 523670ba0d | |||
| 35dea24ffd | |||
| e55104a155 | |||
| 111745c564 | |||
| c7df1ffc6f | |||
| 2b7e75e079 | |||
| bcdaa09c75 | |||
| 2fc65dcb99 | |||
| 44a3b58e52 | |||
| 0a256053ee | |||
| 46de9ac03e | |||
| a53dc1d9c8 | |||
| f0462322fd | |||
| c59d2a6288 | |||
| 3e3ff2a70b | |||
| 16bc11e72e | |||
| 2719f1efaa | |||
| cff1be0ae8 | |||
| 39ac62a1a1 | |||
| f427dbbd60 | |||
| c3f689a7d9 | |||
| 85f3a9f3b0 | |||
| 13ba4b433d | |||
| 96f27a4965 | |||
| 0e502899a8 | |||
| 424b44c247 | |||
| 01a71c366d | |||
| 990fbeb3a4 | |||
| 5a9a898ba2 | |||
| fe1fbe0005 | |||
| c56a139fdc | |||
| df50eda811 | |||
| f5d3313210 | |||
| 97fcc9ff99 | |||
| 8a6b2b4447 | |||
| 757eaeae92 | |||
| b7dd61f6bc | |||
| d2a95a04a4 | |||
| 0cc993f403 | |||
| 3a64580663 | |||
| d087e28dce | |||
| 96adfaebe1 | |||
| ddf84f8257 | |||
| 507f993075 | |||
| 73a6a60785 | |||
| cf4cf58faf | |||
| 6bc3c74c0c | |||
| 598ce1e354 | |||
| 4685b76e08 | |||
| 78c9109f6c | |||
| 7e248fc0ba | |||
| c526fa9119 | |||
| 520e0fd985 | |||
| 54a7eba358 | |||
| 1494ba2e6e | |||
| a5b3548ede | |||
| 8318aa0113 | |||
| e69c42956b | |||
| 53ca589c11 | |||
| ca8ff8718e | |||
| e8e48e4c4a | |||
| 67e17ed3f8 | |||
| 2a6a40e93b | |||
| eda34423d7 | |||
| 7ce1f6e736 | |||
| 5c53620a72 | |||
| 5f94cec1e2 | |||
| 646350fa7f | |||
| e162a055cc | |||
| 28d3ad3ada | |||
| 0bd44a7764 | |||
| 8be6d887e2 | |||
| 66b14a0d32 | |||
| 153a612253 | |||
| 1a1b55e133 | |||
| 879de20edf | |||
| e77ad3f9bb | |||
| 4ce86ff5fa | |||
| e290c010e6 | |||
| 33d267fa1b | |||
| 601a744159 | |||
| f630d7c3fa | |||
| 91bfefcf8c | |||
| a1b01e6d5f | |||
| 48594617b5 | |||
| b35b9dcff7 | |||
| a3e317773a | |||
| 16431d222c | |||
| ee49a23220 | |||
| a9eef521ec | |||
| 901d33b59c | |||
| 255116fde7 | |||
| 00ebea2536 | |||
| dedf9774c7 | |||
| 6b1c62133d | |||
| d4251b2545 | |||
| b9d1698d74 | |||
| 7c696e1cb6 | |||
| 165d60421d | |||
| c7962118f8 | |||
| 892a204013 | |||
| 0e6aedc7ed | |||
| c547a4d835 | |||
| fc9668baa5 | |||
| ba17d46f15 | |||
| 54a4f93854 | |||
| bdd816488d | |||
| 36dcfee2f7 | |||
| 4d13ddf6b3 | |||
| 9e25475475 | |||
| e955aa7f2a | |||
| 81d2b54dfd | |||
| 7956ff0313 | |||
| 9ff25fb64b | |||
| 04df69f633 | |||
| 908eb57795 | |||
| ecfae074dc | |||
| be5d394e56 | |||
| 849a27ee61 | |||
| 062f3ea43a | |||
| 5cfedcfe33 | |||
| 4d2fc530d0 | |||
| 3970204009 | |||
| f046f557fa | |||
| 401958938d | |||
| 566cffe53d | |||
| 028bc2f9be | |||
| 813d9bc316 | |||
| 79ba458051 | |||
| cf220be9b5 | |||
| c433572585 | |||
| a42b576382 | |||
| fb9b53026d | |||
| 2ac54e5a7b | |||
| 8eecdc6d1f | |||
| 50577e2bd2 | |||
| 7bc1f986e8 | |||
| d796621ccc | |||
| 751e9fb7be | |||
| f6113264f4 | |||
| a3534a730b | |||
| 7f8b8a0e43 | |||
| bd6f7b6d83 | |||
| b0a4beb66a | |||
| 472c2d828c | |||
| 01ee49045e | |||
| 7bd9f821dd | |||
| b20ecc7b54 | |||
| 61eb9d4e29 | |||
| 43eb5a001c | |||
| f58692abb7 | |||
| c1760fb764 | |||
| e9bc0e7e98 | |||
| ffcadcd99e | |||
| 7a733a8d54 | |||
| ce97313fda | |||
| 7b81967a3c | |||
| ff811f594b | |||
| 0bf80b3c89 | |||
| ae3b369fe1 | |||
| 77b15e7194 | |||
| 20537f974e | |||
| 4476a64bdf | |||
| d4b701576e | |||
| 721c053712 | |||
| e3071157f0 | |||
| c07af89e48 | |||
| 9c846106fa | |||
| cf94d1f1f1 | |||
| 6187440f35 | |||
| 57b7c3494f | |||
| dda18c28c5 | |||
| f8d6eaaa96 | |||
| 47d4fabb58 | |||
| 80039f60d5 | |||
| 5a5e9b8a89 | |||
| b7ed3b77bd | |||
| 75b925c326 | |||
| 91d419ee6c | |||
| 23345098ea | |||
| 7ce91ea1a1 | |||
| 41079f1015 | |||
| 712dfa40cd | |||
| decfd6108c | |||
| b890bbfa63 | |||
| 0e3a570b85 | |||
| 7060c809c0 | |||
| 9dbfd84c5b | |||
| fce380a044 | |||
| 46ba15ab03 | |||
| 1e39ca39c3 | |||
| 80ef1ae51c | |||
| 4d0715d226 | |||
| 8a274169da | |||
| 21d8298fe1 | |||
| 5d6f6d8d5b | |||
| bacf6156c1 | |||
| 1d1b213f1f | |||
| 1f11af42f1 | |||
| a026c8748f | |||
| 9f7d89b3cd | |||
| 92a77cc78e | |||
| b0c84e3de7 | |||
| bbc914e174 | |||
| 3fca4055d2 | |||
| 66afa16aed | |||
| 9b0a8de7de | |||
| 04bbede17d | |||
| 0e3bafcc54 | |||
| b48f719b8e | |||
| 289fcbd08c | |||
| f6875bb893 | |||
| 7e803adf13 | |||
| 5b5deee5b3 | |||
| 7dae4cb685 | |||
| 27fad98179 | |||
| 58f7e3a829 | |||
| 4a15bd8ff8 | |||
| b030ef1aca | |||
| cc46a99f97 | |||
| becec6cb6b | |||
| bc33db9fc0 | |||
| 7d4579e737 | |||
| b7c90751b0 | |||
| 88fd1cba71 | |||
| e43cc316ff | |||
| e3f24a29fa | |||
| 890e526bde | |||
| 16ce455fca |
@@ -0,0 +1,5 @@
|
|||||||
|
# Config file for markdownlint-cli
|
||||||
|
MD033:
|
||||||
|
allowed_elements:
|
||||||
|
- details
|
||||||
|
- summary
|
||||||
@@ -11,19 +11,23 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build Tests with Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
name: Build Tests with Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.17.x]
|
go-version: [1.17.x, 1.18.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 # v2
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
- name: Build on ${{ matrix.os }}
|
- name: Build on ${{ matrix.os }}
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: FIPS Build Test
|
||||||
|
|
||||||
|
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 BoringCrypto ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.17.11b7, 1.18.3b7]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Setup dockerfile for build test
|
||||||
|
run: |
|
||||||
|
echo "FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:${{ matrix.go-version }}" > Dockerfile.fips.test
|
||||||
|
echo "COPY . /minio" >> Dockerfile.fips.test
|
||||||
|
echo "WORKDIR /minio" >> Dockerfile.fips.test
|
||||||
|
echo "RUN make" >> Dockerfile.fips.test
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.fips.test
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: minio/fips-test:latest
|
||||||
|
|
||||||
|
# This should fail if grep returns non-zero exit
|
||||||
|
- name: Test binary
|
||||||
|
run: |
|
||||||
|
docker run --rm minio/fips-test:latest ./minio --version
|
||||||
|
docker run --rm -i minio/fips-test:latest /bin/bash -c 'go tool nm ./minio' | grep -q FIPS
|
||||||
@@ -11,6 +11,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
@@ -21,9 +24,10 @@ jobs:
|
|||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -37,12 +41,10 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GO111MODULE: on
|
GO111MODULE: on
|
||||||
MINIO_KMS_KES_CERT_FILE: /home/runner/work/minio/minio/.github/workflows/root.cert
|
MINIO_KMS_SECRET_KEY: "my-minio-key:oyArl7zlPECEduNbB1KXgdzDn2Bdpvvw0l8VO51HQnY="
|
||||||
MINIO_KMS_KES_KEY_FILE: /home/runner/work/minio/minio/.github/workflows/root.key
|
|
||||||
MINIO_KMS_KES_ENDPOINT: "https://play.min.io:7373"
|
|
||||||
MINIO_KMS_KES_KEY_NAME: "my-minio-key"
|
|
||||||
MINIO_KMS_AUTO_ENCRYPTION: on
|
MINIO_KMS_AUTO_ENCRYPTION: on
|
||||||
run: |
|
run: |
|
||||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
make verify-healing
|
make verify-healing
|
||||||
|
make verify-healing-inconsistent-versions
|
||||||
|
|||||||
@@ -11,19 +11,23 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.17.x]
|
go-version: [1.17.x, 1.18.x]
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -11,19 +11,23 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Go ${{ matrix.go-version }} on ${{ matrix.os }} - healing
|
name: Go ${{ matrix.go-version }} on ${{ matrix.os }} - healing
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.17.x]
|
go-version: [1.17.x, 1.18.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
iam-matrix-test:
|
iam-matrix-test:
|
||||||
name: "[Go=${{ matrix.go-version }}|ldap=${{ matrix.ldap }}|etcd=${{ matrix.etcd }}|openid=${{ matrix.openid }}]"
|
name: "[Go=${{ matrix.go-version }}|ldap=${{ matrix.ldap }}|etcd=${{ matrix.etcd }}|openid=${{ matrix.openid }}]"
|
||||||
@@ -44,6 +47,14 @@ jobs:
|
|||||||
- "5556:5556"
|
- "5556:5556"
|
||||||
env:
|
env:
|
||||||
DEX_LDAP_SERVER: "openldap:389"
|
DEX_LDAP_SERVER: "openldap:389"
|
||||||
|
openid2:
|
||||||
|
image: quay.io/minio/dex
|
||||||
|
ports:
|
||||||
|
- "5557:5557"
|
||||||
|
env:
|
||||||
|
DEX_LDAP_SERVER: "openldap:389"
|
||||||
|
DEX_ISSUER: "http://127.0.0.1:5557/dex"
|
||||||
|
DEX_WEB_HTTP: "0.0.0.0:5557"
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
# When ldap, etcd or openid vars are empty below, those external servers
|
# When ldap, etcd or openid vars are empty below, those external servers
|
||||||
@@ -65,9 +76,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -85,6 +97,28 @@ jobs:
|
|||||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
make test-iam
|
make test-iam
|
||||||
|
- name: Test with multiple OpenID providers
|
||||||
|
if: matrix.openid == 'http://127.0.0.1:5556/dex'
|
||||||
|
env:
|
||||||
|
LDAP_TEST_SERVER: ${{ matrix.ldap }}
|
||||||
|
ETCD_SERVER: ${{ matrix.etcd }}
|
||||||
|
OPENID_TEST_SERVER: ${{ matrix.openid }}
|
||||||
|
OPENID_TEST_SERVER_2: "http://127.0.0.1:5557/dex"
|
||||||
|
run: |
|
||||||
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
|
make test-iam
|
||||||
|
- name: Test with Access Management Plugin enabled
|
||||||
|
env:
|
||||||
|
LDAP_TEST_SERVER: ${{ matrix.ldap }}
|
||||||
|
ETCD_SERVER: ${{ matrix.etcd }}
|
||||||
|
OPENID_TEST_SERVER: ${{ matrix.openid }}
|
||||||
|
POLICY_PLUGIN_ENDPOINT: "http://127.0.0.1:8080"
|
||||||
|
run: |
|
||||||
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
|
go run docs/iam/access-manager-plugin.go &
|
||||||
|
make test-iam
|
||||||
- name: Test LDAP for automatic site replication
|
- name: Test LDAP for automatic site replication
|
||||||
if: matrix.ldap == 'localhost:389'
|
if: matrix.ldap == 'localhost:389'
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Lint all docs
|
name: Lint all docs
|
||||||
@@ -22,4 +25,6 @@ jobs:
|
|||||||
- name: Lint all docs
|
- name: Lint all docs
|
||||||
run: |
|
run: |
|
||||||
npm install -g markdownlint-cli
|
npm install -g markdownlint-cli
|
||||||
markdownlint --fix '**/*.md' --disable MD013 MD040
|
markdownlint --fix '**/*.md' \
|
||||||
|
--config /home/runner/work/minio/minio/.github/markdown-lint-cfg.yaml \
|
||||||
|
--disable MD013 MD040
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
replication-test:
|
replication-test:
|
||||||
name: Replication Tests with Go ${{ matrix.go-version }}
|
name: Replication Tests with Go ${{ matrix.go-version }}
|
||||||
@@ -22,9 +25,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
@@ -22,10 +25,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
- name: Start upgrade tests
|
- name: Start upgrade tests
|
||||||
run: |
|
run: |
|
||||||
make test-upgrade
|
make test-upgrade
|
||||||
|
|||||||
+7
-2
@@ -9,8 +9,7 @@ site/
|
|||||||
/.idea/
|
/.idea/
|
||||||
/Minio.iml
|
/Minio.iml
|
||||||
**/access.log
|
**/access.log
|
||||||
vendor/**/*.js
|
vendor/
|
||||||
vendor/**/*.json
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.syso
|
*.syso
|
||||||
coverage.txt
|
coverage.txt
|
||||||
@@ -33,3 +32,9 @@ minio.RELEASE*
|
|||||||
mc
|
mc
|
||||||
nancy
|
nancy
|
||||||
inspects/*
|
inspects/*
|
||||||
|
docs/debugging/s3-verify/s3-verify
|
||||||
|
docs/debugging/xl-meta/xl-meta
|
||||||
|
docs/debugging/s3-check-md5/s3-check-md5
|
||||||
|
docs/debugging/hash-set/hash-set
|
||||||
|
docs/debugging/healing-bin/healing-bin
|
||||||
|
docs/debugging/inspect/inspect
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ linters:
|
|||||||
- varcheck
|
- varcheck
|
||||||
- gocritic
|
- gocritic
|
||||||
- gofumpt
|
- gofumpt
|
||||||
|
- tenv
|
||||||
|
- durationcheck
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
gofumpt:
|
gofumpt:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ 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 v1.43.0
|
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.45.2
|
||||||
@echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.1.7-0.20211026165309-e818a1881b0e
|
@echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.1.7-0.20211026165309-e818a1881b0e
|
||||||
@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
|
||||||
|
|
||||||
@@ -53,11 +53,12 @@ test-iam: build ## verify IAM (external IDP, etcd backends)
|
|||||||
@echo "Running tests for IAM (external IDP, etcd backends)"
|
@echo "Running tests for IAM (external IDP, etcd backends)"
|
||||||
@CGO_ENABLED=0 go test -tags kqueue -v -run TestIAM* ./cmd
|
@CGO_ENABLED=0 go test -tags kqueue -v -run TestIAM* ./cmd
|
||||||
@echo "Running tests for IAM (external IDP, etcd backends) with -race"
|
@echo "Running tests for IAM (external IDP, etcd backends) with -race"
|
||||||
@CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
|
@GORACE=history_size=7 CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
|
||||||
|
|
||||||
test-replication: install ## verify multi site replication
|
test-replication: install ## verify multi site replication
|
||||||
@echo "Running tests for replicating three sites"
|
@echo "Running tests for replicating three sites"
|
||||||
@(env bash $(PWD)/docs/bucket/replication/setup_3site_replication.sh)
|
@(env bash $(PWD)/docs/bucket/replication/setup_3site_replication.sh)
|
||||||
|
@(env bash $(PWD)/docs/bucket/replication/setup_2site_existing_replication.sh)
|
||||||
|
|
||||||
test-site-replication-ldap: install ## verify automatic site replication
|
test-site-replication-ldap: install ## verify automatic site replication
|
||||||
@echo "Running tests for automatic site replication of IAM (with LDAP)"
|
@echo "Running tests for automatic site replication of IAM (with LDAP)"
|
||||||
@@ -73,15 +74,20 @@ test-site-replication-minio: install ## verify automatic site replication
|
|||||||
|
|
||||||
verify: ## verify minio various setups
|
verify: ## verify minio various setups
|
||||||
@echo "Verifying build with race"
|
@echo "Verifying build with race"
|
||||||
@CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||||
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||||
|
|
||||||
verify-healing: ## verify healing and replacing disks with minio binary
|
verify-healing: ## verify healing and replacing disks with minio binary
|
||||||
@echo "Verify healing build with race"
|
@echo "Verify healing build with race"
|
||||||
@CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||||
@(env bash $(PWD)/buildscripts/verify-healing.sh)
|
@(env bash $(PWD)/buildscripts/verify-healing.sh)
|
||||||
@(env bash $(PWD)/buildscripts/unaligned-healing.sh)
|
@(env bash $(PWD)/buildscripts/unaligned-healing.sh)
|
||||||
|
|
||||||
|
verify-healing-inconsistent-versions: ## verify resolving inconsistent versions
|
||||||
|
@echo "Verify resolving inconsistent versions build with race"
|
||||||
|
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||||
|
@(env bash $(PWD)/buildscripts/resolve-right-versions.sh)
|
||||||
|
|
||||||
build: checks ## builds minio to $(PWD)
|
build: checks ## builds minio to $(PWD)
|
||||||
@echo "Building minio binary to './minio'"
|
@echo "Building minio binary to './minio'"
|
||||||
@CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
@CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# MinIO FIPS Builds
|
||||||
|
|
||||||
|
MinIO creates FIPS builds using a patched version of the Go compiler (that uses BoringCrypto, from BoringSSL, which is [FIPS 140-2 validated](https://csrc.nist.gov/csrc/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp2964.pdf)) published by the Golang Team [here](https://github.com/golang/go/tree/dev.boringcrypto/misc/boring).
|
||||||
|
|
||||||
|
MinIO FIPS executables are available at http://dl.min.io - they are only published for `linux-amd64` architecture as binary files with the suffix `.fips`. We also publish corresponding container images to our official image repositories.
|
||||||
|
|
||||||
|
We are not making any statements or representations about the suitability of this code or build in relation to the FIPS 140-2 standard. Interested users will have to evaluate for themselves whether this is useful for their own purposes.
|
||||||
@@ -196,12 +196,6 @@ iptables -A INPUT -p tcp --dport 9000:9010 -j ACCEPT
|
|||||||
service iptables restart
|
service iptables restart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pre-existing data
|
|
||||||
|
|
||||||
When deployed on a single drive, MinIO server lets clients access any pre-existing data in the data directory. For example, if MinIO is started with the command `minio server /mnt/data`, any pre-existing data in the `/mnt/data` directory would be accessible to the clients.
|
|
||||||
|
|
||||||
The above statement is also valid for all gateway backends.
|
|
||||||
|
|
||||||
## Test MinIO Connectivity
|
## Test MinIO Connectivity
|
||||||
|
|
||||||
### Test using MinIO Console
|
### Test using MinIO Console
|
||||||
@@ -242,16 +236,16 @@ Upgrades require zero downtime in MinIO, all upgrades are non-disruptive, all tr
|
|||||||
mc admin update <minio alias, e.g., myminio>
|
mc admin update <minio alias, e.g., myminio>
|
||||||
```
|
```
|
||||||
|
|
||||||
- For deployments without external internet access (e.g. airgapped environments), download the binary from <https://dl.min.io> and replace the existing MinIO binary let's say for example `/opt/bin/minio`, apply executable permissions `chmod +x /opt/bin/minio` and do `mc admin service restart alias/`.
|
- For deployments without external internet access (e.g. airgapped environments), download the binary from <https://dl.min.io> and replace the existing MinIO binary let's say for example `/opt/bin/minio`, apply executable permissions `chmod +x /opt/bin/minio` and proceed to perform `mc admin service restart alias/`.
|
||||||
|
|
||||||
- For RPM/DEB installations, upgrade packages **parallelly** on all servers. Once upgraded, perform `systemctl restart minio` across all nodes in **parallel**. RPM/DEB based installations are usually automated using [`ansible`](https://github.com/minio/ansible-minio).
|
- For installations using Systemd MinIO service, upgrade via RPM/DEB packages **parallelly** on all servers or replace the binary lets say `/opt/bin/minio` on all nodes, apply executable permissions `chmod +x /opt/bin/minio` and process to perform `mc admin service restart alias/`.
|
||||||
|
|
||||||
### Upgrade Checklist
|
### Upgrade Checklist
|
||||||
|
|
||||||
- Test all upgrades in a lower environment (DEV, QA, UAT) before applying to production. Performing blind upgrades in production environments carries significant risk.
|
- Test all upgrades in a lower environment (DEV, QA, UAT) before applying to production. Performing blind upgrades in production environments carries significant risk.
|
||||||
- Read the release notes for the targeted MinIO release *before* performing any installation, there is no forced requirement to upgrade to latest releases every week. If it has a bug fix you are looking for then yes, else avoid actively upgrading a running production system.
|
- Read the release notes for MinIO *before* performing any upgrade, there is no forced requirement to upgrade to latest releases upon every releases. Some releases may not be relevant to your setup, avoid upgrading production environments unnecessarily.
|
||||||
- Make sure MinIO process has write access to `/opt/bin` if you plan to use `mc admin update`. This is needed for MinIO to download the latest binary from <https://dl.min.io> and save it locally for upgrades.
|
- If you plan to use `mc admin update`, MinIO process must have write access to the parent directory where the binary is present on the host system.
|
||||||
- `mc admin update` is not supported in kubernetes/container environments, container environments provide their own mechanisms for container updates.
|
- `mc admin update` is not supported and should be avoided in kubernetes/container environments, please upgrade containers by upgrading relevant container images.
|
||||||
- **We do not recommend upgrading one MinIO server at a time, the product is designed to support parallel upgrades please follow our recommended guidelines.**
|
- **We do not recommend upgrading one MinIO server at a time, the product is designed to support parallel upgrades please follow our recommended guidelines.**
|
||||||
|
|
||||||
## Explore Further
|
## Explore Further
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
export GORACE="history_size=7"
|
||||||
## TODO remove `dsync` from race detector once this is merged and released https://go-review.googlesource.com/c/go/+/333529/
|
## TODO remove `dsync` from race detector once this is merged and released https://go-review.googlesource.com/c/go/+/333529/
|
||||||
for d in $(go list ./... | grep -v dsync); do
|
for d in $(go list ./... | grep -v dsync); do
|
||||||
CGO_ENABLED=1 go test -v -race --timeout 100m "$d"
|
CGO_ENABLED=1 go test -v -race --timeout 100m "$d"
|
||||||
|
|||||||
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
set -E
|
||||||
|
set -o pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||||
|
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
||||||
|
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server )
|
||||||
|
|
||||||
|
if [ ! -x "$PWD/minio" ]; then
|
||||||
|
echo "minio executable binary not found in current directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
function start_minio_5drive() {
|
||||||
|
start_port=$1
|
||||||
|
|
||||||
|
export MINIO_ROOT_USER=minio
|
||||||
|
export MINIO_ROOT_PASSWORD=minio123
|
||||||
|
export MC_HOST_minio="http://minio:minio123@127.0.0.1:${start_port}/"
|
||||||
|
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||||
|
export MINIO_CI_CD=1
|
||||||
|
|
||||||
|
MC_BUILD_DIR="mc-$RANDOM"
|
||||||
|
if ! git clone --quiet https://github.com/minio/mc "$MC_BUILD_DIR"; then
|
||||||
|
echo "failed to download https://github.com/minio/mc"
|
||||||
|
purge "${MC_BUILD_DIR}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
(cd "${MC_BUILD_DIR}" && go build -o "$WORK_DIR/mc")
|
||||||
|
|
||||||
|
# remove mc source.
|
||||||
|
purge "${MC_BUILD_DIR}"
|
||||||
|
|
||||||
|
"${WORK_DIR}/mc" cp --quiet -r "buildscripts/cicd-corpus/" "${WORK_DIR}/cicd-corpus/"
|
||||||
|
|
||||||
|
"${MINIO[@]}" --address ":$start_port" "${WORK_DIR}/cicd-corpus/disk{1...5}" > "${WORK_DIR}/server1.log" 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
disown $pid
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||||
|
echo "server1 log:"
|
||||||
|
cat "${WORK_DIR}/server1.log"
|
||||||
|
echo "FAILED"
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${WORK_DIR}/mc" stat minio/bucket/testobj
|
||||||
|
|
||||||
|
pkill minio
|
||||||
|
sleep 3
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
start_port=$(shuf -i 10000-65000 -n 1)
|
||||||
|
|
||||||
|
start_minio_5drive ${start_port}
|
||||||
|
}
|
||||||
|
|
||||||
|
function purge()
|
||||||
|
{
|
||||||
|
rm -rf "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
( main "$@" )
|
||||||
|
rv=$?
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit "$rv"
|
||||||
@@ -120,7 +120,7 @@ func (a adminAPIHandlers) GetBucketQuotaConfigHandler(w http.ResponseWriter, r *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket)
|
config, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -145,7 +145,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
|||||||
bucket := pathClean(vars["bucket"])
|
bucket := pathClean(vars["bucket"])
|
||||||
update := r.Form.Get("update") == "true"
|
update := r.Form.Get("update") == "true"
|
||||||
|
|
||||||
if !globalIsErasure {
|
if globalIsGateway {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -274,7 +274,8 @@ func (a adminAPIHandlers) ListRemoteTargetsHandler(w http.ResponseWriter, r *htt
|
|||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := pathClean(vars["bucket"])
|
bucket := pathClean(vars["bucket"])
|
||||||
arnType := vars["type"]
|
arnType := vars["type"]
|
||||||
if !globalIsErasure {
|
|
||||||
|
if globalIsGateway {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -314,7 +315,7 @@ func (a adminAPIHandlers) RemoveRemoteTargetHandler(w http.ResponseWriter, r *ht
|
|||||||
bucket := pathClean(vars["bucket"])
|
bucket := pathClean(vars["bucket"])
|
||||||
arn := vars["arn"]
|
arn := vars["arn"]
|
||||||
|
|
||||||
if !globalIsErasure {
|
if globalIsGateway {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
|||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "XMinioAdminTierBackendInUse",
|
Code: "XMinioAdminTierBackendInUse",
|
||||||
Description: err.Error(),
|
Description: err.Error(),
|
||||||
HTTPStatusCode: http.StatusConflict,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
}
|
}
|
||||||
case errors.Is(err, errTierBackendNotEmpty):
|
case errors.Is(err, errTierBackendNotEmpty):
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ import (
|
|||||||
"github.com/minio/minio/internal/config/etcd"
|
"github.com/minio/minio/internal/config/etcd"
|
||||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||||
"github.com/minio/minio/internal/config/identity/openid"
|
"github.com/minio/minio/internal/config/identity/openid"
|
||||||
"github.com/minio/minio/internal/config/policy/opa"
|
idplugin "github.com/minio/minio/internal/config/identity/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/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
iampolicy "github.com/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
@@ -97,13 +98,14 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applyDynamic(ctx context.Context, objectAPI ObjectLayer, cfg config.Config, subSys string,
|
func applyDynamic(ctx context.Context, objectAPI ObjectLayer, cfg config.Config, subSys string,
|
||||||
r *http.Request, w http.ResponseWriter) {
|
r *http.Request, w http.ResponseWriter,
|
||||||
|
) {
|
||||||
// Apply dynamic values.
|
// Apply dynamic values.
|
||||||
if err := applyDynamicConfigForSubSys(GlobalContext, objectAPI, cfg, subSys); err != nil {
|
if err := applyDynamicConfigForSubSys(GlobalContext, objectAPI, cfg, subSys); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
globalNotificationSys.SignalService(serviceReloadDynamic)
|
globalNotificationSys.SignalConfigReload(subSys)
|
||||||
// Tell the client that dynamic config was applied.
|
// Tell the client that dynamic config was applied.
|
||||||
w.Header().Set(madmin.ConfigAppliedHeader, madmin.ConfigAppliedTrue)
|
w.Header().Set(madmin.ConfigAppliedHeader, madmin.ConfigAppliedTrue)
|
||||||
}
|
}
|
||||||
@@ -435,14 +437,16 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
off = !cache.Enabled(kv)
|
off = !cache.Enabled(kv)
|
||||||
case config.StorageClassSubSys:
|
case config.StorageClassSubSys:
|
||||||
off = !storageclass.Enabled(kv)
|
off = !storageclass.Enabled(kv)
|
||||||
case config.PolicyOPASubSys:
|
case config.PolicyPluginSubSys:
|
||||||
off = !opa.Enabled(kv)
|
off = !polplugin.Enabled(kv)
|
||||||
case config.IdentityOpenIDSubSys:
|
case config.IdentityOpenIDSubSys:
|
||||||
off = !openid.Enabled(kv)
|
off = !openid.Enabled(kv)
|
||||||
case config.IdentityLDAPSubSys:
|
case config.IdentityLDAPSubSys:
|
||||||
off = !xldap.Enabled(kv)
|
off = !xldap.Enabled(kv)
|
||||||
case config.IdentityTLSSubSys:
|
case config.IdentityTLSSubSys:
|
||||||
off = !globalSTSTLSConfig.Enabled
|
off = !globalSTSTLSConfig.Enabled
|
||||||
|
case config.IdentityPluginSubSys:
|
||||||
|
off = !idplugin.Enabled(kv)
|
||||||
}
|
}
|
||||||
if off {
|
if off {
|
||||||
s.WriteString(config.KvComment)
|
s.WriteString(config.KvComment)
|
||||||
|
|||||||
@@ -58,6 +58,16 @@ func (a adminAPIHandlers) StartDecommission(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ep := globalEndpoints[idx].Endpoints[0]; !ep.IsLocal {
|
||||||
|
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||||
|
if proxyEp.Endpoint.Host == ep.Host {
|
||||||
|
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := pools.Decommission(r.Context(), idx); err != nil {
|
if err := pools.Decommission(r.Context(), idx); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -96,6 +106,16 @@ func (a adminAPIHandlers) CancelDecommission(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ep := globalEndpoints[idx].Endpoints[0]; !ep.IsLocal {
|
||||||
|
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||||
|
if proxyEp.Endpoint.Host == ep.Host {
|
||||||
|
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := pools.DecommissionCancel(ctx, idx); err != nil {
|
if err := pools.DecommissionCancel(ctx, idx); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -115,10 +115,12 @@ func (a adminAPIHandlers) SRPeerBucketOps(w http.ResponseWriter, r *http.Request
|
|||||||
case madmin.MakeWithVersioningBktOp:
|
case madmin.MakeWithVersioningBktOp:
|
||||||
_, isLockEnabled := r.Form["lockEnabled"]
|
_, isLockEnabled := r.Form["lockEnabled"]
|
||||||
_, isVersioningEnabled := r.Form["versioningEnabled"]
|
_, isVersioningEnabled := r.Form["versioningEnabled"]
|
||||||
|
_, isForceCreate := r.Form["forceCreate"]
|
||||||
opts := BucketOptions{
|
opts := BucketOptions{
|
||||||
Location: r.Form.Get("location"),
|
Location: r.Form.Get("location"),
|
||||||
LockEnabled: isLockEnabled,
|
LockEnabled: isLockEnabled,
|
||||||
VersioningEnabled: isVersioningEnabled,
|
VersioningEnabled: isVersioningEnabled,
|
||||||
|
ForceCreate: isForceCreate,
|
||||||
}
|
}
|
||||||
err = globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(ctx, bucket, opts)
|
err = globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(ctx, bucket, opts)
|
||||||
case madmin.ConfigureReplBktOp:
|
case madmin.ConfigureReplBktOp:
|
||||||
@@ -239,6 +241,8 @@ func (a adminAPIHandlers) SRPeerReplicateBucketItem(w http.ResponseWriter, r *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case madmin.SRBucketMetaTypeVersionConfig:
|
||||||
|
err = globalSiteReplicationSys.PeerBucketVersioningHandler(ctx, item.Bucket, item.Versioning)
|
||||||
case madmin.SRBucketMetaTypeTags:
|
case madmin.SRBucketMetaTypeTags:
|
||||||
err = globalSiteReplicationSys.PeerBucketTaggingHandler(ctx, item.Bucket, item.Tags)
|
err = globalSiteReplicationSys.PeerBucketTaggingHandler(ctx, item.Bucket, item.Tags)
|
||||||
case madmin.SRBucketMetaTypeObjectLockConfig:
|
case madmin.SRBucketMetaTypeObjectLockConfig:
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -41,11 +42,15 @@ func runAllIAMConcurrencyTests(suite *TestSuiteIAM, c *check) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIAMInternalIDPConcurrencyServerSuite(t *testing.T) {
|
func TestIAMInternalIDPConcurrencyServerSuite(t *testing.T) {
|
||||||
|
if runtime.GOOS == globalWindowsOSName {
|
||||||
|
t.Skip("windows is clunky")
|
||||||
|
}
|
||||||
|
|
||||||
baseTestCases := []TestSuiteCommon{
|
baseTestCases := []TestSuiteCommon{
|
||||||
// Init and run test on FS backend with signature v4.
|
// Init and run test on ErasureSD backend with signature v4.
|
||||||
{serverType: "FS", signer: signerV4},
|
{serverType: "ErasureSD", signer: signerV4},
|
||||||
// Init and run test on FS backend, with tls enabled.
|
// Init and run test on ErasureSD backend, with tls enabled.
|
||||||
{serverType: "FS", signer: signerV4, secure: true},
|
{serverType: "ErasureSD", signer: signerV4, secure: true},
|
||||||
// Init and run test on Erasure backend.
|
// Init and run test on Erasure backend.
|
||||||
{serverType: "Erasure", signer: signerV4},
|
{serverType: "Erasure", signer: signerV4},
|
||||||
// Init and run test on ErasureSet backend.
|
// Init and run test on ErasureSet backend.
|
||||||
@@ -73,7 +78,7 @@ func TestIAMInternalIDPConcurrencyServerSuite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *TestSuiteIAM) TestDeleteUserRace(c *check) {
|
func (s *TestSuiteIAM) TestDeleteUserRace(c *check) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
bucket := getRandomBucketName()
|
bucket := getRandomBucketName()
|
||||||
|
|||||||
+87
-40
@@ -90,7 +90,7 @@ func (a adminAPIHandlers) ListBucketUsers(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
password := cred.SecretKey
|
password := cred.SecretKey
|
||||||
|
|
||||||
allCredentials, err := globalIAMSys.ListBucketUsers(bucket)
|
allCredentials, err := globalIAMSys.ListBucketUsers(ctx, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -124,12 +124,24 @@ func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
password := cred.SecretKey
|
password := cred.SecretKey
|
||||||
|
|
||||||
allCredentials, err := globalIAMSys.ListUsers()
|
allCredentials, err := globalIAMSys.ListUsers(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add ldap users which have mapped policies if in LDAP mode
|
||||||
|
// FIXME(vadmeste): move this to policy info in the future
|
||||||
|
ldapUsers, err := globalIAMSys.ListLDAPUsers()
|
||||||
|
if err != nil && err != errIAMActionNotAllowed {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for k, v := range ldapUsers {
|
||||||
|
allCredentials[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the response
|
||||||
data, err := json.Marshal(allCredentials)
|
data, err := json.Marshal(allCredentials)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
@@ -229,6 +241,15 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ
|
|||||||
if updReq.IsRemove {
|
if updReq.IsRemove {
|
||||||
err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
|
err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
|
||||||
} else {
|
} else {
|
||||||
|
// Check if group already exists
|
||||||
|
if _, gerr := globalIAMSys.GetGroupDescription(updReq.Group); gerr != nil {
|
||||||
|
// If group does not exist, then check if the group has beginning and end space characters
|
||||||
|
// we will reject such group names.
|
||||||
|
if errors.Is(gerr, errNoSuchGroup) && hasSpaceBE(updReq.Group) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members)
|
err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -430,6 +451,12 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if accessKey has beginning and end space characters, this only applies to new users.
|
||||||
|
if !exists && hasSpaceBE(accessKey) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
checkDenyOnly := false
|
checkDenyOnly := false
|
||||||
if accessKey == cred.AccessKey {
|
if accessKey == cred.AccessKey {
|
||||||
// Check that there is no explicit deny - otherwise it's allowed
|
// Check that there is no explicit deny - otherwise it's allowed
|
||||||
@@ -521,6 +548,12 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// service account access key cannot have space characters beginning and end of the string.
|
||||||
|
if hasSpaceBE(createReq.AccessKey) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
targetUser string
|
targetUser string
|
||||||
targetGroups []string
|
targetGroups []string
|
||||||
@@ -605,7 +638,6 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
||||||
IsOwner: owner,
|
IsOwner: owner,
|
||||||
Claims: claims,
|
Claims: claims,
|
||||||
DenyOnly: true,
|
|
||||||
}) {
|
}) {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
return
|
return
|
||||||
@@ -645,29 +677,6 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call hook for cluster-replication if the service account is not for a
|
|
||||||
// root user.
|
|
||||||
if newCred.ParentUser != globalActiveCred.AccessKey {
|
|
||||||
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
|
||||||
Type: madmin.SRIAMItemSvcAcc,
|
|
||||||
SvcAccChange: &madmin.SRSvcAccChange{
|
|
||||||
Create: &madmin.SRSvcAccCreate{
|
|
||||||
Parent: newCred.ParentUser,
|
|
||||||
AccessKey: newCred.AccessKey,
|
|
||||||
SecretKey: newCred.SecretKey,
|
|
||||||
Groups: newCred.Groups,
|
|
||||||
Claims: opts.claims,
|
|
||||||
SessionPolicy: createReq.Policy,
|
|
||||||
Status: auth.AccountOn,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createResp := madmin.AddServiceAccountResp{
|
createResp := madmin.AddServiceAccountResp{
|
||||||
Credentials: madmin.Credentials{
|
Credentials: madmin.Credentials{
|
||||||
AccessKey: newCred.AccessKey,
|
AccessKey: newCred.AccessKey,
|
||||||
@@ -688,6 +697,29 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeSuccessResponseJSON(w, encryptedData)
|
writeSuccessResponseJSON(w, encryptedData)
|
||||||
|
|
||||||
|
// Call hook for cluster-replication if the service account is not for a
|
||||||
|
// root user.
|
||||||
|
if newCred.ParentUser != globalActiveCred.AccessKey {
|
||||||
|
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||||
|
Type: madmin.SRIAMItemSvcAcc,
|
||||||
|
SvcAccChange: &madmin.SRSvcAccChange{
|
||||||
|
Create: &madmin.SRSvcAccCreate{
|
||||||
|
Parent: newCred.ParentUser,
|
||||||
|
AccessKey: newCred.AccessKey,
|
||||||
|
SecretKey: newCred.SecretKey,
|
||||||
|
Groups: newCred.Groups,
|
||||||
|
Claims: opts.claims,
|
||||||
|
SessionPolicy: createReq.Policy,
|
||||||
|
Status: auth.AccountOn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateServiceAccount - POST /minio/admin/v3/update-service-account
|
// UpdateServiceAccount - POST /minio/admin/v3/update-service-account
|
||||||
@@ -982,11 +1014,9 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, serviceAccount)
|
// We do not care if service account is readable or not at this point,
|
||||||
if err != nil {
|
// since this is a delete call we shall allow it to be deleted if possible.
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
svcAccount, _, _ := globalIAMSys.GetServiceAccount(ctx, serviceAccount)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
adminPrivilege := globalIAMSys.IsAllowed(iampolicy.Args{
|
adminPrivilege := globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
@@ -1001,7 +1031,7 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
|
|||||||
if cred.ParentUser != "" {
|
if cred.ParentUser != "" {
|
||||||
parentUser = cred.ParentUser
|
parentUser = cred.ParentUser
|
||||||
}
|
}
|
||||||
if parentUser != svcAccount.ParentUser {
|
if svcAccount.ParentUser != "" && parentUser != svcAccount.ParentUser {
|
||||||
// The service account belongs to another user but return not
|
// The service account belongs to another user but return not
|
||||||
// found error to mitigate brute force attacks. or the
|
// found error to mitigate brute force attacks. or the
|
||||||
// serviceAccount doesn't exist.
|
// serviceAccount doesn't exist.
|
||||||
@@ -1010,23 +1040,21 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = globalIAMSys.DeleteServiceAccount(ctx, serviceAccount, true)
|
if err := globalIAMSys.DeleteServiceAccount(ctx, serviceAccount, true); err != nil {
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call site replication hook - non-root user accounts are replicated.
|
// Call site replication hook - non-root user accounts are replicated.
|
||||||
if svcAccount.ParentUser != globalActiveCred.AccessKey {
|
if svcAccount.ParentUser != "" && svcAccount.ParentUser != globalActiveCred.AccessKey {
|
||||||
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||||
Type: madmin.SRIAMItemSvcAcc,
|
Type: madmin.SRIAMItemSvcAcc,
|
||||||
SvcAccChange: &madmin.SRSvcAccChange{
|
SvcAccChange: &madmin.SRSvcAccChange{
|
||||||
Delete: &madmin.SRSvcAccDelete{
|
Delete: &madmin.SRSvcAccDelete{
|
||||||
AccessKey: serviceAccount,
|
AccessKey: serviceAccount,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1077,6 +1105,19 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
rd = true
|
rd = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
|
Action: iampolicy.GetBucketLocationAction,
|
||||||
|
BucketName: bucketName,
|
||||||
|
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
||||||
|
IsOwner: owner,
|
||||||
|
ObjectName: "",
|
||||||
|
Claims: claims,
|
||||||
|
}) {
|
||||||
|
rd = true
|
||||||
|
}
|
||||||
|
|
||||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
Groups: cred.Groups,
|
Groups: cred.Groups,
|
||||||
@@ -1171,8 +1212,8 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
lcfg, _ := globalBucketObjectLockSys.Get(bucket.Name)
|
lcfg, _ := globalBucketObjectLockSys.Get(bucket.Name)
|
||||||
quota, _ := globalBucketQuotaSys.Get(ctx, bucket.Name)
|
quota, _ := globalBucketQuotaSys.Get(ctx, bucket.Name)
|
||||||
rcfg, _ := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket.Name)
|
rcfg, _, _ := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket.Name)
|
||||||
tcfg, _ := globalBucketMetadataSys.GetTaggingConfig(bucket.Name)
|
tcfg, _, _ := globalBucketMetadataSys.GetTaggingConfig(bucket.Name)
|
||||||
|
|
||||||
acctInfo.Buckets = append(acctInfo.Buckets, madmin.BucketAccessInfo{
|
acctInfo.Buckets = append(acctInfo.Buckets, madmin.BucketAccessInfo{
|
||||||
Name: bucket.Name,
|
Name: bucket.Name,
|
||||||
@@ -1373,6 +1414,12 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
|||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
policyName := vars["name"]
|
policyName := vars["name"]
|
||||||
|
|
||||||
|
// Policy has space characters in begin and end reject such inputs.
|
||||||
|
if hasSpaceBE(policyName) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Error out if Content-Length is missing.
|
// Error out if Content-Length is missing.
|
||||||
if r.ContentLength <= 0 {
|
if r.ContentLength <= 0 {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -50,6 +51,8 @@ const (
|
|||||||
type TestSuiteIAM struct {
|
type TestSuiteIAM struct {
|
||||||
TestSuiteCommon
|
TestSuiteCommon
|
||||||
|
|
||||||
|
ServerTypeDescription string
|
||||||
|
|
||||||
// Flag to turn on tests for etcd backend IAM
|
// Flag to turn on tests for etcd backend IAM
|
||||||
withEtcdBackend bool
|
withEtcdBackend bool
|
||||||
|
|
||||||
@@ -59,7 +62,15 @@ type TestSuiteIAM struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newTestSuiteIAM(c TestSuiteCommon, withEtcdBackend bool) *TestSuiteIAM {
|
func newTestSuiteIAM(c TestSuiteCommon, withEtcdBackend bool) *TestSuiteIAM {
|
||||||
return &TestSuiteIAM{TestSuiteCommon: c, withEtcdBackend: withEtcdBackend}
|
etcdStr := ""
|
||||||
|
if withEtcdBackend {
|
||||||
|
etcdStr = " (with etcd backend)"
|
||||||
|
}
|
||||||
|
return &TestSuiteIAM{
|
||||||
|
TestSuiteCommon: c,
|
||||||
|
ServerTypeDescription: fmt.Sprintf("%s%s", c.serverType, etcdStr),
|
||||||
|
withEtcdBackend: withEtcdBackend,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TestSuiteIAM) iamSetup(c *check) {
|
func (s *TestSuiteIAM) iamSetup(c *check) {
|
||||||
@@ -87,6 +98,29 @@ func (s *TestSuiteIAM) iamSetup(c *check) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List of all IAM test suites (i.e. test server configuration combinations)
|
||||||
|
// common to tests.
|
||||||
|
var iamTestSuites = func() []*TestSuiteIAM {
|
||||||
|
baseTestCases := []TestSuiteCommon{
|
||||||
|
// Init and run test on ErasureSD backend with signature v4.
|
||||||
|
{serverType: "ErasureSD", signer: signerV4},
|
||||||
|
// Init and run test on ErasureSD backend, with tls enabled.
|
||||||
|
{serverType: "ErasureSD", signer: signerV4, secure: true},
|
||||||
|
// Init and run test on Erasure backend.
|
||||||
|
{serverType: "Erasure", signer: signerV4},
|
||||||
|
// Init and run test on ErasureSet backend.
|
||||||
|
{serverType: "ErasureSet", signer: signerV4},
|
||||||
|
}
|
||||||
|
testCases := []*TestSuiteIAM{}
|
||||||
|
for _, bt := range baseTestCases {
|
||||||
|
testCases = append(testCases,
|
||||||
|
newTestSuiteIAM(bt, false),
|
||||||
|
newTestSuiteIAM(bt, true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return testCases
|
||||||
|
}()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EnvTestEtcdBackend = "ETCD_SERVER"
|
EnvTestEtcdBackend = "ETCD_SERVER"
|
||||||
)
|
)
|
||||||
@@ -156,30 +190,12 @@ func (s *TestSuiteIAM) getUserClient(c *check, accessKey, secretKey, sessionToke
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIAMInternalIDPServerSuite(t *testing.T) {
|
func TestIAMInternalIDPServerSuite(t *testing.T) {
|
||||||
baseTestCases := []TestSuiteCommon{
|
if runtime.GOOS == globalWindowsOSName {
|
||||||
// Init and run test on FS backend with signature v4.
|
t.Skip("windows is clunky disable these tests")
|
||||||
{serverType: "FS", signer: signerV4},
|
|
||||||
// Init and run test on FS backend, with tls enabled.
|
|
||||||
{serverType: "FS", signer: signerV4, secure: true},
|
|
||||||
// Init and run test on Erasure backend.
|
|
||||||
{serverType: "Erasure", signer: signerV4},
|
|
||||||
// Init and run test on ErasureSet backend.
|
|
||||||
{serverType: "ErasureSet", signer: signerV4},
|
|
||||||
}
|
|
||||||
testCases := []*TestSuiteIAM{}
|
|
||||||
for _, bt := range baseTestCases {
|
|
||||||
testCases = append(testCases,
|
|
||||||
newTestSuiteIAM(bt, false),
|
|
||||||
newTestSuiteIAM(bt, true),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
etcdStr := ""
|
|
||||||
if testCase.withEtcdBackend {
|
|
||||||
etcdStr = " (with etcd backend)"
|
|
||||||
}
|
}
|
||||||
|
for i, testCase := range iamTestSuites {
|
||||||
t.Run(
|
t.Run(
|
||||||
fmt.Sprintf("Test: %d, ServerType: %s%s", i+1, testCase.serverType, etcdStr),
|
fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription),
|
||||||
func(t *testing.T) {
|
func(t *testing.T) {
|
||||||
suite := testCase
|
suite := testCase
|
||||||
c := &check{t, testCase.serverType}
|
c := &check{t, testCase.serverType}
|
||||||
@@ -226,6 +242,7 @@ func (s *TestSuiteIAM) TestUserCreate(c *check) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatalf("unable to set policy: %v", err)
|
c.Fatalf("unable to set policy: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := s.getUserClient(c, accessKey, secretKey, "")
|
client := s.getUserClient(c, accessKey, secretKey, "")
|
||||||
err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
|
err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -890,6 +907,9 @@ func (s *TestSuiteIAM) TestServiceAccountOpsByUser(c *check) {
|
|||||||
|
|
||||||
// 5. Check that service account can be deleted.
|
// 5. Check that service account can be deleted.
|
||||||
c.assertSvcAccDeletion(ctx, s, userAdmClient, accessKey, bucket)
|
c.assertSvcAccDeletion(ctx, s, userAdmClient, accessKey, bucket)
|
||||||
|
|
||||||
|
// 6. Check that service account cannot be created for some other user.
|
||||||
|
c.mustNotCreateSvcAccount(ctx, globalActiveCred.AccessKey, userAdmClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TestSuiteIAM) TestServiceAccountOpsByAdmin(c *check) {
|
func (s *TestSuiteIAM) TestServiceAccountOpsByAdmin(c *check) {
|
||||||
@@ -960,6 +980,166 @@ func (s *TestSuiteIAM) TestServiceAccountOpsByAdmin(c *check) {
|
|||||||
c.assertSvcAccDeletion(ctx, s, s.adm, accessKey, bucket)
|
c.assertSvcAccDeletion(ctx, s, s.adm, accessKey, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TestSuiteIAM) SetUpAccMgmtPlugin(c *check) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
pluginEndpoint := os.Getenv("POLICY_PLUGIN_ENDPOINT")
|
||||||
|
if pluginEndpoint == "" {
|
||||||
|
c.Skip("POLICY_PLUGIN_ENDPOINT not given - skipping.")
|
||||||
|
}
|
||||||
|
|
||||||
|
configCmds := []string{
|
||||||
|
"policy_plugin",
|
||||||
|
"url=" + pluginEndpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.adm.SetConfigKV(ctx, strings.Join(configCmds, " "))
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("unable to setup access management plugin for tests: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.RestartIAMSuite(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIAM_AMPInternalIDPServerSuite - tests for access management plugin
|
||||||
|
func TestIAM_AMPInternalIDPServerSuite(t *testing.T) {
|
||||||
|
for i, testCase := range iamTestSuites {
|
||||||
|
t.Run(
|
||||||
|
fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription),
|
||||||
|
func(t *testing.T) {
|
||||||
|
suite := testCase
|
||||||
|
c := &check{t, testCase.serverType}
|
||||||
|
|
||||||
|
suite.SetUpSuite(c)
|
||||||
|
defer suite.TearDownSuite(c)
|
||||||
|
|
||||||
|
suite.SetUpAccMgmtPlugin(c)
|
||||||
|
|
||||||
|
suite.TestAccMgmtPlugin(c)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAccMgmtPlugin - this test assumes that the access-management-plugin is
|
||||||
|
// the same as the example in `docs/iam/access-manager-plugin.go` -
|
||||||
|
// specifically, it denies only `s3:Put*` operations on non-root accounts.
|
||||||
|
func (s *TestSuiteIAM) TestAccMgmtPlugin(c *check) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 0. Check that owner is able to make-bucket.
|
||||||
|
bucket := getRandomBucketName()
|
||||||
|
err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("bucket creat error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Create a user.
|
||||||
|
accessKey, secretKey := mustGenerateCredentials(c)
|
||||||
|
err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("Unable to set user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check new user appears in listing
|
||||||
|
usersMap, err := s.adm.ListUsers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("error listing: %v", err)
|
||||||
|
}
|
||||||
|
v, ok := usersMap[accessKey]
|
||||||
|
if !ok {
|
||||||
|
c.Fatalf("user not listed: %s", accessKey)
|
||||||
|
}
|
||||||
|
c.Assert(v.Status, madmin.AccountEnabled)
|
||||||
|
|
||||||
|
// 3. Check that user is able to make a bucket.
|
||||||
|
client := s.getUserClient(c, accessKey, secretKey, "")
|
||||||
|
err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("user not create bucket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.1 check user has access to bucket
|
||||||
|
c.mustListObjects(ctx, client, bucket)
|
||||||
|
|
||||||
|
// 3.2 check that user cannot upload an object.
|
||||||
|
_, err = client.PutObject(ctx, bucket, "objectName", bytes.NewBuffer([]byte("some content")), 12, minio.PutObjectOptions{})
|
||||||
|
if err == nil {
|
||||||
|
c.Fatalf("user was able to upload unexpectedly")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an madmin client with user creds
|
||||||
|
userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
|
||||||
|
Creds: cr.NewStaticV4(accessKey, secretKey, ""),
|
||||||
|
Secure: s.secure,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("Err creating user admin client: %v", err)
|
||||||
|
}
|
||||||
|
userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport)
|
||||||
|
|
||||||
|
// Create svc acc
|
||||||
|
cr := c.mustCreateSvcAccount(ctx, accessKey, userAdmClient)
|
||||||
|
|
||||||
|
// 1. Check that svc account appears in listing
|
||||||
|
c.assertSvcAccAppearsInListing(ctx, userAdmClient, accessKey, cr.AccessKey)
|
||||||
|
|
||||||
|
// 2. Check that svc account info can be queried
|
||||||
|
c.assertSvcAccInfoQueryable(ctx, userAdmClient, accessKey, cr.AccessKey, false)
|
||||||
|
|
||||||
|
// 3. Check S3 access
|
||||||
|
c.assertSvcAccS3Access(ctx, s, cr, bucket)
|
||||||
|
|
||||||
|
// Check that session policies do not apply - as policy enforcement is
|
||||||
|
// delegated to plugin.
|
||||||
|
{
|
||||||
|
svcAK, svcSK := mustGenerateCredentials(c)
|
||||||
|
|
||||||
|
// This policy does not allow listing objects.
|
||||||
|
policyBytes := []byte(fmt.Sprintf(`{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"s3:PutObject",
|
||||||
|
"s3:GetObject"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::%s/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, bucket))
|
||||||
|
cr, err := userAdmClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
|
||||||
|
Policy: policyBytes,
|
||||||
|
TargetUser: accessKey,
|
||||||
|
AccessKey: svcAK,
|
||||||
|
SecretKey: svcSK,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("Unable to create svc acc: %v", err)
|
||||||
|
}
|
||||||
|
svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
|
||||||
|
// Though the attached policy does not allow listing, it will be
|
||||||
|
// ignored because the plugin allows it.
|
||||||
|
c.mustListObjects(ctx, svcClient, bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check that service account's secret key and account status can be
|
||||||
|
// updated.
|
||||||
|
c.assertSvcAccSecretKeyAndStatusUpdate(ctx, s, userAdmClient, accessKey, bucket)
|
||||||
|
|
||||||
|
// 5. Check that service account can be deleted.
|
||||||
|
c.assertSvcAccDeletion(ctx, s, userAdmClient, accessKey, bucket)
|
||||||
|
|
||||||
|
// 6. Check that service account **can** be created for some other user.
|
||||||
|
// This is possible because of the policy enforced in the plugin.
|
||||||
|
c.mustCreateSvcAccount(ctx, globalActiveCred.AccessKey, userAdmClient)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *check) mustCreateIAMUser(ctx context.Context, admClnt *madmin.AdminClient) madmin.Credentials {
|
func (c *check) mustCreateIAMUser(ctx context.Context, admClnt *madmin.AdminClient) madmin.Credentials {
|
||||||
randUser := mustGetUUID()
|
randUser := mustGetUUID()
|
||||||
randPass := mustGetUUID()
|
randPass := mustGetUUID()
|
||||||
@@ -1013,7 +1193,7 @@ func (c *check) mustNotListObjects(ctx context.Context, client *minio.Client, bu
|
|||||||
res := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
|
res := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
|
||||||
v, ok := <-res
|
v, ok := <-res
|
||||||
if !ok || v.Err == nil {
|
if !ok || v.Err == nil {
|
||||||
c.Fatalf("user was able to list unexpectedly!")
|
c.Fatalf("user was able to list unexpectedly! on %s", bucket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1021,11 +1201,20 @@ func (c *check) mustListObjects(ctx context.Context, client *minio.Client, bucke
|
|||||||
res := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
|
res := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
|
||||||
v, ok := <-res
|
v, ok := <-res
|
||||||
if ok && v.Err != nil {
|
if ok && v.Err != nil {
|
||||||
msg := fmt.Sprintf("user was unable to list: %v", v.Err)
|
c.Fatalf("user was unable to list: %v", v.Err)
|
||||||
c.Fatalf(msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *check) mustNotUpload(ctx context.Context, client *minio.Client, bucket string) {
|
||||||
|
_, err := client.PutObject(ctx, bucket, "some-object", bytes.NewBuffer([]byte("stuff")), 5, minio.PutObjectOptions{})
|
||||||
|
if e, ok := err.(minio.ErrorResponse); ok {
|
||||||
|
if e.Code == "AccessDenied" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Fatalf("upload did not get an AccessDenied error - got %#v instead", err)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *check) assertSvcAccS3Access(ctx context.Context, s *TestSuiteIAM, cr madmin.Credentials, bucket string) {
|
func (c *check) assertSvcAccS3Access(ctx context.Context, s *TestSuiteIAM, cr madmin.Credentials, bucket string) {
|
||||||
svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
|
svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
|
||||||
c.mustListObjects(ctx, svcClient, bucket)
|
c.mustListObjects(ctx, svcClient, bucket)
|
||||||
|
|||||||
+405
-106
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||||
//
|
//
|
||||||
// This file is part of MinIO Object Storage stack
|
// This file is part of MinIO Object Storage stack
|
||||||
//
|
//
|
||||||
@@ -18,14 +18,16 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -56,6 +58,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
maxEConfigJSONSize = 262272
|
maxEConfigJSONSize = 262272
|
||||||
|
kubernetesVersionEndpoint = "https://kubernetes.default.svc/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Only valid query params for mgmt admin APIs.
|
// Only valid query params for mgmt admin APIs.
|
||||||
@@ -288,6 +291,8 @@ type ServerHTTPStats struct {
|
|||||||
CurrentS3Requests ServerHTTPAPIStats `json:"currentS3Requests"`
|
CurrentS3Requests ServerHTTPAPIStats `json:"currentS3Requests"`
|
||||||
TotalS3Requests ServerHTTPAPIStats `json:"totalS3Requests"`
|
TotalS3Requests ServerHTTPAPIStats `json:"totalS3Requests"`
|
||||||
TotalS3Errors ServerHTTPAPIStats `json:"totalS3Errors"`
|
TotalS3Errors ServerHTTPAPIStats `json:"totalS3Errors"`
|
||||||
|
TotalS35xxErrors ServerHTTPAPIStats `json:"totalS35xxErrors"`
|
||||||
|
TotalS34xxErrors ServerHTTPAPIStats `json:"totalS34xxErrors"`
|
||||||
TotalS3Canceled ServerHTTPAPIStats `json:"totalS3Canceled"`
|
TotalS3Canceled ServerHTTPAPIStats `json:"totalS3Canceled"`
|
||||||
TotalS3RejectedAuth uint64 `json:"totalS3RejectedAuth"`
|
TotalS3RejectedAuth uint64 `json:"totalS3RejectedAuth"`
|
||||||
TotalS3RejectedTime uint64 `json:"totalS3RejectedTime"`
|
TotalS3RejectedTime uint64 `json:"totalS3RejectedTime"`
|
||||||
@@ -499,7 +504,7 @@ func (a adminAPIHandlers) TopLocksHandler(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StartProfilingResult contains the status of the starting
|
// StartProfilingResult contains the status of the starting
|
||||||
// profiling action in a given server
|
// profiling action in a given server - deprecated API
|
||||||
type StartProfilingResult struct {
|
type StartProfilingResult struct {
|
||||||
NodeName string `json:"nodeName"`
|
NodeName string `json:"nodeName"`
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
@@ -593,6 +598,83 @@ func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.R
|
|||||||
writeSuccessResponseJSON(w, startProfilingResultInBytes)
|
writeSuccessResponseJSON(w, startProfilingResultInBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProfileHandler - POST /minio/admin/v3/profile/?profilerType={profilerType}
|
||||||
|
// ----------
|
||||||
|
// Enable server profiling
|
||||||
|
func (a adminAPIHandlers) ProfileHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "Profile")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
// Validate request signature.
|
||||||
|
_, adminAPIErr := checkAdminRequestAuth(ctx, r, iampolicy.ProfilingAdminAction, "")
|
||||||
|
if adminAPIErr != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalNotificationSys == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
profileStr := r.Form.Get("profilerType")
|
||||||
|
profiles := strings.Split(profileStr, ",")
|
||||||
|
duration := time.Minute
|
||||||
|
if dstr := r.Form.Get("duration"); dstr != "" {
|
||||||
|
var err error
|
||||||
|
duration, err = time.ParseDuration(dstr)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// read request body
|
||||||
|
io.CopyN(ioutil.Discard, r.Body, 1)
|
||||||
|
|
||||||
|
globalProfilerMu.Lock()
|
||||||
|
|
||||||
|
if globalProfiler == nil {
|
||||||
|
globalProfiler = make(map[string]minioProfiler, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop profiler of all types if already running
|
||||||
|
for k, v := range globalProfiler {
|
||||||
|
v.Stop()
|
||||||
|
delete(globalProfiler, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start profiling on remote servers.
|
||||||
|
for _, profiler := range profiles {
|
||||||
|
globalNotificationSys.StartProfiling(profiler)
|
||||||
|
|
||||||
|
// Start profiling locally as well.
|
||||||
|
prof, err := startProfiler(profiler)
|
||||||
|
if err == nil {
|
||||||
|
globalProfiler[profiler] = prof
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalProfilerMu.Unlock()
|
||||||
|
|
||||||
|
timer := time.NewTimer(duration)
|
||||||
|
defer timer.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
for k, v := range globalProfiler {
|
||||||
|
v.Stop()
|
||||||
|
delete(globalProfiler, k)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
if !globalNotificationSys.DownloadProfilingData(ctx, w) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminProfilerNotEnabled), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// dummyFileInfo represents a dummy representation of a profile data file
|
// dummyFileInfo represents a dummy representation of a profile data file
|
||||||
// present only in memory, it helps to generate the zip stream.
|
// present only in memory, it helps to generate the zip stream.
|
||||||
type dummyFileInfo struct {
|
type dummyFileInfo struct {
|
||||||
@@ -613,7 +695,7 @@ func (f dummyFileInfo) Sys() interface{} { return f.sys }
|
|||||||
|
|
||||||
// DownloadProfilingHandler - POST /minio/admin/v3/profiling/download
|
// DownloadProfilingHandler - POST /minio/admin/v3/profiling/download
|
||||||
// ----------
|
// ----------
|
||||||
// Download profiling information of all nodes in a zip format
|
// Download profiling information of all nodes in a zip format - deprecated API
|
||||||
func (a adminAPIHandlers) DownloadProfilingHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) DownloadProfilingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "DownloadProfiling")
|
ctx := newContext(r, w, "DownloadProfiling")
|
||||||
|
|
||||||
@@ -725,8 +807,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this setup has an erasure coded backend.
|
if globalIsGateway {
|
||||||
if !globalIsErasure {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrHealNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrHealNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -918,7 +999,7 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this setup has an erasure coded backend.
|
// Check if this setup has an erasure coded backend.
|
||||||
if !globalIsErasure {
|
if globalIsGateway {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrHealNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrHealNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -935,6 +1016,50 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NetperfHandler - perform mesh style network throughput test
|
||||||
|
func (a adminAPIHandlers) NetperfHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "NetperfHandler")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !globalIsDistErasure {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsLock := objectAPI.NewNSLock(minioMetaBucket, "netperf")
|
||||||
|
lkctx, err := nsLock.GetLock(ctx, globalOperationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(toAPIErrorCode(ctx, err)), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer nsLock.Unlock(lkctx.Cancel)
|
||||||
|
|
||||||
|
durationStr := r.Form.Get(peerRESTDuration)
|
||||||
|
duration, err := time.ParseDuration(durationStr)
|
||||||
|
if err != nil {
|
||||||
|
duration = globalNetPerfMinDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
if duration < globalNetPerfMinDuration {
|
||||||
|
// We need sample size of minimum 10 secs.
|
||||||
|
duration = globalNetPerfMinDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
duration = duration.Round(time.Second)
|
||||||
|
|
||||||
|
results := globalNotificationSys.Netperf(ctx, duration)
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
if err := enc.Encode(madmin.NetperfResult{NodeResults: results}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SpeedtestHandler - Deprecated. See ObjectSpeedtestHandler
|
// SpeedtestHandler - Deprecated. See ObjectSpeedtestHandler
|
||||||
func (a adminAPIHandlers) SpeedtestHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) SpeedtestHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
a.ObjectSpeedtestHandler(w, r)
|
a.ObjectSpeedtestHandler(w, r)
|
||||||
@@ -954,7 +1079,7 @@ func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !globalIsErasure {
|
if globalIsGateway {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -984,38 +1109,30 @@ func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.
|
|||||||
duration = time.Second * 10
|
duration = time.Second * 10
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignores any errors here.
|
sufficientCapacity, canAutotune, capacityErrMsg := validateObjPerfOptions(ctx, objectAPI, concurrent, size, autotune)
|
||||||
storageInfo, _ := objectAPI.StorageInfo(ctx)
|
|
||||||
capacityNeeded := uint64(concurrent * size)
|
|
||||||
capacity := uint64(GetTotalUsableCapacityFree(storageInfo.Disks, storageInfo))
|
|
||||||
|
|
||||||
if capacity < capacityNeeded {
|
if !sufficientCapacity {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, AdminError{
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, AdminError{
|
||||||
Code: "XMinioSpeedtestInsufficientCapacity",
|
Code: "XMinioSpeedtestInsufficientCapacity",
|
||||||
Message: fmt.Sprintf("not enough usable space available to perform speedtest - expected %s, got %s",
|
Message: capacityErrMsg,
|
||||||
humanize.IBytes(capacityNeeded), humanize.IBytes(capacity)),
|
|
||||||
StatusCode: http.StatusInsufficientStorage,
|
StatusCode: http.StatusInsufficientStorage,
|
||||||
}), r.URL)
|
}), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify if we can employ autotune without running out of capacity,
|
if autotune && !canAutotune {
|
||||||
// if we do run out of capacity, make sure to turn-off autotuning
|
|
||||||
// in such situations.
|
|
||||||
newConcurrent := concurrent + (concurrent+1)/2
|
|
||||||
autoTunedCapacityNeeded := uint64(newConcurrent * size)
|
|
||||||
if autotune && capacity < autoTunedCapacityNeeded {
|
|
||||||
// Turn-off auto-tuning if next possible concurrency would reach beyond disk capacity.
|
|
||||||
autotune = false
|
autotune = false
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteBucket := func() {
|
bucketExists, err := makeObjectPerfBucket(ctx, objectAPI)
|
||||||
objectAPI.DeleteBucket(context.Background(), pathJoin(minioMetaBucket, "speedtest"), DeleteBucketOptions{
|
if err != nil {
|
||||||
Force: true,
|
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
NoRecreate: true,
|
return
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if !bucketExists {
|
||||||
|
defer deleteObjectPerfBucket(objectAPI)
|
||||||
}
|
}
|
||||||
defer deleteBucket()
|
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -1027,7 +1144,7 @@ func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.
|
|||||||
defer keepAliveTicker.Stop()
|
defer keepAliveTicker.Stop()
|
||||||
|
|
||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
ch := speedTest(ctx, speedTestOpts{size, concurrent, duration, autotune, storageClass})
|
ch := objectSpeedTest(ctx, speedTestOpts{size, concurrent, duration, autotune, storageClass})
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -1050,6 +1167,50 @@ func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeObjectPerfBucket(ctx context.Context, objectAPI ObjectLayer) (bucketExists bool, err error) {
|
||||||
|
err = objectAPI.MakeBucketWithLocation(ctx, globalObjectPerfBucket, BucketOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(BucketExists); !ok {
|
||||||
|
// Only BucketExists error can be ignored.
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
bucketExists = true
|
||||||
|
}
|
||||||
|
return bucketExists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteObjectPerfBucket(objectAPI ObjectLayer) {
|
||||||
|
objectAPI.DeleteBucket(context.Background(), globalObjectPerfBucket, DeleteBucketOptions{
|
||||||
|
Force: true,
|
||||||
|
NoRecreate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateObjPerfOptions(ctx context.Context, objectAPI ObjectLayer, concurrent int, size int, autotune bool) (sufficientCapacity bool, canAutotune bool, capacityErrMsg string) {
|
||||||
|
storageInfo, _ := objectAPI.StorageInfo(ctx)
|
||||||
|
capacityNeeded := uint64(concurrent * size)
|
||||||
|
capacity := uint64(GetTotalUsableCapacityFree(storageInfo.Disks, storageInfo))
|
||||||
|
|
||||||
|
if capacity < capacityNeeded {
|
||||||
|
return false, false, fmt.Sprintf("not enough usable space available to perform speedtest - expected %s, got %s",
|
||||||
|
humanize.IBytes(capacityNeeded), humanize.IBytes(capacity))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify if we can employ autotune without running out of capacity,
|
||||||
|
// if we do run out of capacity, make sure to turn-off autotuning
|
||||||
|
// in such situations.
|
||||||
|
if autotune {
|
||||||
|
newConcurrent := concurrent + (concurrent+1)/2
|
||||||
|
autoTunedCapacityNeeded := uint64(newConcurrent * size)
|
||||||
|
if capacity < autoTunedCapacityNeeded {
|
||||||
|
// Turn-off auto-tuning if next possible concurrency would reach beyond disk capacity.
|
||||||
|
return true, false, ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, autotune, ""
|
||||||
|
}
|
||||||
|
|
||||||
// NetSpeedtestHandler - reports maximum network throughput
|
// NetSpeedtestHandler - reports maximum network throughput
|
||||||
func (a adminAPIHandlers) NetSpeedtestHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) NetSpeedtestHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "NetSpeedtestHandler")
|
ctx := newContext(r, w, "NetSpeedtestHandler")
|
||||||
@@ -1068,7 +1229,7 @@ func (a adminAPIHandlers) DriveSpeedtestHandler(w http.ResponseWriter, r *http.R
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !globalIsErasure {
|
if globalIsGateway {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1250,9 +1411,15 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
peers, _ := newPeerRestClients(globalEndpoints)
|
peers, _ := newPeerRestClients(globalEndpoints)
|
||||||
|
|
||||||
globalTrace.Subscribe(traceCh, ctx.Done(), func(entry interface{}) bool {
|
traceFn := func(entry interface{}) bool {
|
||||||
return mustTrace(entry, traceOpts)
|
return mustTrace(entry, traceOpts)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
err = globalTrace.Subscribe(traceCh, ctx.Done(), traceFn)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrSlowDown), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
if peer == nil {
|
if peer == nil {
|
||||||
@@ -1324,7 +1491,11 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
peers, _ := newPeerRestClients(globalEndpoints)
|
peers, _ := newPeerRestClients(globalEndpoints)
|
||||||
|
|
||||||
globalConsoleSys.Subscribe(logCh, ctx.Done(), node, limitLines, logKind, nil)
|
err = globalConsoleSys.Subscribe(logCh, ctx.Done(), node, limitLines, logKind, nil)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrSlowDown), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
if peer == nil {
|
if peer == nil {
|
||||||
@@ -1538,6 +1709,7 @@ func getServerInfo(ctx context.Context, r *http.Request) madmin.InfoMessage {
|
|||||||
|
|
||||||
buckets := madmin.Buckets{}
|
buckets := madmin.Buckets{}
|
||||||
objects := madmin.Objects{}
|
objects := madmin.Objects{}
|
||||||
|
versions := madmin.Versions{}
|
||||||
usage := madmin.Usage{}
|
usage := madmin.Usage{}
|
||||||
|
|
||||||
objectAPI := newObjectLayerFn()
|
objectAPI := newObjectLayerFn()
|
||||||
@@ -1549,6 +1721,7 @@ func getServerInfo(ctx context.Context, r *http.Request) madmin.InfoMessage {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
buckets = madmin.Buckets{Count: dataUsageInfo.BucketsCount}
|
buckets = madmin.Buckets{Count: dataUsageInfo.BucketsCount}
|
||||||
objects = madmin.Objects{Count: dataUsageInfo.ObjectsTotalCount}
|
objects = madmin.Objects{Count: dataUsageInfo.ObjectsTotalCount}
|
||||||
|
versions = madmin.Versions{Count: dataUsageInfo.VersionsTotalCount}
|
||||||
usage = madmin.Usage{Size: dataUsageInfo.ObjectsTotalSize}
|
usage = madmin.Usage{Size: dataUsageInfo.ObjectsTotalSize}
|
||||||
} else {
|
} else {
|
||||||
buckets = madmin.Buckets{Error: err.Error()}
|
buckets = madmin.Buckets{Error: err.Error()}
|
||||||
@@ -1597,6 +1770,7 @@ func getServerInfo(ctx context.Context, r *http.Request) madmin.InfoMessage {
|
|||||||
DeploymentID: globalDeploymentID,
|
DeploymentID: globalDeploymentID,
|
||||||
Buckets: buckets,
|
Buckets: buckets,
|
||||||
Objects: objects,
|
Objects: objects,
|
||||||
|
Versions: versions,
|
||||||
Usage: usage,
|
Usage: usage,
|
||||||
Services: services,
|
Services: services,
|
||||||
Backend: backend,
|
Backend: backend,
|
||||||
@@ -1604,6 +1778,36 @@ func getServerInfo(ctx context.Context, r *http.Request) madmin.InfoMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getKubernetesInfo(dctx context.Context) madmin.KubernetesInfo {
|
||||||
|
ctx, cancel := context.WithCancel(dctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ki := madmin.KubernetesInfo{}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, kubernetesVersionEndpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
ki.Error = err.Error()
|
||||||
|
return ki
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: NewGatewayHTTPTransport(),
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
ki.Error = err.Error()
|
||||||
|
return ki
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
if err := decoder.Decode(&ki); err != nil {
|
||||||
|
ki.Error = err.Error()
|
||||||
|
}
|
||||||
|
return ki
|
||||||
|
}
|
||||||
|
|
||||||
// HealthInfoHandler - GET /minio/admin/v3/healthinfo
|
// HealthInfoHandler - GET /minio/admin/v3/healthinfo
|
||||||
// ----------
|
// ----------
|
||||||
// Get server health info
|
// Get server health info
|
||||||
@@ -1618,13 +1822,17 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := r.Form
|
query := r.Form
|
||||||
healthInfo := madmin.HealthInfo{Version: madmin.HealthInfoVersion}
|
healthInfo := madmin.HealthInfo{
|
||||||
|
Version: madmin.HealthInfoVersion,
|
||||||
|
Minio: madmin.MinioHealthInfo{
|
||||||
|
Info: madmin.MinioInfo{
|
||||||
|
DeploymentID: globalDeploymentID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
healthInfoCh := make(chan madmin.HealthInfo)
|
healthInfoCh := make(chan madmin.HealthInfo)
|
||||||
|
|
||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
partialWrite := func(oinfo madmin.HealthInfo) {
|
|
||||||
healthInfoCh <- oinfo
|
|
||||||
}
|
|
||||||
|
|
||||||
setCommonHeaders(w)
|
setCommonHeaders(w)
|
||||||
|
|
||||||
@@ -1640,7 +1848,12 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
logger.LogIf(ctx, enc.Encode(healthInfo))
|
logger.LogIf(ctx, enc.Encode(healthInfo))
|
||||||
}
|
}
|
||||||
|
|
||||||
deadline := 1 * time.Hour
|
deadline := 10 * time.Second // Default deadline is 10secs for health diagnostics.
|
||||||
|
if query.Get(string(madmin.HealthDataTypePerfNet)) != "" ||
|
||||||
|
query.Get(string(madmin.HealthDataTypePerfDrive)) != "" ||
|
||||||
|
query.Get(string(madmin.HealthDataTypePerfObj)) != "" {
|
||||||
|
deadline = 1 * time.Hour
|
||||||
|
}
|
||||||
if dstr := r.Form.Get("deadline"); dstr != "" {
|
if dstr := r.Form.Get("deadline"); dstr != "" {
|
||||||
var err error
|
var err error
|
||||||
deadline, err = time.ParseDuration(dstr)
|
deadline, err = time.ParseDuration(dstr)
|
||||||
@@ -1650,9 +1863,6 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deadlinedCtx, deadlineCancel := context.WithTimeout(ctx, deadline)
|
|
||||||
defer deadlineCancel()
|
|
||||||
|
|
||||||
nsLock := objectAPI.NewNSLock(minioMetaBucket, "health-check-in-progress")
|
nsLock := objectAPI.NewNSLock(minioMetaBucket, "health-check-in-progress")
|
||||||
lkctx, err := nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline))
|
lkctx, err := nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline))
|
||||||
if err != nil { // returns a locked lock
|
if err != nil { // returns a locked lock
|
||||||
@@ -1661,6 +1871,9 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
defer nsLock.Unlock(lkctx.Cancel)
|
defer nsLock.Unlock(lkctx.Cancel)
|
||||||
|
|
||||||
|
healthCtx, healthCancel := context.WithTimeout(lkctx.Context(), deadline)
|
||||||
|
defer healthCancel()
|
||||||
|
|
||||||
hostAnonymizer := createHostAnonymizer()
|
hostAnonymizer := createHostAnonymizer()
|
||||||
// anonAddr - Anonymizes hosts in given input string.
|
// anonAddr - Anonymizes hosts in given input string.
|
||||||
anonAddr := func(addr string) string {
|
anonAddr := func(addr string) string {
|
||||||
@@ -1679,13 +1892,27 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
info.SetAddr(anonAddr(info.GetAddr()))
|
info.SetAddr(anonAddr(info.GetAddr()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partialWrite := func(oinfo madmin.HealthInfo) {
|
||||||
|
select {
|
||||||
|
case healthInfoCh <- oinfo:
|
||||||
|
case <-healthCtx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAndWritePlatformInfo := func() {
|
||||||
|
if IsKubernetes() {
|
||||||
|
healthInfo.Sys.KubernetesInfo = getKubernetesInfo(healthCtx)
|
||||||
|
partialWrite(healthInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getAndWriteCPUs := func() {
|
getAndWriteCPUs := func() {
|
||||||
if query.Get("syscpu") == "true" {
|
if query.Get("syscpu") == "true" {
|
||||||
localCPUInfo := madmin.GetCPUs(deadlinedCtx, globalLocalNodeName)
|
localCPUInfo := madmin.GetCPUs(healthCtx, globalLocalNodeName)
|
||||||
anonymizeAddr(&localCPUInfo)
|
anonymizeAddr(&localCPUInfo)
|
||||||
healthInfo.Sys.CPUInfo = append(healthInfo.Sys.CPUInfo, localCPUInfo)
|
healthInfo.Sys.CPUInfo = append(healthInfo.Sys.CPUInfo, localCPUInfo)
|
||||||
|
|
||||||
peerCPUInfo := globalNotificationSys.GetCPUs(deadlinedCtx)
|
peerCPUInfo := globalNotificationSys.GetCPUs(healthCtx)
|
||||||
for _, cpuInfo := range peerCPUInfo {
|
for _, cpuInfo := range peerCPUInfo {
|
||||||
anonymizeAddr(&cpuInfo)
|
anonymizeAddr(&cpuInfo)
|
||||||
healthInfo.Sys.CPUInfo = append(healthInfo.Sys.CPUInfo, cpuInfo)
|
healthInfo.Sys.CPUInfo = append(healthInfo.Sys.CPUInfo, cpuInfo)
|
||||||
@@ -1697,11 +1924,11 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
getAndWritePartitions := func() {
|
getAndWritePartitions := func() {
|
||||||
if query.Get("sysdrivehw") == "true" {
|
if query.Get("sysdrivehw") == "true" {
|
||||||
localPartitions := madmin.GetPartitions(deadlinedCtx, globalLocalNodeName)
|
localPartitions := madmin.GetPartitions(healthCtx, globalLocalNodeName)
|
||||||
anonymizeAddr(&localPartitions)
|
anonymizeAddr(&localPartitions)
|
||||||
healthInfo.Sys.Partitions = append(healthInfo.Sys.Partitions, localPartitions)
|
healthInfo.Sys.Partitions = append(healthInfo.Sys.Partitions, localPartitions)
|
||||||
|
|
||||||
peerPartitions := globalNotificationSys.GetPartitions(deadlinedCtx)
|
peerPartitions := globalNotificationSys.GetPartitions(healthCtx)
|
||||||
for _, p := range peerPartitions {
|
for _, p := range peerPartitions {
|
||||||
anonymizeAddr(&p)
|
anonymizeAddr(&p)
|
||||||
healthInfo.Sys.Partitions = append(healthInfo.Sys.Partitions, p)
|
healthInfo.Sys.Partitions = append(healthInfo.Sys.Partitions, p)
|
||||||
@@ -1712,11 +1939,11 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
getAndWriteOSInfo := func() {
|
getAndWriteOSInfo := func() {
|
||||||
if query.Get("sysosinfo") == "true" {
|
if query.Get("sysosinfo") == "true" {
|
||||||
localOSInfo := madmin.GetOSInfo(deadlinedCtx, globalLocalNodeName)
|
localOSInfo := madmin.GetOSInfo(healthCtx, globalLocalNodeName)
|
||||||
anonymizeAddr(&localOSInfo)
|
anonymizeAddr(&localOSInfo)
|
||||||
healthInfo.Sys.OSInfo = append(healthInfo.Sys.OSInfo, localOSInfo)
|
healthInfo.Sys.OSInfo = append(healthInfo.Sys.OSInfo, localOSInfo)
|
||||||
|
|
||||||
peerOSInfos := globalNotificationSys.GetOSInfo(deadlinedCtx)
|
peerOSInfos := globalNotificationSys.GetOSInfo(healthCtx)
|
||||||
for _, o := range peerOSInfos {
|
for _, o := range peerOSInfos {
|
||||||
anonymizeAddr(&o)
|
anonymizeAddr(&o)
|
||||||
healthInfo.Sys.OSInfo = append(healthInfo.Sys.OSInfo, o)
|
healthInfo.Sys.OSInfo = append(healthInfo.Sys.OSInfo, o)
|
||||||
@@ -1727,11 +1954,11 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
getAndWriteMemInfo := func() {
|
getAndWriteMemInfo := func() {
|
||||||
if query.Get("sysmem") == "true" {
|
if query.Get("sysmem") == "true" {
|
||||||
localMemInfo := madmin.GetMemInfo(deadlinedCtx, globalLocalNodeName)
|
localMemInfo := madmin.GetMemInfo(healthCtx, globalLocalNodeName)
|
||||||
anonymizeAddr(&localMemInfo)
|
anonymizeAddr(&localMemInfo)
|
||||||
healthInfo.Sys.MemInfo = append(healthInfo.Sys.MemInfo, localMemInfo)
|
healthInfo.Sys.MemInfo = append(healthInfo.Sys.MemInfo, localMemInfo)
|
||||||
|
|
||||||
peerMemInfos := globalNotificationSys.GetMemInfo(deadlinedCtx)
|
peerMemInfos := globalNotificationSys.GetMemInfo(healthCtx)
|
||||||
for _, m := range peerMemInfos {
|
for _, m := range peerMemInfos {
|
||||||
anonymizeAddr(&m)
|
anonymizeAddr(&m)
|
||||||
healthInfo.Sys.MemInfo = append(healthInfo.Sys.MemInfo, m)
|
healthInfo.Sys.MemInfo = append(healthInfo.Sys.MemInfo, m)
|
||||||
@@ -1742,12 +1969,12 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
getAndWriteSysErrors := func() {
|
getAndWriteSysErrors := func() {
|
||||||
if query.Get(string(madmin.HealthDataTypeSysErrors)) == "true" {
|
if query.Get(string(madmin.HealthDataTypeSysErrors)) == "true" {
|
||||||
localSysErrors := madmin.GetSysErrors(deadlinedCtx, globalLocalNodeName)
|
localSysErrors := madmin.GetSysErrors(healthCtx, globalLocalNodeName)
|
||||||
anonymizeAddr(&localSysErrors)
|
anonymizeAddr(&localSysErrors)
|
||||||
healthInfo.Sys.SysErrs = append(healthInfo.Sys.SysErrs, localSysErrors)
|
healthInfo.Sys.SysErrs = append(healthInfo.Sys.SysErrs, localSysErrors)
|
||||||
partialWrite(healthInfo)
|
partialWrite(healthInfo)
|
||||||
|
|
||||||
peerSysErrs := globalNotificationSys.GetSysErrors(deadlinedCtx)
|
peerSysErrs := globalNotificationSys.GetSysErrors(healthCtx)
|
||||||
for _, se := range peerSysErrs {
|
for _, se := range peerSysErrs {
|
||||||
anonymizeAddr(&se)
|
anonymizeAddr(&se)
|
||||||
healthInfo.Sys.SysErrs = append(healthInfo.Sys.SysErrs, se)
|
healthInfo.Sys.SysErrs = append(healthInfo.Sys.SysErrs, se)
|
||||||
@@ -1758,12 +1985,12 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
getAndWriteSysConfig := func() {
|
getAndWriteSysConfig := func() {
|
||||||
if query.Get(string(madmin.HealthDataTypeSysConfig)) == "true" {
|
if query.Get(string(madmin.HealthDataTypeSysConfig)) == "true" {
|
||||||
localSysConfig := madmin.GetSysConfig(deadlinedCtx, globalLocalNodeName)
|
localSysConfig := madmin.GetSysConfig(healthCtx, globalLocalNodeName)
|
||||||
anonymizeAddr(&localSysConfig)
|
anonymizeAddr(&localSysConfig)
|
||||||
healthInfo.Sys.SysConfig = append(healthInfo.Sys.SysConfig, localSysConfig)
|
healthInfo.Sys.SysConfig = append(healthInfo.Sys.SysConfig, localSysConfig)
|
||||||
partialWrite(healthInfo)
|
partialWrite(healthInfo)
|
||||||
|
|
||||||
peerSysConfig := globalNotificationSys.GetSysConfig(deadlinedCtx)
|
peerSysConfig := globalNotificationSys.GetSysConfig(healthCtx)
|
||||||
for _, sc := range peerSysConfig {
|
for _, sc := range peerSysConfig {
|
||||||
anonymizeAddr(&sc)
|
anonymizeAddr(&sc)
|
||||||
healthInfo.Sys.SysConfig = append(healthInfo.Sys.SysConfig, sc)
|
healthInfo.Sys.SysConfig = append(healthInfo.Sys.SysConfig, sc)
|
||||||
@@ -1774,12 +2001,12 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
getAndWriteSysServices := func() {
|
getAndWriteSysServices := func() {
|
||||||
if query.Get(string(madmin.HealthDataTypeSysServices)) == "true" {
|
if query.Get(string(madmin.HealthDataTypeSysServices)) == "true" {
|
||||||
localSysServices := madmin.GetSysServices(deadlinedCtx, globalLocalNodeName)
|
localSysServices := madmin.GetSysServices(healthCtx, globalLocalNodeName)
|
||||||
anonymizeAddr(&localSysServices)
|
anonymizeAddr(&localSysServices)
|
||||||
healthInfo.Sys.SysServices = append(healthInfo.Sys.SysServices, localSysServices)
|
healthInfo.Sys.SysServices = append(healthInfo.Sys.SysServices, localSysServices)
|
||||||
partialWrite(healthInfo)
|
partialWrite(healthInfo)
|
||||||
|
|
||||||
peerSysServices := globalNotificationSys.GetSysServices(deadlinedCtx)
|
peerSysServices := globalNotificationSys.GetSysServices(healthCtx)
|
||||||
for _, ss := range peerSysServices {
|
for _, ss := range peerSysServices {
|
||||||
anonymizeAddr(&ss)
|
anonymizeAddr(&ss)
|
||||||
healthInfo.Sys.SysServices = append(healthInfo.Sys.SysServices, ss)
|
healthInfo.Sys.SysServices = append(healthInfo.Sys.SysServices, ss)
|
||||||
@@ -1854,10 +2081,10 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
getAndWriteProcInfo := func() {
|
getAndWriteProcInfo := func() {
|
||||||
if query.Get("sysprocess") == "true" {
|
if query.Get("sysprocess") == "true" {
|
||||||
localProcInfo := madmin.GetProcInfo(deadlinedCtx, globalLocalNodeName)
|
localProcInfo := madmin.GetProcInfo(healthCtx, globalLocalNodeName)
|
||||||
anonymizeProcInfo(&localProcInfo)
|
anonymizeProcInfo(&localProcInfo)
|
||||||
healthInfo.Sys.ProcInfo = append(healthInfo.Sys.ProcInfo, localProcInfo)
|
healthInfo.Sys.ProcInfo = append(healthInfo.Sys.ProcInfo, localProcInfo)
|
||||||
peerProcInfos := globalNotificationSys.GetProcInfo(deadlinedCtx)
|
peerProcInfos := globalNotificationSys.GetProcInfo(healthCtx)
|
||||||
for _, p := range peerProcInfos {
|
for _, p := range peerProcInfos {
|
||||||
anonymizeProcInfo(&p)
|
anonymizeProcInfo(&p)
|
||||||
healthInfo.Sys.ProcInfo = append(healthInfo.Sys.ProcInfo, p)
|
healthInfo.Sys.ProcInfo = append(healthInfo.Sys.ProcInfo, p)
|
||||||
@@ -1883,49 +2110,98 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAndWriteDrivePerfInfo := func() {
|
getAndWriteDrivePerfInfo := func() {
|
||||||
if query.Get("perfdrive") == "true" {
|
if query.Get(string(madmin.HealthDataTypePerfDrive)) == "true" {
|
||||||
localDPI := getDrivePerfInfos(deadlinedCtx, globalLocalNodeName)
|
// Freeze all incoming S3 API calls before running speedtest.
|
||||||
anonymizeAddr(&localDPI)
|
globalNotificationSys.ServiceFreeze(ctx, true)
|
||||||
healthInfo.Perf.Drives = append(healthInfo.Perf.Drives, localDPI)
|
// unfreeze all incoming S3 API calls after speedtest.
|
||||||
partialWrite(healthInfo)
|
defer globalNotificationSys.ServiceFreeze(ctx, false)
|
||||||
|
|
||||||
perfCh := globalNotificationSys.GetDrivePerfInfos(deadlinedCtx)
|
opts := madmin.DriveSpeedTestOpts{
|
||||||
|
Serial: false,
|
||||||
|
BlockSize: 4 * humanize.MiByte,
|
||||||
|
FileSize: 1 * humanize.GiByte,
|
||||||
|
}
|
||||||
|
|
||||||
|
localDPI := driveSpeedTest(ctx, opts)
|
||||||
|
healthInfo.Perf.DrivePerf = append(healthInfo.Perf.DrivePerf, localDPI)
|
||||||
|
|
||||||
|
perfCh := globalNotificationSys.DriveSpeedTest(ctx, opts)
|
||||||
for perfInfo := range perfCh {
|
for perfInfo := range perfCh {
|
||||||
anonymizeAddr(&perfInfo)
|
healthInfo.Perf.DrivePerf = append(healthInfo.Perf.DrivePerf, perfInfo)
|
||||||
healthInfo.Perf.Drives = append(healthInfo.Perf.Drives, perfInfo)
|
}
|
||||||
partialWrite(healthInfo)
|
partialWrite(healthInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAndWriteObjPerfInfo := func() {
|
||||||
|
if query.Get(string(madmin.HealthDataTypePerfObj)) == "true" {
|
||||||
|
concurrent := 32
|
||||||
|
size := 64 * humanize.MiByte
|
||||||
|
autotune := true
|
||||||
|
|
||||||
|
sufficientCapacity, canAutotune, capacityErrMsg := validateObjPerfOptions(ctx, objectAPI, concurrent, size, autotune)
|
||||||
|
|
||||||
|
if !sufficientCapacity {
|
||||||
|
healthInfo.Perf.Error = capacityErrMsg
|
||||||
|
partialWrite(healthInfo)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
anonymizeNetPerfInfo := func(npi *madmin.NetPerfInfo) {
|
if !canAutotune {
|
||||||
anonymizeAddr(npi)
|
autotune = false
|
||||||
rps := npi.RemotePeers
|
}
|
||||||
for idx, peer := range rps {
|
|
||||||
anonymizeAddr(&peer)
|
bucketExists, err := makeObjectPerfBucket(ctx, objectAPI)
|
||||||
rps[idx] = peer
|
if err != nil {
|
||||||
|
healthInfo.Perf.Error = "Could not make object perf bucket: " + err.Error()
|
||||||
|
partialWrite(healthInfo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bucketExists {
|
||||||
|
defer deleteObjectPerfBucket(objectAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Freeze all incoming S3 API calls before running speedtest.
|
||||||
|
globalNotificationSys.ServiceFreeze(ctx, true)
|
||||||
|
|
||||||
|
// unfreeze all incoming S3 API calls after speedtest.
|
||||||
|
defer globalNotificationSys.ServiceFreeze(ctx, false)
|
||||||
|
|
||||||
|
opts := speedTestOpts{
|
||||||
|
throughputSize: size,
|
||||||
|
concurrencyStart: concurrent,
|
||||||
|
duration: 10 * time.Second,
|
||||||
|
autotune: autotune,
|
||||||
|
}
|
||||||
|
|
||||||
|
perfCh := objectSpeedTest(ctx, opts)
|
||||||
|
for perfInfo := range perfCh {
|
||||||
|
healthInfo.Perf.ObjPerf = append(healthInfo.Perf.ObjPerf, perfInfo)
|
||||||
|
}
|
||||||
|
partialWrite(healthInfo)
|
||||||
}
|
}
|
||||||
npi.RemotePeers = rps
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAndWriteNetPerfInfo := func() {
|
getAndWriteNetPerfInfo := func() {
|
||||||
if globalIsDistErasure && query.Get("perfnet") == "true" {
|
if query.Get(string(madmin.HealthDataTypePerfObj)) == "true" {
|
||||||
localNPI := globalNotificationSys.GetNetPerfInfo(deadlinedCtx)
|
if !globalIsDistErasure {
|
||||||
anonymizeNetPerfInfo(&localNPI)
|
return
|
||||||
healthInfo.Perf.Net = append(healthInfo.Perf.Net, localNPI)
|
|
||||||
|
|
||||||
partialWrite(healthInfo)
|
|
||||||
|
|
||||||
netInfos := globalNotificationSys.DispatchNetPerfChan(deadlinedCtx)
|
|
||||||
for netInfo := range netInfos {
|
|
||||||
anonymizeNetPerfInfo(&netInfo)
|
|
||||||
healthInfo.Perf.Net = append(healthInfo.Perf.Net, netInfo)
|
|
||||||
partialWrite(healthInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ppi := globalNotificationSys.GetParallelNetPerfInfo(deadlinedCtx)
|
nsLock := objectAPI.NewNSLock(minioMetaBucket, "netperf")
|
||||||
anonymizeNetPerfInfo(&ppi)
|
lkctx, err := nsLock.GetLock(ctx, globalOperationTimeout)
|
||||||
healthInfo.Perf.NetParallel = ppi
|
if err != nil {
|
||||||
|
healthInfo.Perf.Error = "Could not acquire lock for netperf: " + err.Error()
|
||||||
|
} else {
|
||||||
|
defer nsLock.Unlock(lkctx.Cancel)
|
||||||
|
|
||||||
|
netPerf := globalNotificationSys.Netperf(ctx, time.Second*10)
|
||||||
|
for _, np := range netPerf {
|
||||||
|
np.Endpoint = anonAddr(np.Endpoint)
|
||||||
|
healthInfo.Perf.NetPerf = append(healthInfo.Perf.NetPerf, np)
|
||||||
|
}
|
||||||
|
}
|
||||||
partialWrite(healthInfo)
|
partialWrite(healthInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1951,7 +2227,8 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
go func() {
|
go func() {
|
||||||
defer close(healthInfoCh)
|
defer close(healthInfoCh)
|
||||||
|
|
||||||
partialWrite(healthInfo) // Write first message with only version populated
|
partialWrite(healthInfo) // Write first message with only version and deployment id populated
|
||||||
|
getAndWritePlatformInfo()
|
||||||
getAndWriteCPUs()
|
getAndWriteCPUs()
|
||||||
getAndWritePartitions()
|
getAndWritePartitions()
|
||||||
getAndWriteOSInfo()
|
getAndWriteOSInfo()
|
||||||
@@ -1959,6 +2236,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
getAndWriteProcInfo()
|
getAndWriteProcInfo()
|
||||||
getAndWriteMinioConfig()
|
getAndWriteMinioConfig()
|
||||||
getAndWriteDrivePerfInfo()
|
getAndWriteDrivePerfInfo()
|
||||||
|
getAndWriteObjPerfInfo()
|
||||||
getAndWriteNetPerfInfo()
|
getAndWriteNetPerfInfo()
|
||||||
getAndWriteSysErrors()
|
getAndWriteSysErrors()
|
||||||
getAndWriteSysServices()
|
getAndWriteSysServices()
|
||||||
@@ -2032,7 +2310,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
case <-deadlinedCtx.Done():
|
case <-healthCtx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2287,19 +2565,9 @@ func checkConnection(endpointStr string, timeout time.Duration) error {
|
|||||||
ctx, cancel := context.WithTimeout(GlobalContext, timeout)
|
ctx, cancel := context.WithTimeout(GlobalContext, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
client := &http.Client{Transport: &http.Transport{
|
client := &http.Client{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Transport: globalProxyTransport,
|
||||||
DialContext: xhttp.NewCustomDialContext(timeout),
|
}
|
||||||
ResponseHeaderTimeout: 5 * time.Second,
|
|
||||||
TLSHandshakeTimeout: 5 * time.Second,
|
|
||||||
ExpectContinueTimeout: 5 * time.Second,
|
|
||||||
TLSClientConfig: &tls.Config{RootCAs: globalRootCAs},
|
|
||||||
// Go net/http automatically unzip if content-type is
|
|
||||||
// gzip disable this feature, as we are always interested
|
|
||||||
// in raw stream.
|
|
||||||
DisableCompression: true,
|
|
||||||
}}
|
|
||||||
defer client.CloseIdleConnections()
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodHead, endpointStr, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodHead, endpointStr, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2392,8 +2660,7 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
// of profiling data of all nodes
|
// of profiling data of all nodes
|
||||||
zipWriter := zip.NewWriter(encw)
|
zipWriter := zip.NewWriter(encw)
|
||||||
defer zipWriter.Close()
|
defer zipWriter.Close()
|
||||||
|
rawDataFn := func(r io.Reader, host, disk, filename string, si StatInfo) error {
|
||||||
err = o.GetRawData(ctx, volume, file, func(r io.Reader, host, disk, filename string, si StatInfo) error {
|
|
||||||
// Prefix host+disk
|
// Prefix host+disk
|
||||||
filename = path.Join(host, disk, filename)
|
filename = path.Join(host, disk, filename)
|
||||||
if si.Dir {
|
if si.Dir {
|
||||||
@@ -2404,6 +2671,10 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
// Not, set it to default.
|
// Not, set it to default.
|
||||||
si.Mode = 0o600
|
si.Mode = 0o600
|
||||||
}
|
}
|
||||||
|
if si.ModTime.IsZero() {
|
||||||
|
// Set time to now.
|
||||||
|
si.ModTime = time.Now()
|
||||||
|
}
|
||||||
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
||||||
name: filename,
|
name: filename,
|
||||||
size: si.Size,
|
size: si.Size,
|
||||||
@@ -2426,10 +2697,35 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
}
|
||||||
|
err = o.GetRawData(ctx, volume, file, rawDataFn)
|
||||||
|
if !errors.Is(err, errFileNotFound) {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// save the format.json as part of inspect by default
|
||||||
|
if volume != minioMetaBucket && file != formatConfigFile {
|
||||||
|
err = o.GetRawData(ctx, minioMetaBucket, formatConfigFile, rawDataFn)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, errFileNotFound) {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
// save args passed to inspect command
|
||||||
|
inspectArgs := []string{fmt.Sprintf(" Inspect path: %s%s%s\n", volume, slashSeparator, file)}
|
||||||
|
cmdLine := []string{"Server command line args: "}
|
||||||
|
for _, pool := range globalEndpoints {
|
||||||
|
cmdLine = append(cmdLine, pool.CmdLine)
|
||||||
|
}
|
||||||
|
cmdLine = append(cmdLine, "\n")
|
||||||
|
inspectArgs = append(inspectArgs, cmdLine...)
|
||||||
|
inspectArgsBytes := []byte(strings.Join(inspectArgs, " "))
|
||||||
|
if err = rawDataFn(bytes.NewReader(inspectArgsBytes), "", "", "inspect-input.txt", StatInfo{
|
||||||
|
Size: int64(len(inspectArgsBytes)),
|
||||||
|
}); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createHostAnonymizerForFSMode() map[string]string {
|
func createHostAnonymizerForFSMode() map[string]string {
|
||||||
hostAnonymizer := map[string]string{
|
hostAnonymizer := map[string]string{
|
||||||
globalLocalNodeName: "server1",
|
globalLocalNodeName: "server1",
|
||||||
@@ -2473,6 +2769,7 @@ func anonymizeHost(hostAnonymizer map[string]string, endpoint Endpoint, poolNum
|
|||||||
if !found {
|
if !found {
|
||||||
// In distributed setup, anonymized addr = 'poolNum.serverNum'
|
// In distributed setup, anonymized addr = 'poolNum.serverNum'
|
||||||
newHost := fmt.Sprintf("pool%d.server%d", poolNum, srvrNum)
|
newHost := fmt.Sprintf("pool%d.server%d", poolNum, srvrNum)
|
||||||
|
schemePfx := endpoint.Scheme + "://"
|
||||||
|
|
||||||
// Hostname
|
// Hostname
|
||||||
mapIfNotPresent(hostAnonymizer, endpoint.Hostname(), newHost)
|
mapIfNotPresent(hostAnonymizer, endpoint.Hostname(), newHost)
|
||||||
@@ -2482,6 +2779,7 @@ func anonymizeHost(hostAnonymizer map[string]string, endpoint Endpoint, poolNum
|
|||||||
// Host + port
|
// Host + port
|
||||||
newHostPort = newHost + ":" + endpoint.Port()
|
newHostPort = newHost + ":" + endpoint.Port()
|
||||||
mapIfNotPresent(hostAnonymizer, endpoint.Host, newHostPort)
|
mapIfNotPresent(hostAnonymizer, endpoint.Host, newHostPort)
|
||||||
|
mapIfNotPresent(hostAnonymizer, schemePfx+endpoint.Host, newHostPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
newHostPortPath := newHostPort
|
newHostPortPath := newHostPort
|
||||||
@@ -2490,10 +2788,11 @@ func anonymizeHost(hostAnonymizer map[string]string, endpoint Endpoint, poolNum
|
|||||||
currentHostPortPath := endpoint.Host + endpoint.Path
|
currentHostPortPath := endpoint.Host + endpoint.Path
|
||||||
newHostPortPath = newHostPort + endpoint.Path
|
newHostPortPath = newHostPort + endpoint.Path
|
||||||
mapIfNotPresent(hostAnonymizer, currentHostPortPath, newHostPortPath)
|
mapIfNotPresent(hostAnonymizer, currentHostPortPath, newHostPortPath)
|
||||||
|
mapIfNotPresent(hostAnonymizer, schemePfx+currentHostPortPath, newHostPortPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full url
|
// Full url
|
||||||
hostAnonymizer[currentURL] = endpoint.Scheme + "://" + newHostPortPath
|
hostAnonymizer[currentURL] = schemePfx + newHostPortPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,6 @@ func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-periodicTimer.C:
|
case <-periodicTimer.C:
|
||||||
periodicTimer.Reset(time.Minute * 5)
|
|
||||||
now := UTCNow()
|
now := UTCNow()
|
||||||
ahs.Lock()
|
ahs.Lock()
|
||||||
for path, h := range ahs.healSeqMap {
|
for path, h := range ahs.healSeqMap {
|
||||||
@@ -203,6 +202,8 @@ func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ahs.Unlock()
|
ahs.Unlock()
|
||||||
|
|
||||||
|
periodicTimer.Reset(time.Minute * 5)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// server could be restarting - need
|
// server could be restarting - need
|
||||||
// to exit immediately
|
// to exit immediately
|
||||||
@@ -581,12 +582,7 @@ func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
|||||||
// heal-results in memory and the client has not consumed it
|
// heal-results in memory and the client has not consumed it
|
||||||
// for too long.
|
// for too long.
|
||||||
unconsumedTimer := time.NewTimer(healUnconsumedTimeout)
|
unconsumedTimer := time.NewTimer(healUnconsumedTimeout)
|
||||||
defer func() {
|
defer unconsumedTimer.Stop()
|
||||||
// stop the timeout timer so it is garbage collected.
|
|
||||||
if !unconsumedTimer.Stop() {
|
|
||||||
<-unconsumedTimer.C
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var itemsLen int
|
var itemsLen int
|
||||||
for {
|
for {
|
||||||
@@ -700,8 +696,9 @@ func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItem
|
|||||||
}
|
}
|
||||||
if source.opts != nil {
|
if source.opts != nil {
|
||||||
task.opts = *source.opts
|
task.opts = *source.opts
|
||||||
|
} else {
|
||||||
|
task.opts.ScanMode = madmin.HealNormalScan
|
||||||
}
|
}
|
||||||
task.opts.ScanMode = globalHealConfig.ScanMode()
|
|
||||||
|
|
||||||
h.mutex.Lock()
|
h.mutex.Lock()
|
||||||
h.scannedItemsMap[healType]++
|
h.scannedItemsMap[healType]++
|
||||||
|
|||||||
+4
-4
@@ -84,10 +84,12 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/pools/cancel").HandlerFunc(gz(httpTraceAll(adminAPI.CancelDecommission))).Queries("pool", "{pool:.*}")
|
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/pools/cancel").HandlerFunc(gz(httpTraceAll(adminAPI.CancelDecommission))).Queries("pool", "{pool:.*}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Profiling operations
|
// Profiling operations - deprecated API
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/profiling/start").HandlerFunc(gz(httpTraceAll(adminAPI.StartProfilingHandler))).
|
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/profiling/start").HandlerFunc(gz(httpTraceAll(adminAPI.StartProfilingHandler))).
|
||||||
Queries("profilerType", "{profilerType:.*}")
|
Queries("profilerType", "{profilerType:.*}")
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/profiling/download").HandlerFunc(gz(httpTraceAll(adminAPI.DownloadProfilingHandler)))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/profiling/download").HandlerFunc(gz(httpTraceAll(adminAPI.DownloadProfilingHandler)))
|
||||||
|
// Profiling operations
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/profile").HandlerFunc(gz(httpTraceAll(adminAPI.ProfileHandler)))
|
||||||
|
|
||||||
// Config KV operations.
|
// Config KV operations.
|
||||||
if enableConfigOps {
|
if enableConfigOps {
|
||||||
@@ -168,7 +170,6 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
// Set Group Status
|
// Set Group Status
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetGroupStatus))).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetGroupStatus))).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
|
||||||
|
|
||||||
if globalIsDistErasure || globalIsErasure {
|
|
||||||
// GetBucketQuotaConfig
|
// GetBucketQuotaConfig
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
|
||||||
gz(httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
|
gz(httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
|
||||||
@@ -211,7 +212,6 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationEdit)))
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationEdit)))
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerEdit)))
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerEdit)))
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerRemove)))
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerRemove)))
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
// Top locks
|
// Top locks
|
||||||
@@ -224,7 +224,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest").HandlerFunc(httpTraceHdrs(adminAPI.SpeedtestHandler))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest").HandlerFunc(httpTraceHdrs(adminAPI.SpeedtestHandler))
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/object").HandlerFunc(httpTraceHdrs(adminAPI.ObjectSpeedtestHandler))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/object").HandlerFunc(httpTraceHdrs(adminAPI.ObjectSpeedtestHandler))
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/drive").HandlerFunc(httpTraceHdrs(adminAPI.DriveSpeedtestHandler))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/drive").HandlerFunc(httpTraceHdrs(adminAPI.DriveSpeedtestHandler))
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/net").HandlerFunc(httpTraceHdrs(adminAPI.NetSpeedtestHandler))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/net").HandlerFunc(httpTraceHdrs(adminAPI.NetperfHandler))
|
||||||
|
|
||||||
// HTTP Trace
|
// HTTP Trace
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/trace").HandlerFunc(gz(http.HandlerFunc(adminAPI.TraceHandler)))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/trace").HandlerFunc(gz(http.HandlerFunc(adminAPI.TraceHandler)))
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ import (
|
|||||||
// local endpoints from given list of endpoints
|
// local endpoints from given list of endpoints
|
||||||
func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Request) madmin.ServerProperties {
|
func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Request) madmin.ServerProperties {
|
||||||
var localEndpoints Endpoints
|
var localEndpoints Endpoints
|
||||||
addr := r.Host
|
addr := globalLocalNodeName
|
||||||
|
if r != nil {
|
||||||
|
addr = r.Host
|
||||||
|
}
|
||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
addr = globalLocalNodeName
|
addr = globalLocalNodeName
|
||||||
}
|
}
|
||||||
@@ -40,7 +43,7 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
|
|||||||
for _, endpoint := range ep.Endpoints {
|
for _, endpoint := range ep.Endpoints {
|
||||||
nodeName := endpoint.Host
|
nodeName := endpoint.Host
|
||||||
if nodeName == "" {
|
if nodeName == "" {
|
||||||
nodeName = r.Host
|
nodeName = addr
|
||||||
}
|
}
|
||||||
if endpoint.IsLocal {
|
if endpoint.IsLocal {
|
||||||
// Only proceed for local endpoints
|
// Only proceed for local endpoints
|
||||||
@@ -50,7 +53,7 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
|
|||||||
}
|
}
|
||||||
_, present := network[nodeName]
|
_, present := network[nodeName]
|
||||||
if !present {
|
if !present {
|
||||||
if err := isServerResolvable(endpoint, 2*time.Second); err == nil {
|
if err := isServerResolvable(endpoint, 5*time.Second); err == nil {
|
||||||
network[nodeName] = string(madmin.ItemOnline)
|
network[nodeName] = string(madmin.ItemOnline)
|
||||||
} else {
|
} else {
|
||||||
network[nodeName] = string(madmin.ItemOffline)
|
network[nodeName] = string(madmin.ItemOffline)
|
||||||
|
|||||||
+18
-7
@@ -131,7 +131,7 @@ const (
|
|||||||
ErrReplicationNeedsVersioningError
|
ErrReplicationNeedsVersioningError
|
||||||
ErrReplicationBucketNeedsVersioningError
|
ErrReplicationBucketNeedsVersioningError
|
||||||
ErrReplicationDenyEditError
|
ErrReplicationDenyEditError
|
||||||
ErrReplicationNoMatchingRuleError
|
ErrReplicationNoExistingObjects
|
||||||
ErrObjectRestoreAlreadyInProgress
|
ErrObjectRestoreAlreadyInProgress
|
||||||
ErrNoSuchKey
|
ErrNoSuchKey
|
||||||
ErrNoSuchUpload
|
ErrNoSuchUpload
|
||||||
@@ -280,6 +280,7 @@ const (
|
|||||||
ErrSiteReplicationBucketConfigError
|
ErrSiteReplicationBucketConfigError
|
||||||
ErrSiteReplicationBucketMetaError
|
ErrSiteReplicationBucketMetaError
|
||||||
ErrSiteReplicationIAMError
|
ErrSiteReplicationIAMError
|
||||||
|
ErrSiteReplicationConfigMissing
|
||||||
|
|
||||||
// Bucket Quota error codes
|
// Bucket Quota error codes
|
||||||
ErrAdminBucketQuotaExceeded
|
ErrAdminBucketQuotaExceeded
|
||||||
@@ -383,6 +384,7 @@ const (
|
|||||||
ErrAdminProfilerNotEnabled
|
ErrAdminProfilerNotEnabled
|
||||||
ErrInvalidDecompressedSize
|
ErrInvalidDecompressedSize
|
||||||
ErrAddUserInvalidArgument
|
ErrAddUserInvalidArgument
|
||||||
|
ErrAdminResourceInvalidArgument
|
||||||
ErrAdminAccountNotEligible
|
ErrAdminAccountNotEligible
|
||||||
ErrAccountNotEligible
|
ErrAccountNotEligible
|
||||||
ErrAdminServiceAccountNotFound
|
ErrAdminServiceAccountNotFound
|
||||||
@@ -891,15 +893,15 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Bandwidth limit for remote target must be atleast 100MBps",
|
Description: "Bandwidth limit for remote target must be atleast 100MBps",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrReplicationNoMatchingRuleError: {
|
ErrReplicationNoExistingObjects: {
|
||||||
Code: "XMinioReplicationNoMatchingRule",
|
Code: "XMinioReplicationNoExistingObjects",
|
||||||
Description: "No matching replication rule found for this object prefix",
|
Description: "No matching ExistingsObjects rule enabled",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
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",
|
||||||
HTTPStatusCode: http.StatusConflict,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrBucketRemoteIdenticalToSource: {
|
ErrBucketRemoteIdenticalToSource: {
|
||||||
Code: "XMinioAdminRemoteIdenticalToSource",
|
Code: "XMinioAdminRemoteIdenticalToSource",
|
||||||
@@ -1165,7 +1167,7 @@ var errorCodes = errorCodeMap{
|
|||||||
ErrObjectExistsAsDirectory: {
|
ErrObjectExistsAsDirectory: {
|
||||||
Code: "XMinioObjectExistsAsDirectory",
|
Code: "XMinioObjectExistsAsDirectory",
|
||||||
Description: "Object name already exists as a directory.",
|
Description: "Object name already exists as a directory.",
|
||||||
HTTPStatusCode: http.StatusConflict,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrInvalidObjectName: {
|
ErrInvalidObjectName: {
|
||||||
Code: "XMinioInvalidObjectName",
|
Code: "XMinioInvalidObjectName",
|
||||||
@@ -1339,7 +1341,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Error while replicating an IAM item",
|
Description: "Error while replicating an IAM item",
|
||||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
},
|
},
|
||||||
|
ErrSiteReplicationConfigMissing: {
|
||||||
|
Code: "XMinioSiteReplicationConfigMissingError",
|
||||||
|
Description: "Site not found in site replication configuration",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrMaximumExpires: {
|
ErrMaximumExpires: {
|
||||||
Code: "AuthorizationQueryParametersError",
|
Code: "AuthorizationQueryParametersError",
|
||||||
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
|
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
|
||||||
@@ -1825,6 +1831,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "User is not allowed to be same as admin access key",
|
Description: "User is not allowed to be same as admin access key",
|
||||||
HTTPStatusCode: http.StatusForbidden,
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
|
ErrAdminResourceInvalidArgument: {
|
||||||
|
Code: "XMinioInvalidResource",
|
||||||
|
Description: "Policy, user or group names are not allowed to begin or end with space characters",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrAdminAccountNotEligible: {
|
ErrAdminAccountNotEligible: {
|
||||||
Code: "XMinioInvalidIAMCredentials",
|
Code: "XMinioInvalidIAMCredentials",
|
||||||
Description: "The administrator key is not eligible for this operation",
|
Description: "The administrator key is not eligible for this operation",
|
||||||
|
|||||||
@@ -126,6 +126,11 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
|||||||
|
|
||||||
// Set all other user defined metadata.
|
// Set all other user defined metadata.
|
||||||
for k, v := range objInfo.UserDefined {
|
for k, v := range objInfo.UserDefined {
|
||||||
|
// Empty values for object lock and retention can be skipped.
|
||||||
|
if v == "" && equals(k, xhttp.AmzObjectLockMode, xhttp.AmzObjectLockRetainUntilDate) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||||
// Do not need to send any internal metadata
|
// Do not need to send any internal metadata
|
||||||
// values to client.
|
// values to client.
|
||||||
|
|||||||
@@ -728,6 +728,14 @@ func generateMultiDeleteResponse(quiet bool, deletedObjects []DeletedObject, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) {
|
func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) {
|
||||||
|
if statusCode == 0 {
|
||||||
|
statusCode = 200
|
||||||
|
}
|
||||||
|
// Similar check to http.checkWriteHeaderCode
|
||||||
|
if statusCode < 100 || statusCode > 999 {
|
||||||
|
logger.Error(fmt.Sprintf("invalid WriteHeader code %v", statusCode))
|
||||||
|
statusCode = http.StatusInternalServerError
|
||||||
|
}
|
||||||
setCommonHeaders(w)
|
setCommonHeaders(w)
|
||||||
if mType != mimeNone {
|
if mType != mimeNone {
|
||||||
w.Header().Set(xhttp.ContentType, string(mType))
|
w.Header().Set(xhttp.ContentType, string(mType))
|
||||||
@@ -791,6 +799,12 @@ func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError
|
|||||||
err.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalSite.Region)
|
err.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalSite.Region)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Similar check to http.checkWriteHeaderCode
|
||||||
|
if err.HTTPStatusCode < 100 || err.HTTPStatusCode > 999 {
|
||||||
|
logger.Error(fmt.Sprintf("invalid WriteHeader code %v from %v", err.HTTPStatusCode, err.Code))
|
||||||
|
err.HTTPStatusCode = http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
// 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), globalDeploymentID)
|
||||||
|
|||||||
+2
-2
@@ -342,7 +342,7 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
collectAPIStats("getbucketnotification", maxClients(gz(httpTraceAll(api.GetBucketNotificationHandler))))).Queries("notification", "")
|
collectAPIStats("getbucketnotification", maxClients(gz(httpTraceAll(api.GetBucketNotificationHandler))))).Queries("notification", "")
|
||||||
// ListenNotification
|
// ListenNotification
|
||||||
router.Methods(http.MethodGet).HandlerFunc(
|
router.Methods(http.MethodGet).HandlerFunc(
|
||||||
collectAPIStats("listennotification", maxClients(gz(httpTraceAll(api.ListenNotificationHandler))))).Queries("events", "{events:.*}")
|
collectAPIStats("listennotification", gz(httpTraceAll(api.ListenNotificationHandler)))).Queries("events", "{events:.*}")
|
||||||
// ResetBucketReplicationStatus - MinIO extension API
|
// ResetBucketReplicationStatus - MinIO extension API
|
||||||
router.Methods(http.MethodGet).HandlerFunc(
|
router.Methods(http.MethodGet).HandlerFunc(
|
||||||
collectAPIStats("resetbucketreplicationstatus", maxClients(gz(httpTraceAll(api.ResetBucketReplicationStatusHandler))))).Queries("replication-reset-status", "")
|
collectAPIStats("resetbucketreplicationstatus", maxClients(gz(httpTraceAll(api.ResetBucketReplicationStatusHandler))))).Queries("replication-reset-status", "")
|
||||||
@@ -474,7 +474,7 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
|
|
||||||
// ListenNotification
|
// ListenNotification
|
||||||
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
|
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
|
||||||
collectAPIStats("listennotification", maxClients(gz(httpTraceAll(api.ListenNotificationHandler))))).Queries("events", "{events:.*}")
|
collectAPIStats("listennotification", gz(httpTraceAll(api.ListenNotificationHandler)))).Queries("events", "{events:.*}")
|
||||||
|
|
||||||
// ListBuckets
|
// ListBuckets
|
||||||
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
|
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
|
||||||
|
|||||||
+106
-104
File diff suppressed because one or more lines are too long
+43
-16
@@ -197,13 +197,7 @@ func mustGetClaimsFromToken(r *http.Request) map[string]interface{} {
|
|||||||
return claims
|
return claims
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch claims in the security token returned by the client.
|
func getClaimsFromTokenWithSecret(token, secret string) (map[string]interface{}, error) {
|
||||||
func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
|
||||||
if token == "" {
|
|
||||||
claims := xjwt.NewMapClaims()
|
|
||||||
return claims.Map(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// JWT token for x-amz-security-token is signed with admin
|
// JWT token for x-amz-security-token is signed with admin
|
||||||
// secret key, temporary credentials become invalid if
|
// secret key, temporary credentials become invalid if
|
||||||
// server admin credentials change. This is done to ensure
|
// server admin credentials change. This is done to ensure
|
||||||
@@ -212,13 +206,19 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
|||||||
// 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 an 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, globalActiveCred.SecretKey)
|
claims, err := auth.ExtractClaims(token, secret)
|
||||||
|
if err != nil {
|
||||||
|
if subtle.ConstantTimeCompare([]byte(secret), []byte(globalActiveCred.SecretKey)) == 1 {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
claims, err = auth.ExtractClaims(token, globalActiveCred.SecretKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errAuthentication
|
return nil, errAuthentication
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If OPA is set, return without any further checks.
|
// If AuthZPlugin is set, return without any further checks.
|
||||||
if globalPolicyOPA != nil {
|
if newGlobalAuthZPluginFn() != nil {
|
||||||
return claims.Map(), nil
|
return claims.Map(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,31 +235,58 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
|||||||
logger.LogIf(GlobalContext, err, logger.Application)
|
logger.LogIf(GlobalContext, err, logger.Application)
|
||||||
return nil, errAuthentication
|
return nil, errAuthentication
|
||||||
}
|
}
|
||||||
claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes)
|
claims.MapClaims[sessionPolicyNameExtracted] = string(spBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
return claims.Map(), nil
|
return claims.Map(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch claims in the security token returned by the client.
|
||||||
|
func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
||||||
|
return getClaimsFromTokenWithSecret(token, globalActiveCred.SecretKey)
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch claims in the security token returned by the client and validate the token.
|
// Fetch claims in the security token returned by the client and validate the token.
|
||||||
func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]interface{}, APIErrorCode) {
|
func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]interface{}, APIErrorCode) {
|
||||||
token := getSessionToken(r)
|
token := getSessionToken(r)
|
||||||
if token != "" && cred.AccessKey == "" {
|
if token != "" && cred.AccessKey == "" {
|
||||||
|
// x-amz-security-token is not allowed for anonymous access.
|
||||||
return nil, ErrNoAccessKey
|
return nil, ErrNoAccessKey
|
||||||
}
|
}
|
||||||
if cred.IsServiceAccount() && token == "" {
|
|
||||||
token = cred.SessionToken
|
if token == "" && cred.IsTemp() {
|
||||||
}
|
// Temporary credentials should always have x-amz-security-token
|
||||||
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
|
|
||||||
return nil, ErrInvalidToken
|
return nil, ErrInvalidToken
|
||||||
}
|
}
|
||||||
claims, err := getClaimsFromToken(token)
|
|
||||||
|
if token != "" && !cred.IsTemp() {
|
||||||
|
// x-amz-security-token should not present for static credentials.
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if cred.IsTemp() && subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
|
||||||
|
// validate token for temporary credentials only.
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := globalActiveCred.SecretKey
|
||||||
|
if cred.IsServiceAccount() {
|
||||||
|
token = cred.SessionToken
|
||||||
|
secret = cred.SecretKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != "" {
|
||||||
|
claims, err := getClaimsFromTokenWithSecret(token, secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, toAPIErrorCode(r.Context(), err)
|
return nil, toAPIErrorCode(r.Context(), err)
|
||||||
}
|
}
|
||||||
return claims, ErrNone
|
return claims, ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
claims := xjwt.NewMapClaims()
|
||||||
|
return claims.Map(), ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
// Check request auth type verifies the incoming http request
|
// Check request auth type verifies the incoming http request
|
||||||
// - validates the request signature
|
// - validates the request signature
|
||||||
// - validates the policy action if anonymous tests bucket policies if any,
|
// - validates the policy action if anonymous tests bucket policies if any,
|
||||||
|
|||||||
@@ -341,7 +341,8 @@ func mustNewSignedEmptyMD5Request(method string, urlStr string, contentLength in
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mustNewSignedBadMD5Request(method string, urlStr string, contentLength int64,
|
func mustNewSignedBadMD5Request(method string, urlStr string, contentLength int64,
|
||||||
body io.ReadSeeker, t *testing.T) *http.Request {
|
body io.ReadSeeker, t *testing.T,
|
||||||
|
) *http.Request {
|
||||||
req := mustNewRequest(method, urlStr, contentLength, body, t)
|
req := mustNewRequest(method, urlStr, contentLength, body, t)
|
||||||
req.Header.Set("Content-Md5", "YWFhYWFhYWFhYWFhYWFhCg==")
|
req.Header.Set("Content-Md5", "YWFhYWFhYWFhYWFhYWFhCg==")
|
||||||
cred := globalActiveCred
|
cred := globalActiveCred
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...)
|
globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...)
|
||||||
|
|
||||||
if drivesToHeal := globalBackgroundHealState.healDriveCount(); drivesToHeal > 0 {
|
if drivesToHeal := globalBackgroundHealState.healDriveCount(); drivesToHeal > 0 {
|
||||||
logger.Info(fmt.Sprintf("Found drives to heal %d, waiting until %s to heal the content...",
|
logger.Info(fmt.Sprintf("Found drives to heal %d, waiting until %s to heal the content - use 'mc admin heal alias/ --verbose' to check the status",
|
||||||
drivesToHeal, defaultMonitorNewDiskInterval))
|
drivesToHeal, defaultMonitorNewDiskInterval))
|
||||||
|
|
||||||
// Heal any disk format and metadata early, if possible.
|
// Heal any disk format and metadata early, if possible.
|
||||||
@@ -277,14 +277,6 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bgSeq.healDiskMeta(objAPI); err != nil {
|
|
||||||
if newObjectLayerFn() != nil {
|
|
||||||
// log only in situations, when object layer
|
|
||||||
// has fully initialized.
|
|
||||||
logger.LogIf(bgSeq.ctx, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go monitorLocalDisksAndHeal(ctx, z, bgSeq)
|
go monitorLocalDisksAndHeal(ctx, z, bgSeq)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,9 +312,6 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-diskCheckTimer.C:
|
case <-diskCheckTimer.C:
|
||||||
// Reset to next interval.
|
|
||||||
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
|
||||||
|
|
||||||
var erasureSetInPoolDisksToHeal []map[int][]StorageAPI
|
var erasureSetInPoolDisksToHeal []map[int][]StorageAPI
|
||||||
|
|
||||||
healDisks := globalBackgroundHealState.getHealLocalDiskEndpoints()
|
healDisks := globalBackgroundHealState.getHealLocalDiskEndpoints()
|
||||||
@@ -333,7 +322,7 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
|
|||||||
// Ensure that reformatting disks is finished
|
// Ensure that reformatting disks is finished
|
||||||
bgSeq.queueHealTask(healSource{bucket: nopHeal}, madmin.HealItemMetadata)
|
bgSeq.queueHealTask(healSource{bucket: nopHeal}, madmin.HealItemMetadata)
|
||||||
|
|
||||||
logger.Info(fmt.Sprintf("Found drives to heal %d, proceeding to heal content...",
|
logger.Info(fmt.Sprintf("Found drives to heal %d, proceeding to heal - 'mc admin heal alias/ --verbose' to check the status.",
|
||||||
len(healDisks)))
|
len(healDisks)))
|
||||||
|
|
||||||
erasureSetInPoolDisksToHeal = make([]map[int][]StorageAPI, len(z.serverPools))
|
erasureSetInPoolDisksToHeal = make([]map[int][]StorageAPI, len(z.serverPools))
|
||||||
@@ -404,13 +393,15 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
|
|||||||
go func(setIndex int, disks []StorageAPI) {
|
go func(setIndex int, disks []StorageAPI) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for _, disk := range disks {
|
for _, disk := range disks {
|
||||||
|
if serverDebugLog {
|
||||||
logger.Info("Healing disk '%v' on %s pool", disk, humanize.Ordinal(i+1))
|
logger.Info("Healing disk '%v' on %s pool", disk, humanize.Ordinal(i+1))
|
||||||
|
}
|
||||||
|
|
||||||
// So someone changed the drives underneath, healing tracker missing.
|
// So someone changed the drives underneath, healing tracker missing.
|
||||||
tracker, err := loadHealingTracker(ctx, disk)
|
tracker, err := loadHealingTracker(ctx, disk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Info("Healing tracker missing on '%s', disk was swapped again on %s pool",
|
logger.LogIf(ctx, fmt.Errorf("Healing tracker missing on '%s', disk was swapped again on %s pool: %w",
|
||||||
disk, humanize.Ordinal(i+1))
|
disk, humanize.Ordinal(i+1), err))
|
||||||
tracker = newHealingTracker(disk)
|
tracker = newHealingTracker(disk)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,12 +429,14 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if serverDebugLog {
|
||||||
logger.Info("Healing disk '%s' on %s pool, %s set complete", disk,
|
logger.Info("Healing disk '%s' on %s pool, %s set complete", disk,
|
||||||
humanize.Ordinal(i+1), humanize.Ordinal(setIndex+1))
|
humanize.Ordinal(i+1), humanize.Ordinal(setIndex+1))
|
||||||
logger.Info("Summary:\n")
|
logger.Info("Summary:\n")
|
||||||
tracker.printTo(os.Stdout)
|
tracker.printTo(os.Stdout)
|
||||||
logger.LogIf(ctx, tracker.delete(ctx))
|
|
||||||
logger.Info("\n")
|
logger.Info("\n")
|
||||||
|
}
|
||||||
|
logger.LogIf(ctx, tracker.delete(ctx))
|
||||||
|
|
||||||
// Only upon success pop the healed disk.
|
// Only upon success pop the healed disk.
|
||||||
globalBackgroundHealState.popHealLocalDisks(disk.Endpoint())
|
globalBackgroundHealState.popHealLocalDisks(disk.Endpoint())
|
||||||
@@ -452,6 +445,9 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
// Reset for next interval.
|
||||||
|
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -27,6 +26,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/minio/highwayhash"
|
"github.com/minio/highwayhash"
|
||||||
|
"github.com/minio/minio/internal/hash/sha256"
|
||||||
"golang.org/x/crypto/blake2b"
|
"golang.org/x/crypto/blake2b"
|
||||||
|
|
||||||
xioutil "github.com/minio/minio/internal/ioutil"
|
xioutil "github.com/minio/minio/internal/ioutil"
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ func verifyServerSystemConfig(ctx context.Context, endpointServerPools EndpointS
|
|||||||
for _, clnt := range clnts {
|
for _, clnt := range clnts {
|
||||||
if err := clnt.Verify(ctx, srcCfg); err != nil {
|
if err := clnt.Verify(ctx, srcCfg); err != nil {
|
||||||
if !isNetworkError(err) {
|
if !isNetworkError(err) {
|
||||||
logger.Info(fmt.Errorf("%s has incorrect configuration: %w", clnt.String(), err).Error())
|
logger.LogIf(ctx, fmt.Errorf("%s has incorrect configuration: %w", clnt.String(), err))
|
||||||
}
|
}
|
||||||
offlineEndpoints = append(offlineEndpoints, clnt.String())
|
offlineEndpoints = append(offlineEndpoints, clnt.String())
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ func (api objectAPIHandlers) GetBucketEncryptionHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetSSEConfig(bucket)
|
config, _, err := globalBucketMetadataSys.GetSSEConfig(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -206,6 +206,15 @@ func (api objectAPIHandlers) DeleteBucketEncryptionHandler(w http.ResponseWriter
|
|||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Call site replication hook.
|
||||||
|
//
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeSSEConfig,
|
||||||
|
Bucket: bucket,
|
||||||
|
SSEConfig: nil,
|
||||||
|
}); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ func (sys *BucketSSEConfigSys) Get(bucket string) (*sse.BucketSSEConfig, error)
|
|||||||
return nil, BucketSSEConfigNotFound{Bucket: bucket}
|
return nil, BucketSSEConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
|
|
||||||
return globalBucketMetadataSys.GetSSEConfig(bucket)
|
sseCfg, _, err := globalBucketMetadataSys.GetSSEConfig(bucket)
|
||||||
|
return sseCfg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateBucketSSEConfig parses bucket encryption configuration and validates if it is supported by MinIO.
|
// validateBucketSSEConfig parses bucket encryption configuration and validates if it is supported by MinIO.
|
||||||
|
|||||||
+55
-404
@@ -21,7 +21,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -33,7 +32,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -364,6 +362,18 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
}) {
|
}) {
|
||||||
bucketsInfo[n] = bucketInfo
|
bucketsInfo[n] = bucketInfo
|
||||||
n++
|
n++
|
||||||
|
} else if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
|
Action: iampolicy.GetBucketLocationAction,
|
||||||
|
BucketName: bucketInfo.Name,
|
||||||
|
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
||||||
|
IsOwner: owner,
|
||||||
|
ObjectName: "",
|
||||||
|
Claims: cred.Claims,
|
||||||
|
}) {
|
||||||
|
bucketsInfo[n] = bucketInfo
|
||||||
|
n++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bucketsInfo = bucketsInfo[:n]
|
bucketsInfo = bucketsInfo[:n]
|
||||||
@@ -471,9 +481,6 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
hasLockEnabled = true
|
hasLockEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
versioned := globalBucketVersioningSys.Enabled(bucket)
|
|
||||||
suspended := globalBucketVersioningSys.Suspended(bucket)
|
|
||||||
|
|
||||||
type deleteResult struct {
|
type deleteResult struct {
|
||||||
delInfo DeletedObject
|
delInfo DeletedObject
|
||||||
errInfo DeleteError
|
errInfo DeleteError
|
||||||
@@ -481,8 +488,8 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
|
|
||||||
deleteResults := make([]deleteResult, len(deleteObjectsReq.Objects))
|
deleteResults := make([]deleteResult, len(deleteObjectsReq.Objects))
|
||||||
|
|
||||||
|
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 := checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, object.ObjectName); apiErrCode != ErrNone {
|
||||||
if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID {
|
if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID {
|
||||||
@@ -514,11 +521,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
|
|
||||||
opts := ObjectOptions{
|
opts := ObjectOptions{
|
||||||
VersionID: object.VersionID,
|
VersionID: object.VersionID,
|
||||||
Versioned: versioned,
|
Versioned: vc.PrefixEnabled(object.ObjectName),
|
||||||
VersionSuspended: suspended,
|
VersionSuspended: vc.Suspended(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if replicateDeletes || object.VersionID != "" && hasLockEnabled || !globalTierConfigMgr.Empty() {
|
if replicateDeletes || hasLockEnabled || !globalTierConfigMgr.Empty() {
|
||||||
if !globalTierConfigMgr.Empty() && object.VersionID == "" && opts.VersionSuspended {
|
if !globalTierConfigMgr.Empty() && object.VersionID == "" && opts.VersionSuspended {
|
||||||
opts.VersionID = nullVersionID
|
opts.VersionID = nullVersionID
|
||||||
}
|
}
|
||||||
@@ -526,7 +533,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !globalTierConfigMgr.Empty() {
|
if !globalTierConfigMgr.Empty() {
|
||||||
oss[index] = newObjSweeper(bucket, object.ObjectName).WithVersion(opts.VersionID).WithVersioning(versioned, suspended)
|
oss[index] = newObjSweeper(bucket, object.ObjectName).WithVersion(opts.VersionID).WithVersioning(opts.Versioned, opts.VersionSuspended)
|
||||||
oss[index].SetTransitionState(goi.TransitionedObject)
|
oss[index].SetTransitionState(goi.TransitionedObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,7 +554,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
object.ReplicateDecisionStr = dsc.String()
|
object.ReplicateDecisionStr = dsc.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if object.VersionID != "" && hasLockEnabled {
|
if hasLockEnabled {
|
||||||
if apiErrCode := enforceRetentionBypassForDelete(ctx, r, bucket, object, goi, gerr); apiErrCode != ErrNone {
|
if apiErrCode := enforceRetentionBypassForDelete(ctx, r, bucket, object, goi, gerr); apiErrCode != ErrNone {
|
||||||
apiErr := errorCodes.ToAPIErr(apiErrCode)
|
apiErr := errorCodes.ToAPIErr(apiErrCode)
|
||||||
deleteResults[index].errInfo = DeleteError{
|
deleteResults[index].errInfo = DeleteError{
|
||||||
@@ -581,8 +588,8 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
|
|
||||||
deleteList := toNames(objectsToDelete)
|
deleteList := toNames(objectsToDelete)
|
||||||
dObjects, errs := deleteObjectsFn(ctx, bucket, deleteList, ObjectOptions{
|
dObjects, errs := deleteObjectsFn(ctx, bucket, deleteList, ObjectOptions{
|
||||||
Versioned: versioned,
|
PrefixEnabledFn: vc.PrefixEnabled,
|
||||||
VersionSuspended: suspended,
|
VersionSuspended: vc.Suspended(),
|
||||||
})
|
})
|
||||||
|
|
||||||
for i := range errs {
|
for i := range errs {
|
||||||
@@ -638,23 +645,13 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if replicateDeletes {
|
if replicateDeletes && (dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == Pending) {
|
||||||
if dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == Pending {
|
|
||||||
dv := DeletedObjectReplicationInfo{
|
dv := DeletedObjectReplicationInfo{
|
||||||
DeletedObject: dobj,
|
DeletedObject: dobj,
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
}
|
}
|
||||||
scheduleReplicationDelete(ctx, dv, objectAPI)
|
scheduleReplicationDelete(ctx, dv, objectAPI)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify deleted event for objects.
|
|
||||||
for _, dobj := range deletedObjects {
|
|
||||||
if dobj.ObjectName == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
eventName := event.ObjectRemovedDelete
|
eventName := event.ObjectRemovedDelete
|
||||||
objInfo := ObjectInfo{
|
objInfo := ObjectInfo{
|
||||||
@@ -706,13 +703,27 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
objectLockEnabled := false
|
objectLockEnabled := false
|
||||||
if vs, found := r.Header[http.CanonicalHeaderKey("x-amz-bucket-object-lock-enabled")]; found {
|
if vs := r.Header.Get(xhttp.AmzObjectLockEnabled); len(vs) > 0 {
|
||||||
v := strings.ToLower(strings.Join(vs, ""))
|
v := strings.ToLower(vs)
|
||||||
if v != "true" && v != "false" {
|
switch v {
|
||||||
|
case "true", "false":
|
||||||
|
objectLockEnabled = v == "true"
|
||||||
|
default:
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forceCreate := false
|
||||||
|
if vs := r.Header.Get(xhttp.MinIOForceCreate); len(vs) > 0 {
|
||||||
|
v := strings.ToLower(vs)
|
||||||
|
switch v {
|
||||||
|
case "true", "false":
|
||||||
|
forceCreate = v == "true"
|
||||||
|
default:
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
objectLockEnabled = v == "true"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.CreateBucketAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.CreateBucketAction, bucket, ""); s3Error != ErrNone {
|
||||||
@@ -737,6 +748,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
opts := BucketOptions{
|
opts := BucketOptions{
|
||||||
Location: location,
|
Location: location,
|
||||||
LockEnabled: objectLockEnabled,
|
LockEnabled: objectLockEnabled,
|
||||||
|
ForceCreate: forceCreate,
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalDNSConfig != nil {
|
if globalDNSConfig != nil {
|
||||||
@@ -1270,6 +1282,17 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
rcfg, err := getReplicationConfig(ctx, bucket)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
if _, ok := err.(BucketReplicationConfigNotFound); !ok {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case rcfg.HasActiveRules("", true):
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1339,7 +1362,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
|||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !globalIsErasure {
|
if globalIsGateway {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1363,7 +1386,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deny object locking configuration settings on existing buckets without object lock enabled.
|
// Deny object locking configuration settings on existing buckets without object lock enabled.
|
||||||
if _, err = globalBucketMetadataSys.GetObjectLockConfig(bucket); err != nil {
|
if _, _, err = globalBucketMetadataSys.GetObjectLockConfig(bucket); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1416,7 +1439,7 @@ func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
|
config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1518,7 +1541,7 @@ func (api objectAPIHandlers) GetBucketTaggingHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetTaggingConfig(bucket)
|
config, _, err := globalBucketMetadataSys.GetTaggingConfig(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1571,375 +1594,3 @@ func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r
|
|||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutBucketReplicationConfigHandler - PUT Bucket replication configuration.
|
|
||||||
// ----------
|
|
||||||
// Add a replication configuration on the specified bucket as specified in https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketReplication.html
|
|
||||||
func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "PutBucketReplicationConfig")
|
|
||||||
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 !globalIsErasure {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Check if bucket exists.
|
|
||||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if globalSiteReplicationSys.isEnabled() {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if versioned := globalBucketVersioningSys.Enabled(bucket); !versioned {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNeedsVersioningError), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
replicationConfig, err := replication.ParseConfig(io.LimitReader(r.Body, r.ContentLength))
|
|
||||||
if err != nil {
|
|
||||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
|
||||||
apiErr.Description = err.Error()
|
|
||||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig)
|
|
||||||
if apiErr != noError {
|
|
||||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Validate the received bucket replication config
|
|
||||||
if err = replicationConfig.Validate(bucket, sameTarget); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
configData, err := xml.Marshal(replicationConfig)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, configData); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBucketReplicationConfigHandler - GET Bucket replication configuration.
|
|
||||||
// ----------
|
|
||||||
// Gets the replication configuration for a bucket.
|
|
||||||
func (api objectAPIHandlers) GetBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "GetBucketReplicationConfig")
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if user has permissions to perform this operation
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Check if bucket exists.
|
|
||||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
configData, err := xml.Marshal(config)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessResponseXML(w, configData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBucketReplicationConfigHandler - DELETE Bucket replication config.
|
|
||||||
// ----------
|
|
||||||
func (api objectAPIHandlers) DeleteBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "DeleteBucketReplicationConfig")
|
|
||||||
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.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Check if bucket exists.
|
|
||||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if globalSiteReplicationSys.isEnabled() {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, nil); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBucketReplicationMetricsHandler - GET Bucket replication metrics.
|
|
||||||
// ----------
|
|
||||||
// Gets the replication metrics for a bucket.
|
|
||||||
func (api objectAPIHandlers) GetBucketReplicationMetricsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "GetBucketReplicationMetrics")
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if user has permissions to perform this operation
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if bucket exists.
|
|
||||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var usageInfo BucketUsageInfo
|
|
||||||
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
|
||||||
if err == nil && !dataUsageInfo.LastUpdate.IsZero() {
|
|
||||||
usageInfo = dataUsageInfo.BucketsUsage[bucket]
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set(xhttp.ContentType, string(mimeJSON))
|
|
||||||
|
|
||||||
enc := json.NewEncoder(w)
|
|
||||||
if err = enc.Encode(getLatestReplicationStats(bucket, usageInfo)); err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetBucketReplicationStartHandler - starts a replication reset for all objects in a bucket which
|
|
||||||
// qualify for replication and re-sync the object(s) to target, provided ExistingObjectReplication is
|
|
||||||
// enabled for the qualifying rule. This API is a MinIO only extension provided for situations where
|
|
||||||
// remote target is entirely lost,and previously replicated objects need to be re-synced. If resync is
|
|
||||||
// already in progress it returns an error
|
|
||||||
func (api objectAPIHandlers) ResetBucketReplicationStartHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "ResetBucketReplicationStart")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
durationStr := r.URL.Query().Get("older-than")
|
|
||||||
arn := r.URL.Query().Get("arn")
|
|
||||||
resetID := r.URL.Query().Get("reset-id")
|
|
||||||
if resetID == "" {
|
|
||||||
resetID = mustGetUUID()
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
days time.Duration
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if durationStr != "" {
|
|
||||||
days, err = time.ParseDuration(durationStr)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, InvalidArgument{
|
|
||||||
Bucket: bucket,
|
|
||||||
Err: fmt.Errorf("invalid query parameter older-than %s for %s : %w", durationStr, bucket, err),
|
|
||||||
}), r.URL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resetBeforeDate := UTCNow().AddDate(0, 0, -1*int(days/24))
|
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
|
||||||
if objectAPI == nil {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if bucket exists.
|
|
||||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !config.HasActiveRules("", true) {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNoMatchingRuleError), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tgtArns := config.FilterTargetArns(
|
|
||||||
replication.ObjectOpts{
|
|
||||||
OpType: replication.ResyncReplicationType,
|
|
||||||
TargetArn: arn,
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(tgtArns) == 0 {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
|
||||||
Bucket: bucket,
|
|
||||||
Err: fmt.Errorf("Remote target ARN %s missing or ineligible for replication resync", arn),
|
|
||||||
}), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tgtArns) > 1 && arn == "" {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
|
||||||
Bucket: bucket,
|
|
||||||
Err: fmt.Errorf("ARN should be specified for replication reset"),
|
|
||||||
}), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var rinfo ResyncTargetsInfo
|
|
||||||
target := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, tgtArns[0])
|
|
||||||
target.ResetBeforeDate = UTCNow().AddDate(0, 0, -1*int(days/24))
|
|
||||||
target.ResetID = resetID
|
|
||||||
rinfo.Targets = append(rinfo.Targets, ResyncTarget{Arn: tgtArns[0], ResetID: target.ResetID})
|
|
||||||
if err = globalBucketTargetSys.SetTarget(ctx, bucket, &target, true); err != nil {
|
|
||||||
switch err.(type) {
|
|
||||||
case BucketRemoteConnectionErr:
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationRemoteConnectionError, err), r.URL)
|
|
||||||
default:
|
|
||||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := startReplicationResync(ctx, bucket, arn, resetID, resetBeforeDate, objectAPI); err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
|
||||||
Bucket: bucket,
|
|
||||||
Err: err,
|
|
||||||
}), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(rinfo)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessResponseJSON(w, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetBucketReplicationStatusHandler - returns the status of replication reset.
|
|
||||||
// This API is a MinIO only extension
|
|
||||||
func (api objectAPIHandlers) ResetBucketReplicationStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "ResetBucketReplicationStatus")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
arn := r.URL.Query().Get("arn")
|
|
||||||
var err error
|
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
|
||||||
if objectAPI == nil {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if bucket exists.
|
|
||||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
globalReplicationPool.resyncState.RLock()
|
|
||||||
brs, ok := globalReplicationPool.resyncState.statusMap[bucket]
|
|
||||||
if !ok {
|
|
||||||
brs, err = loadBucketResyncMetadata(ctx, bucket, objectAPI)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
|
||||||
Bucket: bucket,
|
|
||||||
Err: fmt.Errorf("No replication resync status available for %s", arn),
|
|
||||||
}), r.URL)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var rinfo ResyncTargetsInfo
|
|
||||||
for tarn, st := range brs.TargetsMap {
|
|
||||||
if arn != "" && tarn != arn {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rinfo.Targets = append(rinfo.Targets, ResyncTarget{
|
|
||||||
Arn: tarn,
|
|
||||||
ResetID: st.ResyncID,
|
|
||||||
StartTime: st.StartTime,
|
|
||||||
EndTime: st.EndTime,
|
|
||||||
ResyncStatus: st.ResyncStatus.String(),
|
|
||||||
ReplicatedSize: st.ReplicatedSize,
|
|
||||||
ReplicatedCount: st.ReplicatedCount,
|
|
||||||
FailedSize: st.FailedSize,
|
|
||||||
FailedCount: st.FailedCount,
|
|
||||||
Bucket: st.Bucket,
|
|
||||||
Object: st.Object,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
globalReplicationPool.resyncState.RUnlock()
|
|
||||||
data, err := json.Marshal(rinfo)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessResponseJSON(w, data)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ func TestRemoveBucketHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testRemoveBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testRemoveBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
_, err := obj.PutObject(GlobalContext, bucketName, "test-object", mustGetPutObjReader(t, bytes.NewReader([]byte{}), int64(0), "", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), ObjectOptions{})
|
_, err := obj.PutObject(GlobalContext, bucketName, "test-object", mustGetPutObjReader(t, bytes.NewReader([]byte{}), int64(0), "", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), ObjectOptions{})
|
||||||
// if object upload fails stop the test.
|
// if object upload fails stop the test.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ func TestBucketLifecycleWrongCredentials(t *testing.T) {
|
|||||||
|
|
||||||
// Test for authentication
|
// Test for authentication
|
||||||
func testBucketLifecycleHandlersWrongCredentials(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testBucketLifecycleHandlersWrongCredentials(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
// test cases with sample input and expected output.
|
// test cases with sample input and expected output.
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
method string
|
method string
|
||||||
|
|||||||
+11
-11
@@ -247,11 +247,11 @@ func (t *transitionState) addLastDayStats(tier string, ts tierStats) {
|
|||||||
t.lastDayStats[tier].addStats(ts)
|
t.lastDayStats[tier].addStats(ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *transitionState) getDailyAllTierStats() dailyAllTierStats {
|
func (t *transitionState) getDailyAllTierStats() DailyAllTierStats {
|
||||||
t.lastDayMu.RLock()
|
t.lastDayMu.RLock()
|
||||||
defer t.lastDayMu.RUnlock()
|
defer t.lastDayMu.RUnlock()
|
||||||
|
|
||||||
res := make(dailyAllTierStats, len(t.lastDayStats))
|
res := make(DailyAllTierStats, len(t.lastDayStats))
|
||||||
for tier, st := range t.lastDayStats {
|
for tier, st := range t.lastDayStats {
|
||||||
res[tier] = st.clone()
|
res[tier] = st.clone()
|
||||||
}
|
}
|
||||||
@@ -330,7 +330,7 @@ const (
|
|||||||
// 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, action expireAction) error {
|
||||||
var opts ObjectOptions
|
var opts ObjectOptions
|
||||||
opts.Versioned = globalBucketVersioningSys.Enabled(oi.Bucket)
|
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 {
|
switch action {
|
||||||
@@ -415,8 +415,8 @@ func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo)
|
|||||||
ETag: oi.ETag,
|
ETag: oi.ETag,
|
||||||
},
|
},
|
||||||
VersionID: oi.VersionID,
|
VersionID: oi.VersionID,
|
||||||
Versioned: globalBucketVersioningSys.Enabled(oi.Bucket),
|
Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
|
||||||
VersionSuspended: globalBucketVersioningSys.Suspended(oi.Bucket),
|
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
|
||||||
MTime: oi.ModTime,
|
MTime: oi.ModTime,
|
||||||
}
|
}
|
||||||
return tier, objectAPI.TransitionObject(ctx, oi.Bucket, oi.Name, opts)
|
return tier, objectAPI.TransitionObject(ctx, oi.Bucket, oi.Name, opts)
|
||||||
@@ -575,8 +575,8 @@ func (r *RestoreObjectRequest) validate(ctx context.Context, objAPI ObjectLayer)
|
|||||||
|
|
||||||
// postRestoreOpts returns ObjectOptions with version-id from the POST restore object request for a given bucket and object.
|
// postRestoreOpts returns ObjectOptions with version-id from the POST restore object request for a given bucket and object.
|
||||||
func postRestoreOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) {
|
func postRestoreOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) {
|
||||||
versioned := globalBucketVersioningSys.Enabled(bucket)
|
versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
|
||||||
versionSuspended := globalBucketVersioningSys.Suspended(bucket)
|
versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
|
||||||
vid := strings.TrimSpace(r.Form.Get(xhttp.VersionID))
|
vid := strings.TrimSpace(r.Form.Get(xhttp.VersionID))
|
||||||
if vid != "" && vid != nullVersionID {
|
if vid != "" && vid != nullVersionID {
|
||||||
_, err := uuid.Parse(vid)
|
_, err := uuid.Parse(vid)
|
||||||
@@ -627,8 +627,8 @@ func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo O
|
|||||||
meta[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES
|
meta[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES
|
||||||
}
|
}
|
||||||
return ObjectOptions{
|
return ObjectOptions{
|
||||||
Versioned: globalBucketVersioningSys.Enabled(bucket),
|
Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, object),
|
||||||
VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
|
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, object),
|
||||||
UserDefined: meta,
|
UserDefined: meta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -640,8 +640,8 @@ func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo O
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ObjectOptions{
|
return ObjectOptions{
|
||||||
Versioned: globalBucketVersioningSys.Enabled(bucket),
|
Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, object),
|
||||||
VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
|
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, object),
|
||||||
UserDefined: meta,
|
UserDefined: meta,
|
||||||
VersionID: objInfo.VersionID,
|
VersionID: objInfo.VersionID,
|
||||||
MTime: objInfo.ModTime,
|
MTime: objInfo.ModTime,
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ func TestObjectIsRemote(t *testing.T) {
|
|||||||
if got := fi.IsRemote(); got != tc.remote {
|
if got := fi.IsRemote(); got != tc.remote {
|
||||||
t.Fatalf("Test %d.a: expected %v got %v", i+1, tc.remote, got)
|
t.Fatalf("Test %d.a: expected %v got %v", i+1, tc.remote, got)
|
||||||
}
|
}
|
||||||
oi := fi.ToObjectInfo("bucket", "object")
|
oi := fi.ToObjectInfo("bucket", "object", false)
|
||||||
if got := oi.IsRemote(); got != tc.remote {
|
if got := oi.IsRemote(); got != tc.remote {
|
||||||
t.Fatalf("Test %d.b: expected %v got %v", i+1, tc.remote, got)
|
t.Fatalf("Test %d.b: expected %v got %v", i+1, tc.remote, got)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -26,26 +27,9 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func concurrentDecryptETag(ctx context.Context, objects []ObjectInfo) {
|
|
||||||
g := errgroup.WithNErrs(len(objects)).WithConcurrency(500)
|
|
||||||
for index := range objects {
|
|
||||||
index := index
|
|
||||||
g.Go(func() error {
|
|
||||||
size, err := objects[index].GetActualSize()
|
|
||||||
if err == nil {
|
|
||||||
objects[index].Size = size
|
|
||||||
}
|
|
||||||
objects[index].ETag = objects[index].GetActualETag(nil)
|
|
||||||
return nil
|
|
||||||
}, index)
|
|
||||||
}
|
|
||||||
g.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all the ListObjects query arguments, returns an APIErrorCode
|
// Validate all the ListObjects query arguments, returns an APIErrorCode
|
||||||
// if one of the args do not meet the required conditions.
|
// if one of the args do not meet the required conditions.
|
||||||
// Special conditions required by MinIO server are as below
|
// Special conditions required by MinIO server are as below
|
||||||
@@ -116,7 +100,11 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
concurrentDecryptETag(ctx, listObjectVersionsInfo.Objects)
|
if err = DecryptETags(ctx, GlobalKMS, listObjectVersionsInfo.Objects); err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Failed to decrypt ETag: %v", err)) // TODO(aead): Remove once we are confident that decryption does not fail accidentially
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
response := generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType, maxkeys, listObjectVersionsInfo)
|
response := generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType, maxkeys, listObjectVersionsInfo)
|
||||||
|
|
||||||
@@ -178,7 +166,11 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
concurrentDecryptETag(ctx, listObjectsV2Info.Objects)
|
if err = DecryptETags(ctx, GlobalKMS, listObjectsV2Info.Objects); err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Failed to decrypt ETag: %v", err)) // TODO(aead): Remove once we are confident that decryption does not fail accidentially
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// The next continuation token has id@node_index format to optimize paginated listing
|
// The next continuation token has id@node_index format to optimize paginated listing
|
||||||
nextContinuationToken := listObjectsV2Info.NextContinuationToken
|
nextContinuationToken := listObjectsV2Info.NextContinuationToken
|
||||||
@@ -253,7 +245,11 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
concurrentDecryptETag(ctx, listObjectsV2Info.Objects)
|
if err = DecryptETags(ctx, GlobalKMS, listObjectsV2Info.Objects); err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Failed to decrypt ETag: %v", err)) // TODO(aead): Remove once we are confident that decryption does not fail accidentially
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -350,7 +346,11 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
concurrentDecryptETag(ctx, listObjectsInfo.Objects)
|
if err = DecryptETags(ctx, GlobalKMS, listObjectsInfo.Objects); err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Failed to decrypt ETag: %v", err)) // TODO(aead): Remove once we are confident that decryption does not fail accidentially
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
|
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
|
||||||
|
|
||||||
|
|||||||
+57
-56
@@ -23,6 +23,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/minio/madmin-go"
|
"github.com/minio/madmin-go"
|
||||||
"github.com/minio/minio-go/v7/pkg/tags"
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
@@ -111,31 +112,29 @@ func (sys *BucketMetadataSys) Update(ctx context.Context, bucket string, configF
|
|||||||
switch configFile {
|
switch configFile {
|
||||||
case bucketPolicyConfig:
|
case bucketPolicyConfig:
|
||||||
meta.PolicyConfigJSON = configData
|
meta.PolicyConfigJSON = configData
|
||||||
|
meta.PolicyConfigUpdatedAt = UTCNow()
|
||||||
case bucketNotificationConfig:
|
case bucketNotificationConfig:
|
||||||
meta.NotificationConfigXML = configData
|
meta.NotificationConfigXML = configData
|
||||||
case bucketLifecycleConfig:
|
case bucketLifecycleConfig:
|
||||||
meta.LifecycleConfigXML = configData
|
meta.LifecycleConfigXML = configData
|
||||||
case bucketSSEConfig:
|
case bucketSSEConfig:
|
||||||
meta.EncryptionConfigXML = configData
|
meta.EncryptionConfigXML = configData
|
||||||
|
meta.EncryptionConfigUpdatedAt = UTCNow()
|
||||||
case bucketTaggingConfig:
|
case bucketTaggingConfig:
|
||||||
meta.TaggingConfigXML = configData
|
meta.TaggingConfigXML = configData
|
||||||
|
meta.TaggingConfigUpdatedAt = UTCNow()
|
||||||
case bucketQuotaConfigFile:
|
case bucketQuotaConfigFile:
|
||||||
meta.QuotaConfigJSON = configData
|
meta.QuotaConfigJSON = configData
|
||||||
|
meta.QuotaConfigUpdatedAt = UTCNow()
|
||||||
case objectLockConfig:
|
case objectLockConfig:
|
||||||
if !globalIsErasure && !globalIsDistErasure {
|
|
||||||
return NotImplemented{}
|
|
||||||
}
|
|
||||||
meta.ObjectLockConfigXML = configData
|
meta.ObjectLockConfigXML = configData
|
||||||
|
meta.ObjectLockConfigUpdatedAt = UTCNow()
|
||||||
case bucketVersioningConfig:
|
case bucketVersioningConfig:
|
||||||
if !globalIsErasure && !globalIsDistErasure {
|
|
||||||
return NotImplemented{}
|
|
||||||
}
|
|
||||||
meta.VersioningConfigXML = configData
|
meta.VersioningConfigXML = configData
|
||||||
|
meta.VersioningConfigUpdatedAt = UTCNow()
|
||||||
case bucketReplicationConfig:
|
case bucketReplicationConfig:
|
||||||
if !globalIsErasure && !globalIsDistErasure {
|
|
||||||
return NotImplemented{}
|
|
||||||
}
|
|
||||||
meta.ReplicationConfigXML = configData
|
meta.ReplicationConfigXML = configData
|
||||||
|
meta.ReplicationConfigUpdatedAt = UTCNow()
|
||||||
case bucketTargetsFile:
|
case bucketTargetsFile:
|
||||||
meta.BucketTargetsConfigJSON, meta.BucketTargetsConfigMetaJSON, err = encryptBucketMetadata(meta.Name, configData, kms.Context{
|
meta.BucketTargetsConfigJSON, meta.BucketTargetsConfigMetaJSON, err = encryptBucketMetadata(meta.Name, configData, kms.Context{
|
||||||
bucket: meta.Name,
|
bucket: meta.Name,
|
||||||
@@ -186,44 +185,47 @@ 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, 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 {
|
||||||
return nil, err
|
if errors.Is(err, errConfigNotFound) {
|
||||||
|
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, meta.Created, nil
|
||||||
}
|
}
|
||||||
return meta.versioningConfig, nil
|
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, time.Time{}, err
|
||||||
|
}
|
||||||
|
return meta.versioningConfig, meta.VersioningConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, 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, BucketTaggingNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketTaggingNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
if meta.taggingConfig == nil {
|
if meta.taggingConfig == nil {
|
||||||
return nil, BucketTaggingNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketTaggingNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return meta.taggingConfig, nil
|
return meta.taggingConfig, meta.TaggingConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, 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, BucketObjectLockConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketObjectLockConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
if meta.objectLockConfig == nil {
|
if meta.objectLockConfig == nil {
|
||||||
return nil, BucketObjectLockConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketObjectLockConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return meta.objectLockConfig, nil
|
return meta.objectLockConfig, meta.ObjectLockConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLifecycleConfig returns configured lifecycle config
|
// GetLifecycleConfig returns configured lifecycle config
|
||||||
@@ -283,69 +285,79 @@ 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, 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, BucketSSEConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketSSEConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
if meta.sseConfig == nil {
|
if meta.sseConfig == nil {
|
||||||
return nil, BucketSSEConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketSSEConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return meta.sseConfig, nil
|
return meta.sseConfig, meta.EncryptionConfigUpdatedAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAt returns the time of creation of bucket
|
||||||
|
func (sys *BucketMetadataSys) CreatedAt(bucket string) (time.Time, error) {
|
||||||
|
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
return meta.Created.UTC(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, error) {
|
func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, time.Time, error) {
|
||||||
if globalIsGateway {
|
if globalIsGateway {
|
||||||
objAPI := newObjectLayerFn()
|
objAPI := newObjectLayerFn()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return nil, errServerNotInitialized
|
return nil, time.Time{}, errServerNotInitialized
|
||||||
}
|
}
|
||||||
return objAPI.GetBucketPolicy(GlobalContext, bucket)
|
p, err := objAPI.GetBucketPolicy(GlobalContext, bucket)
|
||||||
|
return p, UTCNow(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
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, BucketPolicyNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
if meta.policyConfig == nil {
|
if meta.policyConfig == nil {
|
||||||
return nil, BucketPolicyNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return meta.policyConfig, nil
|
return meta.policyConfig, meta.PolicyConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, 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 {
|
||||||
return nil, err
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
return meta.quotaConfig, nil
|
return meta.quotaConfig, meta.QuotaConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, error) {
|
func (sys *BucketMetadataSys) GetReplicationConfig(ctx context.Context, bucket string) (*replication.Config, 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, BucketReplicationConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketReplicationConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if meta.replicationConfig == nil {
|
if meta.replicationConfig == nil {
|
||||||
return nil, BucketReplicationConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketReplicationConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return meta.replicationConfig, nil
|
return meta.replicationConfig, meta.ReplicationConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketTargetsConfig returns configured bucket targets for this bucket
|
// GetBucketTargetsConfig returns configured bucket targets for this bucket
|
||||||
@@ -353,6 +365,9 @@ func (sys *BucketMetadataSys) GetReplicationConfig(ctx context.Context, bucket s
|
|||||||
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, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, errConfigNotFound) {
|
||||||
|
return nil, BucketRemoteTargetNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if meta.bucketTargetConfig == nil {
|
if meta.bucketTargetConfig == nil {
|
||||||
@@ -361,20 +376,6 @@ func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.Buc
|
|||||||
return meta.bucketTargetConfig, nil
|
return meta.bucketTargetConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketTarget returns the target for the bucket and arn.
|
|
||||||
func (sys *BucketMetadataSys) GetBucketTarget(bucket string, arn string) (madmin.BucketTarget, error) {
|
|
||||||
targets, err := sys.GetBucketTargetsConfig(bucket)
|
|
||||||
if err != nil {
|
|
||||||
return madmin.BucketTarget{}, err
|
|
||||||
}
|
|
||||||
for _, t := range targets.Targets {
|
|
||||||
if t.Arn == arn {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return madmin.BucketTarget{}, errConfigNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConfig returns a specific configuration from the bucket metadata.
|
// GetConfig returns a specific configuration from the bucket metadata.
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (BucketMetadata, error) {
|
func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (BucketMetadata, error) {
|
||||||
|
|||||||
+46
-2
@@ -81,6 +81,13 @@ type BucketMetadata struct {
|
|||||||
ReplicationConfigXML []byte
|
ReplicationConfigXML []byte
|
||||||
BucketTargetsConfigJSON []byte
|
BucketTargetsConfigJSON []byte
|
||||||
BucketTargetsConfigMetaJSON []byte
|
BucketTargetsConfigMetaJSON []byte
|
||||||
|
PolicyConfigUpdatedAt time.Time
|
||||||
|
ObjectLockConfigUpdatedAt time.Time
|
||||||
|
EncryptionConfigUpdatedAt time.Time
|
||||||
|
TaggingConfigUpdatedAt time.Time
|
||||||
|
QuotaConfigUpdatedAt time.Time
|
||||||
|
ReplicationConfigUpdatedAt time.Time
|
||||||
|
VersioningConfigUpdatedAt time.Time
|
||||||
|
|
||||||
// Unexported fields. Must be updated atomically.
|
// Unexported fields. Must be updated atomically.
|
||||||
policyConfig *policy.Policy
|
policyConfig *policy.Policy
|
||||||
@@ -98,9 +105,10 @@ type BucketMetadata struct {
|
|||||||
|
|
||||||
// newBucketMetadata creates BucketMetadata with the supplied name and Created to Now.
|
// newBucketMetadata creates BucketMetadata with the supplied name and Created to Now.
|
||||||
func newBucketMetadata(name string) BucketMetadata {
|
func newBucketMetadata(name string) BucketMetadata {
|
||||||
|
now := UTCNow()
|
||||||
return BucketMetadata{
|
return BucketMetadata{
|
||||||
Name: name,
|
Name: name,
|
||||||
Created: UTCNow(),
|
Created: now,
|
||||||
notificationConfig: &event.Config{
|
notificationConfig: &event.Config{
|
||||||
XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/",
|
XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/",
|
||||||
},
|
},
|
||||||
@@ -157,8 +165,13 @@ func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket strin
|
|||||||
if err := b.convertLegacyConfigs(ctx, objectAPI); err != nil {
|
if err := b.convertLegacyConfigs(ctx, objectAPI); err != nil {
|
||||||
return b, err
|
return b, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// migrate unencrypted remote targets
|
// migrate unencrypted remote targets
|
||||||
return b, b.migrateTargetConfig(ctx, objectAPI)
|
if err = b.migrateTargetConfig(ctx, objectAPI); err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
b.defaultTimestamps()
|
||||||
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseAllConfigs will parse all configs and populate the private fields.
|
// parseAllConfigs will parse all configs and populate the private fields.
|
||||||
@@ -347,6 +360,37 @@ func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI Obj
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// default timestamps to metadata Created timestamp if unset.
|
||||||
|
func (b *BucketMetadata) defaultTimestamps() {
|
||||||
|
if b.PolicyConfigUpdatedAt.IsZero() {
|
||||||
|
b.PolicyConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.EncryptionConfigUpdatedAt.IsZero() {
|
||||||
|
b.EncryptionConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.TaggingConfigUpdatedAt.IsZero() {
|
||||||
|
b.TaggingConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.ObjectLockConfigUpdatedAt.IsZero() {
|
||||||
|
b.ObjectLockConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.QuotaConfigUpdatedAt.IsZero() {
|
||||||
|
b.QuotaConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.ReplicationConfigUpdatedAt.IsZero() {
|
||||||
|
b.ReplicationConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.VersioningConfigUpdatedAt.IsZero() {
|
||||||
|
b.VersioningConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save config to supplied ObjectLayer api.
|
// Save config to supplied ObjectLayer api.
|
||||||
func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error {
|
func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error {
|
||||||
if err := b.parseAllConfigs(ctx, api); err != nil {
|
if err := b.parseAllConfigs(ctx, api); err != nil {
|
||||||
|
|||||||
+180
-5
@@ -108,6 +108,48 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
|
err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "PolicyConfigUpdatedAt":
|
||||||
|
z.PolicyConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "PolicyConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "ObjectLockConfigUpdatedAt":
|
||||||
|
z.ObjectLockConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjectLockConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "EncryptionConfigUpdatedAt":
|
||||||
|
z.EncryptionConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "EncryptionConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "TaggingConfigUpdatedAt":
|
||||||
|
z.TaggingConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "TaggingConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "QuotaConfigUpdatedAt":
|
||||||
|
z.QuotaConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "QuotaConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "ReplicationConfigUpdatedAt":
|
||||||
|
z.ReplicationConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "VersioningConfigUpdatedAt":
|
||||||
|
z.VersioningConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err = dc.Skip()
|
err = dc.Skip()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,9 +163,9 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
|
|
||||||
// EncodeMsg implements msgp.Encodable
|
// EncodeMsg implements msgp.Encodable
|
||||||
func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
|
func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
// map header, size 14
|
// map header, size 21
|
||||||
// write "Name"
|
// write "Name"
|
||||||
err = en.Append(0x8e, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
err = en.Append(0xde, 0x0, 0x15, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -262,15 +304,85 @@ func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
|
err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// write "PolicyConfigUpdatedAt"
|
||||||
|
err = en.Append(0xb5, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.PolicyConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "PolicyConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "ObjectLockConfigUpdatedAt"
|
||||||
|
err = en.Append(0xb9, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.ObjectLockConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjectLockConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "EncryptionConfigUpdatedAt"
|
||||||
|
err = en.Append(0xb9, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.EncryptionConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "EncryptionConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "TaggingConfigUpdatedAt"
|
||||||
|
err = en.Append(0xb6, 0x54, 0x61, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.TaggingConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "TaggingConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "QuotaConfigUpdatedAt"
|
||||||
|
err = en.Append(0xb4, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.QuotaConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "QuotaConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "ReplicationConfigUpdatedAt"
|
||||||
|
err = en.Append(0xba, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.ReplicationConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "VersioningConfigUpdatedAt"
|
||||||
|
err = en.Append(0xb9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.VersioningConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalMsg implements msgp.Marshaler
|
// MarshalMsg implements msgp.Marshaler
|
||||||
func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
|
func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
o = msgp.Require(b, z.Msgsize())
|
o = msgp.Require(b, z.Msgsize())
|
||||||
// map header, size 14
|
// map header, size 21
|
||||||
// string "Name"
|
// string "Name"
|
||||||
o = append(o, 0x8e, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
o = append(o, 0xde, 0x0, 0x15, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||||
o = msgp.AppendString(o, z.Name)
|
o = msgp.AppendString(o, z.Name)
|
||||||
// string "Created"
|
// string "Created"
|
||||||
o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
|
o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
|
||||||
@@ -311,6 +423,27 @@ func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
// string "BucketTargetsConfigMetaJSON"
|
// string "BucketTargetsConfigMetaJSON"
|
||||||
o = append(o, 0xbb, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e)
|
o = append(o, 0xbb, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e)
|
||||||
o = msgp.AppendBytes(o, z.BucketTargetsConfigMetaJSON)
|
o = msgp.AppendBytes(o, z.BucketTargetsConfigMetaJSON)
|
||||||
|
// string "PolicyConfigUpdatedAt"
|
||||||
|
o = append(o, 0xb5, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.PolicyConfigUpdatedAt)
|
||||||
|
// string "ObjectLockConfigUpdatedAt"
|
||||||
|
o = append(o, 0xb9, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.ObjectLockConfigUpdatedAt)
|
||||||
|
// string "EncryptionConfigUpdatedAt"
|
||||||
|
o = append(o, 0xb9, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.EncryptionConfigUpdatedAt)
|
||||||
|
// string "TaggingConfigUpdatedAt"
|
||||||
|
o = append(o, 0xb6, 0x54, 0x61, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.TaggingConfigUpdatedAt)
|
||||||
|
// string "QuotaConfigUpdatedAt"
|
||||||
|
o = append(o, 0xb4, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.QuotaConfigUpdatedAt)
|
||||||
|
// string "ReplicationConfigUpdatedAt"
|
||||||
|
o = append(o, 0xba, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.ReplicationConfigUpdatedAt)
|
||||||
|
// string "VersioningConfigUpdatedAt"
|
||||||
|
o = append(o, 0xb9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.VersioningConfigUpdatedAt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,6 +549,48 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
|
err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "PolicyConfigUpdatedAt":
|
||||||
|
z.PolicyConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "PolicyConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "ObjectLockConfigUpdatedAt":
|
||||||
|
z.ObjectLockConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjectLockConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "EncryptionConfigUpdatedAt":
|
||||||
|
z.EncryptionConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "EncryptionConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "TaggingConfigUpdatedAt":
|
||||||
|
z.TaggingConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "TaggingConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "QuotaConfigUpdatedAt":
|
||||||
|
z.QuotaConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "QuotaConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "ReplicationConfigUpdatedAt":
|
||||||
|
z.ReplicationConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "VersioningConfigUpdatedAt":
|
||||||
|
z.VersioningConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
bts, err = msgp.Skip(bts)
|
bts, err = msgp.Skip(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -430,6 +605,6 @@ func (z *BucketMetadata) 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 *BucketMetadata) Msgsize() (s int) {
|
func (z *BucketMetadata) Msgsize() (s int) {
|
||||||
s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON)
|
s = 3 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON) + 22 + msgp.TimeSize + 26 + msgp.TimeSize + 26 + msgp.TimeSize + 23 + msgp.TimeSize + 21 + msgp.TimeSize + 27 + msgp.TimeSize + 26 + msgp.TimeSize
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (sys *BucketObjectLockSys) Get(bucketName string) (r objectlock.Retention,
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetObjectLockConfig(bucketName)
|
config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(BucketObjectLockConfigNotFound); ok {
|
if _, ok := err.(BucketObjectLockConfigNotFound); ok {
|
||||||
return r, nil
|
return r, nil
|
||||||
|
|||||||
@@ -113,7 +113,8 @@ func TestCreateBucket(t *testing.T) {
|
|||||||
|
|
||||||
// testCreateBucket - Test for calling Create Bucket and ensure we get one and only one success.
|
// testCreateBucket - Test for calling Create Bucket and ensure we get one and only one success.
|
||||||
func testCreateBucket(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testCreateBucket(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
||||||
|
|
||||||
const n = 100
|
const n = 100
|
||||||
@@ -378,7 +379,8 @@ func TestGetBucketPolicyHandler(t *testing.T) {
|
|||||||
|
|
||||||
// testGetBucketPolicyHandler - Test for end point which fetches the access policy json of the given bucket.
|
// testGetBucketPolicyHandler - Test for end point which fetches the access policy json of the given bucket.
|
||||||
func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
// template for constructing HTTP request body for PUT bucket policy.
|
// template for constructing HTTP request body for PUT bucket policy.
|
||||||
bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Action":["s3:GetBucketLocation","s3:ListBucket"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s"]},{"Action":["s3:GetObject"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s/this*"]}]}`
|
bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Action":["s3:GetBucketLocation","s3:ListBucket"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s"]},{"Action":["s3:GetObject"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s/this*"]}]}`
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ type PolicySys struct{}
|
|||||||
|
|
||||||
// Get returns stored bucket policy
|
// Get returns stored bucket policy
|
||||||
func (sys *PolicySys) Get(bucket string) (*policy.Policy, error) {
|
func (sys *PolicySys) Get(bucket string) (*policy.Policy, error) {
|
||||||
return globalBucketMetadataSys.GetPolicyConfig(bucket)
|
policy, _, err := globalBucketMetadataSys.GetPolicyConfig(bucket)
|
||||||
|
return policy, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
||||||
|
|||||||
+2
-2
@@ -42,8 +42,8 @@ func (sys *BucketQuotaSys) Get(ctx context.Context, bucketName string) (*madmin.
|
|||||||
}
|
}
|
||||||
return &madmin.BucketQuota{}, nil
|
return &madmin.BucketQuota{}, nil
|
||||||
}
|
}
|
||||||
|
qCfg, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucketName)
|
||||||
return globalBucketMetadataSys.GetQuotaConfig(ctx, bucketName)
|
return qCfg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBucketQuotaSys returns initialized BucketQuotaSys
|
// NewBucketQuotaSys returns initialized BucketQuotaSys
|
||||||
|
|||||||
@@ -0,0 +1,404 @@
|
|||||||
|
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
|
"github.com/minio/minio/internal/logger"
|
||||||
|
"github.com/minio/pkg/bucket/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PutBucketReplicationConfigHandler - PUT Bucket replication configuration.
|
||||||
|
// ----------
|
||||||
|
// Add a replication configuration on the specified bucket as specified in https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketReplication.html
|
||||||
|
func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "PutBucketReplicationConfig")
|
||||||
|
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 globalIsGateway {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Check if bucket exists.
|
||||||
|
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if versioned := globalBucketVersioningSys.Enabled(bucket); !versioned {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNeedsVersioningError), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
replicationConfig, err := replication.ParseConfig(io.LimitReader(r.Body, r.ContentLength))
|
||||||
|
if err != nil {
|
||||||
|
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||||
|
apiErr.Description = err.Error()
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig, true)
|
||||||
|
if apiErr != noError {
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Validate the received bucket replication config
|
||||||
|
if err = replicationConfig.Validate(bucket, sameTarget); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configData, err := xml.Marshal(replicationConfig)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, configData); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write success response.
|
||||||
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBucketReplicationConfigHandler - GET Bucket replication configuration.
|
||||||
|
// ----------
|
||||||
|
// Gets the replication configuration for a bucket.
|
||||||
|
func (api objectAPIHandlers) GetBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "GetBucketReplicationConfig")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user has permissions to perform this operation
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Check if bucket exists.
|
||||||
|
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write success response.
|
||||||
|
writeSuccessResponseXML(w, configData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBucketReplicationConfigHandler - DELETE Bucket replication config.
|
||||||
|
// ----------
|
||||||
|
func (api objectAPIHandlers) DeleteBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "DeleteBucketReplicationConfig")
|
||||||
|
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.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Check if bucket exists.
|
||||||
|
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if globalSiteReplicationSys.isEnabled() {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, nil); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write success response.
|
||||||
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBucketReplicationMetricsHandler - GET Bucket replication metrics.
|
||||||
|
// ----------
|
||||||
|
// Gets the replication metrics for a bucket.
|
||||||
|
func (api objectAPIHandlers) GetBucketReplicationMetricsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "GetBucketReplicationMetrics")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user has permissions to perform this operation
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if bucket exists.
|
||||||
|
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var usageInfo BucketUsageInfo
|
||||||
|
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
||||||
|
if err == nil && !dataUsageInfo.LastUpdate.IsZero() {
|
||||||
|
usageInfo = dataUsageInfo.BucketsUsage[bucket]
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set(xhttp.ContentType, string(mimeJSON))
|
||||||
|
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
if err = enc.Encode(getLatestReplicationStats(bucket, usageInfo)); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetBucketReplicationStartHandler - starts a replication reset for all objects in a bucket which
|
||||||
|
// qualify for replication and re-sync the object(s) to target, provided ExistingObjectReplication is
|
||||||
|
// enabled for the qualifying rule. This API is a MinIO only extension provided for situations where
|
||||||
|
// remote target is entirely lost,and previously replicated objects need to be re-synced. If resync is
|
||||||
|
// already in progress it returns an error
|
||||||
|
func (api objectAPIHandlers) ResetBucketReplicationStartHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "ResetBucketReplicationStart")
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucket := vars["bucket"]
|
||||||
|
durationStr := r.URL.Query().Get("older-than")
|
||||||
|
arn := r.URL.Query().Get("arn")
|
||||||
|
resetID := r.URL.Query().Get("reset-id")
|
||||||
|
if resetID == "" {
|
||||||
|
resetID = mustGetUUID()
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
days time.Duration
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if durationStr != "" {
|
||||||
|
days, err = time.ParseDuration(durationStr)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, InvalidArgument{
|
||||||
|
Bucket: bucket,
|
||||||
|
Err: fmt.Errorf("invalid query parameter older-than %s for %s : %w", durationStr, bucket, err),
|
||||||
|
}), r.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetBeforeDate := UTCNow().AddDate(0, 0, -1*int(days/24))
|
||||||
|
|
||||||
|
objectAPI := api.ObjectAPI()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if bucket exists.
|
||||||
|
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !config.HasExistingObjectReplication(arn) {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNoExistingObjects), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tgtArns := config.FilterTargetArns(
|
||||||
|
replication.ObjectOpts{
|
||||||
|
OpType: replication.ResyncReplicationType,
|
||||||
|
TargetArn: arn,
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(tgtArns) == 0 {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||||
|
Bucket: bucket,
|
||||||
|
Err: fmt.Errorf("Remote target ARN %s missing or ineligible for replication resync", arn),
|
||||||
|
}), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tgtArns) > 1 && arn == "" {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||||
|
Bucket: bucket,
|
||||||
|
Err: fmt.Errorf("ARN should be specified for replication reset"),
|
||||||
|
}), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var rinfo ResyncTargetsInfo
|
||||||
|
target := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, tgtArns[0])
|
||||||
|
target.ResetBeforeDate = UTCNow().AddDate(0, 0, -1*int(days/24))
|
||||||
|
target.ResetID = resetID
|
||||||
|
rinfo.Targets = append(rinfo.Targets, ResyncTarget{Arn: tgtArns[0], ResetID: target.ResetID})
|
||||||
|
if err = globalBucketTargetSys.SetTarget(ctx, bucket, &target, true); err != nil {
|
||||||
|
switch err.(type) {
|
||||||
|
case BucketRemoteConnectionErr:
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationRemoteConnectionError, err), r.URL)
|
||||||
|
default:
|
||||||
|
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := startReplicationResync(ctx, bucket, arn, resetID, resetBeforeDate, objectAPI); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||||
|
Bucket: bucket,
|
||||||
|
Err: err,
|
||||||
|
}), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(rinfo)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Write success response.
|
||||||
|
writeSuccessResponseJSON(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetBucketReplicationStatusHandler - returns the status of replication reset.
|
||||||
|
// This API is a MinIO only extension
|
||||||
|
func (api objectAPIHandlers) ResetBucketReplicationStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "ResetBucketReplicationStatus")
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucket := vars["bucket"]
|
||||||
|
arn := r.URL.Query().Get("arn")
|
||||||
|
var err error
|
||||||
|
|
||||||
|
objectAPI := api.ObjectAPI()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if bucket exists.
|
||||||
|
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
globalReplicationPool.resyncState.RLock()
|
||||||
|
brs, ok := globalReplicationPool.resyncState.statusMap[bucket]
|
||||||
|
globalReplicationPool.resyncState.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
brs, err = loadBucketResyncMetadata(ctx, bucket, objectAPI)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||||
|
Bucket: bucket,
|
||||||
|
Err: fmt.Errorf("No replication resync status available for %s", arn),
|
||||||
|
}), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rinfo ResyncTargetsInfo
|
||||||
|
for tarn, st := range brs.TargetsMap {
|
||||||
|
if arn != "" && tarn != arn {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rinfo.Targets = append(rinfo.Targets, ResyncTarget{
|
||||||
|
Arn: tarn,
|
||||||
|
ResetID: st.ResyncID,
|
||||||
|
StartTime: st.StartTime,
|
||||||
|
EndTime: st.EndTime,
|
||||||
|
ResyncStatus: st.ResyncStatus.String(),
|
||||||
|
ReplicatedSize: st.ReplicatedSize,
|
||||||
|
ReplicatedCount: st.ReplicatedCount,
|
||||||
|
FailedSize: st.FailedSize,
|
||||||
|
FailedCount: st.FailedCount,
|
||||||
|
Bucket: st.Bucket,
|
||||||
|
Object: st.Object,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(rinfo)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write success response.
|
||||||
|
writeSuccessResponseJSON(w, data)
|
||||||
|
}
|
||||||
@@ -135,6 +135,23 @@ func (r *ReplicationStats) GetInitialUsage(bucket string) BucketReplicationStats
|
|||||||
return st.Clone()
|
return st.Clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAll returns replication metrics for all buckets at once.
|
||||||
|
func (r *ReplicationStats) GetAll() map[string]BucketReplicationStats {
|
||||||
|
if r == nil {
|
||||||
|
return map[string]BucketReplicationStats{}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.RLock()
|
||||||
|
defer r.RUnlock()
|
||||||
|
|
||||||
|
bucketReplicationStats := make(map[string]BucketReplicationStats, len(r.Cache))
|
||||||
|
for k, v := range r.Cache {
|
||||||
|
bucketReplicationStats[k] = v.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucketReplicationStats
|
||||||
|
}
|
||||||
|
|
||||||
// Get replication metrics for a bucket from this node since this node came up.
|
// Get replication metrics for a bucket from this node since this node came up.
|
||||||
func (r *ReplicationStats) Get(bucket string) BucketReplicationStats {
|
func (r *ReplicationStats) Get(bucket string) BucketReplicationStats {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
@@ -161,7 +178,7 @@ func NewReplicationStats(ctx context.Context, objectAPI ObjectLayer) *Replicatio
|
|||||||
|
|
||||||
// load replication metrics at cluster start from initial data usage
|
// load replication metrics at cluster start from initial data usage
|
||||||
func (r *ReplicationStats) loadInitialReplicationMetrics(ctx context.Context) {
|
func (r *ReplicationStats) loadInitialReplicationMetrics(ctx context.Context) {
|
||||||
rTimer := time.NewTimer(time.Minute * 1)
|
rTimer := time.NewTimer(time.Minute)
|
||||||
defer rTimer.Stop()
|
defer rTimer.Stop()
|
||||||
var (
|
var (
|
||||||
dui DataUsageInfo
|
dui DataUsageInfo
|
||||||
@@ -174,13 +191,12 @@ outer:
|
|||||||
return
|
return
|
||||||
case <-rTimer.C:
|
case <-rTimer.C:
|
||||||
dui, err = loadDataUsageFromBackend(GlobalContext, newObjectLayerFn())
|
dui, err = loadDataUsageFromBackend(GlobalContext, newObjectLayerFn())
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// If LastUpdate is set, data usage is available.
|
// If LastUpdate is set, data usage is available.
|
||||||
if !dui.LastUpdate.IsZero() {
|
if err == nil && !dui.LastUpdate.IsZero() {
|
||||||
break outer
|
break outer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rTimer.Reset(time.Minute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -27,6 +28,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio/internal/bucket/replication"
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate msgp -file=$GOFILE
|
//go:generate msgp -file=$GOFILE
|
||||||
@@ -507,7 +509,7 @@ func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) Repl
|
|||||||
ObjectName: oi.Name,
|
ObjectName: oi.Name,
|
||||||
VersionID: oi.VersionID,
|
VersionID: oi.VersionID,
|
||||||
},
|
},
|
||||||
}, oi, ObjectOptions{}, nil)
|
}, oi, ObjectOptions{VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name)}, nil)
|
||||||
} else {
|
} else {
|
||||||
dsc = mustReplicate(GlobalContext, oi.Bucket, oi.Name, getMustReplicateOptions(ObjectInfo{
|
dsc = mustReplicate(GlobalContext, oi.Bucket, oi.Name, getMustReplicateOptions(ObjectInfo{
|
||||||
UserDefined: oi.UserDefined,
|
UserDefined: oi.UserDefined,
|
||||||
@@ -699,3 +701,26 @@ func newBucketResyncStatus(bucket string) BucketReplicationResyncStatus {
|
|||||||
Version: resyncMetaVersion,
|
Version: resyncMetaVersion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var contentRangeRegexp = regexp.MustCompile(`bytes ([0-9]+)-([0-9]+)/([0-9]+|\\*)`)
|
||||||
|
|
||||||
|
// parse size from content-range header
|
||||||
|
func parseSizeFromContentRange(h http.Header) (sz int64, err error) {
|
||||||
|
cr := h.Get(xhttp.ContentRange)
|
||||||
|
if cr == "" {
|
||||||
|
return sz, fmt.Errorf("Content-Range not set")
|
||||||
|
}
|
||||||
|
parts := contentRangeRegexp.FindStringSubmatch(cr)
|
||||||
|
if len(parts) != 4 {
|
||||||
|
return sz, fmt.Errorf("invalid Content-Range header %s", cr)
|
||||||
|
}
|
||||||
|
if parts[3] == "*" {
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
var usz uint64
|
||||||
|
usz, err = strconv.ParseUint(parts[3], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return sz, err
|
||||||
|
}
|
||||||
|
return int64(usz), nil
|
||||||
|
}
|
||||||
|
|||||||
+129
-48
@@ -81,12 +81,13 @@ func getReplicationConfig(ctx context.Context, bucketName string) (rc *replicati
|
|||||||
return rc, BucketReplicationConfigNotFound{Bucket: bucketName}
|
return rc, BucketReplicationConfigNotFound{Bucket: bucketName}
|
||||||
}
|
}
|
||||||
|
|
||||||
return globalBucketMetadataSys.GetReplicationConfig(ctx, bucketName)
|
rCfg, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucketName)
|
||||||
|
return rCfg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateReplicationDestination returns error if replication destination bucket missing or not configured
|
// validateReplicationDestination returns error if replication destination bucket missing or not configured
|
||||||
// It also returns true if replication destination is same as this server.
|
// It also returns true if replication destination is same as this server.
|
||||||
func validateReplicationDestination(ctx context.Context, bucket string, rCfg *replication.Config) (bool, APIError) {
|
func validateReplicationDestination(ctx context.Context, bucket string, rCfg *replication.Config, checkRemote bool) (bool, APIError) {
|
||||||
var arns []string
|
var arns []string
|
||||||
if rCfg.RoleArn != "" {
|
if rCfg.RoleArn != "" {
|
||||||
arns = append(arns, rCfg.RoleArn)
|
arns = append(arns, rCfg.RoleArn)
|
||||||
@@ -95,26 +96,29 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
|
|||||||
arns = append(arns, rule.Destination.String())
|
arns = append(arns, rule.Destination.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var sameTarget bool
|
||||||
for _, arnStr := range arns {
|
for _, arnStr := range arns {
|
||||||
arn, err := madmin.ParseARN(arnStr)
|
arn, err := madmin.ParseARN(arnStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errorCodes.ToAPIErrWithErr(ErrBucketRemoteArnInvalid, err)
|
return sameTarget, errorCodes.ToAPIErrWithErr(ErrBucketRemoteArnInvalid, err)
|
||||||
}
|
}
|
||||||
if arn.Type != madmin.ReplicationService {
|
if arn.Type != madmin.ReplicationService {
|
||||||
return false, toAPIError(ctx, BucketRemoteArnTypeInvalid{Bucket: bucket})
|
return sameTarget, toAPIError(ctx, BucketRemoteArnTypeInvalid{Bucket: bucket})
|
||||||
}
|
}
|
||||||
clnt := globalBucketTargetSys.GetRemoteTargetClient(ctx, arnStr)
|
clnt := globalBucketTargetSys.GetRemoteTargetClient(ctx, arnStr)
|
||||||
if clnt == nil {
|
if clnt == nil {
|
||||||
return false, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
|
return sameTarget, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
|
||||||
}
|
}
|
||||||
|
if checkRemote { // validate remote bucket
|
||||||
if found, err := clnt.BucketExists(ctx, arn.Bucket); !found {
|
if found, err := clnt.BucketExists(ctx, arn.Bucket); !found {
|
||||||
return false, errorCodes.ToAPIErrWithErr(ErrRemoteDestinationNotFoundError, err)
|
return sameTarget, errorCodes.ToAPIErrWithErr(ErrRemoteDestinationNotFoundError, err)
|
||||||
}
|
}
|
||||||
if ret, err := globalBucketObjectLockSys.Get(bucket); err == nil {
|
if ret, err := globalBucketObjectLockSys.Get(bucket); err == nil {
|
||||||
if ret.LockEnabled {
|
if ret.LockEnabled {
|
||||||
lock, _, _, _, err := clnt.GetObjectLockConfig(ctx, arn.Bucket)
|
lock, _, _, _, err := clnt.GetObjectLockConfig(ctx, arn.Bucket)
|
||||||
if err != nil || lock != "Enabled" {
|
if err != nil || lock != "Enabled" {
|
||||||
return false, errorCodes.ToAPIErrWithErr(ErrReplicationDestinationMissingLock, err)
|
return sameTarget, errorCodes.ToAPIErrWithErr(ErrReplicationDestinationMissingLock, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,13 +126,20 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
|
|||||||
c, ok := globalBucketTargetSys.arnRemotesMap[arnStr]
|
c, ok := globalBucketTargetSys.arnRemotesMap[arnStr]
|
||||||
if ok {
|
if ok {
|
||||||
if c.EndpointURL().String() == clnt.EndpointURL().String() {
|
if c.EndpointURL().String() == clnt.EndpointURL().String() {
|
||||||
sameTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort)
|
selfTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort)
|
||||||
return sameTarget, toAPIError(ctx, nil)
|
if !sameTarget {
|
||||||
|
sameTarget = selfTarget
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(arns) == 0 {
|
||||||
return false, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
|
return false, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
|
||||||
}
|
}
|
||||||
|
return sameTarget, toAPIError(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
type mustReplicateOptions struct {
|
type mustReplicateOptions struct {
|
||||||
meta map[string]string
|
meta map[string]string
|
||||||
@@ -179,6 +190,17 @@ func mustReplicate(ctx context.Context, bucket, object string, mopts mustReplica
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// object layer not initialized we return with no decision.
|
||||||
|
if newObjectLayerFn() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable server-side replication on object prefixes which are excluded
|
||||||
|
// from versioning via the MinIO bucket versioning extension.
|
||||||
|
if globalBucketVersioningSys.PrefixSuspended(bucket, object) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
replStatus := mopts.ReplicationStatus()
|
replStatus := mopts.ReplicationStatus()
|
||||||
if replStatus == replication.Replica && !mopts.isMetadataReplication() {
|
if replStatus == replication.Replica && !mopts.isMetadataReplication() {
|
||||||
return
|
return
|
||||||
@@ -263,6 +285,11 @@ func checkReplicateDelete(ctx context.Context, bucket string, dobj ObjectToDelet
|
|||||||
if delOpts.ReplicationRequest {
|
if delOpts.ReplicationRequest {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Skip replication if this object's prefix is excluded from being
|
||||||
|
// versioned.
|
||||||
|
if !delOpts.Versioned {
|
||||||
|
return
|
||||||
|
}
|
||||||
opts := replication.ObjectOpts{
|
opts := replication.ObjectOpts{
|
||||||
Name: dobj.ObjectName,
|
Name: dobj.ObjectName,
|
||||||
SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
|
SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
|
||||||
@@ -425,7 +452,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(index int, tgt *TargetClient) {
|
go func(index int, tgt *TargetClient) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
rinfo := replicateDeleteToTarget(ctx, dobj, objectAPI, tgt)
|
rinfo := replicateDeleteToTarget(ctx, dobj, tgt)
|
||||||
rinfos.Targets[index] = rinfo
|
rinfos.Targets[index] = rinfo
|
||||||
}(idx, tgt)
|
}(idx, tgt)
|
||||||
}
|
}
|
||||||
@@ -456,8 +483,8 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
|||||||
VersionID: versionID,
|
VersionID: versionID,
|
||||||
MTime: dobj.DeleteMarkerMTime.Time,
|
MTime: dobj.DeleteMarkerMTime.Time,
|
||||||
DeleteReplication: drs,
|
DeleteReplication: drs,
|
||||||
Versioned: globalBucketVersioningSys.Enabled(bucket),
|
Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, dobj.ObjectName),
|
||||||
VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
|
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, dobj.ObjectName),
|
||||||
})
|
})
|
||||||
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))
|
logger.LogIf(ctx, fmt.Errorf("Unable to update replication metadata for %s/%s(%s): %s", bucket, dobj.ObjectName, versionID, err))
|
||||||
@@ -482,7 +509,7 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationInfo, objectAPI ObjectLayer, tgt *TargetClient) (rinfo replicatedTargetInfo) {
|
func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationInfo, tgt *TargetClient) (rinfo replicatedTargetInfo) {
|
||||||
versionID := dobj.DeleteMarkerVersionID
|
versionID := dobj.DeleteMarkerVersionID
|
||||||
if versionID == "" {
|
if versionID == "" {
|
||||||
versionID = dobj.VersionID
|
versionID = dobj.VersionID
|
||||||
@@ -1018,8 +1045,13 @@ func replicateObjectToTarget(ctx context.Context, ri ReplicateObjectInfo, object
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
versioned := globalBucketVersioningSys.PrefixEnabled(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{}, readLock, ObjectOptions{
|
||||||
VersionID: objInfo.VersionID,
|
VersionID: objInfo.VersionID,
|
||||||
|
Versioned: versioned,
|
||||||
|
VersionSuspended: versionSuspended,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
@@ -1531,16 +1563,21 @@ func initBackgroundReplication(ctx context.Context, objectAPI ObjectLayer) {
|
|||||||
go globalReplicationStats.loadInitialReplicationMetrics(ctx)
|
go globalReplicationStats.loadInitialReplicationMetrics(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type proxyResult struct {
|
||||||
|
Proxy bool
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
// get Reader from replication target if active-active replication is in place and
|
// get Reader from replication target if active-active replication is in place and
|
||||||
// this node returns a 404
|
// this node returns a 404
|
||||||
func proxyGetToReplicationTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (gr *GetObjectReader, proxy bool) {
|
func proxyGetToReplicationTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (gr *GetObjectReader, proxy proxyResult, err error) {
|
||||||
tgt, oi, proxy := proxyHeadToRepTarget(ctx, bucket, object, opts, proxyTargets)
|
tgt, oi, proxy := proxyHeadToRepTarget(ctx, bucket, object, rs, opts, proxyTargets)
|
||||||
if !proxy {
|
if !proxy.Proxy {
|
||||||
return nil, false
|
return nil, proxy, nil
|
||||||
}
|
}
|
||||||
fn, off, length, err := NewGetObjectReader(rs, oi, opts)
|
fn, _, _, err := NewGetObjectReader(nil, oi, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil, proxy, err
|
||||||
}
|
}
|
||||||
gopts := miniogo.GetObjectOptions{
|
gopts := miniogo.GetObjectOptions{
|
||||||
VersionID: opts.VersionID,
|
VersionID: opts.VersionID,
|
||||||
@@ -1548,33 +1585,47 @@ func proxyGetToReplicationTarget(ctx context.Context, bucket, object string, rs
|
|||||||
Internal: miniogo.AdvancedGetOptions{
|
Internal: miniogo.AdvancedGetOptions{
|
||||||
ReplicationProxyRequest: "true",
|
ReplicationProxyRequest: "true",
|
||||||
},
|
},
|
||||||
|
PartNumber: opts.PartNumber,
|
||||||
}
|
}
|
||||||
// get correct offsets for encrypted object
|
// get correct offsets for encrypted object
|
||||||
if off >= 0 && length >= 0 {
|
if rs != nil {
|
||||||
if err := gopts.SetRange(off, off+length-1); err != nil {
|
h, err := rs.ToHeader()
|
||||||
return nil, false
|
if err != nil {
|
||||||
|
return nil, proxy, err
|
||||||
}
|
}
|
||||||
|
gopts.Set(xhttp.Range, h)
|
||||||
}
|
}
|
||||||
// Make sure to match ETag when proxying.
|
// Make sure to match ETag when proxying.
|
||||||
if err = gopts.SetMatchETag(oi.ETag); err != nil {
|
if err = gopts.SetMatchETag(oi.ETag); err != nil {
|
||||||
return nil, false
|
return nil, proxy, err
|
||||||
}
|
}
|
||||||
c := miniogo.Core{Client: tgt.Client}
|
c := miniogo.Core{Client: tgt.Client}
|
||||||
obj, _, _, err := c.GetObject(ctx, bucket, object, gopts)
|
obj, _, h, err := c.GetObject(ctx, bucket, object, gopts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil, proxy, err
|
||||||
}
|
}
|
||||||
closeReader := func() { obj.Close() }
|
closeReader := func() { obj.Close() }
|
||||||
|
|
||||||
reader, err := fn(obj, h, closeReader)
|
reader, err := fn(obj, h, closeReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil, proxy, err
|
||||||
}
|
}
|
||||||
reader.ObjInfo = oi.Clone()
|
reader.ObjInfo = oi.Clone()
|
||||||
return reader, true
|
if rs != nil {
|
||||||
|
contentSize, err := parseSizeFromContentRange(h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, proxy, err
|
||||||
|
}
|
||||||
|
reader.ObjInfo.Size = contentSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader, proxyResult{Proxy: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProxyTargets(ctx context.Context, bucket, object string, opts ObjectOptions) (tgts *madmin.BucketTargets) {
|
||||||
|
if opts.VersionSuspended {
|
||||||
|
return &madmin.BucketTargets{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getproxyTargets(ctx context.Context, bucket, object string, opts ObjectOptions) (tgts *madmin.BucketTargets) {
|
|
||||||
cfg, err := getReplicationConfig(ctx, bucket)
|
cfg, err := getReplicationConfig(ctx, bucket)
|
||||||
if err != nil || cfg == nil {
|
if err != nil || cfg == nil {
|
||||||
return &madmin.BucketTargets{}
|
return &madmin.BucketTargets{}
|
||||||
@@ -1590,11 +1641,11 @@ func getproxyTargets(ctx context.Context, bucket, object string, opts ObjectOpti
|
|||||||
return tgts
|
return tgts
|
||||||
}
|
}
|
||||||
|
|
||||||
func proxyHeadToRepTarget(ctx context.Context, bucket, object string, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (tgt *TargetClient, oi ObjectInfo, proxy bool) {
|
func proxyHeadToRepTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (tgt *TargetClient, oi ObjectInfo, proxy proxyResult) {
|
||||||
// this option is set when active-active replication is in place between site A -> B,
|
// this option is set when active-active replication is in place between site A -> B,
|
||||||
// and site B does not have the object yet.
|
// and site B does not have the object yet.
|
||||||
if opts.ProxyRequest || (opts.ProxyHeaderSet && !opts.ProxyRequest) { // true only when site B sets MinIOSourceProxyRequest header
|
if opts.ProxyRequest || (opts.ProxyHeaderSet && !opts.ProxyRequest) { // true only when site B sets MinIOSourceProxyRequest header
|
||||||
return nil, oi, false
|
return nil, oi, proxy
|
||||||
}
|
}
|
||||||
for _, t := range proxyTargets.Targets {
|
for _, t := range proxyTargets.Targets {
|
||||||
tgt = globalBucketTargetSys.GetRemoteTargetClient(ctx, t.Arn)
|
tgt = globalBucketTargetSys.GetRemoteTargetClient(ctx, t.Arn)
|
||||||
@@ -1612,9 +1663,22 @@ func proxyHeadToRepTarget(ctx context.Context, bucket, object string, opts Objec
|
|||||||
Internal: miniogo.AdvancedGetOptions{
|
Internal: miniogo.AdvancedGetOptions{
|
||||||
ReplicationProxyRequest: "true",
|
ReplicationProxyRequest: "true",
|
||||||
},
|
},
|
||||||
|
PartNumber: opts.PartNumber,
|
||||||
}
|
}
|
||||||
|
if rs != nil {
|
||||||
|
h, err := rs.ToHeader()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Invalid range header for %s/%s(%s) - %w", bucket, object, opts.VersionID, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
gopts.Set(xhttp.Range, h)
|
||||||
|
}
|
||||||
|
|
||||||
objInfo, err := tgt.StatObject(ctx, t.TargetBucket, object, gopts)
|
objInfo, err := tgt.StatObject(ctx, t.TargetBucket, object, gopts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if isErrInvalidRange(ErrorRespToObjectError(err, bucket, object)) {
|
||||||
|
return nil, oi, proxyResult{Err: err}
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1645,15 +1709,15 @@ func proxyHeadToRepTarget(ctx context.Context, bucket, object string, opts Objec
|
|||||||
if ok {
|
if ok {
|
||||||
oi.ContentEncoding = ce
|
oi.ContentEncoding = ce
|
||||||
}
|
}
|
||||||
return tgt, oi, true
|
return tgt, oi, proxyResult{Proxy: true}
|
||||||
}
|
}
|
||||||
return nil, oi, false
|
return nil, oi, proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
// get object info from replication target if active-active replication is in place and
|
// get object info from replication target if active-active replication is in place and
|
||||||
// this node returns a 404
|
// this node returns a 404
|
||||||
func proxyHeadToReplicationTarget(ctx context.Context, bucket, object string, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (oi ObjectInfo, proxy bool) {
|
func proxyHeadToReplicationTarget(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, opts ObjectOptions, proxyTargets *madmin.BucketTargets) (oi ObjectInfo, proxy proxyResult) {
|
||||||
_, oi, proxy = proxyHeadToRepTarget(ctx, bucket, object, opts, proxyTargets)
|
_, oi, proxy = proxyHeadToRepTarget(ctx, bucket, object, rs, opts, proxyTargets)
|
||||||
return oi, proxy
|
return oi, proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1698,10 +1762,6 @@ func (c replicationConfig) Resync(ctx context.Context, oi ObjectInfo, dsc *Repli
|
|||||||
if c.Empty() {
|
if c.Empty() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// existing object replication does not apply to un-versioned objects
|
|
||||||
if oi.VersionID == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now overlay existing object replication choices for target
|
// Now overlay existing object replication choices for target
|
||||||
if oi.DeleteMarker {
|
if oi.DeleteMarker {
|
||||||
@@ -1802,9 +1862,26 @@ func resyncTarget(oi ObjectInfo, arn string, resetID string, resetBeforeDate tim
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the most current of in-memory replication stats and data usage info from crawler.
|
func getAllLatestReplicationStats(bucketsUsage map[string]BucketUsageInfo) (bucketsReplicationStats map[string]BucketReplicationStats) {
|
||||||
func getLatestReplicationStats(bucket string, u BucketUsageInfo) (s BucketReplicationStats) {
|
peerBucketStatsList := globalNotificationSys.GetClusterAllBucketStats(GlobalContext)
|
||||||
bucketStats := globalNotificationSys.GetClusterBucketStats(GlobalContext, bucket)
|
bucketsReplicationStats = make(map[string]BucketReplicationStats, len(bucketsUsage))
|
||||||
|
|
||||||
|
for bucket, u := range bucketsUsage {
|
||||||
|
bucketStats := make([]BucketStats, len(peerBucketStatsList))
|
||||||
|
for i, peerBucketStats := range peerBucketStatsList {
|
||||||
|
bucketStat, ok := peerBucketStats[bucket]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bucketStats[i] = bucketStat
|
||||||
|
}
|
||||||
|
bucketsReplicationStats[bucket] = calculateBucketReplicationStats(bucket, u, bucketStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucketsReplicationStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateBucketReplicationStats(bucket string, u BucketUsageInfo, bucketStats []BucketStats) (s BucketReplicationStats) {
|
||||||
// accumulate cluster bucket stats
|
// accumulate cluster bucket stats
|
||||||
stats := make(map[string]*BucketReplicationStat)
|
stats := make(map[string]*BucketReplicationStat)
|
||||||
var totReplicaSize int64
|
var totReplicaSize int64
|
||||||
@@ -1873,6 +1950,12 @@ func getLatestReplicationStats(bucket string, u BucketUsageInfo) (s BucketReplic
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get the most current of in-memory replication stats and data usage info from crawler.
|
||||||
|
func getLatestReplicationStats(bucket string, u BucketUsageInfo) (s BucketReplicationStats) {
|
||||||
|
bucketStats := globalNotificationSys.GetClusterBucketStats(GlobalContext, bucket)
|
||||||
|
return calculateBucketReplicationStats(bucket, u, bucketStats)
|
||||||
|
}
|
||||||
|
|
||||||
const resyncTimeInterval = time.Minute * 10
|
const resyncTimeInterval = time.Minute * 10
|
||||||
|
|
||||||
// periodicResyncMetaSave saves in-memory resync meta stats to disk in periodic intervals
|
// periodicResyncMetaSave saves in-memory resync meta stats to disk in periodic intervals
|
||||||
@@ -1883,7 +1966,6 @@ func (p *ReplicationPool) periodicResyncMetaSave(ctx context.Context, objectAPI
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-resyncTimer.C:
|
case <-resyncTimer.C:
|
||||||
resyncTimer.Reset(resyncTimeInterval)
|
|
||||||
now := UTCNow()
|
now := UTCNow()
|
||||||
p.resyncState.RLock()
|
p.resyncState.RLock()
|
||||||
for bucket, brs := range p.resyncState.statusMap {
|
for bucket, brs := range p.resyncState.statusMap {
|
||||||
@@ -1904,6 +1986,8 @@ func (p *ReplicationPool) periodicResyncMetaSave(ctx context.Context, objectAPI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.resyncState.RUnlock()
|
p.resyncState.RUnlock()
|
||||||
|
|
||||||
|
resyncTimer.Reset(resyncTimeInterval)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// server could be restarting - need
|
// server could be restarting - need
|
||||||
// to exit immediately
|
// to exit immediately
|
||||||
@@ -1923,6 +2007,7 @@ func resyncBucket(ctx context.Context, bucket, arn string, heal bool, objectAPI
|
|||||||
st.EndTime = UTCNow()
|
st.EndTime = UTCNow()
|
||||||
st.ResyncStatus = resyncStatus
|
st.ResyncStatus = resyncStatus
|
||||||
m.TargetsMap[arn] = st
|
m.TargetsMap[arn] = st
|
||||||
|
globalReplicationPool.resyncState.statusMap[bucket] = m
|
||||||
globalReplicationPool.resyncState.Unlock()
|
globalReplicationPool.resyncState.Unlock()
|
||||||
}()
|
}()
|
||||||
// Allocate new results channel to receive ObjectInfo.
|
// Allocate new results channel to receive ObjectInfo.
|
||||||
@@ -1983,7 +2068,6 @@ func resyncBucket(ctx context.Context, bucket, arn string, heal bool, objectAPI
|
|||||||
}
|
}
|
||||||
|
|
||||||
if roi.DeleteMarker || !roi.VersionPurgeStatus.Empty() {
|
if roi.DeleteMarker || !roi.VersionPurgeStatus.Empty() {
|
||||||
|
|
||||||
versionID := ""
|
versionID := ""
|
||||||
dmVersionID := ""
|
dmVersionID := ""
|
||||||
if roi.VersionPurgeStatus.Empty() {
|
if roi.VersionPurgeStatus.Empty() {
|
||||||
@@ -2030,6 +2114,7 @@ func resyncBucket(ctx context.Context, bucket, arn string, heal bool, objectAPI
|
|||||||
st.ReplicatedSize += roi.Size
|
st.ReplicatedSize += roi.Size
|
||||||
}
|
}
|
||||||
m.TargetsMap[arn] = st
|
m.TargetsMap[arn] = st
|
||||||
|
globalReplicationPool.resyncState.statusMap[bucket] = m
|
||||||
globalReplicationPool.resyncState.Unlock()
|
globalReplicationPool.resyncState.Unlock()
|
||||||
}
|
}
|
||||||
resyncStatus = ResyncCompleted
|
resyncStatus = ResyncCompleted
|
||||||
@@ -2110,10 +2195,6 @@ func (p *ReplicationPool) initResync(ctx context.Context, buckets []BucketInfo,
|
|||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return errServerNotInitialized
|
return errServerNotInitialized
|
||||||
}
|
}
|
||||||
// replication applies only to erasure coded setups
|
|
||||||
if !globalIsErasure {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Load bucket metadata sys in background
|
// Load bucket metadata sys in background
|
||||||
go p.loadResync(ctx, buckets, objAPI)
|
go p.loadResync(ctx, buckets, objAPI)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ func (rl *ReplicationLatency) update(size int64, duration time.Duration) {
|
|||||||
rl.UploadHistogram.Add(size, duration)
|
rl.UploadHistogram.Add(size, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BucketStatsMap captures bucket statistics for all buckets
|
||||||
|
type BucketStatsMap map[string]BucketStats
|
||||||
|
|
||||||
// BucketStats bucket statistics
|
// BucketStats bucket statistics
|
||||||
type BucketStats struct {
|
type BucketStats struct {
|
||||||
ReplicationStats BucketReplicationStats
|
ReplicationStats BucketReplicationStats
|
||||||
|
|||||||
@@ -792,6 +792,183 @@ func (z *BucketStats) Msgsize() (s int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DecodeMsg implements msgp.Decodable
|
||||||
|
func (z *BucketStatsMap) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||||
|
var zb0003 uint32
|
||||||
|
zb0003, err = dc.ReadMapHeader()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (*z) == nil {
|
||||||
|
(*z) = make(BucketStatsMap, zb0003)
|
||||||
|
} else if len((*z)) > 0 {
|
||||||
|
for key := range *z {
|
||||||
|
delete((*z), key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for zb0003 > 0 {
|
||||||
|
zb0003--
|
||||||
|
var zb0001 string
|
||||||
|
var zb0002 BucketStats
|
||||||
|
zb0001, err = dc.ReadString()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var field []byte
|
||||||
|
_ = field
|
||||||
|
var zb0004 uint32
|
||||||
|
zb0004, err = dc.ReadMapHeader()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, zb0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for zb0004 > 0 {
|
||||||
|
zb0004--
|
||||||
|
field, err = dc.ReadMapKeyPtr()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, zb0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch msgp.UnsafeString(field) {
|
||||||
|
case "ReplicationStats":
|
||||||
|
err = zb0002.ReplicationStats.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, zb0001, "ReplicationStats")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = dc.Skip()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, zb0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*z)[zb0001] = zb0002
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeMsg implements msgp.Encodable
|
||||||
|
func (z BucketStatsMap) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
|
err = en.WriteMapHeader(uint32(len(z)))
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for zb0005, zb0006 := range z {
|
||||||
|
err = en.WriteString(zb0005)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// map header, size 1
|
||||||
|
// write "ReplicationStats"
|
||||||
|
err = en.Append(0x81, 0xb0, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = zb0006.ReplicationStats.EncodeMsg(en)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, zb0005, "ReplicationStats")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalMsg implements msgp.Marshaler
|
||||||
|
func (z BucketStatsMap) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
|
o = msgp.Require(b, z.Msgsize())
|
||||||
|
o = msgp.AppendMapHeader(o, uint32(len(z)))
|
||||||
|
for zb0005, zb0006 := range z {
|
||||||
|
o = msgp.AppendString(o, zb0005)
|
||||||
|
// map header, size 1
|
||||||
|
// string "ReplicationStats"
|
||||||
|
o = append(o, 0x81, 0xb0, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73)
|
||||||
|
o, err = zb0006.ReplicationStats.MarshalMsg(o)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, zb0005, "ReplicationStats")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalMsg implements msgp.Unmarshaler
|
||||||
|
func (z *BucketStatsMap) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||||
|
var zb0003 uint32
|
||||||
|
zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (*z) == nil {
|
||||||
|
(*z) = make(BucketStatsMap, zb0003)
|
||||||
|
} else if len((*z)) > 0 {
|
||||||
|
for key := range *z {
|
||||||
|
delete((*z), key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for zb0003 > 0 {
|
||||||
|
var zb0001 string
|
||||||
|
var zb0002 BucketStats
|
||||||
|
zb0003--
|
||||||
|
zb0001, bts, err = msgp.ReadStringBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var field []byte
|
||||||
|
_ = field
|
||||||
|
var zb0004 uint32
|
||||||
|
zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, zb0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for zb0004 > 0 {
|
||||||
|
zb0004--
|
||||||
|
field, bts, err = msgp.ReadMapKeyZC(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, zb0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch msgp.UnsafeString(field) {
|
||||||
|
case "ReplicationStats":
|
||||||
|
bts, err = zb0002.ReplicationStats.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, zb0001, "ReplicationStats")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
bts, err = msgp.Skip(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, zb0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*z)[zb0001] = zb0002
|
||||||
|
}
|
||||||
|
o = bts
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||||
|
func (z BucketStatsMap) Msgsize() (s int) {
|
||||||
|
s = msgp.MapHeaderSize
|
||||||
|
if z != nil {
|
||||||
|
for zb0005, zb0006 := range z {
|
||||||
|
_ = zb0006
|
||||||
|
s += msgp.StringPrefixSize + len(zb0005) + 1 + 17 + zb0006.ReplicationStats.Msgsize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// DecodeMsg implements msgp.Decodable
|
// DecodeMsg implements msgp.Decodable
|
||||||
func (z *ReplicationLatency) DecodeMsg(dc *msgp.Reader) (err error) {
|
func (z *ReplicationLatency) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||||
var field []byte
|
var field []byte
|
||||||
|
|||||||
@@ -348,6 +348,119 @@ func BenchmarkDecodeBucketStats(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMarshalUnmarshalBucketStatsMap(t *testing.T) {
|
||||||
|
v := BucketStatsMap{}
|
||||||
|
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 BenchmarkMarshalMsgBucketStatsMap(b *testing.B) {
|
||||||
|
v := BucketStatsMap{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgBucketStatsMap(b *testing.B) {
|
||||||
|
v := BucketStatsMap{}
|
||||||
|
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 BenchmarkUnmarshalBucketStatsMap(b *testing.B) {
|
||||||
|
v := BucketStatsMap{}
|
||||||
|
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 TestEncodeDecodeBucketStatsMap(t *testing.T) {
|
||||||
|
v := BucketStatsMap{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodeBucketStatsMap Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := BucketStatsMap{}
|
||||||
|
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 BenchmarkEncodeBucketStatsMap(b *testing.B) {
|
||||||
|
v := BucketStatsMap{}
|
||||||
|
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 BenchmarkDecodeBucketStatsMap(b *testing.B) {
|
||||||
|
v := BucketStatsMap{}
|
||||||
|
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 TestMarshalUnmarshalReplicationLatency(t *testing.T) {
|
func TestMarshalUnmarshalReplicationLatency(t *testing.T) {
|
||||||
v := ReplicationLatency{}
|
v := ReplicationLatency{}
|
||||||
bts, err := v.MarshalMsg(nil)
|
bts, err := v.MarshalMsg(nil)
|
||||||
|
|||||||
@@ -120,9 +120,6 @@ func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *m
|
|||||||
return BucketRemoteConnectionErr{Bucket: tgt.TargetBucket, Err: err}
|
return BucketRemoteConnectionErr{Bucket: tgt.TargetBucket, Err: err}
|
||||||
}
|
}
|
||||||
if tgt.Type == madmin.ReplicationService {
|
if tgt.Type == madmin.ReplicationService {
|
||||||
if !globalIsErasure {
|
|
||||||
return NotImplemented{Message: "Replication is not implemented in " + getMinioMode()}
|
|
||||||
}
|
|
||||||
if !globalBucketVersioningSys.Enabled(bucket) {
|
if !globalBucketVersioningSys.Enabled(bucket) {
|
||||||
return BucketReplicationSourceNotVersioned{Bucket: bucket}
|
return BucketReplicationSourceNotVersioned{Bucket: bucket}
|
||||||
}
|
}
|
||||||
@@ -184,9 +181,6 @@ func (sys *BucketTargetSys) RemoveTarget(ctx context.Context, bucket, arnStr str
|
|||||||
if globalIsGateway {
|
if globalIsGateway {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !globalIsErasure {
|
|
||||||
return NotImplemented{Message: "Replication is not implemented in " + getMinioMode()}
|
|
||||||
}
|
|
||||||
|
|
||||||
if arnStr == "" {
|
if arnStr == "" {
|
||||||
return BucketRemoteArnInvalid{Bucket: bucket}
|
return BucketRemoteArnInvalid{Bucket: bucket}
|
||||||
@@ -201,7 +195,7 @@ func (sys *BucketTargetSys) RemoveTarget(ctx context.Context, bucket, arnStr str
|
|||||||
// reject removal of remote target if replication configuration is present
|
// reject removal of remote target if replication configuration is present
|
||||||
rcfg, err := getReplicationConfig(ctx, bucket)
|
rcfg, err := getReplicationConfig(ctx, bucket)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, tgtArn := range rcfg.FilterTargetArns(replication.ObjectOpts{}) {
|
for _, tgtArn := range rcfg.FilterTargetArns(replication.ObjectOpts{OpType: replication.AllReplicationType}) {
|
||||||
if err == nil && (tgtArn == arnStr || rcfg.RoleArn == arnStr) {
|
if err == nil && (tgtArn == arnStr || rcfg.RoleArn == arnStr) {
|
||||||
sys.RLock()
|
sys.RLock()
|
||||||
_, ok := sys.arnRemotesMap[arnStr]
|
_, ok := sys.arnRemotesMap[arnStr]
|
||||||
|
|||||||
@@ -18,12 +18,14 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
humanize "github.com/dustin/go-humanize"
|
humanize "github.com/dustin/go-humanize"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/madmin-go"
|
||||||
"github.com/minio/minio/internal/bucket/versioning"
|
"github.com/minio/minio/internal/bucket/versioning"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/bucket/policy"
|
"github.com/minio/pkg/bucket/policy"
|
||||||
@@ -63,28 +65,28 @@ func (api objectAPIHandlers) PutBucketVersioningHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalSiteReplicationSys.isEnabled() {
|
if globalSiteReplicationSys.isEnabled() && !v.Enabled() {
|
||||||
writeErrorResponse(ctx, w, APIError{
|
writeErrorResponse(ctx, w, APIError{
|
||||||
Code: "InvalidBucketState",
|
Code: "InvalidBucketState",
|
||||||
Description: "Cluster replication is enabled for this site, so the versioning state cannot be changed.",
|
Description: "Cluster replication is enabled on this site, versioning cannot be suspended on bucket.",
|
||||||
HTTPStatusCode: http.StatusConflict,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
}, r.URL)
|
}, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled && v.Suspended() {
|
if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled && (v.Suspended() || v.PrefixesExcluded()) {
|
||||||
writeErrorResponse(ctx, w, APIError{
|
writeErrorResponse(ctx, w, APIError{
|
||||||
Code: "InvalidBucketState",
|
Code: "InvalidBucketState",
|
||||||
Description: "An Object Lock configuration is present on this bucket, so the versioning state cannot be changed.",
|
Description: "An Object Lock configuration is present on this bucket, versioning cannot be suspended.",
|
||||||
HTTPStatusCode: http.StatusConflict,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
}, r.URL)
|
}, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := getReplicationConfig(ctx, bucket); err == nil && v.Suspended() {
|
if _, err := getReplicationConfig(ctx, bucket); err == nil && v.Suspended() {
|
||||||
writeErrorResponse(ctx, w, APIError{
|
writeErrorResponse(ctx, w, APIError{
|
||||||
Code: "InvalidBucketState",
|
Code: "InvalidBucketState",
|
||||||
Description: "A replication configuration is present on this bucket, so the versioning state cannot be changed.",
|
Description: "A replication configuration is present on this bucket, bucket wide versioning cannot be suspended.",
|
||||||
HTTPStatusCode: http.StatusConflict,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
}, r.URL)
|
}, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -100,6 +102,20 @@ func (api objectAPIHandlers) PutBucketVersioningHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call site replication hook.
|
||||||
|
//
|
||||||
|
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||||
|
// errors.
|
||||||
|
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeVersionConfig,
|
||||||
|
Bucket: bucket,
|
||||||
|
Versioning: &cfgStr,
|
||||||
|
}); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+40
-15
@@ -17,44 +17,69 @@
|
|||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import "github.com/minio/minio/internal/bucket/versioning"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/minio/minio/internal/bucket/versioning"
|
||||||
|
"github.com/minio/minio/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
// BucketVersioningSys - policy subsystem.
|
// BucketVersioningSys - policy subsystem.
|
||||||
type BucketVersioningSys struct{}
|
type BucketVersioningSys struct{}
|
||||||
|
|
||||||
// Enabled enabled versioning?
|
// Enabled enabled versioning?
|
||||||
func (sys *BucketVersioningSys) Enabled(bucket string) bool {
|
func (sys *BucketVersioningSys) Enabled(bucket string) bool {
|
||||||
vc, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
|
vc, err := sys.Get(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
logger.CriticalIf(GlobalContext, err)
|
||||||
}
|
}
|
||||||
return vc.Enabled()
|
return vc.Enabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrefixEnabled returns true is versioning is enabled at bucket level and if
|
||||||
|
// the given prefix doesn't match any excluded prefixes pattern. This is
|
||||||
|
// part of a MinIO versioning configuration extension.
|
||||||
|
func (sys *BucketVersioningSys) PrefixEnabled(bucket, prefix string) bool {
|
||||||
|
vc, err := sys.Get(bucket)
|
||||||
|
if err != nil {
|
||||||
|
logger.CriticalIf(GlobalContext, err)
|
||||||
|
}
|
||||||
|
return vc.PrefixEnabled(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
// Suspended suspended versioning?
|
// Suspended suspended versioning?
|
||||||
func (sys *BucketVersioningSys) Suspended(bucket string) bool {
|
func (sys *BucketVersioningSys) Suspended(bucket string) bool {
|
||||||
vc, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
|
vc, err := sys.Get(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
logger.CriticalIf(GlobalContext, err)
|
||||||
}
|
}
|
||||||
return vc.Suspended()
|
return vc.Suspended()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrefixSuspended returns true if the given prefix matches an excluded prefix
|
||||||
|
// pattern. This is part of a MinIO versioning configuration extension.
|
||||||
|
func (sys *BucketVersioningSys) PrefixSuspended(bucket, prefix string) bool {
|
||||||
|
vc, err := sys.Get(bucket)
|
||||||
|
if err != nil {
|
||||||
|
logger.CriticalIf(GlobalContext, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vc.PrefixSuspended(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
// Get returns stored bucket policy
|
// Get returns stored bucket policy
|
||||||
func (sys *BucketVersioningSys) Get(bucket string) (*versioning.Versioning, error) {
|
func (sys *BucketVersioningSys) Get(bucket string) (*versioning.Versioning, error) {
|
||||||
if globalIsGateway {
|
if globalIsGateway {
|
||||||
objAPI := newObjectLayerFn()
|
// Gateway does not implement versioning.
|
||||||
if objAPI == nil {
|
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, nil
|
||||||
return nil, errServerNotInitialized
|
|
||||||
}
|
|
||||||
return nil, NotImplemented{}
|
|
||||||
}
|
|
||||||
return globalBucketMetadataSys.GetVersioningConfig(bucket)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset BucketVersioningSys to initial state.
|
if bucket == minioMetaBucket || strings.HasPrefix(bucket, minioMetaBucket) {
|
||||||
func (sys *BucketVersioningSys) Reset() {
|
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, nil
|
||||||
// There is currently no internal state.
|
}
|
||||||
|
|
||||||
|
vcfg, _, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
|
||||||
|
return vcfg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBucketVersioningSys - creates new versioning system.
|
// NewBucketVersioningSys - creates new versioning system.
|
||||||
|
|||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/madmin-go"
|
||||||
|
"github.com/minio/minio/internal/logger"
|
||||||
|
uatomic "go.uber.org/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// callhomeSchemaVersion1 is callhome schema version 1
|
||||||
|
callhomeSchemaVersion1 = "1"
|
||||||
|
|
||||||
|
// callhomeSchemaVersion is current callhome schema version.
|
||||||
|
callhomeSchemaVersion = callhomeSchemaVersion1
|
||||||
|
|
||||||
|
// callhomeCycleDefault is the default interval between two callhome cycles (24hrs)
|
||||||
|
callhomeCycleDefault = 24 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
// CallhomeInfo - Contains callhome information
|
||||||
|
type CallhomeInfo struct {
|
||||||
|
SchemaVersion string `json:"schema_version"`
|
||||||
|
AdminInfo madmin.InfoMessage `json:"admin_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
enableCallhome = uatomic.NewBool(false)
|
||||||
|
callhomeLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
||||||
|
callhomeFreq = uatomic.NewDuration(callhomeCycleDefault)
|
||||||
|
)
|
||||||
|
|
||||||
|
func updateCallhomeParams(ctx context.Context, objAPI ObjectLayer) {
|
||||||
|
alreadyEnabled := enableCallhome.Load()
|
||||||
|
enableCallhome.Store(globalCallhomeConfig.Enable)
|
||||||
|
callhomeFreq.Store(globalCallhomeConfig.Frequency)
|
||||||
|
|
||||||
|
// If callhome was disabled earlier and has now been enabled,
|
||||||
|
// initialize the callhome process again.
|
||||||
|
if !alreadyEnabled && enableCallhome.Load() {
|
||||||
|
initCallhome(ctx, objAPI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initCallhome will start the callhome task in the background.
|
||||||
|
func initCallhome(ctx context.Context, objAPI ObjectLayer) {
|
||||||
|
go func() {
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
// Leader node (that successfully acquires the lock inside runCallhome)
|
||||||
|
// will keep performing the callhome. If the leader goes down for some reason,
|
||||||
|
// the lock will be released and another node will acquire it and take over
|
||||||
|
// because of this loop.
|
||||||
|
for {
|
||||||
|
runCallhome(ctx, objAPI)
|
||||||
|
if !enableCallhome.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// callhome running on a different node.
|
||||||
|
// sleep for some time and try again.
|
||||||
|
duration := time.Duration(r.Float64() * float64(callhomeFreq.Load()))
|
||||||
|
if duration < time.Second {
|
||||||
|
// Make sure to sleep atleast a second to avoid high CPU ticks.
|
||||||
|
duration = time.Second
|
||||||
|
}
|
||||||
|
time.Sleep(duration)
|
||||||
|
|
||||||
|
if !enableCallhome.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCallhome(ctx context.Context, objAPI ObjectLayer) {
|
||||||
|
// Make sure only 1 callhome is running on the cluster.
|
||||||
|
locker := objAPI.NewNSLock(minioMetaBucket, "callhome/runCallhome.lock")
|
||||||
|
lkctx, err := locker.GetLock(ctx, callhomeLeaderLockTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer locker.Unlock(lkctx.Cancel)
|
||||||
|
|
||||||
|
callhomeTimer := time.NewTimer(callhomeFreq.Load())
|
||||||
|
defer callhomeTimer.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-callhomeTimer.C:
|
||||||
|
if !enableCallhome.Load() {
|
||||||
|
// Stop the processing as callhome got disabled
|
||||||
|
return
|
||||||
|
}
|
||||||
|
performCallhome(ctx)
|
||||||
|
|
||||||
|
// Reset the timer for next cycle.
|
||||||
|
callhomeTimer.Reset(callhomeFreq.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func performCallhome(ctx context.Context) {
|
||||||
|
err := sendCallhomeInfo(
|
||||||
|
CallhomeInfo{
|
||||||
|
SchemaVersion: callhomeSchemaVersion,
|
||||||
|
AdminInfo: getServerInfo(ctx, nil),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to perform callhome: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
-31
@@ -24,6 +24,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -32,6 +33,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -100,12 +102,16 @@ func init() {
|
|||||||
PersistOnFailure: false,
|
PersistOnFailure: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalIsCICD = env.Get("MINIO_CI_CD", "") != "" || env.Get("CI", "") != ""
|
||||||
|
|
||||||
|
containers := IsKubernetes() || IsDocker() || IsBOSH() || IsDCOS() || IsPCFTile()
|
||||||
|
|
||||||
// Call to refresh will refresh names in cache. If you pass true, it will also
|
// Call to refresh will refresh names in cache. If you pass true, it will also
|
||||||
// remove cached names not looked up since the last call to Refresh. It is a good idea
|
// remove cached names not looked up since the last call to Refresh. It is a good idea
|
||||||
// to call this method on a regular interval.
|
// to call this method on a regular interval.
|
||||||
go func() {
|
go func() {
|
||||||
var t *time.Ticker
|
var t *time.Ticker
|
||||||
if IsKubernetes() || IsDocker() || IsBOSH() || IsDCOS() || IsPCFTile() {
|
if containers {
|
||||||
t = time.NewTicker(1 * time.Minute)
|
t = time.NewTicker(1 * time.Minute)
|
||||||
} else {
|
} else {
|
||||||
t = time.NewTicker(10 * time.Minute)
|
t = time.NewTicker(10 * time.Minute)
|
||||||
@@ -172,11 +178,22 @@ func minioConfigToConsoleFeatures() {
|
|||||||
os.Setenv("CONSOLE_LOG_QUERY_AUTH_TOKEN", value)
|
os.Setenv("CONSOLE_LOG_QUERY_AUTH_TOKEN", value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// pass the console subpath configuration
|
||||||
|
if value := env.Get(config.EnvMinIOBrowserRedirectURL, ""); value != "" {
|
||||||
|
subPath := path.Clean(pathJoin(strings.TrimSpace(globalBrowserRedirectURL.Path), SlashSeparator))
|
||||||
|
if subPath != SlashSeparator {
|
||||||
|
os.Setenv("CONSOLE_SUBPATH", subPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Enable if prometheus URL is set.
|
// Enable if prometheus URL is set.
|
||||||
if value := env.Get("MINIO_PROMETHEUS_URL", ""); value != "" {
|
if value := env.Get("MINIO_PROMETHEUS_URL", ""); value != "" {
|
||||||
os.Setenv("CONSOLE_PROMETHEUS_URL", value)
|
os.Setenv("CONSOLE_PROMETHEUS_URL", value)
|
||||||
if value := env.Get("MINIO_PROMETHEUS_JOB_ID", "minio-job"); value != "" {
|
if value := env.Get("MINIO_PROMETHEUS_JOB_ID", "minio-job"); value != "" {
|
||||||
os.Setenv("CONSOLE_PROMETHEUS_JOB_ID", value)
|
os.Setenv("CONSOLE_PROMETHEUS_JOB_ID", value)
|
||||||
|
// Support additional labels for more granular filtering.
|
||||||
|
if value := env.Get("MINIO_PROMETHEUS_EXTRA_LABELS", ""); value != "" {
|
||||||
|
os.Setenv("CONSOLE_PROMETHEUS_EXTRA_LABELS", value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Enable if LDAP is enabled.
|
// Enable if LDAP is enabled.
|
||||||
@@ -184,23 +201,23 @@ func minioConfigToConsoleFeatures() {
|
|||||||
os.Setenv("CONSOLE_LDAP_ENABLED", config.EnableOn)
|
os.Setenv("CONSOLE_LDAP_ENABLED", config.EnableOn)
|
||||||
}
|
}
|
||||||
// if IDP is enabled, set IDP environment variables
|
// if IDP is enabled, set IDP environment variables
|
||||||
if globalOpenIDConfig.URL != nil {
|
if globalOpenIDConfig.ProviderCfgs[config.Default] != nil {
|
||||||
os.Setenv("CONSOLE_IDP_URL", globalOpenIDConfig.URL.String())
|
os.Setenv("CONSOLE_IDP_URL", globalOpenIDConfig.ProviderCfgs[config.Default].URL.String())
|
||||||
os.Setenv("CONSOLE_IDP_CLIENT_ID", globalOpenIDConfig.ClientID)
|
os.Setenv("CONSOLE_IDP_CLIENT_ID", globalOpenIDConfig.ProviderCfgs[config.Default].ClientID)
|
||||||
os.Setenv("CONSOLE_IDP_SECRET", globalOpenIDConfig.ClientSecret)
|
os.Setenv("CONSOLE_IDP_SECRET", globalOpenIDConfig.ProviderCfgs[config.Default].ClientSecret)
|
||||||
os.Setenv("CONSOLE_IDP_HMAC_SALT", globalDeploymentID)
|
os.Setenv("CONSOLE_IDP_HMAC_SALT", globalDeploymentID)
|
||||||
os.Setenv("CONSOLE_IDP_HMAC_PASSPHRASE", globalOpenIDConfig.ClientID)
|
os.Setenv("CONSOLE_IDP_HMAC_PASSPHRASE", globalOpenIDConfig.ProviderCfgs[config.Default].ClientID)
|
||||||
os.Setenv("CONSOLE_IDP_SCOPES", strings.Join(globalOpenIDConfig.DiscoveryDoc.ScopesSupported, ","))
|
os.Setenv("CONSOLE_IDP_SCOPES", strings.Join(globalOpenIDConfig.ProviderCfgs[config.Default].DiscoveryDoc.ScopesSupported, ","))
|
||||||
if globalOpenIDConfig.ClaimUserinfo {
|
if globalOpenIDConfig.ProviderCfgs[config.Default].ClaimUserinfo {
|
||||||
os.Setenv("CONSOLE_IDP_USERINFO", config.EnableOn)
|
os.Setenv("CONSOLE_IDP_USERINFO", config.EnableOn)
|
||||||
}
|
}
|
||||||
if globalOpenIDConfig.RedirectURIDynamic {
|
if globalOpenIDConfig.ProviderCfgs[config.Default].RedirectURIDynamic {
|
||||||
// Enable dynamic redirect-uri's based on incoming 'host' header,
|
// Enable dynamic redirect-uri's based on incoming 'host' header,
|
||||||
// Overrides any other callback URL.
|
// Overrides any other callback URL.
|
||||||
os.Setenv("CONSOLE_IDP_CALLBACK_DYNAMIC", config.EnableOn)
|
os.Setenv("CONSOLE_IDP_CALLBACK_DYNAMIC", config.EnableOn)
|
||||||
}
|
}
|
||||||
if globalOpenIDConfig.RedirectURI != "" {
|
if globalOpenIDConfig.ProviderCfgs[config.Default].RedirectURI != "" {
|
||||||
os.Setenv("CONSOLE_IDP_CALLBACK", globalOpenIDConfig.RedirectURI)
|
os.Setenv("CONSOLE_IDP_CALLBACK", globalOpenIDConfig.ProviderCfgs[config.Default].RedirectURI)
|
||||||
} else {
|
} else {
|
||||||
os.Setenv("CONSOLE_IDP_CALLBACK", getConsoleEndpoints()[0]+"/oauth_callback")
|
os.Setenv("CONSOLE_IDP_CALLBACK", getConsoleEndpoints()[0]+"/oauth_callback")
|
||||||
}
|
}
|
||||||
@@ -213,8 +230,8 @@ func minioConfigToConsoleFeatures() {
|
|||||||
if globalSubnetConfig.APIKey != "" {
|
if globalSubnetConfig.APIKey != "" {
|
||||||
os.Setenv("CONSOLE_SUBNET_API_KEY", globalSubnetConfig.APIKey)
|
os.Setenv("CONSOLE_SUBNET_API_KEY", globalSubnetConfig.APIKey)
|
||||||
}
|
}
|
||||||
if globalSubnetConfig.Proxy != "" {
|
if globalSubnetConfig.ProxyURL != nil {
|
||||||
os.Setenv("CONSOLE_SUBNET_PROXY", globalSubnetConfig.Proxy)
|
os.Setenv("CONSOLE_SUBNET_PROXY", globalSubnetConfig.ProxyURL.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +344,7 @@ func checkUpdate(mode string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logStartupMessage(prepareUpdateMessage("\nRun `mc admin update`", lrTime.Sub(crTime)))
|
logger.Info(prepareUpdateMessage("Run `mc admin update`", lrTime.Sub(crTime)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool) {
|
func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool) {
|
||||||
@@ -482,6 +499,13 @@ func parsEnvEntry(envEntry string) (envKV, error) {
|
|||||||
key := envTokens[0]
|
key := envTokens[0]
|
||||||
val := envTokens[1]
|
val := envTokens[1]
|
||||||
|
|
||||||
|
if strings.HasPrefix(key, "#") {
|
||||||
|
// Skip commented lines
|
||||||
|
return envKV{
|
||||||
|
Skip: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Remove quotes from the value if found
|
// Remove quotes from the value if found
|
||||||
if len(val) >= 2 {
|
if len(val) >= 2 {
|
||||||
quote := val[0]
|
quote := val[0]
|
||||||
@@ -502,9 +526,6 @@ func parsEnvEntry(envEntry string) (envKV, error) {
|
|||||||
func minioEnvironFromFile(envConfigFile string) ([]envKV, error) {
|
func minioEnvironFromFile(envConfigFile string) ([]envKV, error) {
|
||||||
f, err := os.Open(envConfigFile)
|
f, err := os.Open(envConfigFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) { // ignore if file doesn't exist.
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
@@ -600,7 +621,7 @@ func loadEnvVarsFromFiles() {
|
|||||||
|
|
||||||
if env.IsSet(config.EnvConfigEnvFile) {
|
if env.IsSet(config.EnvConfigEnvFile) {
|
||||||
ekvs, err := minioEnvironFromFile(env.Get(config.EnvConfigEnvFile, ""))
|
ekvs, err := minioEnvironFromFile(env.Get(config.EnvConfigEnvFile, ""))
|
||||||
if err != nil {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
logger.Fatal(err, "Unable to read the config environment file")
|
logger.Fatal(err, "Unable to read the config environment file")
|
||||||
}
|
}
|
||||||
for _, ekv := range ekvs {
|
for _, ekv := range ekvs {
|
||||||
@@ -625,12 +646,11 @@ func handleCommonEnvVars() {
|
|||||||
}
|
}
|
||||||
// Look for if URL has invalid values and return error.
|
// Look for if URL has invalid values and return error.
|
||||||
if !((u.Scheme == "http" || u.Scheme == "https") &&
|
if !((u.Scheme == "http" || u.Scheme == "https") &&
|
||||||
(u.Path == "/" || u.Path == "") && 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 of http(s)://minio.example.com 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")
|
||||||
}
|
}
|
||||||
u.Path = "" // remove any path component such as `/`
|
|
||||||
globalBrowserRedirectURL = u
|
globalBrowserRedirectURL = u
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -664,8 +684,6 @@ func handleCommonEnvVars() {
|
|||||||
globalRootDiskThreshold = size
|
globalRootDiskThreshold = size
|
||||||
}
|
}
|
||||||
|
|
||||||
globalIsCICD = env.Get("MINIO_CI_CD", "") != "" || env.Get("CI", "") != ""
|
|
||||||
|
|
||||||
domains := env.Get(config.EnvDomain, "")
|
domains := env.Get(config.EnvDomain, "")
|
||||||
if len(domains) != 0 {
|
if len(domains) != 0 {
|
||||||
for _, domainName := range strings.Split(domains, config.ValueSeparator) {
|
for _, domainName := range strings.Split(domains, config.ValueSeparator) {
|
||||||
@@ -762,7 +780,7 @@ func handleCommonEnvVars() {
|
|||||||
" Please use %s and %s",
|
" Please use %s and %s",
|
||||||
config.EnvAccessKey, config.EnvSecretKey,
|
config.EnvAccessKey, config.EnvSecretKey,
|
||||||
config.EnvRootUser, config.EnvRootPassword)
|
config.EnvRootUser, config.EnvRootPassword)
|
||||||
logStartupMessage(color.RedBold(msg))
|
logger.Info(color.RedBold(msg))
|
||||||
}
|
}
|
||||||
globalActiveCred = cred
|
globalActiveCred = cred
|
||||||
}
|
}
|
||||||
@@ -796,7 +814,29 @@ func handleCommonEnvVars() {
|
|||||||
endpoints = append(endpoints, strings.Join(lbls, ""))
|
endpoints = append(endpoints, strings.Join(lbls, ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
certificate, err := tls.LoadX509KeyPair(env.Get(config.EnvKESClientCert, ""), env.Get(config.EnvKESClientKey, ""))
|
// Manually load the certificate and private key into memory.
|
||||||
|
// We need to check whether the private key is encrypted, and
|
||||||
|
// if so, decrypt it using the user-provided password.
|
||||||
|
certBytes, err := os.ReadFile(env.Get(config.EnvKESClientCert, ""))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err, "Unable to load KES client certificate as specified by the shell environment")
|
||||||
|
}
|
||||||
|
keyBytes, err := os.ReadFile(env.Get(config.EnvKESClientKey, ""))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err, "Unable to load KES client private key as specified by the shell environment")
|
||||||
|
}
|
||||||
|
privateKeyPEM, rest := pem.Decode(bytes.TrimSpace(keyBytes))
|
||||||
|
if len(rest) != 0 {
|
||||||
|
logger.Fatal(errors.New("private key contains additional data"), "Unable to load KES client private key as specified by the shell environment")
|
||||||
|
}
|
||||||
|
if x509.IsEncryptedPEMBlock(privateKeyPEM) {
|
||||||
|
keyBytes, err = x509.DecryptPEMBlock(privateKeyPEM, []byte(env.Get(config.EnvKESClientPassword, "")))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err, "Unable to decrypt KES client private key as specified by the shell environment")
|
||||||
|
}
|
||||||
|
keyBytes = pem.EncodeToMemory(&pem.Block{Type: privateKeyPEM.Type, Bytes: keyBytes})
|
||||||
|
}
|
||||||
|
certificate, err := tls.X509KeyPair(certBytes, keyBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(err, "Unable to load KES client certificate as specified by the shell environment")
|
logger.Fatal(err, "Unable to load KES client certificate as specified by the shell environment")
|
||||||
}
|
}
|
||||||
@@ -827,13 +867,6 @@ func handleCommonEnvVars() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logStartupMessage(msg string) {
|
|
||||||
if globalConsoleSys != nil {
|
|
||||||
globalConsoleSys.Send(msg, string(logger.All))
|
|
||||||
}
|
|
||||||
logger.StartupMessage(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
|
func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
|
||||||
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
|
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
|
||||||
return nil, nil, false, nil
|
return nil, nil, false, nil
|
||||||
@@ -910,6 +943,9 @@ func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secu
|
|||||||
}
|
}
|
||||||
secureConn = true
|
secureConn = true
|
||||||
|
|
||||||
|
// Certs might be symlinks, reload them every 10 seconds.
|
||||||
|
manager.UpdateReloadDuration(10 * time.Second)
|
||||||
|
|
||||||
// syscall.SIGHUP to reload the certs.
|
// syscall.SIGHUP to reload the certs.
|
||||||
manager.ReloadOnSignal(syscall.SIGHUP)
|
manager.ReloadOnSignal(syscall.SIGHUP)
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,24 @@ export MINIO_ROOT_PASSWORD=minio123`,
|
|||||||
true,
|
true,
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
`
|
||||||
|
# MINIO_ROOT_USER=minioadmin
|
||||||
|
# MINIO_ROOT_PASSWORD=minioadmin
|
||||||
|
MINIO_ROOT_USER=minio
|
||||||
|
MINIO_ROOT_PASSWORD=minio123`,
|
||||||
|
false,
|
||||||
|
[]envKV{
|
||||||
|
{
|
||||||
|
Key: "MINIO_ROOT_USER",
|
||||||
|
Value: "minio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "MINIO_ROOT_PASSWORD",
|
||||||
|
Value: "minio123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
testCase := testCase
|
testCase := testCase
|
||||||
@@ -153,6 +171,11 @@ export MINIO_ROOT_PASSWORD=minio123`,
|
|||||||
if err == nil && testCase.expectedErr {
|
if err == nil && testCase.expectedErr {
|
||||||
t.Error(errors.New("expected error, found success"))
|
t.Error(errors.New("expected error, found success"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(ekvs) != len(testCase.expectedEkvs) {
|
||||||
|
t.Errorf("expected %v keys, got %v keys", len(testCase.expectedEkvs), len(ekvs))
|
||||||
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(ekvs, testCase.expectedEkvs) {
|
if !reflect.DeepEqual(ekvs, testCase.expectedEkvs) {
|
||||||
t.Errorf("expected %v, got %v", testCase.expectedEkvs, ekvs)
|
t.Errorf("expected %v, got %v", testCase.expectedEkvs, ekvs)
|
||||||
}
|
}
|
||||||
|
|||||||
+103
-47
@@ -28,15 +28,18 @@ import (
|
|||||||
"github.com/minio/minio/internal/config"
|
"github.com/minio/minio/internal/config"
|
||||||
"github.com/minio/minio/internal/config/api"
|
"github.com/minio/minio/internal/config/api"
|
||||||
"github.com/minio/minio/internal/config/cache"
|
"github.com/minio/minio/internal/config/cache"
|
||||||
|
"github.com/minio/minio/internal/config/callhome"
|
||||||
"github.com/minio/minio/internal/config/compress"
|
"github.com/minio/minio/internal/config/compress"
|
||||||
"github.com/minio/minio/internal/config/dns"
|
"github.com/minio/minio/internal/config/dns"
|
||||||
"github.com/minio/minio/internal/config/etcd"
|
"github.com/minio/minio/internal/config/etcd"
|
||||||
"github.com/minio/minio/internal/config/heal"
|
"github.com/minio/minio/internal/config/heal"
|
||||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||||
"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"
|
||||||
xtls "github.com/minio/minio/internal/config/identity/tls"
|
xtls "github.com/minio/minio/internal/config/identity/tls"
|
||||||
"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"
|
||||||
"github.com/minio/minio/internal/config/scanner"
|
"github.com/minio/minio/internal/config/scanner"
|
||||||
"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/config/subnet"
|
||||||
@@ -55,7 +58,9 @@ func initHelp() {
|
|||||||
config.IdentityLDAPSubSys: xldap.DefaultKVS,
|
config.IdentityLDAPSubSys: xldap.DefaultKVS,
|
||||||
config.IdentityOpenIDSubSys: openid.DefaultKVS,
|
config.IdentityOpenIDSubSys: openid.DefaultKVS,
|
||||||
config.IdentityTLSSubSys: xtls.DefaultKVS,
|
config.IdentityTLSSubSys: xtls.DefaultKVS,
|
||||||
|
config.IdentityPluginSubSys: idplugin.DefaultKVS,
|
||||||
config.PolicyOPASubSys: opa.DefaultKVS,
|
config.PolicyOPASubSys: opa.DefaultKVS,
|
||||||
|
config.PolicyPluginSubSys: polplugin.DefaultKVS,
|
||||||
config.SiteSubSys: config.DefaultSiteKVS,
|
config.SiteSubSys: config.DefaultSiteKVS,
|
||||||
config.RegionSubSys: config.DefaultRegionKVS,
|
config.RegionSubSys: config.DefaultRegionKVS,
|
||||||
config.APISubSys: api.DefaultKVS,
|
config.APISubSys: api.DefaultKVS,
|
||||||
@@ -66,6 +71,7 @@ func initHelp() {
|
|||||||
config.HealSubSys: heal.DefaultKVS,
|
config.HealSubSys: heal.DefaultKVS,
|
||||||
config.ScannerSubSys: scanner.DefaultKVS,
|
config.ScannerSubSys: scanner.DefaultKVS,
|
||||||
config.SubnetSubSys: subnet.DefaultKVS,
|
config.SubnetSubSys: subnet.DefaultKVS,
|
||||||
|
config.CallhomeSubSys: callhome.DefaultKVS,
|
||||||
}
|
}
|
||||||
for k, v := range notify.DefaultNotificationKVS {
|
for k, v := range notify.DefaultNotificationKVS {
|
||||||
kvs[k] = v
|
kvs[k] = v
|
||||||
@@ -96,6 +102,7 @@ func initHelp() {
|
|||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.IdentityOpenIDSubSys,
|
Key: config.IdentityOpenIDSubSys,
|
||||||
Description: "enable OpenID SSO support",
|
Description: "enable OpenID SSO support",
|
||||||
|
MultipleTargets: true,
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.IdentityLDAPSubSys,
|
Key: config.IdentityLDAPSubSys,
|
||||||
@@ -106,8 +113,12 @@ func initHelp() {
|
|||||||
Description: "enable X.509 TLS certificate SSO support",
|
Description: "enable X.509 TLS certificate SSO support",
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.PolicyOPASubSys,
|
Key: config.IdentityPluginSubSys,
|
||||||
Description: "[DEPRECATED] enable external OPA for policy enforcement",
|
Description: "enable Identity Plugin via external hook",
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: config.PolicyPluginSubSys,
|
||||||
|
Description: "enable Access Management Plugin for policy enforcement",
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.APISubSys,
|
Key: config.APISubSys,
|
||||||
@@ -192,6 +203,12 @@ func initHelp() {
|
|||||||
Description: "set subnet config for the cluster e.g. api key",
|
Description: "set subnet config for the cluster e.g. api key",
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: config.CallhomeSubSys,
|
||||||
|
Type: "string",
|
||||||
|
Description: "enable callhome for the cluster",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalIsErasure {
|
if globalIsErasure {
|
||||||
@@ -217,7 +234,9 @@ func initHelp() {
|
|||||||
config.IdentityOpenIDSubSys: openid.Help,
|
config.IdentityOpenIDSubSys: openid.Help,
|
||||||
config.IdentityLDAPSubSys: xldap.Help,
|
config.IdentityLDAPSubSys: xldap.Help,
|
||||||
config.IdentityTLSSubSys: xtls.Help,
|
config.IdentityTLSSubSys: xtls.Help,
|
||||||
|
config.IdentityPluginSubSys: idplugin.Help,
|
||||||
config.PolicyOPASubSys: opa.Help,
|
config.PolicyOPASubSys: opa.Help,
|
||||||
|
config.PolicyPluginSubSys: polplugin.Help,
|
||||||
config.LoggerWebhookSubSys: logger.Help,
|
config.LoggerWebhookSubSys: logger.Help,
|
||||||
config.AuditWebhookSubSys: logger.HelpWebhook,
|
config.AuditWebhookSubSys: logger.HelpWebhook,
|
||||||
config.AuditKafkaSubSys: logger.HelpKafka,
|
config.AuditKafkaSubSys: logger.HelpKafka,
|
||||||
@@ -232,6 +251,7 @@ func initHelp() {
|
|||||||
config.NotifyWebhookSubSys: notify.HelpWebhook,
|
config.NotifyWebhookSubSys: notify.HelpWebhook,
|
||||||
config.NotifyESSubSys: notify.HelpES,
|
config.NotifyESSubSys: notify.HelpES,
|
||||||
config.SubnetSubSys: subnet.HelpSubnet,
|
config.SubnetSubSys: subnet.HelpSubnet,
|
||||||
|
config.CallhomeSubSys: callhome.HelpCallhome,
|
||||||
}
|
}
|
||||||
|
|
||||||
config.RegisterHelpSubSys(helpMap)
|
config.RegisterHelpSubSys(helpMap)
|
||||||
@@ -242,6 +262,10 @@ func initHelp() {
|
|||||||
Key: config.RegionSubSys,
|
Key: config.RegionSubSys,
|
||||||
Description: "[DEPRECATED - use `site` instead] label the location of the server",
|
Description: "[DEPRECATED - use `site` instead] label the location of the server",
|
||||||
},
|
},
|
||||||
|
config.PolicyOPASubSys: {
|
||||||
|
Key: config.PolicyOPASubSys,
|
||||||
|
Description: "[DEPRECATED - use `policy_plugin` instead] enable external OPA for policy enforcement",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
config.RegisterHelpDeprecatedSubSys(deprecatedHelpKVMap)
|
config.RegisterHelpDeprecatedSubSys(deprecatedHelpKVMap)
|
||||||
@@ -314,7 +338,7 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
|
|||||||
etcdClnt.Close()
|
etcdClnt.Close()
|
||||||
}
|
}
|
||||||
case config.IdentityOpenIDSubSys:
|
case config.IdentityOpenIDSubSys:
|
||||||
if _, err := openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
|
if _, err := openid.LookupConfig(s[config.IdentityOpenIDSubSys],
|
||||||
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region); err != nil {
|
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -334,15 +358,35 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
|
|||||||
if _, err := xtls.Lookup(s[config.IdentityTLSSubSys][config.Default]); err != nil {
|
if _, err := xtls.Lookup(s[config.IdentityTLSSubSys][config.Default]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case config.IdentityPluginSubSys:
|
||||||
|
if _, err := idplugin.LookupConfig(s[config.IdentityPluginSubSys][config.Default],
|
||||||
|
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case config.SubnetSubSys:
|
case config.SubnetSubSys:
|
||||||
if _, err := subnet.LookupConfig(s[config.SubnetSubSys][config.Default]); err != nil {
|
if _, err := subnet.LookupConfig(s[config.SubnetSubSys][config.Default]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case config.CallhomeSubSys:
|
||||||
|
if _, err := callhome.LookupConfig(s[config.CallhomeSubSys][config.Default]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case config.PolicyOPASubSys:
|
case config.PolicyOPASubSys:
|
||||||
|
// In case legacy OPA config is being set, we treat it as if the
|
||||||
|
// AuthZPlugin is being set.
|
||||||
|
subSys = config.PolicyPluginSubSys
|
||||||
|
fallthrough
|
||||||
|
case config.PolicyPluginSubSys:
|
||||||
|
if ppargs, err := polplugin.LookupConfig(s[config.PolicyPluginSubSys][config.Default],
|
||||||
|
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
|
||||||
|
return err
|
||||||
|
} else if ppargs.URL == nil {
|
||||||
|
// Check if legacy opa is configured.
|
||||||
if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
||||||
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
|
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
if config.LoggerSubSystems.Contains(subSys) {
|
if config.LoggerSubSystems.Contains(subSys) {
|
||||||
if err := logger.ValidateSubSysConfig(s, subSys); err != nil {
|
if err := logger.ValidateSubSysConfig(s, subSys); err != nil {
|
||||||
@@ -484,22 +528,6 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
|||||||
getRemoteInstanceTransport = newGatewayHTTPTransport(apiConfig.RemoteTransportDeadline)
|
getRemoteInstanceTransport = newGatewayHTTPTransport(apiConfig.RemoteTransportDeadline)
|
||||||
})
|
})
|
||||||
|
|
||||||
if globalIsErasure && objAPI != nil {
|
|
||||||
setDriveCounts := objAPI.SetDriveCounts()
|
|
||||||
for i, setDriveCount := range setDriveCounts {
|
|
||||||
sc, err := storageclass.LookupConfig(s[config.StorageClassSubSys][config.Default], setDriveCount)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize storage class config: %w", err))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// if we validated all setDriveCounts and it was successful
|
|
||||||
// proceed to store the correct storage class globally.
|
|
||||||
if i == len(setDriveCounts)-1 {
|
|
||||||
globalStorageClass.Update(sc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
globalCacheConfig, err = cache.LookupConfig(s[config.CacheSubSys][config.Default])
|
globalCacheConfig, err = cache.LookupConfig(s[config.CacheSubSys][config.Default])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if globalIsGateway {
|
if globalIsGateway {
|
||||||
@@ -529,30 +557,48 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if globalSTSTLSConfig.InsecureSkipVerify {
|
if globalSTSTLSConfig.InsecureSkipVerify {
|
||||||
logger.Info("CRITICAL: enabling %s is not recommended in a production environment", xtls.EnvIdentityTLSSkipVerify)
|
logger.LogIf(ctx, fmt.Errorf("CRITICAL: enabling %s is not recommended in a production environment", xtls.EnvIdentityTLSSkipVerify))
|
||||||
}
|
}
|
||||||
|
|
||||||
globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
|
globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys],
|
||||||
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region)
|
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
|
||||||
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OPA: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
globalOpenIDValidators = getOpenIDValidators(globalOpenIDConfig)
|
|
||||||
globalPolicyOPA = opa.New(opaCfg)
|
|
||||||
|
|
||||||
globalLDAPConfig, err = xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
globalLDAPConfig, err = xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
||||||
globalRootCAs)
|
globalRootCAs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to parse LDAP configuration: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to parse LDAP configuration: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authNPluginCfg, err := idplugin.LookupConfig(s[config.IdentityPluginSubSys][config.Default],
|
||||||
|
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthNPlugin: %w", err))
|
||||||
|
}
|
||||||
|
globalAuthNPlugin = idplugin.New(authNPluginCfg)
|
||||||
|
|
||||||
|
authZPluginCfg, err := polplugin.LookupConfig(s[config.PolicyPluginSubSys][config.Default],
|
||||||
|
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthZPlugin: %w", err))
|
||||||
|
}
|
||||||
|
if authZPluginCfg.URL == nil {
|
||||||
|
opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
||||||
|
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthZPlugin from legacy OPA config: %w", err))
|
||||||
|
} else {
|
||||||
|
authZPluginCfg.URL = opaCfg.URL
|
||||||
|
authZPluginCfg.AuthToken = opaCfg.AuthToken
|
||||||
|
authZPluginCfg.Transport = opaCfg.Transport
|
||||||
|
authZPluginCfg.CloseRespFn = opaCfg.CloseRespFn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setGlobalAuthZPlugin(polplugin.New(authZPluginCfg))
|
||||||
|
|
||||||
globalSubnetConfig, err = subnet.LookupConfig(s[config.SubnetSubSys][config.Default])
|
globalSubnetConfig, err = subnet.LookupConfig(s[config.SubnetSubSys][config.Default])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to parse subnet configuration: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to parse subnet configuration: %w", err))
|
||||||
@@ -616,7 +662,7 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
|||||||
return fmt.Errorf("Unable to apply scanner config: %w", err)
|
return fmt.Errorf("Unable to apply scanner config: %w", err)
|
||||||
}
|
}
|
||||||
// update dynamic scanner values.
|
// update dynamic scanner values.
|
||||||
scannerCycle.Update(scannerCfg.Cycle)
|
scannerCycle.Store(scannerCfg.Cycle)
|
||||||
logger.LogIf(ctx, scannerSleeper.Update(scannerCfg.Delay, scannerCfg.MaxWait))
|
logger.LogIf(ctx, scannerSleeper.Update(scannerCfg.Delay, scannerCfg.MaxWait))
|
||||||
case config.LoggerWebhookSubSys:
|
case config.LoggerWebhookSubSys:
|
||||||
loggerCfg, err := logger.LookupConfigForSubSys(s, config.LoggerWebhookSubSys)
|
loggerCfg, err := logger.LookupConfigForSubSys(s, config.LoggerWebhookSubSys)
|
||||||
@@ -626,7 +672,7 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
|||||||
userAgent := getUserAgent(getMinioMode())
|
userAgent := getUserAgent(getMinioMode())
|
||||||
for n, l := range loggerCfg.HTTP {
|
for n, l := range loggerCfg.HTTP {
|
||||||
if l.Enabled {
|
if l.Enabled {
|
||||||
l.LogOnce = logger.LogOnceIf
|
l.LogOnce = logger.LogOnceConsoleIf
|
||||||
l.UserAgent = userAgent
|
l.UserAgent = userAgent
|
||||||
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
|
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
|
||||||
loggerCfg.HTTP[n] = l
|
loggerCfg.HTTP[n] = l
|
||||||
@@ -644,7 +690,7 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
|||||||
userAgent := getUserAgent(getMinioMode())
|
userAgent := getUserAgent(getMinioMode())
|
||||||
for n, l := range loggerCfg.AuditWebhook {
|
for n, l := range loggerCfg.AuditWebhook {
|
||||||
if l.Enabled {
|
if l.Enabled {
|
||||||
l.LogOnce = logger.LogOnceIf
|
l.LogOnce = logger.LogOnceConsoleIf
|
||||||
l.UserAgent = userAgent
|
l.UserAgent = userAgent
|
||||||
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
|
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
|
||||||
loggerCfg.AuditWebhook[n] = l
|
loggerCfg.AuditWebhook[n] = l
|
||||||
@@ -670,6 +716,30 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to update audit kafka targets: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to update audit kafka targets: %w", err))
|
||||||
}
|
}
|
||||||
|
case config.StorageClassSubSys:
|
||||||
|
if globalIsErasure && objAPI != nil {
|
||||||
|
setDriveCounts := objAPI.SetDriveCounts()
|
||||||
|
for i, setDriveCount := range setDriveCounts {
|
||||||
|
sc, err := storageclass.LookupConfig(s[config.StorageClassSubSys][config.Default], setDriveCount)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize storage class config: %w", err))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// if we validated all setDriveCounts and it was successful
|
||||||
|
// proceed to store the correct storage class globally.
|
||||||
|
if i == len(setDriveCounts)-1 {
|
||||||
|
globalStorageClass.Update(sc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case config.CallhomeSubSys:
|
||||||
|
callhomeCfg, err := callhome.LookupConfig(s[config.CallhomeSubSys][config.Default])
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to load callhome config: %w", err))
|
||||||
|
} else {
|
||||||
|
globalCallhomeConfig = callhomeCfg
|
||||||
|
updateCallhomeParams(ctx, objAPI)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
globalServerConfigMu.Lock()
|
globalServerConfigMu.Lock()
|
||||||
defer globalServerConfigMu.Unlock()
|
defer globalServerConfigMu.Unlock()
|
||||||
@@ -807,17 +877,3 @@ func loadConfig(objAPI ObjectLayer) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getOpenIDValidators - returns ValidatorList which contains
|
|
||||||
// enabled providers in server config.
|
|
||||||
// A new authentication provider is added like below
|
|
||||||
// * Add a new provider in pkg/iam/openid package.
|
|
||||||
func getOpenIDValidators(cfg openid.Config) *openid.Validators {
|
|
||||||
validators := openid.NewValidators()
|
|
||||||
|
|
||||||
if cfg.Enabled {
|
|
||||||
validators.Add(&cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return validators
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func migrateIAMConfigsEtcdToEncrypted(ctx context.Context, client *etcd.Client)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.Info("Attempting to re-encrypt IAM users and policies on etcd with %q (%s)", stat.DefaultKey, stat.Name)
|
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)
|
listCtx, cancel := context.WithTimeout(ctx, 1*time.Minute)
|
||||||
@@ -143,7 +143,7 @@ func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, encrypted bool) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.Info("Attempting to re-encrypt config, IAM users and policies on MinIO with %q (%s)", stat.DefaultKey, stat.Name)
|
logger.Info(fmt.Sprintf("Attempting to re-encrypt config, IAM users and policies on MinIO with %q (%s)", stat.DefaultKey, stat.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
var marker string
|
var marker string
|
||||||
|
|||||||
@@ -2649,7 +2649,6 @@ func migrateV30ToV31MinioSys(objAPI ObjectLayer) error {
|
|||||||
|
|
||||||
cfg.Version = "31"
|
cfg.Version = "31"
|
||||||
cfg.OpenID = openid.Config{}
|
cfg.OpenID = openid.Config{}
|
||||||
cfg.OpenID.JWKS.URL = &xnet.URL{}
|
|
||||||
|
|
||||||
cfg.Policy.OPA = opa.Args{
|
cfg.Policy.OPA = opa.Args{
|
||||||
URL: &xnet.URL{},
|
URL: &xnet.URL{},
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ type HTTPConsoleLoggerSys struct {
|
|||||||
// NewConsoleLogger - creates new HTTPConsoleLoggerSys with all nodes subscribed to
|
// NewConsoleLogger - creates new HTTPConsoleLoggerSys with all nodes subscribed to
|
||||||
// the console logging pub sub system
|
// the console logging pub sub system
|
||||||
func NewConsoleLogger(ctx context.Context) *HTTPConsoleLoggerSys {
|
func NewConsoleLogger(ctx context.Context) *HTTPConsoleLoggerSys {
|
||||||
ps := pubsub.New()
|
ps := pubsub.New(8)
|
||||||
return &HTTPConsoleLoggerSys{
|
return &HTTPConsoleLoggerSys{
|
||||||
pubsub: ps,
|
pubsub: ps,
|
||||||
console: console.New(),
|
console: console.New(),
|
||||||
@@ -75,7 +75,7 @@ func (sys *HTTPConsoleLoggerSys) HasLogListeners() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe starts console logging for this node.
|
// Subscribe starts console logging for this node.
|
||||||
func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh <-chan struct{}, node string, last int, logKind string, filter func(entry interface{}) bool) {
|
func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh <-chan struct{}, node string, last int, logKind string, filter func(entry interface{}) 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(sys)
|
||||||
@@ -111,11 +111,12 @@ func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh <-chan
|
|||||||
select {
|
select {
|
||||||
case subCh <- entry:
|
case subCh <- entry:
|
||||||
case <-doneCh:
|
case <-doneCh:
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sys.pubsub.Subscribe(subCh, doneCh, filter)
|
|
||||||
|
return sys.pubsub.Subscribe(subCh, doneCh, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init if HTTPConsoleLoggerSys is valid, always returns nil right now
|
// Init if HTTPConsoleLoggerSys is valid, always returns nil right now
|
||||||
|
|||||||
+111
-54
@@ -21,6 +21,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -37,12 +38,14 @@ import (
|
|||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/minio/madmin-go"
|
"github.com/minio/madmin-go"
|
||||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||||
|
"github.com/minio/minio/internal/bucket/object/lock"
|
||||||
"github.com/minio/minio/internal/bucket/replication"
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
"github.com/minio/minio/internal/color"
|
"github.com/minio/minio/internal/color"
|
||||||
"github.com/minio/minio/internal/config/heal"
|
"github.com/minio/minio/internal/config/heal"
|
||||||
"github.com/minio/minio/internal/event"
|
"github.com/minio/minio/internal/event"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/console"
|
"github.com/minio/pkg/console"
|
||||||
|
uatomic "go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -64,9 +67,7 @@ var (
|
|||||||
dataScannerLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
dataScannerLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
||||||
// Sleeper values are updated when config is loaded.
|
// Sleeper values are updated when config is loaded.
|
||||||
scannerSleeper = newDynamicSleeper(10, 10*time.Second)
|
scannerSleeper = newDynamicSleeper(10, 10*time.Second)
|
||||||
scannerCycle = &safeDuration{
|
scannerCycle = uatomic.NewDuration(dataScannerStartDelay)
|
||||||
t: dataScannerStartDelay,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// initDataScanner will start the scanner in the background.
|
// initDataScanner will start the scanner in the background.
|
||||||
@@ -76,7 +77,7 @@ func initDataScanner(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
// Run the data scanner in a loop
|
// Run the data scanner in a loop
|
||||||
for {
|
for {
|
||||||
runDataScanner(ctx, objAPI)
|
runDataScanner(ctx, objAPI)
|
||||||
duration := time.Duration(r.Float64() * float64(scannerCycle.Get()))
|
duration := time.Duration(r.Float64() * float64(scannerCycle.Load()))
|
||||||
if duration < time.Second {
|
if duration < time.Second {
|
||||||
// Make sure to sleep atleast a second to avoid high CPU ticks.
|
// Make sure to sleep atleast a second to avoid high CPU ticks.
|
||||||
duration = time.Second
|
duration = time.Second
|
||||||
@@ -86,21 +87,61 @@ func initDataScanner(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
type safeDuration struct {
|
func getCycleScanMode(currentCycle, bitrotStartCycle uint64, bitrotStartTime time.Time) madmin.HealScanMode {
|
||||||
sync.Mutex
|
bitrotCycle := globalHealConfig.BitrotScanCycle()
|
||||||
t time.Duration
|
switch bitrotCycle {
|
||||||
|
case -1:
|
||||||
|
return madmin.HealNormalScan
|
||||||
|
case 0:
|
||||||
|
return madmin.HealDeepScan
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *safeDuration) Update(t time.Duration) {
|
if currentCycle-bitrotStartCycle < healObjectSelectProb {
|
||||||
s.Lock()
|
return madmin.HealDeepScan
|
||||||
defer s.Unlock()
|
|
||||||
s.t = t
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *safeDuration) Get() time.Duration {
|
if time.Since(bitrotStartTime) > bitrotCycle {
|
||||||
s.Lock()
|
return madmin.HealDeepScan
|
||||||
defer s.Unlock()
|
}
|
||||||
return s.t
|
|
||||||
|
return madmin.HealNormalScan
|
||||||
|
}
|
||||||
|
|
||||||
|
type backgroundHealInfo struct {
|
||||||
|
BitrotStartTime time.Time `json:"bitrotStartTime"`
|
||||||
|
BitrotStartCycle uint64 `json:"bitrotStartCycle"`
|
||||||
|
CurrentScanMode madmin.HealScanMode `json:"currentScanMode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBackgroundHealInfo(ctx context.Context, objAPI ObjectLayer) backgroundHealInfo {
|
||||||
|
// Get last healing information
|
||||||
|
buf, err := readConfig(ctx, objAPI, backgroundHealInfoPath)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, errConfigNotFound) {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return backgroundHealInfo{}
|
||||||
|
}
|
||||||
|
var info backgroundHealInfo
|
||||||
|
err = json.Unmarshal(buf, &info)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return backgroundHealInfo{}
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveBackgroundHealInfo(ctx context.Context, objAPI ObjectLayer, info backgroundHealInfo) {
|
||||||
|
b, err := json.Marshal(info)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Get last healing information
|
||||||
|
err = saveConfig(ctx, objAPI, backgroundHealInfoPath, b)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runDataScanner will start a data scanner.
|
// runDataScanner will start a data scanner.
|
||||||
@@ -130,7 +171,7 @@ func runDataScanner(pctx context.Context, objAPI ObjectLayer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scannerTimer := time.NewTimer(scannerCycle.Get())
|
scannerTimer := time.NewTimer(scannerCycle.Load())
|
||||||
defer scannerTimer.Stop()
|
defer scannerTimer.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -138,19 +179,28 @@ func runDataScanner(pctx context.Context, objAPI ObjectLayer) {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-scannerTimer.C:
|
case <-scannerTimer.C:
|
||||||
// Reset the timer for next cycle.
|
|
||||||
scannerTimer.Reset(scannerCycle.Get())
|
|
||||||
|
|
||||||
if intDataUpdateTracker.debug {
|
if intDataUpdateTracker.debug {
|
||||||
console.Debugln("starting scanner cycle")
|
console.Debugln("starting scanner cycle")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bgHealInfo := readBackgroundHealInfo(ctx, objAPI)
|
||||||
|
scanMode := getCycleScanMode(nextBloomCycle, bgHealInfo.BitrotStartCycle, bgHealInfo.BitrotStartTime)
|
||||||
|
if bgHealInfo.CurrentScanMode != scanMode {
|
||||||
|
newHealInfo := bgHealInfo
|
||||||
|
newHealInfo.CurrentScanMode = scanMode
|
||||||
|
if scanMode == madmin.HealDeepScan {
|
||||||
|
newHealInfo.BitrotStartTime = time.Now().UTC()
|
||||||
|
newHealInfo.BitrotStartCycle = nextBloomCycle
|
||||||
|
}
|
||||||
|
saveBackgroundHealInfo(ctx, objAPI, newHealInfo)
|
||||||
|
}
|
||||||
|
|
||||||
// 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, nextBloomCycle)
|
bf, err := globalNotificationSys.updateBloomFilter(ctx, nextBloomCycle)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
err = objAPI.NSScanner(ctx, bf, results, uint32(nextBloomCycle))
|
err = objAPI.NSScanner(ctx, bf, results, uint32(nextBloomCycle), scanMode)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Store new cycle...
|
// Store new cycle...
|
||||||
@@ -161,6 +211,9 @@ func runDataScanner(pctx context.Context, objAPI ObjectLayer) {
|
|||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the timer for next cycle.
|
||||||
|
scannerTimer.Reset(scannerCycle.Load())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +235,7 @@ type folderScanner struct {
|
|||||||
dataUsageScannerDebug bool
|
dataUsageScannerDebug bool
|
||||||
healFolderInclude uint32 // Include a clean folder one in n cycles.
|
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
|
||||||
|
|
||||||
disks []StorageAPI
|
disks []StorageAPI
|
||||||
disksQuorum int
|
disksQuorum int
|
||||||
@@ -250,7 +304,7 @@ var globalScannerStats scannerStats
|
|||||||
// The returned cache will always be valid, but may not be updated from the existing.
|
// The returned cache will always be valid, but may not be updated from the existing.
|
||||||
// Before each operation sleepDuration is called which can be used to temporarily halt the scanner.
|
// Before each operation sleepDuration is called which can be used to temporarily halt the scanner.
|
||||||
// If the supplied context is canceled the function will return at the first chance.
|
// If the supplied context is canceled the function will return at the first chance.
|
||||||
func scanDataFolder(ctx context.Context, poolIdx, setIdx int, basePath string, cache dataUsageCache, getSize getSizeFn) (dataUsageCache, error) {
|
func scanDataFolder(ctx context.Context, poolIdx, setIdx int, basePath string, cache dataUsageCache, getSize getSizeFn, scanMode madmin.HealScanMode) (dataUsageCache, error) {
|
||||||
t := UTCNow()
|
t := UTCNow()
|
||||||
|
|
||||||
logPrefix := color.Green("data-usage: ")
|
logPrefix := color.Green("data-usage: ")
|
||||||
@@ -279,6 +333,7 @@ func scanDataFolder(ctx context.Context, poolIdx, setIdx int, basePath string, c
|
|||||||
dataUsageScannerDebug: intDataUpdateTracker.debug,
|
dataUsageScannerDebug: intDataUpdateTracker.debug,
|
||||||
healFolderInclude: 0,
|
healFolderInclude: 0,
|
||||||
healObjectSelect: 0,
|
healObjectSelect: 0,
|
||||||
|
scanMode: scanMode,
|
||||||
updates: cache.Info.updates,
|
updates: cache.Info.updates,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +458,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
if filter != nil && ok && existing.Compacted {
|
if filter != nil && ok && existing.Compacted {
|
||||||
// If folder isn't in filter and we have data, skip it completely.
|
// If folder isn't in filter and we have data, skip it completely.
|
||||||
if folder.name != dataUsageRoot && !filter.containsDir(folder.name) {
|
if folder.name != dataUsageRoot && !filter.containsDir(folder.name) {
|
||||||
if f.healObjectSelect == 0 || !thisHash.mod(f.oldCache.Info.NextCycle, f.healFolderInclude/folder.objectHealProbDiv) {
|
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.newCache.copyWithChildren(&f.oldCache, thisHash, folder.parent)
|
||||||
f.updateCache.copyWithChildren(&f.oldCache, thisHash, folder.parent)
|
f.updateCache.copyWithChildren(&f.oldCache, thisHash, folder.parent)
|
||||||
if f.dataUsageScannerDebug {
|
if f.dataUsageScannerDebug {
|
||||||
@@ -482,12 +537,15 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
debug: f.dataUsageScannerDebug,
|
debug: f.dataUsageScannerDebug,
|
||||||
lifeCycle: activeLifeCycle,
|
lifeCycle: activeLifeCycle,
|
||||||
replication: replicationCfg,
|
replication: replicationCfg,
|
||||||
heal: thisHash.mod(f.oldCache.Info.NextCycle, f.healObjectSelect/folder.objectHealProbDiv) && globalIsErasure,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.heal.enabled = thisHash.modAlt(f.oldCache.Info.NextCycle/folder.objectHealProbDiv, f.healObjectSelect/folder.objectHealProbDiv) && globalIsErasure
|
||||||
|
item.heal.bitrot = f.scanMode == madmin.HealDeepScan
|
||||||
|
|
||||||
// if the drive belongs to an erasure set
|
// if the drive belongs to an erasure set
|
||||||
// that is already being healed, skip the
|
// that is already being healed, skip the
|
||||||
// healing attempt on this drive.
|
// healing attempt on this drive.
|
||||||
item.heal = item.heal && f.healObjectSelect > 0
|
item.heal.enabled = item.heal.enabled && f.healObjectSelect > 0
|
||||||
|
|
||||||
sz, err := f.getSize(item)
|
sz, err := f.getSize(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -609,13 +667,13 @@ 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.mod(f.oldCache.Info.NextCycle, f.healFolderInclude/folder.objectHealProbDiv) {
|
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 = dataUsageUpdateDirCycles
|
folder.objectHealProbDiv = f.healFolderInclude
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scanFolder(folder)
|
scanFolder(folder)
|
||||||
@@ -821,7 +879,10 @@ type scannerItem struct {
|
|||||||
replication replicationConfig
|
replication replicationConfig
|
||||||
lifeCycle *lifecycle.Lifecycle
|
lifeCycle *lifecycle.Lifecycle
|
||||||
Typ fs.FileMode
|
Typ fs.FileMode
|
||||||
heal bool // Has the object been selected for heal check?
|
heal struct {
|
||||||
|
enabled bool
|
||||||
|
bitrot bool
|
||||||
|
} // Has the object been selected for heal check?
|
||||||
debug bool
|
debug bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,9 +935,13 @@ func (i *scannerItem) applyHealing(ctx context.Context, o ObjectLayer, oi Object
|
|||||||
console.Debugf(applyActionsLogPrefix+" heal checking: %v/%v\n", i.bucket, i.objectPath())
|
console.Debugf(applyActionsLogPrefix+" heal checking: %v/%v\n", i.bucket, i.objectPath())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
scanMode := madmin.HealNormalScan
|
||||||
|
if i.heal.bitrot {
|
||||||
|
scanMode = madmin.HealDeepScan
|
||||||
|
}
|
||||||
healOpts := madmin.HealOpts{
|
healOpts := madmin.HealOpts{
|
||||||
Remove: healDeleteDangling,
|
Remove: healDeleteDangling,
|
||||||
ScanMode: globalHealConfig.ScanMode(),
|
ScanMode: scanMode,
|
||||||
}
|
}
|
||||||
res, err := o.HealObject(ctx, i.bucket, i.objectPath(), oi.VersionID, healOpts)
|
res, err := o.HealObject(ctx, i.bucket, i.objectPath(), oi.VersionID, healOpts)
|
||||||
if err != nil && !errors.Is(err, NotImplemented{}) {
|
if err != nil && !errors.Is(err, NotImplemented{}) {
|
||||||
@@ -901,20 +966,8 @@ func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, oi Obje
|
|||||||
|
|
||||||
atomic.AddUint64(&globalScannerStats.ilmChecks, 1)
|
atomic.AddUint64(&globalScannerStats.ilmChecks, 1)
|
||||||
versionID := oi.VersionID
|
versionID := oi.VersionID
|
||||||
action := i.lifeCycle.ComputeAction(
|
rCfg, _ := globalBucketObjectLockSys.Get(i.bucket)
|
||||||
lifecycle.ObjectOpts{
|
action := evalActionFromLifecycle(ctx, *i.lifeCycle, rCfg, oi, false)
|
||||||
Name: i.objectPath(),
|
|
||||||
UserTags: oi.UserTags,
|
|
||||||
ModTime: oi.ModTime,
|
|
||||||
VersionID: oi.VersionID,
|
|
||||||
DeleteMarker: oi.DeleteMarker,
|
|
||||||
IsLatest: oi.IsLatest,
|
|
||||||
NumVersions: oi.NumVersions,
|
|
||||||
SuccessorModTime: oi.SuccessorModTime,
|
|
||||||
RestoreOngoing: oi.RestoreOngoing,
|
|
||||||
RestoreExpires: oi.RestoreExpires,
|
|
||||||
TransitionStatus: oi.TransitionedObject.Status,
|
|
||||||
})
|
|
||||||
if i.debug {
|
if i.debug {
|
||||||
if versionID != "" {
|
if versionID != "" {
|
||||||
console.Debugf(applyActionsLogPrefix+" lifecycle: %q (version-id=%s), Initial scan: %v\n", i.objectPath(), versionID, action)
|
console.Debugf(applyActionsLogPrefix+" lifecycle: %q (version-id=%s), Initial scan: %v\n", i.objectPath(), versionID, action)
|
||||||
@@ -986,9 +1039,13 @@ func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ Ob
|
|||||||
fivs = fivs[:lim+1]
|
fivs = fivs[:lim+1]
|
||||||
|
|
||||||
rcfg, _ := globalBucketObjectLockSys.Get(i.bucket)
|
rcfg, _ := globalBucketObjectLockSys.Get(i.bucket)
|
||||||
|
vcfg, _ := globalBucketVersioningSys.Get(i.bucket)
|
||||||
|
|
||||||
|
versioned := vcfg != nil && vcfg.Versioned(i.objectPath())
|
||||||
|
|
||||||
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())
|
obj := fi.ToObjectInfo(i.bucket, i.objectPath(), versioned)
|
||||||
// skip versions with object locking enabled
|
// skip versions with object locking enabled
|
||||||
if rcfg.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
|
if rcfg.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
|
||||||
if i.debug {
|
if i.debug {
|
||||||
@@ -1040,7 +1097,7 @@ func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, oi Object
|
|||||||
// from the current deployment, which means we don't have to call healing
|
// from the current deployment, which means we don't have to call healing
|
||||||
// routine even if we are asked to do via heal flag.
|
// routine even if we are asked to do via heal flag.
|
||||||
if !applied {
|
if !applied {
|
||||||
if i.heal {
|
if i.heal.enabled {
|
||||||
size = i.applyHealing(ctx, o, oi)
|
size = i.applyHealing(ctx, o, oi)
|
||||||
}
|
}
|
||||||
// replicate only if lifecycle rules are not applied.
|
// replicate only if lifecycle rules are not applied.
|
||||||
@@ -1049,7 +1106,7 @@ func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, oi Object
|
|||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, obj ObjectInfo, debug bool) (action lifecycle.Action) {
|
func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr lock.Retention, obj ObjectInfo, debug bool) (action lifecycle.Action) {
|
||||||
action = lc.ComputeAction(obj.ToLifecycleOpts())
|
action = lc.ComputeAction(obj.ToLifecycleOpts())
|
||||||
if debug {
|
if debug {
|
||||||
console.Debugf(applyActionsLogPrefix+" lifecycle: Secondary scan: %v\n", action)
|
console.Debugf(applyActionsLogPrefix+" lifecycle: Secondary scan: %v\n", action)
|
||||||
@@ -1065,9 +1122,7 @@ func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, obj Ob
|
|||||||
if obj.VersionID == "" {
|
if obj.VersionID == "" {
|
||||||
return lifecycle.NoneAction
|
return lifecycle.NoneAction
|
||||||
}
|
}
|
||||||
if rcfg, _ := globalBucketObjectLockSys.Get(obj.Bucket); rcfg.LockEnabled {
|
if lr.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
|
||||||
locked := enforceRetentionForDeletion(ctx, obj)
|
|
||||||
if locked {
|
|
||||||
if debug {
|
if debug {
|
||||||
if obj.VersionID != "" {
|
if obj.VersionID != "" {
|
||||||
console.Debugf(applyActionsLogPrefix+" lifecycle: %s v(%s) is locked, not deleting\n", obj.Name, obj.VersionID)
|
console.Debugf(applyActionsLogPrefix+" lifecycle: %s v(%s) is locked, not deleting\n", obj.Name, obj.VersionID)
|
||||||
@@ -1078,7 +1133,6 @@ func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, obj Ob
|
|||||||
return lifecycle.NoneAction
|
return lifecycle.NoneAction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return action
|
return action
|
||||||
}
|
}
|
||||||
@@ -1116,7 +1170,8 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
|
|||||||
opts.VersionID = obj.VersionID
|
opts.VersionID = obj.VersionID
|
||||||
}
|
}
|
||||||
if opts.VersionID == "" {
|
if opts.VersionID == "" {
|
||||||
opts.Versioned = globalBucketVersioningSys.Enabled(obj.Bucket)
|
opts.Versioned = globalBucketVersioningSys.PrefixEnabled(obj.Bucket, obj.Name)
|
||||||
|
opts.VersionSuspended = globalBucketVersioningSys.PrefixSuspended(obj.Bucket, obj.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
obj, err := objLayer.DeleteObject(ctx, obj.Bucket, obj.Name, opts)
|
obj, err := objLayer.DeleteObject(ctx, obj.Bucket, obj.Name, opts)
|
||||||
@@ -1175,6 +1230,9 @@ func (i *scannerItem) objectPath() string {
|
|||||||
// healReplication will heal a scanned item that has failed replication.
|
// healReplication will heal a scanned item that has failed replication.
|
||||||
func (i *scannerItem) healReplication(ctx context.Context, o ObjectLayer, oi ObjectInfo, sizeS *sizeSummary) {
|
func (i *scannerItem) healReplication(ctx context.Context, o ObjectLayer, oi ObjectInfo, sizeS *sizeSummary) {
|
||||||
roi := getHealReplicateObjectInfo(oi, i.replication)
|
roi := getHealReplicateObjectInfo(oi, i.replication)
|
||||||
|
if !roi.Dsc.ReplicateAny() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
|
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
|
||||||
// heal delete marker replication failure or versioned delete replication failure
|
// heal delete marker replication failure or versioned delete replication failure
|
||||||
@@ -1195,10 +1253,10 @@ func (i *scannerItem) healReplication(ctx context.Context, o ObjectLayer, oi Obj
|
|||||||
roi.OpType = replication.ExistingObjectReplicationType
|
roi.OpType = replication.ExistingObjectReplicationType
|
||||||
}
|
}
|
||||||
|
|
||||||
if roi.TargetStatuses != nil {
|
if sizeS.replTargetStats == nil && len(roi.TargetStatuses) > 0 {
|
||||||
if sizeS.replTargetStats == nil {
|
|
||||||
sizeS.replTargetStats = make(map[string]replTargetSizeSummary)
|
sizeS.replTargetStats = make(map[string]replTargetSizeSummary)
|
||||||
}
|
}
|
||||||
|
|
||||||
for arn, tgtStatus := range roi.TargetStatuses {
|
for arn, tgtStatus := range roi.TargetStatuses {
|
||||||
tgtSizeS, ok := sizeS.replTargetStats[arn]
|
tgtSizeS, ok := sizeS.replTargetStats[arn]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -1215,13 +1273,12 @@ func (i *scannerItem) healReplication(ctx context.Context, o ObjectLayer, oi Obj
|
|||||||
tgtSizeS.failedCount++
|
tgtSizeS.failedCount++
|
||||||
sizeS.failedSize += oi.Size
|
sizeS.failedSize += oi.Size
|
||||||
sizeS.failedCount++
|
sizeS.failedCount++
|
||||||
case replication.Completed, "COMPLETE":
|
case replication.Completed, replication.CompletedLegacy:
|
||||||
tgtSizeS.replicatedSize += oi.Size
|
tgtSizeS.replicatedSize += oi.Size
|
||||||
sizeS.replicatedSize += oi.Size
|
sizeS.replicatedSize += oi.Size
|
||||||
}
|
}
|
||||||
sizeS.replTargetStats[arn] = tgtSizeS
|
sizeS.replTargetStats[arn] = tgtSizeS
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
switch oi.ReplicationStatus {
|
switch oi.ReplicationStatus {
|
||||||
case replication.Pending, replication.Failed:
|
case replication.Pending, replication.Failed:
|
||||||
|
|||||||
@@ -369,6 +369,17 @@ func (h dataUsageHash) mod(cycle uint32, cycles uint32) bool {
|
|||||||
return uint32(xxhash.Sum64String(string(h)))%cycles == cycle%cycles
|
return uint32(xxhash.Sum64String(string(h)))%cycles == cycle%cycles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// modAlt returns true if the hash mod cycles == cycle.
|
||||||
|
// This is out of sync with mod.
|
||||||
|
// If cycles is 0 false is always returned.
|
||||||
|
// If cycles is 1 true is always returned (as expected).
|
||||||
|
func (h dataUsageHash) modAlt(cycle uint32, cycles uint32) bool {
|
||||||
|
if cycles <= 1 {
|
||||||
|
return cycles == 1
|
||||||
|
}
|
||||||
|
return uint32(xxhash.Sum64String(string(h))>>32)%(cycles) == cycle%cycles
|
||||||
|
}
|
||||||
|
|
||||||
// addChild will add a child based on its hash.
|
// addChild will add a child based on its hash.
|
||||||
// If it already exists it will not be added again.
|
// If it already exists it will not be added again.
|
||||||
func (e *dataUsageEntry) addChild(hash dataUsageHash) {
|
func (e *dataUsageEntry) addChild(hash dataUsageHash) {
|
||||||
@@ -526,6 +537,7 @@ func (d *dataUsageCache) dui(path string, buckets []BucketInfo) DataUsageInfo {
|
|||||||
dui := DataUsageInfo{
|
dui := DataUsageInfo{
|
||||||
LastUpdate: d.Info.LastUpdate,
|
LastUpdate: d.Info.LastUpdate,
|
||||||
ObjectsTotalCount: flat.Objects,
|
ObjectsTotalCount: flat.Objects,
|
||||||
|
VersionsTotalCount: flat.Versions,
|
||||||
ObjectsTotalSize: uint64(flat.Size),
|
ObjectsTotalSize: uint64(flat.Size),
|
||||||
BucketsCount: uint64(len(e.Children)),
|
BucketsCount: uint64(len(e.Children)),
|
||||||
BucketsUsage: d.bucketsUsageInfo(buckets),
|
BucketsUsage: d.bucketsUsageInfo(buckets),
|
||||||
@@ -777,6 +789,7 @@ func (d *dataUsageCache) bucketsUsageInfo(buckets []BucketInfo) map[string]Bucke
|
|||||||
flat := d.flatten(*e)
|
flat := d.flatten(*e)
|
||||||
bui := BucketUsageInfo{
|
bui := BucketUsageInfo{
|
||||||
Size: uint64(flat.Size),
|
Size: uint64(flat.Size),
|
||||||
|
VersionsCount: flat.Versions,
|
||||||
ObjectsCount: flat.Objects,
|
ObjectsCount: flat.Objects,
|
||||||
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-18
@@ -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"`
|
||||||
|
VersionsCount uint64 `json:"versionsCount"`
|
||||||
ReplicaSize uint64 `json:"objectReplicaTotalSize"`
|
ReplicaSize uint64 `json:"objectReplicaTotalSize"`
|
||||||
ReplicationInfo map[string]BucketTargetUsageInfo `json:"objectsReplicationInfo"`
|
ReplicationInfo map[string]BucketTargetUsageInfo `json:"objectsReplicationInfo"`
|
||||||
}
|
}
|
||||||
@@ -73,6 +74,9 @@ type DataUsageInfo struct {
|
|||||||
// Objects total count across all buckets
|
// Objects total count across all buckets
|
||||||
ObjectsTotalCount uint64 `json:"objectsCount"`
|
ObjectsTotalCount uint64 `json:"objectsCount"`
|
||||||
|
|
||||||
|
// Objects total count across all buckets
|
||||||
|
VersionsTotalCount uint64 `json:"versionsCount"`
|
||||||
|
|
||||||
// Objects total size across all buckets
|
// Objects total size across all buckets
|
||||||
ObjectsTotalSize uint64 `json:"objectsTotalSize"`
|
ObjectsTotalSize uint64 `json:"objectsTotalSize"`
|
||||||
ReplicationInfo map[string]BucketTargetUsageInfo `json:"objectsReplicationInfo"`
|
ReplicationInfo map[string]BucketTargetUsageInfo `json:"objectsReplicationInfo"`
|
||||||
@@ -92,32 +96,37 @@ type DataUsageInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dui DataUsageInfo) tierStats() []madmin.TierInfo {
|
func (dui DataUsageInfo) tierStats() []madmin.TierInfo {
|
||||||
if globalTierConfigMgr.Empty() {
|
if dui.TierStats == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := make(map[string]madmin.TierStats)
|
cfgs := globalTierConfigMgr.ListTiers()
|
||||||
// Add configured remote tiers
|
if len(cfgs) == 0 {
|
||||||
for tier := range globalTierConfigMgr.Tiers {
|
return nil
|
||||||
ts[tier] = madmin.TierStats{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ts := make(map[string]madmin.TierStats, len(cfgs)+1)
|
||||||
|
infos := make([]madmin.TierInfo, 0, len(ts))
|
||||||
|
|
||||||
// Add STANDARD (hot-tier)
|
// Add STANDARD (hot-tier)
|
||||||
ts[minioHotTier] = madmin.TierStats{}
|
ts[minioHotTier] = madmin.TierStats{}
|
||||||
|
infos = append(infos, madmin.TierInfo{
|
||||||
|
Name: minioHotTier,
|
||||||
|
Type: "internal",
|
||||||
|
})
|
||||||
|
// Add configured remote tiers
|
||||||
|
for _, cfg := range cfgs {
|
||||||
|
ts[cfg.Name] = madmin.TierStats{}
|
||||||
|
infos = append(infos, madmin.TierInfo{
|
||||||
|
Name: cfg.Name,
|
||||||
|
Type: cfg.Type.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
ts = dui.TierStats.adminStats(ts)
|
ts = dui.TierStats.adminStats(ts)
|
||||||
infos := make([]madmin.TierInfo, 0, len(ts))
|
for i := range infos {
|
||||||
for tier, st := range ts {
|
info := infos[i]
|
||||||
var tierType string
|
infos[i].Stats = ts[info.Name]
|
||||||
if tier == minioHotTier {
|
|
||||||
tierType = "internal"
|
|
||||||
} else {
|
|
||||||
tierType = globalTierConfigMgr.Tiers[tier].Type.String()
|
|
||||||
}
|
|
||||||
infos = append(infos, madmin.TierInfo{
|
|
||||||
Name: tier,
|
|
||||||
Type: tierType,
|
|
||||||
Stats: st,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(infos, func(i, j int) bool {
|
sort.Slice(infos, func(i, j int) bool {
|
||||||
@@ -131,3 +140,31 @@ func (dui DataUsageInfo) tierStats() []madmin.TierInfo {
|
|||||||
})
|
})
|
||||||
return infos
|
return infos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dui DataUsageInfo) tierMetrics() (metrics []Metric) {
|
||||||
|
if dui.TierStats == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// e.g minio_cluster_ilm_transitioned_bytes{tier="S3TIER-1"}=136314880
|
||||||
|
// minio_cluster_ilm_transitioned_objects{tier="S3TIER-1"}=1
|
||||||
|
// minio_cluster_ilm_transitioned_versions{tier="S3TIER-1"}=3
|
||||||
|
for tier, st := range dui.TierStats.Tiers {
|
||||||
|
metrics = append(metrics, Metric{
|
||||||
|
Description: getClusterTransitionedBytesMD(),
|
||||||
|
Value: float64(st.TotalSize),
|
||||||
|
VariableLabels: map[string]string{"tier": tier},
|
||||||
|
})
|
||||||
|
metrics = append(metrics, Metric{
|
||||||
|
Description: getClusterTransitionedObjectsMD(),
|
||||||
|
Value: float64(st.NumObjects),
|
||||||
|
VariableLabels: map[string]string{"tier": tier},
|
||||||
|
})
|
||||||
|
metrics = append(metrics, Metric{
|
||||||
|
Description: getClusterTransitionedVersionsMD(),
|
||||||
|
Value: float64(st.NumVersions),
|
||||||
|
VariableLabels: map[string]string{"tier": tier},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ const (
|
|||||||
dataUsageBloomName = ".bloomcycle.bin"
|
dataUsageBloomName = ".bloomcycle.bin"
|
||||||
dataUsageBloomNamePath = bucketMetaPrefix + SlashSeparator + dataUsageBloomName
|
dataUsageBloomNamePath = bucketMetaPrefix + SlashSeparator + dataUsageBloomName
|
||||||
|
|
||||||
|
backgroundHealInfoPath = bucketMetaPrefix + SlashSeparator + ".background-heal.json"
|
||||||
|
|
||||||
dataUsageCacheName = ".usage-cache.bin"
|
dataUsageCacheName = ".usage-cache.bin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func TestDataUsageUpdate(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := scanDataFolder(context.Background(), 0, 0, base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize)
|
got, err := scanDataFolder(context.Background(), 0, 0, base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -178,7 +178,7 @@ func TestDataUsageUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Changed dir must be picked up in this many cycles.
|
// Changed dir must be picked up in this many cycles.
|
||||||
for i := 0; i < dataUsageUpdateDirCycles; i++ {
|
for i := 0; i < dataUsageUpdateDirCycles; i++ {
|
||||||
got, err = scanDataFolder(context.Background(), 0, 0, base, got, getSize)
|
got, err = scanDataFolder(context.Background(), 0, 0, base, got, getSize, 0)
|
||||||
got.Info.NextCycle++
|
got.Info.NextCycle++
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -289,7 +289,7 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
got, err := scanDataFolder(context.Background(), 0, 0, base, dataUsageCache{Info: dataUsageCacheInfo{Name: "bucket"}}, getSize)
|
got, err := scanDataFolder(context.Background(), 0, 0, base, dataUsageCache{Info: dataUsageCacheInfo{Name: "bucket"}}, getSize, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -423,7 +423,7 @@ func TestDataUsageUpdatePrefix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Changed dir must be picked up in this many cycles.
|
// Changed dir must be picked up in this many cycles.
|
||||||
for i := 0; i < dataUsageUpdateDirCycles; i++ {
|
for i := 0; i < dataUsageUpdateDirCycles; i++ {
|
||||||
got, err = scanDataFolder(context.Background(), 0, 0, base, got, getSize)
|
got, err = scanDataFolder(context.Background(), 0, 0, base, got, getSize, 0)
|
||||||
got.Info.NextCycle++
|
got.Info.NextCycle++
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -575,7 +575,7 @@ func TestDataUsageCacheSerialize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
want, err := scanDataFolder(context.Background(), 0, 0, base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize)
|
want, err := scanDataFolder(context.Background(), 0, 0, base, dataUsageCache{Info: dataUsageCacheInfo{Name: bucket}}, getSize, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -252,16 +253,17 @@ func (c *diskCache) diskUsageLow() bool {
|
|||||||
// Returns if the disk usage reaches or exceeds configured cache quota when size is added.
|
// Returns if the disk usage reaches or exceeds configured cache quota when size is added.
|
||||||
// If current usage without size exceeds high watermark a GC is automatically queued.
|
// If current usage without size exceeds high watermark a GC is automatically queued.
|
||||||
func (c *diskCache) diskSpaceAvailable(size int64) bool {
|
func (c *diskCache) diskSpaceAvailable(size int64) bool {
|
||||||
|
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
|
||||||
|
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||||
|
|
||||||
gcTriggerPct := c.quotaPct * c.highWatermark / 100
|
gcTriggerPct := c.quotaPct * c.highWatermark / 100
|
||||||
di, err := disk.GetInfo(c.dir)
|
di, err := disk.GetInfo(c.dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
|
|
||||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if di.Total == 0 {
|
if di.Total == 0 {
|
||||||
logger.Info("diskCache: Received 0 total disk size")
|
logger.LogIf(ctx, errors.New("diskCache: Received 0 total disk size"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
usedPercent := float64(di.Used) * 100 / float64(di.Total)
|
usedPercent := float64(di.Used) * 100 / float64(di.Total)
|
||||||
@@ -882,6 +884,7 @@ func (c *diskCache) put(ctx context.Context, bucket, object string, data io.Read
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(cachePath, 0o777); err != nil {
|
if err := os.MkdirAll(cachePath, 0o777); err != nil {
|
||||||
|
removeAll(cachePath)
|
||||||
return oi, err
|
return oi, err
|
||||||
}
|
}
|
||||||
metadata := cloneMSS(opts.UserDefined)
|
metadata := cloneMSS(opts.UserDefined)
|
||||||
@@ -890,6 +893,7 @@ func (c *diskCache) put(ctx context.Context, bucket, object string, data io.Read
|
|||||||
if globalCacheKMS != nil {
|
if globalCacheKMS != nil {
|
||||||
reader, err = newCacheEncryptReader(data, bucket, object, metadata)
|
reader, err = newCacheEncryptReader(data, bucket, object, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
removeAll(cachePath)
|
||||||
return oi, err
|
return oi, err
|
||||||
}
|
}
|
||||||
actualSize, _ = sio.EncryptedSize(uint64(size))
|
actualSize, _ = sio.EncryptedSize(uint64(size))
|
||||||
@@ -1626,8 +1630,6 @@ func (c *diskCache) cleanupStaleUploads(ctx context.Context) {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
// Reset for the next interval
|
|
||||||
timer.Reset(cacheStaleUploadCleanupInterval)
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
readDirFn(pathJoin(c.dir, minioMetaBucket, cacheMultipartDir), func(shaDir string, typ os.FileMode) error {
|
readDirFn(pathJoin(c.dir, minioMetaBucket, cacheMultipartDir), func(shaDir string, typ os.FileMode) error {
|
||||||
return readDirFn(pathJoin(c.dir, minioMetaBucket, cacheMultipartDir, shaDir), func(uploadIDDir string, typ os.FileMode) error {
|
return readDirFn(pathJoin(c.dir, minioMetaBucket, cacheMultipartDir, shaDir), func(uploadIDDir string, typ os.FileMode) error {
|
||||||
@@ -1658,6 +1660,9 @@ func (c *diskCache) cleanupStaleUploads(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Reset for the next interval
|
||||||
|
timer.Reset(cacheStaleUploadCleanupInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-54
@@ -19,19 +19,17 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
"container/list"
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio/internal/crypto"
|
"github.com/minio/minio/internal/crypto"
|
||||||
"github.com/minio/minio/internal/kms"
|
"github.com/minio/minio/internal/etag"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CacheStatusType - whether the request was served from cache.
|
// CacheStatusType - whether the request was served from cache.
|
||||||
@@ -234,77 +232,85 @@ func isCacheEncrypted(meta map[string]string) bool {
|
|||||||
|
|
||||||
// decryptCacheObjectETag tries to decrypt the ETag saved in encrypted format using the cache KMS
|
// decryptCacheObjectETag tries to decrypt the ETag saved in encrypted format using the cache KMS
|
||||||
func decryptCacheObjectETag(info *ObjectInfo) error {
|
func decryptCacheObjectETag(info *ObjectInfo) error {
|
||||||
// Directories are never encrypted.
|
|
||||||
if info.IsDir {
|
if info.IsDir {
|
||||||
return nil
|
return nil // Directories are never encrypted.
|
||||||
}
|
}
|
||||||
encrypted := crypto.S3.IsEncrypted(info.UserDefined) && isCacheEncrypted(info.UserDefined)
|
|
||||||
|
|
||||||
switch {
|
// Depending on the SSE type we handle ETags slightly
|
||||||
case encrypted:
|
// differently. ETags encrypted with SSE-S3 must be
|
||||||
if globalCacheKMS == nil {
|
// decrypted first, since the client expects that
|
||||||
return errKMSNotConfigured
|
// a single-part SSE-S3 ETag is equal to the content MD5.
|
||||||
}
|
//
|
||||||
if len(info.Parts) > 0 { // multipart ETag is not encrypted since it is not md5sum
|
// For all other SSE types, the ETag is not the content MD5.
|
||||||
return nil
|
// Therefore, we don't decrypt but only format it.
|
||||||
}
|
switch kind, ok := crypto.IsEncrypted(info.UserDefined); {
|
||||||
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(info.UserDefined)
|
case ok && kind == crypto.S3 && isCacheEncrypted(info.UserDefined):
|
||||||
|
ETag, err := etag.Parse(info.ETag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
extKey, err := globalCacheKMS.DecryptKey(keyID, kmsKey, kms.Context{info.Bucket: path.Join(info.Bucket, info.Name)})
|
if !ETag.IsEncrypted() {
|
||||||
if err != nil {
|
info.ETag = ETag.Format().String()
|
||||||
return err
|
|
||||||
}
|
|
||||||
var objectKey crypto.ObjectKey
|
|
||||||
if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), info.Bucket, info.Name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
etagStr := tryDecryptETag(objectKey[:], info.ETag, false)
|
|
||||||
// backend ETag was hex encoded before encrypting, so hex decode to get actual ETag
|
|
||||||
etag, err := hex.DecodeString(etagStr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
info.ETag = string(etag)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
key, err := crypto.S3.UnsealObjectKey(globalCacheKMS, info.UserDefined, info.Bucket, info.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ETag, err = etag.Decrypt(key[:], ETag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info.ETag = ETag.Format().String()
|
||||||
|
case ok && (kind == crypto.S3KMS || kind == crypto.SSEC) && isCacheEncrypted(info.UserDefined):
|
||||||
|
ETag, err := etag.Parse(info.ETag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info.ETag = ETag.Format().String()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// decryptCacheObjectETag tries to decrypt the ETag saved in encrypted format using the cache KMS
|
// decryptCacheObjectETag tries to decrypt the ETag saved in encrypted format using the cache KMS
|
||||||
func decryptCachePartETags(c *cacheMeta) ([]string, error) {
|
func decryptCachePartETags(c *cacheMeta) ([]string, error) {
|
||||||
var partETags []string
|
// Depending on the SSE type we handle ETags slightly
|
||||||
encrypted := crypto.S3.IsEncrypted(c.Meta) && isCacheEncrypted(c.Meta)
|
// differently. ETags encrypted with SSE-S3 must be
|
||||||
|
// decrypted first, since the client expects that
|
||||||
switch {
|
// a single-part SSE-S3 ETag is equal to the content MD5.
|
||||||
case encrypted:
|
//
|
||||||
if globalCacheKMS == nil {
|
// For all other SSE types, the ETag is not the content MD5.
|
||||||
return partETags, errKMSNotConfigured
|
// Therefore, we don't decrypt but only format it.
|
||||||
}
|
switch kind, ok := crypto.IsEncrypted(c.Meta); {
|
||||||
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(c.Meta)
|
case ok && kind == crypto.S3 && isCacheEncrypted(c.Meta):
|
||||||
|
key, err := crypto.S3.UnsealObjectKey(globalCacheKMS, c.Meta, c.Bucket, c.Object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return partETags, err
|
return nil, err
|
||||||
}
|
|
||||||
extKey, err := globalCacheKMS.DecryptKey(keyID, kmsKey, kms.Context{c.Bucket: path.Join(c.Bucket, c.Object)})
|
|
||||||
if err != nil {
|
|
||||||
return partETags, err
|
|
||||||
}
|
|
||||||
var objectKey crypto.ObjectKey
|
|
||||||
if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), c.Bucket, c.Object); err != nil {
|
|
||||||
return partETags, err
|
|
||||||
}
|
}
|
||||||
|
etags := make([]string, 0, len(c.PartETags))
|
||||||
for i := range c.PartETags {
|
for i := range c.PartETags {
|
||||||
etagStr := tryDecryptETag(objectKey[:], c.PartETags[i], false)
|
ETag, err := etag.Parse(c.PartETags[i])
|
||||||
// backend ETag was hex encoded before encrypting, so hex decode to get actual ETag
|
|
||||||
etag, err := hex.DecodeString(etagStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
partETags = append(partETags, string(etag))
|
ETag, err = etag.Decrypt(key[:], ETag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return partETags, nil
|
etags = append(etags, ETag.Format().String())
|
||||||
|
}
|
||||||
|
return etags, nil
|
||||||
|
case ok && (kind == crypto.S3KMS || kind == crypto.SSEC) && isCacheEncrypted(c.Meta):
|
||||||
|
etags := make([]string, 0, len(c.PartETags))
|
||||||
|
for i := range c.PartETags {
|
||||||
|
ETag, err := etag.Parse(c.PartETags[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
etags = append(etags, ETag.Format().String())
|
||||||
|
}
|
||||||
|
return etags, nil
|
||||||
default:
|
default:
|
||||||
return c.PartETags, nil
|
return c.PartETags, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-16
@@ -31,8 +31,8 @@ import (
|
|||||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||||
"github.com/minio/minio/internal/color"
|
"github.com/minio/minio/internal/color"
|
||||||
"github.com/minio/minio/internal/config/cache"
|
"github.com/minio/minio/internal/config/cache"
|
||||||
|
"github.com/minio/minio/internal/disk"
|
||||||
"github.com/minio/minio/internal/hash"
|
"github.com/minio/minio/internal/hash"
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/minio/internal/sync/errgroup"
|
"github.com/minio/minio/internal/sync/errgroup"
|
||||||
"github.com/minio/pkg/wildcard"
|
"github.com/minio/pkg/wildcard"
|
||||||
@@ -594,12 +594,23 @@ func newCache(config cache.Config) ([]*diskCache, bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
var warningMsg string
|
||||||
for i, dir := range config.Drives {
|
for i, dir := range config.Drives {
|
||||||
// skip diskCache creation for cache drives missing a format.json
|
// skip diskCache creation for cache drives missing a format.json
|
||||||
if formats[i] == nil {
|
if formats[i] == nil {
|
||||||
caches = append(caches, nil)
|
caches = append(caches, nil)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !globalIsCICD && len(warningMsg) == 0 {
|
||||||
|
rootDsk, err := disk.IsRootDisk(dir, "/")
|
||||||
|
if err != nil {
|
||||||
|
warningMsg = fmt.Sprintf("Invalid cache dir %s err : %s", dir, err.Error())
|
||||||
|
}
|
||||||
|
if rootDsk {
|
||||||
|
warningMsg = fmt.Sprintf("cache dir cannot be part of root disk: %s", dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := checkAtimeSupport(dir); err != nil {
|
if err := checkAtimeSupport(dir); err != nil {
|
||||||
return nil, false, fmt.Errorf("Atime support required for disk caching, atime check failed with %w", err)
|
return nil, false, fmt.Errorf("Atime support required for disk caching, atime check failed with %w", err)
|
||||||
}
|
}
|
||||||
@@ -610,11 +621,14 @@ func newCache(config cache.Config) ([]*diskCache, bool, error) {
|
|||||||
}
|
}
|
||||||
caches = append(caches, cache)
|
caches = append(caches, cache)
|
||||||
}
|
}
|
||||||
|
if warningMsg != "" {
|
||||||
|
logger.Info(color.Yellow(fmt.Sprintf("WARNING: Usage of root disk for disk caching is deprecated: %s", warningMsg)))
|
||||||
|
}
|
||||||
return caches, migrating, nil
|
return caches, migrating, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cacheObjects) migrateCacheFromV1toV2(ctx context.Context) {
|
func (c *cacheObjects) migrateCacheFromV1toV2(ctx context.Context) {
|
||||||
logStartupMessage(color.Blue("Cache migration initiated ...."))
|
logger.Info(color.Blue("Cache migration initiated ...."))
|
||||||
|
|
||||||
g := errgroup.WithNErrs(len(c.cache))
|
g := errgroup.WithNErrs(len(c.cache))
|
||||||
for index, dc := range c.cache {
|
for index, dc := range c.cache {
|
||||||
@@ -643,7 +657,7 @@ func (c *cacheObjects) migrateCacheFromV1toV2(ctx context.Context) {
|
|||||||
|
|
||||||
// update migration status
|
// update migration status
|
||||||
c.migrating = false
|
c.migrating = false
|
||||||
logStartupMessage(color.Blue("Cache migration completed successfully."))
|
logger.Info(color.Blue("Cache migration completed successfully."))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutObject - caches the uploaded object for single Put operations
|
// PutObject - caches the uploaded object for single Put operations
|
||||||
@@ -724,35 +738,30 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
|
|||||||
// Initialize pipe to stream data to cache
|
// Initialize pipe to stream data to cache
|
||||||
rPipe, wPipe := io.Pipe()
|
rPipe, wPipe := io.Pipe()
|
||||||
infoCh := make(chan ObjectInfo)
|
infoCh := make(chan ObjectInfo)
|
||||||
errorCh := make(chan error)
|
|
||||||
go func() {
|
go func() {
|
||||||
|
defer close(infoCh)
|
||||||
info, err := putObjectFn(ctx, bucket, object, NewPutObjReader(hashReader), opts)
|
info, err := putObjectFn(ctx, bucket, object, NewPutObjReader(hashReader), opts)
|
||||||
if err != nil {
|
|
||||||
close(infoCh)
|
|
||||||
pipeReader.CloseWithError(err)
|
pipeReader.CloseWithError(err)
|
||||||
rPipe.CloseWithError(err)
|
rPipe.CloseWithError(err)
|
||||||
errorCh <- err
|
if err == nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
close(errorCh)
|
|
||||||
infoCh <- info
|
infoCh <- info
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
_, err := dcache.put(lkctx.Context(), bucket, object, rPipe, r.Size(), nil, opts, false, false)
|
_, err := dcache.put(lkctx.Context(), bucket, object, rPipe, r.Size(), nil, opts, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rPipe.CloseWithError(err)
|
logger.LogIf(lkctx.Context(), err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
// We do not care about errors to cached backend.
|
||||||
|
rPipe.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
mwriter := cacheMultiWriter(pipeWriter, wPipe)
|
mwriter := cacheMultiWriter(pipeWriter, wPipe)
|
||||||
_, err = io.Copy(mwriter, r)
|
_, err = io.Copy(mwriter, r)
|
||||||
pipeWriter.Close()
|
pipeWriter.Close()
|
||||||
wPipe.Close()
|
wPipe.Close()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = <-errorCh
|
|
||||||
return ObjectInfo{}, err
|
return ObjectInfo{}, err
|
||||||
}
|
}
|
||||||
info := <-infoCh
|
info := <-infoCh
|
||||||
@@ -788,8 +797,7 @@ func (c *cacheObjects) uploadObject(ctx context.Context, oi ObjectInfo) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var opts ObjectOptions
|
var opts ObjectOptions
|
||||||
opts.UserDefined = make(map[string]string)
|
opts.UserDefined = cloneMSS(oi.UserDefined)
|
||||||
opts.UserDefined[xhttp.ContentMD5] = oi.UserDefined["content-md5"]
|
|
||||||
objInfo, err := c.InnerPutObjectFn(ctx, oi.Bucket, oi.Name, NewPutObjReader(hashReader), opts)
|
objInfo, err := c.InnerPutObjectFn(ctx, oi.Bucket, oi.Name, NewPutObjReader(hashReader), opts)
|
||||||
wbCommitStatus := CommitComplete
|
wbCommitStatus := CommitComplete
|
||||||
size := objInfo.Size
|
size := objInfo.Size
|
||||||
|
|||||||
+121
-5
@@ -19,6 +19,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
@@ -35,6 +36,7 @@ import (
|
|||||||
|
|
||||||
"github.com/minio/kes"
|
"github.com/minio/kes"
|
||||||
"github.com/minio/minio/internal/crypto"
|
"github.com/minio/minio/internal/crypto"
|
||||||
|
"github.com/minio/minio/internal/etag"
|
||||||
"github.com/minio/minio/internal/fips"
|
"github.com/minio/minio/internal/fips"
|
||||||
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"
|
||||||
@@ -71,18 +73,132 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// KMSKeyID returns in AWS compatible KMS KeyID() format.
|
// KMSKeyID returns in AWS compatible KMS KeyID() format.
|
||||||
func (o ObjectInfo) KMSKeyID() string {
|
func (o *ObjectInfo) KMSKeyID() string { return kmsKeyIDFromMetadata(o.UserDefined) }
|
||||||
if len(o.UserDefined) == 0 {
|
|
||||||
|
// KMSKeyID returns in AWS compatible KMS KeyID() format.
|
||||||
|
func (o *MultipartInfo) KMSKeyID() string { return kmsKeyIDFromMetadata(o.UserDefined) }
|
||||||
|
|
||||||
|
// kmsKeyIDFromMetadata returns any AWS S3 KMS key ID in the
|
||||||
|
// metadata, if any. It returns an empty ID if no key ID is
|
||||||
|
// present.
|
||||||
|
func kmsKeyIDFromMetadata(metadata map[string]string) string {
|
||||||
|
const ARNPrefix = "arn:aws:kms:"
|
||||||
|
if len(metadata) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
kmsID, ok := o.UserDefined[crypto.MetaKeyID]
|
kmsID, ok := metadata[crypto.MetaKeyID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(kmsID, "arn:aws:kms:") {
|
if strings.HasPrefix(kmsID, ARNPrefix) {
|
||||||
return kmsID
|
return kmsID
|
||||||
}
|
}
|
||||||
return "arn:aws:kms:" + kmsID
|
return ARNPrefix + kmsID
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptETags decryptes the ETag of all ObjectInfos using the KMS.
|
||||||
|
//
|
||||||
|
// It adjusts the size of all encrypted objects since encrypted
|
||||||
|
// objects are slightly larger due to encryption overhead.
|
||||||
|
// Further, it decrypts all single-part SSE-S3 encrypted objects
|
||||||
|
// and formats ETags of SSE-C / SSE-KMS encrypted objects to
|
||||||
|
// be AWS S3 compliant.
|
||||||
|
//
|
||||||
|
// DecryptETags uses a KMS bulk decryption API, if available, which
|
||||||
|
// is more efficient than decrypting ETags sequentually.
|
||||||
|
func DecryptETags(ctx context.Context, KMS kms.KMS, objects []ObjectInfo) error {
|
||||||
|
const BatchSize = 250 // We process the objects in batches - 250 is a reasonable default.
|
||||||
|
var (
|
||||||
|
metadata = make([]map[string]string, 0, BatchSize)
|
||||||
|
buckets = make([]string, 0, BatchSize)
|
||||||
|
names = make([]string, 0, BatchSize)
|
||||||
|
)
|
||||||
|
for len(objects) > 0 {
|
||||||
|
N := BatchSize
|
||||||
|
if len(objects) < BatchSize {
|
||||||
|
N = len(objects)
|
||||||
|
}
|
||||||
|
batch := objects[:N]
|
||||||
|
|
||||||
|
// We have to decrypt only ETags of SSE-S3 single-part
|
||||||
|
// objects.
|
||||||
|
// Therefore, we remember which objects (there index)
|
||||||
|
// in the current batch are single-part SSE-S3 objects.
|
||||||
|
metadata = metadata[:0:N]
|
||||||
|
buckets = buckets[:0:N]
|
||||||
|
names = names[:0:N]
|
||||||
|
SSES3SinglePartObjects := make(map[int]bool)
|
||||||
|
for i, object := range batch {
|
||||||
|
if kind, ok := crypto.IsEncrypted(object.UserDefined); ok && kind == crypto.S3 && !crypto.IsMultiPart(object.UserDefined) {
|
||||||
|
SSES3SinglePartObjects[i] = true
|
||||||
|
|
||||||
|
metadata = append(metadata, object.UserDefined)
|
||||||
|
buckets = append(buckets, object.Bucket)
|
||||||
|
names = append(names, object.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no SSE-S3 single-part objects
|
||||||
|
// we can skip the decryption process. However,
|
||||||
|
// we still have to adjust the size and ETag
|
||||||
|
// of SSE-C and SSE-KMS objects.
|
||||||
|
if len(SSES3SinglePartObjects) == 0 {
|
||||||
|
for i := range batch {
|
||||||
|
size, err := batch[i].GetActualSize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
batch[i].Size = size
|
||||||
|
|
||||||
|
if _, ok := crypto.IsEncrypted(batch[i].UserDefined); ok {
|
||||||
|
ETag, err := etag.Parse(batch[i].ETag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
batch[i].ETag = ETag.Format().String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
objects = objects[N:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is at least one SSE-S3 single-part object.
|
||||||
|
// For all SSE-S3 single-part objects we have to
|
||||||
|
// fetch their decryption keys. We do this using
|
||||||
|
// a Bulk-Decryption API call, if available.
|
||||||
|
keys, err := crypto.S3.UnsealObjectKeys(ctx, KMS, metadata, buckets, names)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, we have to decrypt the ETags of SSE-S3 single-part
|
||||||
|
// objects and adjust the size and ETags of all encrypted
|
||||||
|
// objects.
|
||||||
|
for i := range batch {
|
||||||
|
size, err := batch[i].GetActualSize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
batch[i].Size = size
|
||||||
|
|
||||||
|
if _, ok := crypto.IsEncrypted(batch[i].UserDefined); ok {
|
||||||
|
ETag, err := etag.Parse(batch[i].ETag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if SSES3SinglePartObjects[i] && ETag.IsEncrypted() {
|
||||||
|
ETag, err = etag.Decrypt(keys[0][:], ETag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
keys = keys[1:]
|
||||||
|
}
|
||||||
|
batch[i].ETag = ETag.Format().String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
objects = objects[N:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isMultipart returns true if the current object is
|
// isMultipart returns true if the current object is
|
||||||
|
|||||||
+1
-1
@@ -582,7 +582,7 @@ func CreateEndpoints(serverAddr string, foundLocal bool, args ...[]string) (Endp
|
|||||||
return endpoints, setupType, config.ErrInvalidFSEndpoint(nil).Msg("use path style endpoint for FS setup")
|
return endpoints, setupType, config.ErrInvalidFSEndpoint(nil).Msg("use path style endpoint for FS setup")
|
||||||
}
|
}
|
||||||
endpoints = append(endpoints, endpoint)
|
endpoints = append(endpoints, endpoint)
|
||||||
setupType = FSSetupType
|
setupType = ErasureSDSetupType
|
||||||
|
|
||||||
// Check for cross device mounts if any.
|
// Check for cross device mounts if any.
|
||||||
if err = checkCrossDeviceMounts(endpoints); err != nil {
|
if err = checkCrossDeviceMounts(endpoints); err != nil {
|
||||||
|
|||||||
@@ -231,10 +231,10 @@ func TestCreateEndpoints(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"localhost", [][]string{}, "", Endpoints{}, -1, fmt.Errorf("address localhost: missing port in address")},
|
{"localhost", [][]string{}, "", Endpoints{}, -1, fmt.Errorf("address localhost: missing port in address")},
|
||||||
|
|
||||||
// FS Setup
|
// Erasure Single Drive
|
||||||
{"localhost:9000", [][]string{{"http://localhost/d1"}}, "", Endpoints{}, -1, fmt.Errorf("use path style endpoint for FS setup")},
|
{"localhost:9000", [][]string{{"http://localhost/d1"}}, "", Endpoints{}, -1, fmt.Errorf("use path style endpoint for FS setup")},
|
||||||
{":443", [][]string{{"/d1"}}, ":443", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, FSSetupType, nil},
|
{":443", [][]string{{"/d1"}}, ":443", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, ErasureSDSetupType, nil},
|
||||||
{"localhost:10000", [][]string{{"/d1"}}, "localhost:10000", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, FSSetupType, nil},
|
{"localhost:10000", [][]string{{"/d1"}}, "localhost:10000", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, ErasureSDSetupType, nil},
|
||||||
{"localhost:9000", [][]string{{"https://127.0.0.1:9000/d1", "https://localhost:9001/d1", "https://example.com/d1", "https://example.com/d2"}}, "", Endpoints{}, -1, fmt.Errorf("path '/d1' can not be served by different port on same address")},
|
{"localhost:9000", [][]string{{"https://127.0.0.1:9000/d1", "https://localhost:9001/d1", "https://example.com/d1", "https://example.com/d2"}}, "", Endpoints{}, -1, fmt.Errorf("path '/d1' can not be served by different port on same address")},
|
||||||
|
|
||||||
// Erasure Setup with PathEndpointType
|
// Erasure Setup with PathEndpointType
|
||||||
|
|||||||
@@ -39,9 +39,11 @@ func (er erasureObjects) MakeBucketWithLocation(ctx context.Context, bucket stri
|
|||||||
defer NSUpdated(bucket, slashSeparator)
|
defer NSUpdated(bucket, slashSeparator)
|
||||||
|
|
||||||
// Verify if bucket is valid.
|
// Verify if bucket is valid.
|
||||||
|
if !isMinioMetaBucketName(bucket) {
|
||||||
if err := s3utils.CheckValidBucketNameStrict(bucket); err != nil {
|
if err := s3utils.CheckValidBucketNameStrict(bucket); err != nil {
|
||||||
return BucketNameInvalid{Bucket: bucket}
|
return BucketNameInvalid{Bucket: bucket}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
storageDisks := er.getDisks()
|
storageDisks := er.getDisks()
|
||||||
|
|
||||||
@@ -53,6 +55,11 @@ func (er erasureObjects) MakeBucketWithLocation(ctx context.Context, bucket stri
|
|||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
if storageDisks[index] != nil {
|
if storageDisks[index] != nil {
|
||||||
if err := storageDisks[index].MakeVol(ctx, bucket); err != nil {
|
if err := storageDisks[index].MakeVol(ctx, bucket); err != nil {
|
||||||
|
if opts.ForceCreate && errors.Is(err, errVolumeExists) {
|
||||||
|
// No need to return error when force create was
|
||||||
|
// requested.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if !errors.Is(err, errVolumeExists) {
|
if !errors.Is(err, errVolumeExists) {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type Erasure struct {
|
|||||||
// NewErasure creates a new ErasureStorage.
|
// NewErasure creates a new ErasureStorage.
|
||||||
func NewErasure(ctx context.Context, dataBlocks, parityBlocks int, blockSize int64) (e Erasure, err error) {
|
func NewErasure(ctx context.Context, dataBlocks, parityBlocks int, blockSize int64) (e Erasure, err error) {
|
||||||
// Check the parameters for sanity now.
|
// Check the parameters for sanity now.
|
||||||
if dataBlocks <= 0 || parityBlocks <= 0 {
|
if dataBlocks <= 0 || parityBlocks < 0 {
|
||||||
return e, reedsolomon.ErrInvShardNum
|
return e, reedsolomon.ErrInvShardNum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func commonTimeAndOccurence(times []time.Time, group time.Duration) (maxTime tim
|
|||||||
groupNano := group.Nanoseconds()
|
groupNano := group.Nanoseconds()
|
||||||
// Ignore the uuid sentinel and count the rest.
|
// Ignore the uuid sentinel and count the rest.
|
||||||
for _, t := range times {
|
for _, t := range times {
|
||||||
if t.Equal(timeSentinel) {
|
if t.Equal(timeSentinel) || t.IsZero() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
nano := t.UnixNano()
|
nano := t.UnixNano()
|
||||||
@@ -83,7 +83,10 @@ func commonTime(modTimes []time.Time) (modTime time.Time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Beginning of unix time is treated as sentinel value here.
|
// Beginning of unix time is treated as sentinel value here.
|
||||||
var timeSentinel = time.Unix(0, 0).UTC()
|
var (
|
||||||
|
timeSentinel = time.Unix(0, 0).UTC()
|
||||||
|
timeSentinel1970 = time.Unix(0, 1).UTC() // 1970 used for special cases when xlmeta.version == 0
|
||||||
|
)
|
||||||
|
|
||||||
// Boot modTimes up to disk count, setting the value to time sentinel.
|
// Boot modTimes up to disk count, setting the value to time sentinel.
|
||||||
func bootModtimes(diskCount int) []time.Time {
|
func bootModtimes(diskCount int) []time.Time {
|
||||||
|
|||||||
+36
-12
@@ -60,7 +60,8 @@ func (fi FileInfo) DataShardFixed() bool {
|
|||||||
// also heals the missing entries for bucket metadata files
|
// also heals the missing entries for bucket metadata files
|
||||||
// `policy.json, notification.xml, listeners.json`.
|
// `policy.json, notification.xml, listeners.json`.
|
||||||
func (er erasureObjects) HealBucket(ctx context.Context, bucket string, opts madmin.HealOpts) (
|
func (er erasureObjects) HealBucket(ctx context.Context, bucket string, opts madmin.HealOpts) (
|
||||||
result madmin.HealResultItem, err error) {
|
result madmin.HealResultItem, err error,
|
||||||
|
) {
|
||||||
if !opts.DryRun {
|
if !opts.DryRun {
|
||||||
defer NSUpdated(bucket, slashSeparator)
|
defer NSUpdated(bucket, slashSeparator)
|
||||||
}
|
}
|
||||||
@@ -417,10 +418,19 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !latestMeta.XLV1 && !latestMeta.Deleted && disksToHealCount > latestMeta.Erasure.ParityBlocks {
|
||||||
|
// When disk to heal count is greater than parity blocks we should simply error out.
|
||||||
|
err := fmt.Errorf("more disks are expected to heal than parity, returned errors: %v -> %s/%s(%s)", errs, bucket, object, versionID)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
|
||||||
|
bucket, object, versionID), err
|
||||||
|
}
|
||||||
|
|
||||||
cleanFileInfo := func(fi FileInfo) FileInfo {
|
cleanFileInfo := func(fi FileInfo) FileInfo {
|
||||||
// Returns a copy of the 'fi' with checksums and parts nil'ed.
|
// Returns a copy of the 'fi' with erasure index, checksums and inline data niled.
|
||||||
nfi := fi
|
nfi := fi
|
||||||
if !fi.IsRemote() {
|
if !nfi.IsRemote() {
|
||||||
|
nfi.Data = nil
|
||||||
nfi.Erasure.Index = 0
|
nfi.Erasure.Index = 0
|
||||||
nfi.Erasure.Checksums = nil
|
nfi.Erasure.Checksums = nil
|
||||||
}
|
}
|
||||||
@@ -431,12 +441,25 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
|||||||
tmpID := mustGetUUID()
|
tmpID := mustGetUUID()
|
||||||
migrateDataDir := mustGetUUID()
|
migrateDataDir := mustGetUUID()
|
||||||
|
|
||||||
|
// Reorder so that we have data disks first and parity disks next.
|
||||||
|
latestDisks := shuffleDisks(availableDisks, latestMeta.Erasure.Distribution)
|
||||||
|
outDatedDisks = shuffleDisks(outDatedDisks, latestMeta.Erasure.Distribution)
|
||||||
|
partsMetadata = shufflePartsMetadata(partsMetadata, latestMeta.Erasure.Distribution)
|
||||||
|
|
||||||
copyPartsMetadata := make([]FileInfo, len(partsMetadata))
|
copyPartsMetadata := make([]FileInfo, len(partsMetadata))
|
||||||
|
for i := range latestDisks {
|
||||||
|
if latestDisks[i] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
copyPartsMetadata[i] = partsMetadata[i]
|
||||||
|
}
|
||||||
|
|
||||||
for i := range outDatedDisks {
|
for i := range outDatedDisks {
|
||||||
if outDatedDisks[i] == nil {
|
if outDatedDisks[i] == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
copyPartsMetadata[i] = partsMetadata[i]
|
// Make sure to write the FileInfo information
|
||||||
|
// that is expected to be in quorum.
|
||||||
partsMetadata[i] = cleanFileInfo(latestMeta)
|
partsMetadata[i] = cleanFileInfo(latestMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,12 +478,6 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
|||||||
inlineBuffers = make([]*bytes.Buffer, len(outDatedDisks))
|
inlineBuffers = make([]*bytes.Buffer, len(outDatedDisks))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reorder so that we have data disks first and parity disks next.
|
|
||||||
latestDisks := shuffleDisks(availableDisks, latestMeta.Erasure.Distribution)
|
|
||||||
outDatedDisks = shuffleDisks(outDatedDisks, latestMeta.Erasure.Distribution)
|
|
||||||
partsMetadata = shufflePartsMetadata(partsMetadata, latestMeta.Erasure.Distribution)
|
|
||||||
copyPartsMetadata = shufflePartsMetadata(copyPartsMetadata, latestMeta.Erasure.Distribution)
|
|
||||||
|
|
||||||
if !latestMeta.Deleted && !latestMeta.IsRemote() {
|
if !latestMeta.Deleted && !latestMeta.IsRemote() {
|
||||||
// Heal each part. erasureHealFile() will write the healed
|
// Heal each part. erasureHealFile() will write the healed
|
||||||
// part to .minio/tmp/uuid/ which needs to be renamed later to
|
// part to .minio/tmp/uuid/ which needs to be renamed later to
|
||||||
@@ -485,7 +502,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
|||||||
}
|
}
|
||||||
checksumInfo := copyPartsMetadata[i].Erasure.GetChecksumInfo(partNumber)
|
checksumInfo := copyPartsMetadata[i].Erasure.GetChecksumInfo(partNumber)
|
||||||
partPath := pathJoin(object, srcDataDir, fmt.Sprintf("part.%d", partNumber))
|
partPath := pathJoin(object, srcDataDir, fmt.Sprintf("part.%d", partNumber))
|
||||||
readers[i] = newBitrotReader(disk, partsMetadata[i].Data, bucket, partPath, tillOffset, checksumAlgo,
|
readers[i] = newBitrotReader(disk, copyPartsMetadata[i].Data, bucket, partPath, tillOffset, checksumAlgo,
|
||||||
checksumInfo.Hash, erasure.ShardSize())
|
checksumInfo.Hash, erasure.ShardSize())
|
||||||
}
|
}
|
||||||
writers := make([]io.Writer, len(outDatedDisks))
|
writers := make([]io.Writer, len(outDatedDisks))
|
||||||
@@ -931,5 +948,12 @@ func (er erasureObjects) HealObject(ctx context.Context, bucket, object, version
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Heal the object.
|
// Heal the object.
|
||||||
return er.healObject(healCtx, bucket, object, versionID, opts)
|
hr, err = er.healObject(healCtx, bucket, object, versionID, opts)
|
||||||
|
if errors.Is(err, errFileCorrupt) && opts.ScanMode != madmin.HealDeepScan {
|
||||||
|
// Instead of returning an error when a bitrot error is detected
|
||||||
|
// during a normal heal scan, heal again with bitrot flag enabled.
|
||||||
|
opts.ScanMode = madmin.HealDeepScan
|
||||||
|
hr, err = er.healObject(healCtx, bucket, object, versionID, opts)
|
||||||
|
}
|
||||||
|
return hr, err
|
||||||
}
|
}
|
||||||
|
|||||||
+213
-60
@@ -382,6 +382,13 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
copy(disks, newDisks)
|
copy(disks, newDisks)
|
||||||
objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Unlock()
|
objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Unlock()
|
||||||
}
|
}
|
||||||
|
getDisk := func(n int) StorageAPI {
|
||||||
|
objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Lock()
|
||||||
|
disk := disks[n]
|
||||||
|
objLayer.(*erasureServerPools).serverPools[0].erasureDisksMu.Unlock()
|
||||||
|
return disk
|
||||||
|
}
|
||||||
|
|
||||||
// Remove 4 disks.
|
// Remove 4 disks.
|
||||||
setDisks(nil, nil, nil, nil)
|
setDisks(nil, nil, nil, nil)
|
||||||
|
|
||||||
@@ -440,8 +447,8 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDisks(orgDisks[:4]...)
|
setDisks(orgDisks[:4]...)
|
||||||
|
disk := getDisk(0)
|
||||||
fileInfoPreHeal, err = disks[0].ReadVersion(context.Background(), bucket, object, "", false)
|
fileInfoPreHeal, err = disk.ReadVersion(context.Background(), bucket, object, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -458,7 +465,8 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInfoPostHeal, err = disks[0].ReadVersion(context.Background(), bucket, object, "", false)
|
disk = getDisk(0)
|
||||||
|
fileInfoPostHeal, err = disk.ReadVersion(context.Background(), bucket, object, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -488,7 +496,8 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
|
|
||||||
setDisks(orgDisks[:4]...)
|
setDisks(orgDisks[:4]...)
|
||||||
|
|
||||||
fileInfoPreHeal, err = disks[0].ReadVersion(context.Background(), bucket, object, "", false)
|
disk = getDisk(0)
|
||||||
|
fileInfoPreHeal, err = disk.ReadVersion(context.Background(), bucket, object, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -505,7 +514,8 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInfoPostHeal, err = disks[0].ReadVersion(context.Background(), bucket, object, "", false)
|
disk = getDisk(0)
|
||||||
|
fileInfoPostHeal, err = disk.ReadVersion(context.Background(), bucket, object, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -748,8 +758,10 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
|||||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fi.DiskMTime = time.Time{}
|
||||||
|
nfi.DiskMTime = time.Time{}
|
||||||
if !reflect.DeepEqual(fi, nfi) {
|
if !reflect.DeepEqual(fi, nfi) {
|
||||||
t.Fatalf("FileInfo not equal after healing")
|
t.Fatalf("FileInfo not equal after healing: %v != %v", fi, nfi)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
||||||
@@ -774,8 +786,10 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
|||||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fi.DiskMTime = time.Time{}
|
||||||
|
nfi.DiskMTime = time.Time{}
|
||||||
if !reflect.DeepEqual(fi, nfi) {
|
if !reflect.DeepEqual(fi, nfi) {
|
||||||
t.Fatalf("FileInfo not equal after healing")
|
t.Fatalf("FileInfo not equal after healing: %v != %v", fi, nfi)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 4: checks if HealObject returns an error when xl.meta is not found
|
// Test 4: checks if HealObject returns an error when xl.meta is not found
|
||||||
@@ -807,7 +821,144 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHealObjectCorrupted(t *testing.T) {
|
func TestHealObjectCorruptedXLMeta(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resetGlobalHealState()
|
||||||
|
defer resetGlobalHealState()
|
||||||
|
|
||||||
|
nDisks := 16
|
||||||
|
fsDirs, err := getRandomDisks(nDisks)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer removeRoots(fsDirs)
|
||||||
|
|
||||||
|
// Everything is fine, should return nil
|
||||||
|
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(fsDirs...))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket := getRandomBucketName()
|
||||||
|
object := getRandomObjectName()
|
||||||
|
data := bytes.Repeat([]byte("a"), 5*1024*1024)
|
||||||
|
var opts ObjectOptions
|
||||||
|
|
||||||
|
err = objLayer.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to make a bucket - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an object with multiple parts uploaded in decreasing
|
||||||
|
// part number.
|
||||||
|
uploadID, err := objLayer.NewMultipartUpload(ctx, bucket, object, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create a multipart upload - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadedParts []CompletePart
|
||||||
|
for _, partID := range []int{2, 1} {
|
||||||
|
pInfo, err1 := objLayer.PutObjectPart(ctx, bucket, object, uploadID, partID, mustGetPutObjReader(t, bytes.NewReader(data), int64(len(data)), "", ""), opts)
|
||||||
|
if err1 != nil {
|
||||||
|
t.Fatalf("Failed to upload a part - %v", err1)
|
||||||
|
}
|
||||||
|
uploadedParts = append(uploadedParts, CompletePart{
|
||||||
|
PartNumber: pInfo.PartNumber,
|
||||||
|
ETag: pInfo.ETag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = objLayer.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, ObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to complete multipart upload - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
z := objLayer.(*erasureServerPools)
|
||||||
|
er := z.serverPools[0].sets[0]
|
||||||
|
erasureDisks := er.getDisks()
|
||||||
|
firstDisk := erasureDisks[0]
|
||||||
|
|
||||||
|
// Test 1: Remove the object backend files from the first disk.
|
||||||
|
fileInfos, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||||
|
fi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to delete a file - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to heal object - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = firstDisk.StatInfoFile(context.Background(), bucket, object+"/"+xlStorageFormatFile, false); err != nil {
|
||||||
|
t.Errorf("Expected xl.meta file to be present but stat failed - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||||
|
nfi1, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fi.DiskMTime = time.Time{}
|
||||||
|
nfi1.DiskMTime = time.Time{}
|
||||||
|
if !reflect.DeepEqual(fi, nfi1) {
|
||||||
|
t.Fatalf("FileInfo not equal after healing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Test with a corrupted xl.meta
|
||||||
|
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), []byte("abcd"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failure during creating part.1 - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected nil but received %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||||
|
nfi2, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fi.DiskMTime = time.Time{}
|
||||||
|
nfi2.DiskMTime = time.Time{}
|
||||||
|
if !reflect.DeepEqual(fi, nfi2) {
|
||||||
|
t.Fatalf("FileInfo not equal after healing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: checks if HealObject returns an error when xl.meta is not found
|
||||||
|
// in more than read quorum number of disks, to create a corrupted situation.
|
||||||
|
for i := 0; i <= nfi2.Erasure.DataBlocks; i++ {
|
||||||
|
erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try healing now, expect to receive errFileNotFound.
|
||||||
|
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(ObjectNotFound); !ok {
|
||||||
|
t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// since majority of xl.meta's are not available, object should be successfully deleted.
|
||||||
|
_, err = objLayer.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||||
|
if _, ok := err.(ObjectNotFound); !ok {
|
||||||
|
t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHealObjectCorruptedParts(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -867,7 +1018,26 @@ func TestHealObjectCorrupted(t *testing.T) {
|
|||||||
er := z.serverPools[0].sets[0]
|
er := z.serverPools[0].sets[0]
|
||||||
erasureDisks := er.getDisks()
|
erasureDisks := er.getDisks()
|
||||||
firstDisk := erasureDisks[0]
|
firstDisk := erasureDisks[0]
|
||||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
secondDisk := erasureDisks[1]
|
||||||
|
|
||||||
|
fileInfos, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
||||||
|
fi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
part1Disk1Origin, err := firstDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read a file - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
part1Disk2Origin, err := secondDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read a file - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 1, remove part.1
|
||||||
|
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to delete a file - %v", err)
|
t.Fatalf("Failed to delete a file - %v", err)
|
||||||
}
|
}
|
||||||
@@ -877,85 +1047,67 @@ func TestHealObjectCorrupted(t *testing.T) {
|
|||||||
t.Fatalf("Failed to heal object - %v", err)
|
t.Fatalf("Failed to heal object - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInfos, errs := readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
part1Replaced, err := firstDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
|
||||||
fi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
t.Fatalf("Failed to read a file - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = firstDisk.StatInfoFile(context.Background(), bucket, object+"/"+xlStorageFormatFile, false); err != nil {
|
if !reflect.DeepEqual(part1Disk1Origin, part1Replaced) {
|
||||||
t.Errorf("Expected xl.meta file to be present but stat failed - %v", err)
|
t.Fatalf("part.1 not healed correctly")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
// Test 2, Corrupt part.1
|
||||||
|
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte("foobytes"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failure during deleting part.1 - %v", err)
|
t.Fatalf("Failed to write a file - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte{})
|
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failure during creating part.1 - %v", err)
|
t.Fatalf("Failed to heal object - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
|
part1Replaced, err = firstDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Expected nil but received %v", err)
|
t.Fatalf("Failed to read a file - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
if !reflect.DeepEqual(part1Disk1Origin, part1Replaced) {
|
||||||
nfi, err := getLatestFileInfo(ctx, fileInfos, errs)
|
t.Fatalf("part.1 not healed correctly")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3, Corrupt one part and remove data in another disk
|
||||||
|
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), []byte("foobytes"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
t.Fatalf("Failed to write a file - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(fi, nfi) {
|
err = secondDisk.Delete(context.Background(), bucket, object, true)
|
||||||
t.Fatalf("FileInfo not equal after healing")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failure during deleting part.1 - %v", err)
|
t.Fatalf("Failed to delete a file - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bdata := bytes.Repeat([]byte("b"), int(nfi.Size))
|
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{ScanMode: madmin.HealNormalScan})
|
||||||
err = firstDisk.WriteAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"), bdata)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failure during creating part.1 - %v", err)
|
t.Fatalf("Failed to heal object - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
|
partReconstructed, err := firstDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Expected nil but received %v", err)
|
t.Fatalf("Failed to read a file - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInfos, errs = readAllFileInfo(ctx, erasureDisks, bucket, object, "", false)
|
if !reflect.DeepEqual(part1Disk1Origin, partReconstructed) {
|
||||||
nfi, err = getLatestFileInfo(ctx, fileInfos, errs)
|
t.Fatalf("part.1 not healed correctly")
|
||||||
|
}
|
||||||
|
|
||||||
|
partReconstructed, err = secondDisk.ReadAll(context.Background(), bucket, pathJoin(object, fi.DataDir, "part.1"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
t.Fatalf("Failed to read a file - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(fi, nfi) {
|
if !reflect.DeepEqual(part1Disk2Origin, partReconstructed) {
|
||||||
t.Fatalf("FileInfo not equal after healing")
|
t.Fatalf("part.1 not healed correctly")
|
||||||
}
|
|
||||||
|
|
||||||
// Test 4: checks if HealObject returns an error when xl.meta is not found
|
|
||||||
// in more than read quorum number of disks, to create a corrupted situation.
|
|
||||||
for i := 0; i <= nfi.Erasure.DataBlocks; i++ {
|
|
||||||
erasureDisks[i].Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try healing now, expect to receive errFileNotFound.
|
|
||||||
_, err = objLayer.HealObject(ctx, bucket, object, "", madmin.HealOpts{DryRun: false, Remove: true, ScanMode: madmin.HealDeepScan})
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(ObjectNotFound); !ok {
|
|
||||||
t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// since majority of xl.meta's are not available, object should be successfully deleted.
|
|
||||||
_, err = objLayer.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
|
||||||
if _, ok := err.(ObjectNotFound); !ok {
|
|
||||||
t.Errorf("Expect %v but received %v", ObjectNotFound{Bucket: bucket, Object: object}, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1017,7 +1169,8 @@ func TestHealObjectErasure(t *testing.T) {
|
|||||||
t.Fatalf("Failed to complete multipart upload - %v", err)
|
t.Fatalf("Failed to complete multipart upload - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = firstDisk.Delete(context.Background(), bucket, pathJoin(object, xlStorageFormatFile), false)
|
// Delete the whole object folder
|
||||||
|
err = firstDisk.Delete(context.Background(), bucket, object, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to delete a file - %v", err)
|
t.Fatalf("Failed to delete a file - %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,15 @@ func readAllFileInfo(ctx context.Context, disks []StorageAPI, bucket, object, ve
|
|||||||
return errDiskNotFound
|
return errDiskNotFound
|
||||||
}
|
}
|
||||||
metadataArray[index], err = disks[index].ReadVersion(ctx, bucket, object, versionID, readData)
|
metadataArray[index], err = disks[index].ReadVersion(ctx, bucket, object, versionID, readData)
|
||||||
if err != nil {
|
return err
|
||||||
|
}, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := g.Wait()
|
||||||
|
for index, err := range errs {
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !IsErr(err, []error{
|
if !IsErr(err, []error{
|
||||||
errFileNotFound,
|
errFileNotFound,
|
||||||
errVolumeNotFound,
|
errVolumeNotFound,
|
||||||
@@ -149,12 +157,9 @@ func readAllFileInfo(ctx context.Context, disks []StorageAPI, bucket, object, ve
|
|||||||
disks[index].String())
|
disks[index].String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return all the metadata.
|
// Return all the metadata.
|
||||||
return metadataArray, g.Wait()
|
return metadataArray, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// shuffleDisksAndPartsMetadataByIndex this function should be always used by GetObjectNInfo()
|
// shuffleDisksAndPartsMetadataByIndex this function should be always used by GetObjectNInfo()
|
||||||
@@ -178,10 +183,6 @@ func shuffleDisksAndPartsMetadataByIndex(disks []StorageAPI, metaArr []FileInfo,
|
|||||||
inconsistent++
|
inconsistent++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(fi.Data) != len(meta.Data) {
|
|
||||||
inconsistent++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if meta.XLV1 != fi.XLV1 {
|
if meta.XLV1 != fi.XLV1 {
|
||||||
inconsistent++
|
inconsistent++
|
||||||
continue
|
continue
|
||||||
@@ -229,12 +230,6 @@ func shuffleDisksAndPartsMetadata(disks []StorageAPI, partsMetadata []FileInfo,
|
|||||||
// if object was ever written previously.
|
// if object was ever written previously.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !init && len(fi.Data) != len(partsMetadata[index].Data) {
|
|
||||||
// Check for length of data parts only when
|
|
||||||
// fi.ModTime is not empty - ModTime is always set,
|
|
||||||
// if object was ever written previously.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !init && fi.XLV1 != partsMetadata[index].XLV1 {
|
if !init && fi.XLV1 != partsMetadata[index].XLV1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user