mirror of
https://github.com/pgsty/minio.git
synced 2026-11-12 18:45:42 +03:00
Compare commits
204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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:
|
||||||
|
|||||||
@@ -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,17 @@ 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 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -82,6 +82,11 @@ verify-healing: ## verify healing and replacing disks with minio binary
|
|||||||
@(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"
|
||||||
|
@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
|
||||||
|
|||||||
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.
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
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ 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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -73,7 +73,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()
|
||||||
|
|||||||
+51
-17
@@ -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
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -1171,8 +1199,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 +1401,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)
|
||||||
|
|||||||
@@ -50,6 +50,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 +61,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) {
|
||||||
@@ -890,6 +900,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) {
|
||||||
|
|||||||
+358
-75
@@ -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,15 +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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -57,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.
|
||||||
@@ -500,7 +502,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"`
|
||||||
@@ -594,6 +596,85 @@ 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)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
if !timer.Stop() {
|
||||||
|
<-timer.C
|
||||||
|
}
|
||||||
|
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 {
|
||||||
@@ -614,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")
|
||||||
|
|
||||||
@@ -936,6 +1017,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)
|
||||||
@@ -985,38 +1110,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)
|
||||||
@@ -1028,7 +1145,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():
|
||||||
@@ -1051,6 +1168,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")
|
||||||
@@ -1539,6 +1700,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()
|
||||||
@@ -1550,6 +1712,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()}
|
||||||
@@ -1598,6 +1761,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,
|
||||||
@@ -1605,6 +1769,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
|
||||||
@@ -1619,7 +1813,14 @@ 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)
|
||||||
@@ -1641,7 +1842,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)
|
||||||
@@ -1680,6 +1886,13 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
info.SetAddr(anonAddr(info.GetAddr()))
|
info.SetAddr(anonAddr(info.GetAddr()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAndWritePlatformInfo := func() {
|
||||||
|
if IsKubernetes() {
|
||||||
|
healthInfo.Sys.KubernetesInfo = getKubernetesInfo(deadlinedCtx)
|
||||||
|
partialWrite(healthInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getAndWriteCPUs := func() {
|
getAndWriteCPUs := func() {
|
||||||
if query.Get("syscpu") == "true" {
|
if query.Get("syscpu") == "true" {
|
||||||
localCPUInfo := madmin.GetCPUs(deadlinedCtx, globalLocalNodeName)
|
localCPUInfo := madmin.GetCPUs(deadlinedCtx, globalLocalNodeName)
|
||||||
@@ -1884,49 +2097,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1952,7 +2214,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()
|
||||||
@@ -1960,6 +2223,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
getAndWriteProcInfo()
|
getAndWriteProcInfo()
|
||||||
getAndWriteMinioConfig()
|
getAndWriteMinioConfig()
|
||||||
getAndWriteDrivePerfInfo()
|
getAndWriteDrivePerfInfo()
|
||||||
|
getAndWriteObjPerfInfo()
|
||||||
getAndWriteNetPerfInfo()
|
getAndWriteNetPerfInfo()
|
||||||
getAndWriteSysErrors()
|
getAndWriteSysErrors()
|
||||||
getAndWriteSysServices()
|
getAndWriteSysServices()
|
||||||
@@ -2288,19 +2552,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 {
|
||||||
@@ -2393,8 +2647,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 {
|
||||||
@@ -2405,6 +2658,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,
|
||||||
@@ -2427,10 +2684,33 @@ 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) {
|
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 {
|
||||||
@@ -2476,6 +2756,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)
|
||||||
@@ -2485,6 +2766,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
|
||||||
@@ -2493,10 +2775,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -701,7 +701,7 @@ 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 {
|
} else {
|
||||||
task.opts.ScanMode = globalHealConfig.ScanMode()
|
task.opts.ScanMode = madmin.HealNormalScan
|
||||||
}
|
}
|
||||||
|
|
||||||
h.mutex.Lock()
|
h.mutex.Lock()
|
||||||
|
|||||||
+4
-2
@@ -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 {
|
||||||
@@ -224,7 +226,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)))
|
||||||
|
|||||||
@@ -50,7 +50,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)
|
||||||
|
|||||||
+12
-1
@@ -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
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+105
-103
File diff suppressed because one or more lines are too long
+41
-14
@@ -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,10 +206,16 @@ 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 OPA is set, return without any further checks.
|
||||||
if globalPolicyOPA != nil {
|
if globalPolicyOPA != 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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
+43
-30
@@ -471,9 +471,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 +478,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,8 +511,8 @@ 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 || object.VersionID != "" && hasLockEnabled || !globalTierConfigMgr.Empty() {
|
||||||
@@ -526,7 +523,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,8 +578,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 +635,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 +693,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 +738,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 +1272,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1363,7 +1376,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 +1429,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 +1531,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
|
||||||
@@ -1666,7 +1679,7 @@ func (api objectAPIHandlers) GetBucketReplicationConfigHandler(w http.ResponseWr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, 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
|
||||||
@@ -1809,7 +1822,7 @@ func (api objectAPIHandlers) ResetBucketReplicationStartHandler(w http.ResponseW
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, 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
|
||||||
@@ -1897,7 +1910,7 @@ func (api objectAPIHandlers) ResetBucketReplicationStatusHandler(w http.Response
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
|
if _, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
+40
-24
@@ -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,21 +112,26 @@ 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 {
|
if !globalIsErasure && !globalIsDistErasure {
|
||||||
return NotImplemented{}
|
return NotImplemented{}
|
||||||
}
|
}
|
||||||
meta.ObjectLockConfigXML = configData
|
meta.ObjectLockConfigXML = configData
|
||||||
|
meta.ObjectLockConfigUpdatedAt = UTCNow()
|
||||||
case bucketVersioningConfig:
|
case bucketVersioningConfig:
|
||||||
if !globalIsErasure && !globalIsDistErasure {
|
if !globalIsErasure && !globalIsDistErasure {
|
||||||
return NotImplemented{}
|
return NotImplemented{}
|
||||||
@@ -136,6 +142,7 @@ func (sys *BucketMetadataSys) Update(ctx context.Context, bucket string, configF
|
|||||||
return NotImplemented{}
|
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,
|
||||||
@@ -189,41 +196,41 @@ func (sys *BucketMetadataSys) Get(bucket string) (BucketMetadata, error) {
|
|||||||
func (sys *BucketMetadataSys) GetVersioningConfig(bucket string) (*versioning.Versioning, error) {
|
func (sys *BucketMetadataSys) GetVersioningConfig(bucket string) (*versioning.Versioning, error) {
|
||||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return &versioning.Versioning{}, err
|
||||||
}
|
}
|
||||||
return meta.versioningConfig, nil
|
return meta.versioningConfig, 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,18 +290,27 @@ 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
|
||||||
@@ -323,29 +339,29 @@ func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, er
|
|||||||
|
|
||||||
// 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
|
||||||
|
|||||||
+40
-2
@@ -81,6 +81,12 @@ 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
|
||||||
|
|
||||||
// Unexported fields. Must be updated atomically.
|
// Unexported fields. Must be updated atomically.
|
||||||
policyConfig *policy.Policy
|
policyConfig *policy.Policy
|
||||||
@@ -98,9 +104,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 +164,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 +359,32 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|||||||
+155
-5
@@ -108,6 +108,42 @@ 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
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err = dc.Skip()
|
err = dc.Skip()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,9 +157,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 20
|
||||||
// write "Name"
|
// write "Name"
|
||||||
err = en.Append(0x8e, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
err = en.Append(0xde, 0x0, 0x14, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -262,15 +298,75 @@ 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
|
||||||
|
}
|
||||||
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 20
|
||||||
// string "Name"
|
// string "Name"
|
||||||
o = append(o, 0x8e, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
o = append(o, 0xde, 0x0, 0x14, 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 +407,24 @@ 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)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,6 +530,42 @@ 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
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
bts, err = msgp.Skip(bts)
|
bts, err = msgp.Skip(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -430,6 +580,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
|
||||||
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*"]}]}`
|
||||||
|
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
+70
-27
@@ -81,7 +81,8 @@ 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
|
||||||
@@ -179,6 +180,11 @@ func mustReplicate(ctx context.Context, bucket, object string, mopts mustReplica
|
|||||||
return
|
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 +269,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 +436,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 +467,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 +493,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
|
||||||
@@ -1531,16 +1542,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 +1564,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 +1620,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 +1642,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 +1688,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,16 +65,16 @@ 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 for this site, so the versioning cannot be suspended.",
|
||||||
HTTPStatusCode: http.StatusConflict,
|
HTTPStatusCode: http.StatusConflict,
|
||||||
}, 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, so the versioning state cannot be changed.",
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ func (sys *BucketVersioningSys) Enabled(bucket string) bool {
|
|||||||
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 := globalBucketMetadataSys.GetVersioningConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
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 := globalBucketMetadataSys.GetVersioningConfig(bucket)
|
||||||
@@ -40,15 +51,19 @@ func (sys *BucketVersioningSys) Suspended(bucket string) bool {
|
|||||||
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 := globalBucketMetadataSys.GetVersioningConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
|
||||||
objAPI := newObjectLayerFn()
|
|
||||||
if objAPI == nil {
|
|
||||||
return nil, errServerNotInitialized
|
|
||||||
}
|
|
||||||
return nil, NotImplemented{}
|
|
||||||
}
|
|
||||||
return globalBucketMetadataSys.GetVersioningConfig(bucket)
|
return globalBucketMetadataSys.GetVersioningConfig(bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+55
-16
@@ -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,6 +178,13 @@ 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)
|
||||||
@@ -184,23 +197,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")
|
||||||
}
|
}
|
||||||
@@ -482,6 +495,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]
|
||||||
@@ -625,12 +645,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 +683,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) {
|
||||||
@@ -796,7 +813,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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-35
@@ -96,6 +96,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,
|
||||||
@@ -107,7 +108,7 @@ func initHelp() {
|
|||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.PolicyOPASubSys,
|
Key: config.PolicyOPASubSys,
|
||||||
Description: "[DEPRECATED] enable external OPA for policy enforcement",
|
Description: "enable external OPA for policy enforcement",
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.APISubSys,
|
Key: config.APISubSys,
|
||||||
@@ -314,7 +315,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
|
||||||
}
|
}
|
||||||
@@ -484,22 +485,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 {
|
||||||
@@ -532,7 +517,7 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
|||||||
logger.LogIf(ctx, fmt.Errorf("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))
|
||||||
@@ -543,8 +528,6 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OPA: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OPA: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
globalOpenIDValidators = getOpenIDValidators(globalOpenIDConfig)
|
|
||||||
globalPolicyOPA = opa.New(opaCfg)
|
globalPolicyOPA = opa.New(opaCfg)
|
||||||
|
|
||||||
globalLDAPConfig, err = xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
globalLDAPConfig, err = xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
||||||
@@ -670,6 +653,22 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
globalServerConfigMu.Lock()
|
globalServerConfigMu.Lock()
|
||||||
defer globalServerConfigMu.Unlock()
|
defer globalServerConfigMu.Unlock()
|
||||||
@@ -807,17 +806,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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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{},
|
||||||
|
|||||||
+104
-34
@@ -21,6 +21,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -37,6 +38,7 @@ 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"
|
||||||
@@ -103,6 +105,63 @@ func (s *safeDuration) Get() time.Duration {
|
|||||||
return s.t
|
return s.t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCycleScanMode(currentCycle, bitrotStartCycle uint64, bitrotStartTime time.Time) madmin.HealScanMode {
|
||||||
|
bitrotCycle := globalHealConfig.BitrotScanCycle()
|
||||||
|
switch bitrotCycle {
|
||||||
|
case -1:
|
||||||
|
return madmin.HealNormalScan
|
||||||
|
case 0:
|
||||||
|
return madmin.HealDeepScan
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentCycle-bitrotStartCycle < healObjectSelectProb {
|
||||||
|
return madmin.HealDeepScan
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(bitrotStartTime) > bitrotCycle {
|
||||||
|
return madmin.HealDeepScan
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
// The function will block until the context is canceled.
|
// The function will block until the context is canceled.
|
||||||
// There should only ever be one scanner running per cluster.
|
// There should only ever be one scanner running per cluster.
|
||||||
@@ -145,12 +204,24 @@ func runDataScanner(pctx context.Context, objAPI ObjectLayer) {
|
|||||||
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...
|
||||||
@@ -182,6 +253,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 +322,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 +351,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 +476,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 +555,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 +685,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 +897,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 +953,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 +984,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)
|
||||||
@@ -1040,7 +1111,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 +1120,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 +1136,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 +1147,6 @@ func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, obj Ob
|
|||||||
return lifecycle.NoneAction
|
return lifecycle.NoneAction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return action
|
return action
|
||||||
}
|
}
|
||||||
@@ -1116,7 +1184,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
obj, err := objLayer.DeleteObject(ctx, obj.Bucket, obj.Name, opts)
|
obj, err := objLayer.DeleteObject(ctx, obj.Bucket, obj.Name, opts)
|
||||||
@@ -1175,6 +1243,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 +1266,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 +1286,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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -884,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)
|
||||||
@@ -892,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))
|
||||||
|
|||||||
+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
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-14
@@ -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,6 +621,9 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -894,6 +908,8 @@ func TestHealObjectCorruptedXLMeta(t *testing.T) {
|
|||||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fi.DiskMTime = time.Time{}
|
||||||
|
nfi1.DiskMTime = time.Time{}
|
||||||
if !reflect.DeepEqual(fi, nfi1) {
|
if !reflect.DeepEqual(fi, nfi1) {
|
||||||
t.Fatalf("FileInfo not equal after healing")
|
t.Fatalf("FileInfo not equal after healing")
|
||||||
}
|
}
|
||||||
@@ -915,6 +931,8 @@ func TestHealObjectCorruptedXLMeta(t *testing.T) {
|
|||||||
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
t.Fatalf("Failed to getLatestFileInfo - %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fi.DiskMTime = time.Time{}
|
||||||
|
nfi2.DiskMTime = time.Time{}
|
||||||
if !reflect.DeepEqual(fi, nfi2) {
|
if !reflect.DeepEqual(fi, nfi2) {
|
||||||
t.Fatalf("FileInfo not equal after healing")
|
t.Fatalf("FileInfo not equal after healing")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func (fi FileInfo) IsValid() bool {
|
|||||||
func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo {
|
func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo {
|
||||||
object = decodeDirObject(object)
|
object = decodeDirObject(object)
|
||||||
versionID := fi.VersionID
|
versionID := fi.VersionID
|
||||||
if (globalBucketVersioningSys.Enabled(bucket) || globalBucketVersioningSys.Suspended(bucket)) && versionID == "" {
|
if (globalBucketVersioningSys.PrefixEnabled(bucket, object) || globalBucketVersioningSys.PrefixSuspended(bucket, object)) && versionID == "" {
|
||||||
versionID = nullVersionID
|
versionID = nullVersionID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,8 @@ func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk Sto
|
|||||||
// towards simplification of multipart APIs.
|
// towards simplification of multipart APIs.
|
||||||
// The resulting ListMultipartsInfo structure is unmarshalled directly as XML.
|
// The resulting ListMultipartsInfo structure is unmarshalled directly as XML.
|
||||||
func (er erasureObjects) ListMultipartUploads(ctx context.Context, bucket, object, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (result ListMultipartsInfo, err error) {
|
func (er erasureObjects) ListMultipartUploads(ctx context.Context, bucket, object, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (result ListMultipartsInfo, err error) {
|
||||||
|
auditObjectErasureSet(ctx, object, &er)
|
||||||
|
|
||||||
result.MaxUploads = maxUploads
|
result.MaxUploads = maxUploads
|
||||||
result.KeyMarker = keyMarker
|
result.KeyMarker = keyMarker
|
||||||
result.Prefix = object
|
result.Prefix = object
|
||||||
@@ -284,8 +286,10 @@ func (er erasureObjects) ListMultipartUploads(ctx context.Context, bucket, objec
|
|||||||
// disks. `uploads.json` carries metadata regarding on-going multipart
|
// disks. `uploads.json` carries metadata regarding on-going multipart
|
||||||
// operation(s) on the object.
|
// operation(s) on the object.
|
||||||
func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string, object string, opts ObjectOptions) (string, error) {
|
func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string, object string, opts ObjectOptions) (string, error) {
|
||||||
|
userDefined := cloneMSS(opts.UserDefined)
|
||||||
|
|
||||||
onlineDisks := er.getDisks()
|
onlineDisks := er.getDisks()
|
||||||
parityDrives := globalStorageClass.GetParityForSC(opts.UserDefined[xhttp.AmzStorageClass])
|
parityDrives := globalStorageClass.GetParityForSC(userDefined[xhttp.AmzStorageClass])
|
||||||
if parityDrives <= 0 {
|
if parityDrives <= 0 {
|
||||||
parityDrives = er.defaultParityCount
|
parityDrives = er.defaultParityCount
|
||||||
}
|
}
|
||||||
@@ -306,7 +310,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if parityOrig != parityDrives {
|
if parityOrig != parityDrives {
|
||||||
opts.UserDefined[minIOErasureUpgraded] = strconv.Itoa(parityOrig) + "->" + strconv.Itoa(parityDrives)
|
userDefined[minIOErasureUpgraded] = strconv.Itoa(parityOrig) + "->" + strconv.Itoa(parityDrives)
|
||||||
}
|
}
|
||||||
|
|
||||||
dataDrives := len(onlineDisks) - parityDrives
|
dataDrives := len(onlineDisks) - parityDrives
|
||||||
@@ -334,8 +338,8 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Guess content-type from the extension if possible.
|
// Guess content-type from the extension if possible.
|
||||||
if opts.UserDefined["content-type"] == "" {
|
if userDefined["content-type"] == "" {
|
||||||
opts.UserDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
|
userDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
|
||||||
}
|
}
|
||||||
|
|
||||||
modTime := opts.MTime
|
modTime := opts.MTime
|
||||||
@@ -350,7 +354,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
|||||||
for index := range partsMetadata {
|
for index := range partsMetadata {
|
||||||
partsMetadata[index].Fresh = true
|
partsMetadata[index].Fresh = true
|
||||||
partsMetadata[index].ModTime = modTime
|
partsMetadata[index].ModTime = modTime
|
||||||
partsMetadata[index].Metadata = opts.UserDefined
|
partsMetadata[index].Metadata = userDefined
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadID := mustGetUUID()
|
uploadID := mustGetUUID()
|
||||||
@@ -371,10 +375,8 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
|||||||
//
|
//
|
||||||
// Implements S3 compatible initiate multipart API.
|
// Implements S3 compatible initiate multipart API.
|
||||||
func (er erasureObjects) NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (string, error) {
|
func (er erasureObjects) NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (string, error) {
|
||||||
// No metadata is set, allocate a new one.
|
auditObjectErasureSet(ctx, object, &er)
|
||||||
if opts.UserDefined == nil {
|
|
||||||
opts.UserDefined = make(map[string]string)
|
|
||||||
}
|
|
||||||
return er.newMultipartUpload(ctx, bucket, object, opts)
|
return er.newMultipartUpload(ctx, bucket, object, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +448,8 @@ func renamePart(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, ds
|
|||||||
//
|
//
|
||||||
// Implements S3 compatible Upload Part API.
|
// Implements S3 compatible Upload Part API.
|
||||||
func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, r *PutObjReader, opts ObjectOptions) (pi PartInfo, err error) {
|
func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, r *PutObjReader, opts ObjectOptions) (pi PartInfo, err error) {
|
||||||
|
auditObjectErasureSet(ctx, object, &er)
|
||||||
|
|
||||||
// Write lock for this part ID.
|
// Write lock for this part ID.
|
||||||
// Held throughout the operation.
|
// Held throughout the operation.
|
||||||
partIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID, strconv.Itoa(partID)))
|
partIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID, strconv.Itoa(partID)))
|
||||||
@@ -683,6 +687,8 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
|||||||
// - encrypted
|
// - encrypted
|
||||||
// - compressed
|
// - compressed
|
||||||
func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (MultipartInfo, error) {
|
func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (MultipartInfo, error) {
|
||||||
|
auditObjectErasureSet(ctx, object, &er)
|
||||||
|
|
||||||
result := MultipartInfo{
|
result := MultipartInfo{
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Object: object,
|
Object: object,
|
||||||
@@ -739,6 +745,8 @@ func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, u
|
|||||||
// ListPartsInfo structure is marshaled directly into XML and
|
// ListPartsInfo structure is marshaled directly into XML and
|
||||||
// replied back to the client.
|
// replied back to the client.
|
||||||
func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
|
func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
|
||||||
|
auditObjectErasureSet(ctx, object, &er)
|
||||||
|
|
||||||
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||||
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
|
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -832,6 +840,8 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
|||||||
//
|
//
|
||||||
// Implements S3 compatible Complete multipart API.
|
// Implements S3 compatible Complete multipart API.
|
||||||
func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, parts []CompletePart, opts ObjectOptions) (oi ObjectInfo, err error) {
|
func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, parts []CompletePart, opts ObjectOptions) (oi ObjectInfo, err error) {
|
||||||
|
auditObjectErasureSet(ctx, object, &er)
|
||||||
|
|
||||||
// Hold read-locks to verify uploaded parts, also disallows
|
// Hold read-locks to verify uploaded parts, also disallows
|
||||||
// parallel part uploads as well.
|
// parallel part uploads as well.
|
||||||
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||||
@@ -1028,6 +1038,8 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
// would be removed from the system, rollback is not possible on this
|
// would be removed from the system, rollback is not possible on this
|
||||||
// operation.
|
// operation.
|
||||||
func (er erasureObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (err error) {
|
func (er erasureObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (err error) {
|
||||||
|
auditObjectErasureSet(ctx, object, &er)
|
||||||
|
|
||||||
lk := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
lk := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+192
-30
@@ -33,6 +33,7 @@ import (
|
|||||||
"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"
|
||||||
"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/event"
|
"github.com/minio/minio/internal/event"
|
||||||
"github.com/minio/minio/internal/hash"
|
"github.com/minio/minio/internal/hash"
|
||||||
@@ -62,6 +63,8 @@ func countOnlineDisks(onlineDisks []StorageAPI) (online int) {
|
|||||||
// if source object and destination object are same we only
|
// if source object and destination object are same we only
|
||||||
// update metadata.
|
// update metadata.
|
||||||
func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (oi ObjectInfo, err error) {
|
func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (oi ObjectInfo, err error) {
|
||||||
|
auditObjectErasureSet(ctx, dstObject, &er)
|
||||||
|
|
||||||
// This call shouldn't be used for anything other than metadata updates or adding self referential versions.
|
// This call shouldn't be used for anything other than metadata updates or adding self referential versions.
|
||||||
if !srcInfo.metadataOnly {
|
if !srcInfo.metadataOnly {
|
||||||
return oi, NotImplemented{}
|
return oi, NotImplemented{}
|
||||||
@@ -80,12 +83,26 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
|||||||
}
|
}
|
||||||
// Read metadata associated with the object from all disks.
|
// Read metadata associated with the object from all disks.
|
||||||
storageDisks := er.getDisks()
|
storageDisks := er.getDisks()
|
||||||
metaArr, errs := readAllFileInfo(ctx, storageDisks, srcBucket, srcObject, srcOpts.VersionID, true)
|
|
||||||
|
|
||||||
// get Quorum for this object
|
var metaArr []FileInfo
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
// Read metadata associated with the object from all disks.
|
||||||
|
if srcOpts.VersionID != "" {
|
||||||
|
metaArr, errs = readAllFileInfo(ctx, storageDisks, srcBucket, srcObject, srcOpts.VersionID, true)
|
||||||
|
} else {
|
||||||
|
metaArr, errs = readAllXL(ctx, storageDisks, srcBucket, srcObject, true)
|
||||||
|
}
|
||||||
|
|
||||||
readQuorum, writeQuorum, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
readQuorum, writeQuorum, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return oi, toObjectErr(err, srcBucket, srcObject)
|
if errors.Is(err, errErasureReadQuorum) && !strings.HasPrefix(srcBucket, minioMetaBucket) {
|
||||||
|
_, derr := er.deleteIfDangling(ctx, srcBucket, srcObject, metaArr, errs, nil, srcOpts)
|
||||||
|
if derr != nil {
|
||||||
|
err = derr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ObjectInfo{}, toObjectErr(err, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all online disks.
|
// List all online disks.
|
||||||
@@ -111,9 +128,20 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
|||||||
// preserve destination versionId if specified.
|
// preserve destination versionId if specified.
|
||||||
if versionID == "" {
|
if versionID == "" {
|
||||||
versionID = mustGetUUID()
|
versionID = mustGetUUID()
|
||||||
|
fi.IsLatest = true // we are creating a new version so this is latest.
|
||||||
}
|
}
|
||||||
modTime = UTCNow()
|
modTime = UTCNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the data is not inlined, we may end up incorrectly
|
||||||
|
// inlining the data here, that leads to an inconsistent
|
||||||
|
// situation where some objects are were not inlined
|
||||||
|
// were now inlined, make sure to `nil` the Data such
|
||||||
|
// that xl.meta is written as expected.
|
||||||
|
if !fi.InlineData() {
|
||||||
|
fi.Data = nil
|
||||||
|
}
|
||||||
|
|
||||||
fi.VersionID = versionID // set any new versionID we might have created
|
fi.VersionID = versionID // set any new versionID we might have created
|
||||||
fi.ModTime = modTime // set modTime for the new versionID
|
fi.ModTime = modTime // set modTime for the new versionID
|
||||||
if !dstOpts.MTime.IsZero() {
|
if !dstOpts.MTime.IsZero() {
|
||||||
@@ -129,6 +157,14 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
|||||||
metaArr[index].ModTime = modTime
|
metaArr[index].ModTime = modTime
|
||||||
metaArr[index].VersionID = versionID
|
metaArr[index].VersionID = versionID
|
||||||
metaArr[index].Metadata = srcInfo.UserDefined
|
metaArr[index].Metadata = srcInfo.UserDefined
|
||||||
|
if !metaArr[index].InlineData() {
|
||||||
|
// If the data is not inlined, we may end up incorrectly
|
||||||
|
// inlining the data here, that leads to an inconsistent
|
||||||
|
// situation where some objects are were not inlined
|
||||||
|
// were now inlined, make sure to `nil` the Data such
|
||||||
|
// that xl.meta is written as expected.
|
||||||
|
metaArr[index].Data = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,14 +173,14 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
|||||||
return oi, toObjectErr(err, srcBucket, srcObject)
|
return oi, toObjectErr(err, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
// we are adding a new version to this object under the namespace lock, so this is the latest version.
|
|
||||||
fi.IsLatest = true
|
|
||||||
return fi.ToObjectInfo(srcBucket, srcObject), nil
|
return fi.ToObjectInfo(srcBucket, srcObject), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObjectNInfo - returns object info and an object
|
// GetObjectNInfo - returns object info and an object
|
||||||
// Read(Closer). When err != nil, the returned reader is always nil.
|
// Read(Closer). When err != nil, the returned reader is always nil.
|
||||||
func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||||
|
auditObjectErasureSet(ctx, object, &er)
|
||||||
|
|
||||||
var unlockOnDefer bool
|
var unlockOnDefer bool
|
||||||
nsUnlocker := func() {}
|
nsUnlocker := func() {}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -381,6 +417,8 @@ func (er erasureObjects) getObjectWithFileInfo(ctx context.Context, bucket, obje
|
|||||||
|
|
||||||
// GetObjectInfo - reads object metadata and replies back ObjectInfo.
|
// GetObjectInfo - reads object metadata and replies back ObjectInfo.
|
||||||
func (er erasureObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (info ObjectInfo, err error) {
|
func (er erasureObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (info ObjectInfo, err error) {
|
||||||
|
auditObjectErasureSet(ctx, object, &er)
|
||||||
|
|
||||||
if !opts.NoLock {
|
if !opts.NoLock {
|
||||||
// Lock the object before reading.
|
// Lock the object before reading.
|
||||||
lk := er.NewNSLock(bucket, object)
|
lk := er.NewNSLock(bucket, object)
|
||||||
@@ -418,11 +456,90 @@ func (er erasureObjects) deleteIfDangling(ctx context.Context, bucket, object st
|
|||||||
return m, err
|
return m, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, readData bool) ([]FileInfo, []error) {
|
||||||
|
metadataArray := make([]*xlMetaV2, len(disks))
|
||||||
|
metaFileInfos := make([]FileInfo, len(metadataArray))
|
||||||
|
metadataShallowVersions := make([][]xlMetaV2ShallowVersion, len(disks))
|
||||||
|
|
||||||
|
g := errgroup.WithNErrs(len(disks))
|
||||||
|
// Read `xl.meta` in parallel across disks.
|
||||||
|
for index := range disks {
|
||||||
|
index := index
|
||||||
|
g.Go(func() (err error) {
|
||||||
|
if disks[index] == nil {
|
||||||
|
return errDiskNotFound
|
||||||
|
}
|
||||||
|
rf, err := disks[index].ReadXL(ctx, bucket, object, readData)
|
||||||
|
if err != nil {
|
||||||
|
if !IsErr(err, []error{
|
||||||
|
errFileNotFound,
|
||||||
|
errVolumeNotFound,
|
||||||
|
errFileVersionNotFound,
|
||||||
|
errDiskNotFound,
|
||||||
|
}...) {
|
||||||
|
logger.LogOnceIf(ctx, fmt.Errorf("Drive %s, path (%s/%s) returned an error (%w)",
|
||||||
|
disks[index], bucket, object, err),
|
||||||
|
disks[index].String())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var xl xlMetaV2
|
||||||
|
if err = xl.LoadOrConvert(rf.Buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
metadataArray[index] = &xl
|
||||||
|
metaFileInfos[index] = FileInfo{
|
||||||
|
DiskMTime: rf.DiskMTime,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := g.Wait()
|
||||||
|
for index := range metadataArray {
|
||||||
|
if metadataArray[index] != nil {
|
||||||
|
metadataShallowVersions[index] = metadataArray[index].versions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readQuorum := (len(disks) + 1) / 2
|
||||||
|
merged := mergeXLV2Versions(readQuorum, false, 1, metadataShallowVersions...)
|
||||||
|
for index := range metadataArray {
|
||||||
|
if metadataArray[index] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataArray[index].versions = merged
|
||||||
|
|
||||||
|
// make sure to preserve this for diskmtime based healing bugfix.
|
||||||
|
diskMTime := metaFileInfos[index].DiskMTime
|
||||||
|
metaFileInfos[index], errs[index] = metadataArray[index].ToFileInfo(bucket, object, "")
|
||||||
|
if errs[index] == nil {
|
||||||
|
versionID := metaFileInfos[index].VersionID
|
||||||
|
if versionID == "" {
|
||||||
|
versionID = nullVersionID
|
||||||
|
}
|
||||||
|
metaFileInfos[index].Data = metadataArray[index].data.find(versionID)
|
||||||
|
metaFileInfos[index].DiskMTime = diskMTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all the metadata.
|
||||||
|
return metaFileInfos, errs
|
||||||
|
}
|
||||||
|
|
||||||
func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object string, opts ObjectOptions, readData bool) (fi FileInfo, metaArr []FileInfo, onlineDisks []StorageAPI, err error) {
|
func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object string, opts ObjectOptions, readData bool) (fi FileInfo, metaArr []FileInfo, onlineDisks []StorageAPI, err error) {
|
||||||
disks := er.getDisks()
|
disks := er.getDisks()
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
|
||||||
// Read metadata associated with the object from all disks.
|
// Read metadata associated with the object from all disks.
|
||||||
metaArr, errs := readAllFileInfo(ctx, disks, bucket, object, opts.VersionID, readData)
|
if opts.VersionID != "" {
|
||||||
|
metaArr, errs = readAllFileInfo(ctx, disks, bucket, object, opts.VersionID, readData)
|
||||||
|
} else {
|
||||||
|
metaArr, errs = readAllXL(ctx, disks, bucket, object, readData)
|
||||||
|
}
|
||||||
|
|
||||||
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -493,11 +610,6 @@ func (er erasureObjects) getObjectInfo(ctx context.Context, bucket, object strin
|
|||||||
return objInfo, toObjectErr(err, bucket, object)
|
return objInfo, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
objInfo = fi.ToObjectInfo(bucket, object)
|
objInfo = fi.ToObjectInfo(bucket, object)
|
||||||
if opts.VersionID != "" && !fi.VersionPurgeStatus().Empty() {
|
|
||||||
// Make sure to return object info to provide extra information.
|
|
||||||
return objInfo, toObjectErr(errMethodNotAllowed, bucket, object)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fi.Deleted {
|
if fi.Deleted {
|
||||||
if opts.VersionID == "" || opts.DeleteMarker {
|
if opts.VersionID == "" || opts.DeleteMarker {
|
||||||
return objInfo, toObjectErr(errFileNotFound, bucket, object)
|
return objInfo, toObjectErr(errFileNotFound, bucket, object)
|
||||||
@@ -726,19 +838,18 @@ func (er erasureObjects) PutObject(ctx context.Context, bucket string, object st
|
|||||||
|
|
||||||
// putObject wrapper for erasureObjects PutObject
|
// putObject wrapper for erasureObjects PutObject
|
||||||
func (er erasureObjects) putObject(ctx context.Context, bucket string, object string, r *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
func (er erasureObjects) putObject(ctx context.Context, bucket string, object string, r *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
|
auditObjectErasureSet(ctx, object, &er)
|
||||||
|
|
||||||
data := r.Reader
|
data := r.Reader
|
||||||
|
|
||||||
// No metadata is set, allocate a new one.
|
userDefined := cloneMSS(opts.UserDefined)
|
||||||
if opts.UserDefined == nil {
|
|
||||||
opts.UserDefined = make(map[string]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
storageDisks := er.getDisks()
|
storageDisks := er.getDisks()
|
||||||
|
|
||||||
parityDrives := len(storageDisks) / 2
|
parityDrives := len(storageDisks) / 2
|
||||||
if !opts.MaxParity {
|
if !opts.MaxParity {
|
||||||
// Get parity and data drive count based on storage class metadata
|
// Get parity and data drive count based on storage class metadata
|
||||||
parityDrives = globalStorageClass.GetParityForSC(opts.UserDefined[xhttp.AmzStorageClass])
|
parityDrives = globalStorageClass.GetParityForSC(userDefined[xhttp.AmzStorageClass])
|
||||||
if parityDrives <= 0 {
|
if parityDrives <= 0 {
|
||||||
parityDrives = er.defaultParityCount
|
parityDrives = er.defaultParityCount
|
||||||
}
|
}
|
||||||
@@ -776,7 +887,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
|||||||
parityDrives = len(storageDisks) / 2
|
parityDrives = len(storageDisks) / 2
|
||||||
}
|
}
|
||||||
if parityOrig != parityDrives {
|
if parityOrig != parityDrives {
|
||||||
opts.UserDefined[minIOErasureUpgraded] = strconv.Itoa(parityOrig) + "->" + strconv.Itoa(parityDrives)
|
userDefined[minIOErasureUpgraded] = strconv.Itoa(parityOrig) + "->" + strconv.Itoa(parityDrives)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataDrives := len(storageDisks) - parityDrives
|
dataDrives := len(storageDisks) - parityDrives
|
||||||
@@ -952,13 +1063,13 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
|||||||
Hash: bitrotWriterSum(w),
|
Hash: bitrotWriterSum(w),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if opts.UserDefined["etag"] == "" {
|
if userDefined["etag"] == "" {
|
||||||
opts.UserDefined["etag"] = r.MD5CurrentHexString()
|
userDefined["etag"] = r.MD5CurrentHexString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guess content-type from the extension if possible.
|
// Guess content-type from the extension if possible.
|
||||||
if opts.UserDefined["content-type"] == "" {
|
if userDefined["content-type"] == "" {
|
||||||
opts.UserDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
|
userDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
|
||||||
}
|
}
|
||||||
|
|
||||||
modTime := opts.MTime
|
modTime := opts.MTime
|
||||||
@@ -969,7 +1080,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
|||||||
// Fill all the necessary metadata.
|
// Fill all the necessary metadata.
|
||||||
// Update `xl.meta` content on each disks.
|
// Update `xl.meta` content on each disks.
|
||||||
for index := range partsMetadata {
|
for index := range partsMetadata {
|
||||||
partsMetadata[index].Metadata = opts.UserDefined
|
partsMetadata[index].Metadata = userDefined
|
||||||
partsMetadata[index].Size = n
|
partsMetadata[index].Size = n
|
||||||
partsMetadata[index].ModTime = modTime
|
partsMetadata[index].ModTime = modTime
|
||||||
}
|
}
|
||||||
@@ -1042,6 +1153,10 @@ func (er erasureObjects) deleteObjectVersion(ctx context.Context, bucket, object
|
|||||||
// into smaller bulks if some object names are found to be duplicated in the delete list, splitting
|
// into smaller bulks if some object names are found to be duplicated in the delete list, splitting
|
||||||
// into smaller bulks will avoid holding twice the write lock of the duplicated object names.
|
// into smaller bulks will avoid holding twice the write lock of the duplicated object names.
|
||||||
func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) {
|
func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) {
|
||||||
|
for _, obj := range objects {
|
||||||
|
auditObjectErasureSet(ctx, obj.ObjectV.ObjectName, &er)
|
||||||
|
}
|
||||||
|
|
||||||
errs := make([]error, len(objects))
|
errs := make([]error, len(objects))
|
||||||
dobjects := make([]DeletedObject, len(objects))
|
dobjects := make([]DeletedObject, len(objects))
|
||||||
writeQuorums := make([]int, len(objects))
|
writeQuorums := make([]int, len(objects))
|
||||||
@@ -1072,7 +1187,13 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
|||||||
// VersionID is not set means delete is not specific about
|
// VersionID is not set means delete is not specific about
|
||||||
// any version, look for if the bucket is versioned or not.
|
// any version, look for if the bucket is versioned or not.
|
||||||
if objects[i].VersionID == "" {
|
if objects[i].VersionID == "" {
|
||||||
if opts.Versioned || opts.VersionSuspended {
|
// MinIO extension to bucket version configuration
|
||||||
|
suspended := opts.VersionSuspended
|
||||||
|
versioned := opts.Versioned
|
||||||
|
if opts.PrefixEnabledFn != nil {
|
||||||
|
versioned = opts.PrefixEnabledFn(objects[i].ObjectName)
|
||||||
|
}
|
||||||
|
if versioned || suspended {
|
||||||
// Bucket is versioned and no version was explicitly
|
// Bucket is versioned and no version was explicitly
|
||||||
// mentioned for deletes, create a delete marker instead.
|
// mentioned for deletes, create a delete marker instead.
|
||||||
vr.ModTime = UTCNow()
|
vr.ModTime = UTCNow()
|
||||||
@@ -1080,7 +1201,7 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
|||||||
// Versioning suspended means that we add a `null` version
|
// Versioning suspended means that we add a `null` version
|
||||||
// delete marker, if not add a new version for this delete
|
// delete marker, if not add a new version for this delete
|
||||||
// marker.
|
// marker.
|
||||||
if opts.Versioned {
|
if versioned {
|
||||||
vr.VersionID = mustGetUUID()
|
vr.VersionID = mustGetUUID()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1222,14 +1343,18 @@ func (er erasureObjects) deletePrefix(ctx context.Context, bucket, prefix string
|
|||||||
// any error as it is not necessary for the handler to reply back a
|
// any error as it is not necessary for the handler to reply back a
|
||||||
// response to the client request.
|
// response to the client request.
|
||||||
func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
|
auditObjectErasureSet(ctx, object, &er)
|
||||||
|
|
||||||
if opts.DeletePrefix {
|
if opts.DeletePrefix {
|
||||||
return ObjectInfo{}, toObjectErr(er.deletePrefix(ctx, bucket, object), bucket, object)
|
return ObjectInfo{}, toObjectErr(er.deletePrefix(ctx, bucket, object), bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
var lc *lifecycle.Lifecycle
|
var lc *lifecycle.Lifecycle
|
||||||
|
var rcfg lock.Retention
|
||||||
if opts.Expiration.Expire {
|
if opts.Expiration.Expire {
|
||||||
// Check if the current bucket has a configured lifecycle policy
|
// Check if the current bucket has a configured lifecycle policy
|
||||||
lc, _ = globalLifecycleSys.Get(bucket)
|
lc, _ = globalLifecycleSys.Get(bucket)
|
||||||
|
rcfg, _ = globalBucketObjectLockSys.Get(bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// expiration attempted on a bucket with no lifecycle
|
// expiration attempted on a bucket with no lifecycle
|
||||||
@@ -1274,7 +1399,7 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.Expiration.Expire {
|
if opts.Expiration.Expire {
|
||||||
action := evalActionFromLifecycle(ctx, *lc, goi, false)
|
action := evalActionFromLifecycle(ctx, *lc, rcfg, goi, false)
|
||||||
var isErr bool
|
var isErr bool
|
||||||
switch action {
|
switch action {
|
||||||
case lifecycle.NoneAction:
|
case lifecycle.NoneAction:
|
||||||
@@ -1321,10 +1446,21 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
|||||||
if opts.VersionPurgeStatus() == Complete {
|
if opts.VersionPurgeStatus() == Complete {
|
||||||
markDelete = false
|
markDelete = false
|
||||||
}
|
}
|
||||||
// determine if the version represents an object delete
|
|
||||||
// deleteMarker = true
|
// Version is found but we do not wish to create more delete markers
|
||||||
if versionFound && !goi.DeleteMarker { // implies a versioned delete of object
|
// now, since VersionPurgeStatus() is already set, we can let the
|
||||||
|
// lower layers decide this. This fixes a regression that was introduced
|
||||||
|
// in PR #14555 where !VersionPurgeStatus.Empty() is automatically
|
||||||
|
// considered as Delete marker true to avoid listing such objects by
|
||||||
|
// regular ListObjects() calls. However for delete replication this
|
||||||
|
// ends up being a problem because "upon" a successful delete this
|
||||||
|
// ends up creating a new delete marker that is spurious and unnecessary.
|
||||||
|
if versionFound {
|
||||||
|
if !goi.VersionPurgeStatus.Empty() {
|
||||||
deleteMarker = false
|
deleteMarker = false
|
||||||
|
} else if !goi.DeleteMarker { // implies a versioned delete of object
|
||||||
|
deleteMarker = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1427,11 +1563,24 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
|
|||||||
|
|
||||||
disks := er.getDisks()
|
disks := er.getDisks()
|
||||||
|
|
||||||
|
var metaArr []FileInfo
|
||||||
|
var errs []error
|
||||||
|
|
||||||
// Read metadata associated with the object from all disks.
|
// Read metadata associated with the object from all disks.
|
||||||
metaArr, errs := readAllFileInfo(ctx, disks, bucket, object, opts.VersionID, false)
|
if opts.VersionID != "" {
|
||||||
|
metaArr, errs = readAllFileInfo(ctx, disks, bucket, object, opts.VersionID, false)
|
||||||
|
} else {
|
||||||
|
metaArr, errs = readAllXL(ctx, disks, bucket, object, false)
|
||||||
|
}
|
||||||
|
|
||||||
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, errErasureReadQuorum) && !strings.HasPrefix(bucket, minioMetaBucket) {
|
||||||
|
_, derr := er.deleteIfDangling(ctx, bucket, object, metaArr, errs, nil, opts)
|
||||||
|
if derr != nil {
|
||||||
|
err = derr
|
||||||
|
}
|
||||||
|
}
|
||||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1487,11 +1636,24 @@ func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object strin
|
|||||||
|
|
||||||
disks := er.getDisks()
|
disks := er.getDisks()
|
||||||
|
|
||||||
|
var metaArr []FileInfo
|
||||||
|
var errs []error
|
||||||
|
|
||||||
// Read metadata associated with the object from all disks.
|
// Read metadata associated with the object from all disks.
|
||||||
metaArr, errs := readAllFileInfo(ctx, disks, bucket, object, opts.VersionID, false)
|
if opts.VersionID != "" {
|
||||||
|
metaArr, errs = readAllFileInfo(ctx, disks, bucket, object, opts.VersionID, false)
|
||||||
|
} else {
|
||||||
|
metaArr, errs = readAllXL(ctx, disks, bucket, object, false)
|
||||||
|
}
|
||||||
|
|
||||||
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, errErasureReadQuorum) && !strings.HasPrefix(bucket, minioMetaBucket) {
|
||||||
|
_, derr := er.deleteIfDangling(ctx, bucket, object, metaArr, errs, nil, opts)
|
||||||
|
if derr != nil {
|
||||||
|
err = derr
|
||||||
|
}
|
||||||
|
}
|
||||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/minio/minio/internal/hash"
|
"github.com/minio/minio/internal/hash"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/console"
|
"github.com/minio/pkg/console"
|
||||||
|
"github.com/minio/pkg/env"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PoolDecommissionInfo currently decommissioning information
|
// PoolDecommissionInfo currently decommissioning information
|
||||||
@@ -141,7 +145,7 @@ func (p *poolMeta) returnResumablePools(n int) []PoolStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *poolMeta) DecommissionComplete(idx int) bool {
|
func (p *poolMeta) DecommissionComplete(idx int) bool {
|
||||||
if p.Pools[idx].Decommission != nil {
|
if p.Pools[idx].Decommission != nil && !p.Pools[idx].Decommission.Complete {
|
||||||
p.Pools[idx].LastUpdate = UTCNow()
|
p.Pools[idx].LastUpdate = UTCNow()
|
||||||
p.Pools[idx].Decommission.Complete = true
|
p.Pools[idx].Decommission.Complete = true
|
||||||
p.Pools[idx].Decommission.Failed = false
|
p.Pools[idx].Decommission.Failed = false
|
||||||
@@ -152,7 +156,7 @@ func (p *poolMeta) DecommissionComplete(idx int) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *poolMeta) DecommissionFailed(idx int) bool {
|
func (p *poolMeta) DecommissionFailed(idx int) bool {
|
||||||
if p.Pools[idx].Decommission != nil {
|
if p.Pools[idx].Decommission != nil && !p.Pools[idx].Decommission.Failed {
|
||||||
p.Pools[idx].LastUpdate = UTCNow()
|
p.Pools[idx].LastUpdate = UTCNow()
|
||||||
p.Pools[idx].Decommission.StartTime = time.Time{}
|
p.Pools[idx].Decommission.StartTime = time.Time{}
|
||||||
p.Pools[idx].Decommission.Complete = false
|
p.Pools[idx].Decommission.Complete = false
|
||||||
@@ -164,7 +168,7 @@ func (p *poolMeta) DecommissionFailed(idx int) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *poolMeta) DecommissionCancel(idx int) bool {
|
func (p *poolMeta) DecommissionCancel(idx int) bool {
|
||||||
if p.Pools[idx].Decommission != nil {
|
if p.Pools[idx].Decommission != nil && !p.Pools[idx].Decommission.Canceled {
|
||||||
p.Pools[idx].LastUpdate = UTCNow()
|
p.Pools[idx].LastUpdate = UTCNow()
|
||||||
p.Pools[idx].Decommission.StartTime = time.Time{}
|
p.Pools[idx].Decommission.StartTime = time.Time{}
|
||||||
p.Pools[idx].Decommission.Complete = false
|
p.Pools[idx].Decommission.Complete = false
|
||||||
@@ -238,41 +242,26 @@ func (p *poolMeta) Decommission(idx int, pi poolSpaceInfo) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
now := UTCNow()
|
// Return an error when there is decommission on going - the user needs
|
||||||
if p.Pools[idx].Decommission == nil {
|
// to explicitly cancel it first in order to restart decommissioning again.
|
||||||
p.Pools[idx].LastUpdate = now
|
if p.Pools[idx].Decommission != nil &&
|
||||||
p.Pools[idx].Decommission = &PoolDecommissionInfo{
|
!p.Pools[idx].Decommission.Complete &&
|
||||||
StartTime: now,
|
!p.Pools[idx].Decommission.Failed &&
|
||||||
StartSize: pi.Free,
|
!p.Pools[idx].Decommission.Canceled {
|
||||||
CurrentSize: pi.Free,
|
|
||||||
TotalSize: pi.Total,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Completed pool doesn't need to be decommissioned again.
|
|
||||||
if p.Pools[idx].Decommission.Complete {
|
|
||||||
return errDecommissionComplete
|
|
||||||
}
|
|
||||||
|
|
||||||
// Canceled or Failed decommission can be triggered again.
|
|
||||||
if p.Pools[idx].Decommission.StartTime.IsZero() {
|
|
||||||
if p.Pools[idx].Decommission.Canceled || p.Pools[idx].Decommission.Failed {
|
|
||||||
p.Pools[idx].LastUpdate = now
|
|
||||||
p.Pools[idx].Decommission = &PoolDecommissionInfo{
|
|
||||||
StartTime: now,
|
|
||||||
StartSize: pi.Free,
|
|
||||||
CurrentSize: pi.Free,
|
|
||||||
TotalSize: pi.Total,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} // In-progress pool doesn't need to be decommissioned again.
|
|
||||||
|
|
||||||
// In all other scenarios an active decommissioning is in progress.
|
|
||||||
return errDecommissionAlreadyRunning
|
return errDecommissionAlreadyRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := UTCNow()
|
||||||
|
p.Pools[idx].LastUpdate = now
|
||||||
|
p.Pools[idx].Decommission = &PoolDecommissionInfo{
|
||||||
|
StartTime: now,
|
||||||
|
StartSize: pi.Free,
|
||||||
|
CurrentSize: pi.Free,
|
||||||
|
TotalSize: pi.Total,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p poolMeta) IsSuspended(idx int) bool {
|
func (p poolMeta) IsSuspended(idx int) bool {
|
||||||
return p.Pools[idx].Decommission != nil
|
return p.Pools[idx].Decommission != nil
|
||||||
}
|
}
|
||||||
@@ -306,9 +295,31 @@ func (p *poolMeta) validate(pools []*erasureSets) (bool, error) {
|
|||||||
specifiedPools[pool.endpoints.CmdLine] = idx
|
specifiedPools[pool.endpoints.CmdLine] = idx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replaceScheme := func(k string) string {
|
||||||
|
// This is needed as fallback when users are changeing
|
||||||
|
// from http->https or https->http, we need to verify
|
||||||
|
// both because MinIO remembers the command-line in
|
||||||
|
// "exact" order - as long as this order is not disturbed
|
||||||
|
// we allow changing the "scheme" i.e internode communication
|
||||||
|
// from plain-text to TLS or from TLS to plain-text.
|
||||||
|
if strings.HasPrefix(k, "http://") {
|
||||||
|
k = strings.ReplaceAll(k, "http://", "https://")
|
||||||
|
} else if strings.HasPrefix(k, "https://") {
|
||||||
|
k = strings.ReplaceAll(k, "https://", "http://")
|
||||||
|
}
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
var update bool
|
||||||
// Check if specified pools need to remove decommissioned pool.
|
// Check if specified pools need to remove decommissioned pool.
|
||||||
for k := range specifiedPools {
|
for k := range specifiedPools {
|
||||||
pi, ok := rememberedPools[k]
|
pi, ok := rememberedPools[k]
|
||||||
|
if !ok {
|
||||||
|
pi, ok = rememberedPools[replaceScheme(k)]
|
||||||
|
if ok {
|
||||||
|
update = true // Looks like user is changing from http->https or https->http
|
||||||
|
}
|
||||||
|
}
|
||||||
if ok && pi.completed {
|
if ok && pi.completed {
|
||||||
return false, fmt.Errorf("pool(%s) = %s is decommissioned, please remove from server command line", humanize.Ordinal(pi.position+1), k)
|
return false, fmt.Errorf("pool(%s) = %s is decommissioned, please remove from server command line", humanize.Ordinal(pi.position+1), k)
|
||||||
}
|
}
|
||||||
@@ -320,6 +331,12 @@ func (p *poolMeta) validate(pools []*erasureSets) (bool, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, ok := specifiedPools[k]
|
_, ok := specifiedPools[k]
|
||||||
|
if !ok {
|
||||||
|
_, ok = specifiedPools[replaceScheme(k)]
|
||||||
|
if ok {
|
||||||
|
update = true // Looks like user is changing from http->https or https->http
|
||||||
|
}
|
||||||
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, fmt.Errorf("pool(%s) = %s is not specified, please specify on server command line", humanize.Ordinal(pi.position+1), k)
|
return false, fmt.Errorf("pool(%s) = %s is not specified, please specify on server command line", humanize.Ordinal(pi.position+1), k)
|
||||||
}
|
}
|
||||||
@@ -329,6 +346,12 @@ func (p *poolMeta) validate(pools []*erasureSets) (bool, error) {
|
|||||||
if len(rememberedPools) == len(specifiedPools) {
|
if len(rememberedPools) == len(specifiedPools) {
|
||||||
for k, pi := range rememberedPools {
|
for k, pi := range rememberedPools {
|
||||||
pos, ok := specifiedPools[k]
|
pos, ok := specifiedPools[k]
|
||||||
|
if !ok {
|
||||||
|
pos, ok = specifiedPools[replaceScheme(k)]
|
||||||
|
if ok {
|
||||||
|
update = true // Looks like user is changing from http->https or https->http
|
||||||
|
}
|
||||||
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, fmt.Errorf("pool(%s) = %s is not specified, please specify on server command line", humanize.Ordinal(pi.position+1), k)
|
return false, fmt.Errorf("pool(%s) = %s is not specified, please specify on server command line", humanize.Ordinal(pi.position+1), k)
|
||||||
}
|
}
|
||||||
@@ -338,7 +361,9 @@ func (p *poolMeta) validate(pools []*erasureSets) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update := len(rememberedPools) != len(specifiedPools)
|
if !update {
|
||||||
|
update = len(rememberedPools) != len(specifiedPools)
|
||||||
|
}
|
||||||
if update {
|
if update {
|
||||||
for k, pi := range rememberedPools {
|
for k, pi := range rememberedPools {
|
||||||
if pi.decomStarted && !pi.completed {
|
if pi.decomStarted && !pi.completed {
|
||||||
@@ -466,21 +491,32 @@ func (z *erasureServerPools) Init(ctx context.Context) error {
|
|||||||
|
|
||||||
// if no update is needed return right away.
|
// if no update is needed return right away.
|
||||||
if !update {
|
if !update {
|
||||||
|
z.poolMeta = meta
|
||||||
|
|
||||||
// We are only supporting single pool decommission at this time
|
// We are only supporting single pool decommission at this time
|
||||||
// so it makes sense to only resume single pools at any given
|
// so it makes sense to only resume single pools at any given
|
||||||
// time, in future meta.returnResumablePools() might take
|
// time, in future meta.returnResumablePools() might take
|
||||||
// '-1' as argument to decommission multiple pools at a time
|
// '-1' as argument to decommission multiple pools at a time
|
||||||
// but this is not a priority at the moment.
|
// but this is not a priority at the moment.
|
||||||
for _, pool := range meta.returnResumablePools(1) {
|
for _, pool := range meta.returnResumablePools(1) {
|
||||||
err := z.Decommission(ctx, pool.ID)
|
idx := globalEndpoints.GetPoolIdx(pool.CmdLine)
|
||||||
switch err {
|
if idx == -1 {
|
||||||
case errDecommissionAlreadyRunning:
|
return fmt.Errorf("unexpected state present for decommission status pool(%s) not found", pool.CmdLine)
|
||||||
fallthrough
|
}
|
||||||
|
if globalEndpoints[idx].Endpoints[0].IsLocal {
|
||||||
|
go func(pool PoolStatus) {
|
||||||
|
switch err := z.Decommission(ctx, pool.ID); err {
|
||||||
case nil:
|
case nil:
|
||||||
go z.doDecommissionInRoutine(ctx, pool.ID)
|
// we already started decommission
|
||||||
|
case errDecommissionAlreadyRunning:
|
||||||
|
// A previous decommission running found restart it.
|
||||||
|
z.doDecommissionInRoutine(ctx, idx)
|
||||||
|
default:
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to resume decommission of pool %v: %w", pool, err))
|
||||||
|
}
|
||||||
|
}(pool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
z.poolMeta = meta
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -505,8 +541,13 @@ func (z *erasureServerPools) Init(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (z *erasureServerPools) decommissionObject(ctx context.Context, bucket string, gr *GetObjectReader) (err error) {
|
func (z *erasureServerPools) decommissionObject(ctx context.Context, bucket string, gr *GetObjectReader) (err error) {
|
||||||
defer gr.Close()
|
|
||||||
objInfo := gr.ObjInfo
|
objInfo := gr.ObjInfo
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
gr.Close()
|
||||||
|
auditLogDecom(ctx, "DecomCopyData", objInfo.Bucket, objInfo.Name, objInfo.VersionID, err)
|
||||||
|
}()
|
||||||
|
|
||||||
if objInfo.isMultipart() {
|
if objInfo.isMultipart() {
|
||||||
uploadID, err := z.NewMultipartUpload(ctx, bucket, objInfo.Name, ObjectOptions{
|
uploadID, err := z.NewMultipartUpload(ctx, bucket, objInfo.Name, ObjectOptions{
|
||||||
VersionID: objInfo.VersionID,
|
VersionID: objInfo.VersionID,
|
||||||
@@ -567,15 +608,19 @@ func (v versionsSorter) reverse() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool *erasureSets, bName string) error {
|
func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool *erasureSets, bName string) error {
|
||||||
var forwardTo string
|
ctx = logger.SetReqInfo(ctx, &logger.ReqInfo{})
|
||||||
// If we resume to the same bucket, forward to last known item.
|
|
||||||
rbucket, robject := z.poolMeta.ResumeBucketObject(idx)
|
var wg sync.WaitGroup
|
||||||
if rbucket != "" && rbucket == bName {
|
wStr := env.Get("_MINIO_DECOMMISSION_WORKERS", strconv.Itoa(len(pool.sets)))
|
||||||
forwardTo = robject
|
workerSize, err := strconv.Atoi(wStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
versioned := globalBucketVersioningSys.Enabled(bName)
|
parallelWorkers := make(chan struct{}, workerSize)
|
||||||
|
|
||||||
for _, set := range pool.sets {
|
for _, set := range pool.sets {
|
||||||
|
set := set
|
||||||
disks := set.getOnlineDisks()
|
disks := set.getOnlineDisks()
|
||||||
if len(disks) == 0 {
|
if len(disks) == 0 {
|
||||||
logger.LogIf(GlobalContext, fmt.Errorf("no online disks found for set with endpoints %s",
|
logger.LogIf(GlobalContext, fmt.Errorf("no online disks found for set with endpoints %s",
|
||||||
@@ -583,7 +628,13 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vc, _ := globalBucketVersioningSys.Get(bName)
|
||||||
decommissionEntry := func(entry metaCacheEntry) {
|
decommissionEntry := func(entry metaCacheEntry) {
|
||||||
|
defer func() {
|
||||||
|
<-parallelWorkers
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
if entry.isDir() {
|
if entry.isDir() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -593,19 +644,22 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need a reversed order for Decommissioning,
|
// We need a reversed order for decommissioning,
|
||||||
// to create the appropriate stack.
|
// to create the appropriate stack.
|
||||||
versionsSorter(fivs.Versions).reverse()
|
versionsSorter(fivs.Versions).reverse()
|
||||||
|
|
||||||
|
var decommissionedCount int
|
||||||
for _, version := range fivs.Versions {
|
for _, version := range fivs.Versions {
|
||||||
// TODO: Skip transitioned objects for now.
|
// TODO: Skip transitioned objects for now.
|
||||||
if version.IsRemote() {
|
if version.IsRemote() {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("found %s/%s transitioned object, transitioned object won't be decommissioned", bName, version.Name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// We will skip decommissioning delete markers
|
// We will skip decommissioning delete markers
|
||||||
// with single version, its as good as there
|
// with single version, its as good as there
|
||||||
// is no data associated with the object.
|
// is no data associated with the object.
|
||||||
if version.Deleted && len(fivs.Versions) == 1 {
|
if version.Deleted && len(fivs.Versions) == 1 {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("found %s/%s delete marked object with no other versions, skipping since there is no content left", bName, version.Name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if version.Deleted {
|
if version.Deleted {
|
||||||
@@ -613,32 +667,29 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
bName,
|
bName,
|
||||||
version.Name,
|
version.Name,
|
||||||
ObjectOptions{
|
ObjectOptions{
|
||||||
Versioned: versioned,
|
Versioned: vc.PrefixEnabled(version.Name),
|
||||||
VersionID: version.VersionID,
|
VersionID: version.VersionID,
|
||||||
MTime: version.ModTime,
|
MTime: version.ModTime,
|
||||||
DeleteReplication: version.ReplicationState,
|
DeleteReplication: version.ReplicationState,
|
||||||
})
|
})
|
||||||
|
var failure bool
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
z.poolMetaMutex.Lock()
|
failure = true
|
||||||
z.poolMeta.CountItem(idx, 0, true)
|
|
||||||
z.poolMetaMutex.Unlock()
|
|
||||||
} else {
|
|
||||||
set.DeleteObject(ctx,
|
|
||||||
bName,
|
|
||||||
version.Name,
|
|
||||||
ObjectOptions{
|
|
||||||
VersionID: version.VersionID,
|
|
||||||
})
|
|
||||||
z.poolMetaMutex.Lock()
|
|
||||||
z.poolMeta.CountItem(idx, 0, false)
|
|
||||||
z.poolMetaMutex.Unlock()
|
|
||||||
}
|
}
|
||||||
|
z.poolMetaMutex.Lock()
|
||||||
|
z.poolMeta.CountItem(idx, 0, failure)
|
||||||
|
z.poolMetaMutex.Unlock()
|
||||||
|
if failure {
|
||||||
|
break // break out on first error
|
||||||
|
}
|
||||||
|
decommissionedCount++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
gr, err := set.GetObjectNInfo(ctx,
|
gr, err := set.GetObjectNInfo(ctx,
|
||||||
bName,
|
bName,
|
||||||
version.Name,
|
encodeDirObject(version.Name),
|
||||||
nil,
|
nil,
|
||||||
http.Header{},
|
http.Header{},
|
||||||
noLock, // all mutations are blocked reads are safe without locks.
|
noLock, // all mutations are blocked reads are safe without locks.
|
||||||
@@ -650,25 +701,36 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
z.poolMetaMutex.Lock()
|
z.poolMetaMutex.Lock()
|
||||||
z.poolMeta.CountItem(idx, version.Size, true)
|
z.poolMeta.CountItem(idx, version.Size, true)
|
||||||
z.poolMetaMutex.Unlock()
|
z.poolMetaMutex.Unlock()
|
||||||
continue
|
break // break out on first error
|
||||||
}
|
}
|
||||||
|
var failure bool
|
||||||
// gr.Close() is ensured by decommissionObject().
|
// gr.Close() is ensured by decommissionObject().
|
||||||
if err = z.decommissionObject(ctx, bName, gr); err != nil {
|
if err = z.decommissionObject(ctx, bName, gr); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
z.poolMetaMutex.Lock()
|
failure = true
|
||||||
z.poolMeta.CountItem(idx, version.Size, true)
|
|
||||||
z.poolMetaMutex.Unlock()
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
set.DeleteObject(ctx,
|
|
||||||
bName,
|
|
||||||
version.Name,
|
|
||||||
ObjectOptions{
|
|
||||||
VersionID: version.VersionID,
|
|
||||||
})
|
|
||||||
z.poolMetaMutex.Lock()
|
z.poolMetaMutex.Lock()
|
||||||
z.poolMeta.CountItem(idx, version.Size, false)
|
z.poolMeta.CountItem(idx, version.Size, failure)
|
||||||
z.poolMetaMutex.Unlock()
|
z.poolMetaMutex.Unlock()
|
||||||
|
if failure {
|
||||||
|
break // break out on first error
|
||||||
|
}
|
||||||
|
decommissionedCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// if all versions were decommissioned, then we can delete the object versions.
|
||||||
|
if decommissionedCount == len(fivs.Versions) {
|
||||||
|
_, err := set.DeleteObject(ctx,
|
||||||
|
bName,
|
||||||
|
entry.name,
|
||||||
|
ObjectOptions{
|
||||||
|
DeletePrefix: true, // use prefix delete to delete all versions at once.
|
||||||
|
},
|
||||||
|
)
|
||||||
|
auditLogDecom(ctx, "DecomDeleteObject", bName, entry.name, "", err)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
z.poolMetaMutex.Lock()
|
z.poolMetaMutex.Lock()
|
||||||
z.poolMeta.TrackCurrentBucketObject(idx, bName, entry.name)
|
z.poolMeta.TrackCurrentBucketObject(idx, bName, entry.name)
|
||||||
@@ -687,26 +749,35 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
bucket: bName,
|
bucket: bName,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := listPathRaw(ctx, listPathRawOptions{
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
err := listPathRaw(ctx, listPathRawOptions{
|
||||||
disks: disks,
|
disks: disks,
|
||||||
bucket: bName,
|
bucket: bName,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
forwardTo: forwardTo,
|
forwardTo: "",
|
||||||
minDisks: len(disks) / 2, // to capture all quorum ratios
|
minDisks: len(disks) / 2, // to capture all quorum ratios
|
||||||
reportNotFound: false,
|
reportNotFound: false,
|
||||||
agreed: decommissionEntry,
|
agreed: func(entry metaCacheEntry) {
|
||||||
|
parallelWorkers <- struct{}{}
|
||||||
|
wg.Add(1)
|
||||||
|
go decommissionEntry(entry)
|
||||||
|
},
|
||||||
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
|
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
|
||||||
entry, ok := entries.resolve(&resolver)
|
entry, ok := entries.resolve(&resolver)
|
||||||
if ok {
|
if ok {
|
||||||
decommissionEntry(*entry)
|
parallelWorkers <- struct{}{}
|
||||||
|
wg.Add(1)
|
||||||
|
go decommissionEntry(*entry)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
finished: nil,
|
finished: nil,
|
||||||
}); err != nil {
|
})
|
||||||
// Decommissioning failed and won't continue
|
logger.LogIf(ctx, err)
|
||||||
return err
|
}()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
wg.Wait()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,18 +815,31 @@ func (z *erasureServerPools) doDecommissionInRoutine(ctx context.Context, idx in
|
|||||||
dctx, z.decommissionCancelers[idx] = context.WithCancel(GlobalContext)
|
dctx, z.decommissionCancelers[idx] = context.WithCancel(GlobalContext)
|
||||||
z.poolMetaMutex.Unlock()
|
z.poolMetaMutex.Unlock()
|
||||||
|
|
||||||
|
// Generate an empty request info so it can be directly modified later by audit
|
||||||
|
dctx = logger.SetReqInfo(dctx, &logger.ReqInfo{})
|
||||||
|
|
||||||
if err := z.decommissionInBackground(dctx, idx); err != nil {
|
if err := z.decommissionInBackground(dctx, idx); err != nil {
|
||||||
logger.LogIf(GlobalContext, err)
|
logger.LogIf(GlobalContext, err)
|
||||||
logger.LogIf(GlobalContext, z.DecommissionFailed(dctx, idx))
|
logger.LogIf(GlobalContext, z.DecommissionFailed(dctx, idx))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
z.poolMetaMutex.Lock()
|
||||||
|
failed := z.poolMeta.Pools[idx].Decommission.ItemsDecommissionFailed > 0
|
||||||
|
z.poolMetaMutex.Unlock()
|
||||||
|
|
||||||
|
if failed {
|
||||||
|
// Decommission failed indicate as such.
|
||||||
|
logger.LogIf(GlobalContext, z.DecommissionFailed(dctx, idx))
|
||||||
|
} else {
|
||||||
// Complete the decommission..
|
// Complete the decommission..
|
||||||
logger.LogIf(GlobalContext, z.CompleteDecommission(dctx, idx))
|
logger.LogIf(GlobalContext, z.CompleteDecommission(dctx, idx))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (z *erasureServerPools) IsSuspended(idx int) bool {
|
func (z *erasureServerPools) IsSuspended(idx int) bool {
|
||||||
z.poolMetaMutex.Lock()
|
z.poolMetaMutex.RLock()
|
||||||
defer z.poolMetaMutex.Unlock()
|
defer z.poolMetaMutex.RUnlock()
|
||||||
return z.poolMeta.IsSuspended(idx)
|
return z.poolMeta.IsSuspended(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,12 +885,7 @@ func (z *erasureServerPools) getDecommissionPoolSpaceInfo(idx int) (pi poolSpace
|
|||||||
if idx+1 > len(z.serverPools) {
|
if idx+1 > len(z.serverPools) {
|
||||||
return pi, errInvalidArgument
|
return pi, errInvalidArgument
|
||||||
}
|
}
|
||||||
info, errs := z.serverPools[idx].StorageInfo(context.Background())
|
info, _ := z.serverPools[idx].StorageInfo(context.Background())
|
||||||
for _, err := range errs {
|
|
||||||
if err != nil {
|
|
||||||
return pi, errInvalidArgument
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info.Backend = z.BackendInfo()
|
info.Backend = z.BackendInfo()
|
||||||
for _, disk := range info.Disks {
|
for _, disk := range info.Disks {
|
||||||
if disk.Healing {
|
if disk.Healing {
|
||||||
@@ -878,7 +957,7 @@ func (z *erasureServerPools) DecommissionCancel(ctx context.Context, idx int) (e
|
|||||||
defer z.poolMetaMutex.Unlock()
|
defer z.poolMetaMutex.Unlock()
|
||||||
|
|
||||||
if z.poolMeta.DecommissionCancel(idx) {
|
if z.poolMeta.DecommissionCancel(idx) {
|
||||||
z.decommissionCancelers[idx]() // cancel any active thread.
|
defer z.decommissionCancelers[idx]() // cancel any active thread.
|
||||||
if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
|
if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -900,7 +979,7 @@ func (z *erasureServerPools) DecommissionFailed(ctx context.Context, idx int) (e
|
|||||||
defer z.poolMetaMutex.Unlock()
|
defer z.poolMetaMutex.Unlock()
|
||||||
|
|
||||||
if z.poolMeta.DecommissionFailed(idx) {
|
if z.poolMeta.DecommissionFailed(idx) {
|
||||||
z.decommissionCancelers[idx]() // cancel any active thread.
|
defer z.decommissionCancelers[idx]() // cancel any active thread.
|
||||||
if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
|
if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -922,6 +1001,7 @@ func (z *erasureServerPools) CompleteDecommission(ctx context.Context, idx int)
|
|||||||
defer z.poolMetaMutex.Unlock()
|
defer z.poolMetaMutex.Unlock()
|
||||||
|
|
||||||
if z.poolMeta.DecommissionComplete(idx) {
|
if z.poolMeta.DecommissionComplete(idx) {
|
||||||
|
defer z.decommissionCancelers[idx]() // cancel any active thread.
|
||||||
if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
|
if err = z.poolMeta.save(ctx, z.serverPools); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -955,6 +1035,21 @@ func (z *erasureServerPools) StartDecommission(ctx context.Context, idx int) (er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create .minio.sys/conifg, .minio.sys/buckets paths if missing,
|
||||||
|
// this code is present to avoid any missing meta buckets on other
|
||||||
|
// pools.
|
||||||
|
for _, metaBucket := range []string{
|
||||||
|
pathJoin(minioMetaBucket, minioConfigPrefix),
|
||||||
|
pathJoin(minioMetaBucket, bucketMetaPrefix),
|
||||||
|
} {
|
||||||
|
var bucketExists BucketExists
|
||||||
|
if err = z.MakeBucketWithLocation(ctx, metaBucket, BucketOptions{}); err != nil {
|
||||||
|
if !errors.As(err, &bucketExists) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Buckets data are dispersed in multiple zones/sets, make
|
// Buckets data are dispersed in multiple zones/sets, make
|
||||||
// sure to decommission the necessary metadata.
|
// sure to decommission the necessary metadata.
|
||||||
buckets = append(buckets, BucketInfo{
|
buckets = append(buckets, BucketInfo{
|
||||||
@@ -994,3 +1089,16 @@ func (z *erasureServerPools) StartDecommission(ctx context.Context, idx int) (er
|
|||||||
globalNotificationSys.ReloadPoolMeta(ctx)
|
globalNotificationSys.ReloadPoolMeta(ctx)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func auditLogDecom(ctx context.Context, apiName, bucket, object, versionID string, err error) {
|
||||||
|
errStr := ""
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
}
|
||||||
|
auditLogInternal(ctx, bucket, object, AuditLogOptions{
|
||||||
|
Trigger: "decommissioning",
|
||||||
|
APIName: apiName,
|
||||||
|
VersionID: versionID,
|
||||||
|
Error: errStr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/madmin-go"
|
"github.com/minio/madmin-go"
|
||||||
"github.com/minio/minio-go/v7/pkg/set"
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
"github.com/minio/minio-go/v7/pkg/tags"
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||||
"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"
|
||||||
"github.com/minio/minio/internal/sync/errgroup"
|
"github.com/minio/minio/internal/sync/errgroup"
|
||||||
@@ -128,7 +130,9 @@ func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServ
|
|||||||
if !configRetriableErrors(err) {
|
if !configRetriableErrors(err) {
|
||||||
logger.Fatal(err, "Unable to initialize backend")
|
logger.Fatal(err, "Unable to initialize backend")
|
||||||
}
|
}
|
||||||
time.Sleep(time.Duration(r.Float64() * float64(5*time.Second)))
|
retry := time.Duration(r.Float64() * float64(5*time.Second))
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize backend: %w, retrying in %s", err, retry))
|
||||||
|
time.Sleep(retry)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -528,7 +532,7 @@ func (z *erasureServerPools) StorageInfo(ctx context.Context) (StorageInfo, []er
|
|||||||
return storageInfo, errs
|
return storageInfo, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z *erasureServerPools) NSScanner(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo, wantCycle uint32) error {
|
func (z *erasureServerPools) NSScanner(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo, wantCycle uint32, healScanMode madmin.HealScanMode) error {
|
||||||
// Updates must be closed before we return.
|
// Updates must be closed before we return.
|
||||||
defer close(updates)
|
defer close(updates)
|
||||||
|
|
||||||
@@ -573,7 +577,7 @@ func (z *erasureServerPools) NSScanner(ctx context.Context, bf *bloomFilter, upd
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
// Start scanner. Blocks until done.
|
// Start scanner. Blocks until done.
|
||||||
err := erObj.nsScanner(ctx, allBuckets, bf, wantCycle, updates)
|
err := erObj.nsScanner(ctx, allBuckets, bf, wantCycle, updates, healScanMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -661,6 +665,9 @@ func (z *erasureServerPools) MakeBucketWithLocation(ctx context.Context, bucket
|
|||||||
for index := range z.serverPools {
|
for index := range z.serverPools {
|
||||||
index := index
|
index := index
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
|
if z.IsSuspended(index) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return z.serverPools[index].MakeBucketWithLocation(ctx, bucket, opts)
|
return z.serverPools[index].MakeBucketWithLocation(ctx, bucket, opts)
|
||||||
}, index)
|
}, index)
|
||||||
}
|
}
|
||||||
@@ -672,7 +679,8 @@ func (z *erasureServerPools) MakeBucketWithLocation(ctx context.Context, bucket
|
|||||||
if _, ok := err.(BucketExists); !ok {
|
if _, ok := err.(BucketExists); !ok {
|
||||||
// Delete created buckets, ignoring errors.
|
// Delete created buckets, ignoring errors.
|
||||||
z.DeleteBucket(context.Background(), bucket, DeleteBucketOptions{
|
z.DeleteBucket(context.Background(), bucket, DeleteBucketOptions{
|
||||||
Force: false, NoRecreate: true,
|
Force: false,
|
||||||
|
NoRecreate: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -813,6 +821,11 @@ func (z *erasureServerPools) getLatestObjectInfoWithIdx(ctx context.Context, buc
|
|||||||
// should be returned upwards.
|
// should be returned upwards.
|
||||||
return res.oi, res.zIdx, err
|
return res.oi, res.zIdx, err
|
||||||
}
|
}
|
||||||
|
// When its a delete marker and versionID is empty
|
||||||
|
// we should simply return the error right away.
|
||||||
|
if res.oi.DeleteMarker && opts.VersionID == "" {
|
||||||
|
return res.oi, res.zIdx, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object = decodeDirObject(object)
|
object = decodeDirObject(object)
|
||||||
@@ -1146,6 +1159,12 @@ func maxKeysPlusOne(maxKeys int, addOne bool) int {
|
|||||||
func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
||||||
var loi ListObjectsInfo
|
var loi ListObjectsInfo
|
||||||
|
|
||||||
|
// Automatically remove the object/version is an expiry lifecycle rule can be applied
|
||||||
|
lc, _ := globalLifecycleSys.Get(bucket)
|
||||||
|
|
||||||
|
// Check if bucket is object locked.
|
||||||
|
rcfg, _ := globalBucketObjectLockSys.Get(bucket)
|
||||||
|
|
||||||
if len(prefix) > 0 && maxKeys == 1 && delimiter == "" && marker == "" {
|
if len(prefix) > 0 && maxKeys == 1 && delimiter == "" && marker == "" {
|
||||||
// Optimization for certain applications like
|
// Optimization for certain applications like
|
||||||
// - Cohesity
|
// - Cohesity
|
||||||
@@ -1156,6 +1175,15 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
|
|||||||
// to avoid the need for ListObjects().
|
// to avoid the need for ListObjects().
|
||||||
objInfo, err := z.GetObjectInfo(ctx, bucket, prefix, ObjectOptions{NoLock: true})
|
objInfo, err := z.GetObjectInfo(ctx, bucket, prefix, ObjectOptions{NoLock: true})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
if lc != nil {
|
||||||
|
action := evalActionFromLifecycle(ctx, *lc, rcfg, objInfo, false)
|
||||||
|
switch action {
|
||||||
|
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
||||||
|
fallthrough
|
||||||
|
case lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||||
|
return loi, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
loi.Objects = append(loi.Objects, objInfo)
|
loi.Objects = append(loi.Objects, objInfo)
|
||||||
return loi, nil
|
return loi, nil
|
||||||
}
|
}
|
||||||
@@ -1169,7 +1197,10 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
|
|||||||
Marker: marker,
|
Marker: marker,
|
||||||
InclDeleted: false,
|
InclDeleted: false,
|
||||||
AskDisks: globalAPIConfig.getListQuorum(),
|
AskDisks: globalAPIConfig.getListQuorum(),
|
||||||
|
Lifecycle: lc,
|
||||||
|
Retention: rcfg,
|
||||||
}
|
}
|
||||||
|
|
||||||
merged, err := z.listPath(ctx, &opts)
|
merged, err := z.listPath(ctx, &opts)
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
if !isErrBucketNotFound(err) {
|
if !isErrBucketNotFound(err) {
|
||||||
@@ -1478,6 +1509,9 @@ func (z *erasureServerPools) DeleteBucket(ctx context.Context, bucket string, op
|
|||||||
for index := range z.serverPools {
|
for index := range z.serverPools {
|
||||||
index := index
|
index := index
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
|
if z.IsSuspended(index) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return z.serverPools[index].DeleteBucket(ctx, bucket, opts)
|
return z.serverPools[index].DeleteBucket(ctx, bucket, opts)
|
||||||
}, index)
|
}, index)
|
||||||
}
|
}
|
||||||
@@ -1690,14 +1724,16 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
path := baseDirFromPrefix(prefix)
|
path := baseDirFromPrefix(prefix)
|
||||||
if path == "" {
|
filterPrefix := strings.Trim(strings.TrimPrefix(prefix, path), slashSeparator)
|
||||||
path = prefix
|
if path == prefix {
|
||||||
|
filterPrefix = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
lopts := listPathRawOptions{
|
lopts := listPathRawOptions{
|
||||||
disks: disks,
|
disks: disks,
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
path: path,
|
path: path,
|
||||||
|
filterPrefix: filterPrefix,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
forwardTo: "",
|
forwardTo: "",
|
||||||
minDisks: 1,
|
minDisks: 1,
|
||||||
@@ -1750,14 +1786,16 @@ func listAndHeal(ctx context.Context, bucket, prefix string, set *erasureObjects
|
|||||||
}
|
}
|
||||||
|
|
||||||
path := baseDirFromPrefix(prefix)
|
path := baseDirFromPrefix(prefix)
|
||||||
if path == "" {
|
filterPrefix := strings.Trim(strings.TrimPrefix(prefix, path), slashSeparator)
|
||||||
path = prefix
|
if path == prefix {
|
||||||
|
filterPrefix = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
lopts := listPathRawOptions{
|
lopts := listPathRawOptions{
|
||||||
disks: disks,
|
disks: disks,
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
path: path,
|
path: path,
|
||||||
|
filterPrefix: filterPrefix,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
forwardTo: "",
|
forwardTo: "",
|
||||||
minDisks: 1,
|
minDisks: 1,
|
||||||
|
|||||||
+4
-20
@@ -572,6 +572,7 @@ func (a *auditObjectErasureMap) MarshalJSON() ([]byte, error) {
|
|||||||
return json.Marshal(mapCopy)
|
return json.Marshal(mapCopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add erasure set information to the current context
|
||||||
func auditObjectErasureSet(ctx context.Context, object string, set *erasureObjects) {
|
func auditObjectErasureSet(ctx context.Context, object string, set *erasureObjects) {
|
||||||
if len(logger.AuditTargets()) == 0 {
|
if len(logger.AuditTargets()) == 0 {
|
||||||
return
|
return
|
||||||
@@ -933,21 +934,18 @@ func (s *erasureSets) ListBuckets(ctx context.Context) (buckets []BucketInfo, er
|
|||||||
// GetObjectNInfo - returns object info and locked object ReadCloser
|
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||||
func (s *erasureSets) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
func (s *erasureSets) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||||
set := s.getHashedSet(object)
|
set := s.getHashedSet(object)
|
||||||
auditObjectErasureSet(ctx, object, set)
|
|
||||||
return set.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
return set.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutObject - writes an object to hashedSet based on the object name.
|
// PutObject - writes an object to hashedSet based on the object name.
|
||||||
func (s *erasureSets) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
func (s *erasureSets) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
set := s.getHashedSet(object)
|
set := s.getHashedSet(object)
|
||||||
auditObjectErasureSet(ctx, object, set)
|
|
||||||
return set.PutObject(ctx, bucket, object, data, opts)
|
return set.PutObject(ctx, bucket, object, data, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObjectInfo - reads object metadata from the hashedSet based on the object name.
|
// GetObjectInfo - reads object metadata from the hashedSet based on the object name.
|
||||||
func (s *erasureSets) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
func (s *erasureSets) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
set := s.getHashedSet(object)
|
set := s.getHashedSet(object)
|
||||||
auditObjectErasureSet(ctx, object, set)
|
|
||||||
return set.GetObjectInfo(ctx, bucket, object, opts)
|
return set.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -967,13 +965,11 @@ func (s *erasureSets) deletePrefix(ctx context.Context, bucket string, prefix st
|
|||||||
|
|
||||||
// DeleteObject - deletes an object from the hashedSet based on the object name.
|
// DeleteObject - deletes an object from the hashedSet based on the object name.
|
||||||
func (s *erasureSets) DeleteObject(ctx context.Context, bucket string, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
func (s *erasureSets) DeleteObject(ctx context.Context, bucket string, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
set := s.getHashedSet(object)
|
|
||||||
auditObjectErasureSet(ctx, object, set)
|
|
||||||
|
|
||||||
if opts.DeletePrefix {
|
if opts.DeletePrefix {
|
||||||
err := s.deletePrefix(ctx, bucket, object)
|
err := s.deletePrefix(ctx, bucket, object)
|
||||||
return ObjectInfo{}, err
|
return ObjectInfo{}, err
|
||||||
}
|
}
|
||||||
|
set := s.getHashedSet(object)
|
||||||
return set.DeleteObject(ctx, bucket, object, opts)
|
return set.DeleteObject(ctx, bucket, object, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1030,9 +1026,6 @@ func (s *erasureSets) DeleteObjects(ctx context.Context, bucket string, objects
|
|||||||
for i, obj := range group {
|
for i, obj := range group {
|
||||||
delErrs[obj.origIndex] = errs[i]
|
delErrs[obj.origIndex] = errs[i]
|
||||||
delObjects[obj.origIndex] = dobjects[i]
|
delObjects[obj.origIndex] = dobjects[i]
|
||||||
if errs[i] == nil {
|
|
||||||
auditObjectErasureSet(ctx, obj.object.ObjectName, set)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}(s.sets[setIdx], objsGroup)
|
}(s.sets[setIdx], objsGroup)
|
||||||
}
|
}
|
||||||
@@ -1046,8 +1039,6 @@ func (s *erasureSets) CopyObject(ctx context.Context, srcBucket, srcObject, dstB
|
|||||||
srcSet := s.getHashedSet(srcObject)
|
srcSet := s.getHashedSet(srcObject)
|
||||||
dstSet := s.getHashedSet(dstObject)
|
dstSet := s.getHashedSet(dstObject)
|
||||||
|
|
||||||
auditObjectErasureSet(ctx, dstObject, dstSet)
|
|
||||||
|
|
||||||
cpSrcDstSame := srcSet == dstSet
|
cpSrcDstSame := srcSet == dstSet
|
||||||
// Check if this request is only metadata update.
|
// Check if this request is only metadata update.
|
||||||
if cpSrcDstSame && srcInfo.metadataOnly {
|
if cpSrcDstSame && srcInfo.metadataOnly {
|
||||||
@@ -1086,57 +1077,50 @@ func (s *erasureSets) ListMultipartUploads(ctx context.Context, bucket, prefix,
|
|||||||
// In list multipart uploads we are going to treat input prefix as the object,
|
// In list multipart uploads we are going to treat input prefix as the object,
|
||||||
// this means that we are not supporting directory navigation.
|
// this means that we are not supporting directory navigation.
|
||||||
set := s.getHashedSet(prefix)
|
set := s.getHashedSet(prefix)
|
||||||
auditObjectErasureSet(ctx, prefix, set)
|
|
||||||
return set.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
return set.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initiate a new multipart upload on a hashedSet based on object name.
|
// Initiate a new multipart upload on a hashedSet based on object name.
|
||||||
func (s *erasureSets) NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (uploadID string, err error) {
|
func (s *erasureSets) NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (uploadID string, err error) {
|
||||||
set := s.getHashedSet(object)
|
set := s.getHashedSet(object)
|
||||||
auditObjectErasureSet(ctx, object, set)
|
|
||||||
return set.NewMultipartUpload(ctx, bucket, object, opts)
|
return set.NewMultipartUpload(ctx, bucket, object, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copies a part of an object from source hashedSet to destination hashedSet.
|
// Copies a part of an object from source hashedSet to destination hashedSet.
|
||||||
func (s *erasureSets) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string, partID int,
|
func (s *erasureSets) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string, partID int,
|
||||||
startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (partInfo PartInfo, err error) {
|
startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions,
|
||||||
|
) (partInfo PartInfo, err error) {
|
||||||
destSet := s.getHashedSet(destObject)
|
destSet := s.getHashedSet(destObject)
|
||||||
auditObjectErasureSet(ctx, destObject, destSet)
|
|
||||||
return destSet.PutObjectPart(ctx, destBucket, destObject, uploadID, partID, NewPutObjReader(srcInfo.Reader), dstOpts)
|
return destSet.PutObjectPart(ctx, destBucket, destObject, uploadID, partID, NewPutObjReader(srcInfo.Reader), dstOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutObjectPart - writes part of an object to hashedSet based on the object name.
|
// PutObjectPart - writes part of an object to hashedSet based on the object name.
|
||||||
func (s *erasureSets) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error) {
|
func (s *erasureSets) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error) {
|
||||||
set := s.getHashedSet(object)
|
set := s.getHashedSet(object)
|
||||||
auditObjectErasureSet(ctx, object, set)
|
|
||||||
return set.PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
|
return set.PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMultipartInfo - return multipart metadata info uploaded at hashedSet.
|
// GetMultipartInfo - return multipart metadata info uploaded at hashedSet.
|
||||||
func (s *erasureSets) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (result MultipartInfo, err error) {
|
func (s *erasureSets) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (result MultipartInfo, err error) {
|
||||||
set := s.getHashedSet(object)
|
set := s.getHashedSet(object)
|
||||||
auditObjectErasureSet(ctx, object, set)
|
|
||||||
return set.GetMultipartInfo(ctx, bucket, object, uploadID, opts)
|
return set.GetMultipartInfo(ctx, bucket, object, uploadID, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListObjectParts - lists all uploaded parts to an object in hashedSet.
|
// ListObjectParts - lists all uploaded parts to an object in hashedSet.
|
||||||
func (s *erasureSets) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
|
func (s *erasureSets) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
|
||||||
set := s.getHashedSet(object)
|
set := s.getHashedSet(object)
|
||||||
auditObjectErasureSet(ctx, object, set)
|
|
||||||
return set.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
return set.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aborts an in-progress multipart operation on hashedSet based on the object name.
|
// Aborts an in-progress multipart operation on hashedSet based on the object name.
|
||||||
func (s *erasureSets) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error {
|
func (s *erasureSets) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error {
|
||||||
set := s.getHashedSet(object)
|
set := s.getHashedSet(object)
|
||||||
auditObjectErasureSet(ctx, object, set)
|
|
||||||
return set.AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
|
return set.AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompleteMultipartUpload - completes a pending multipart transaction, on hashedSet based on object name.
|
// CompleteMultipartUpload - completes a pending multipart transaction, on hashedSet based on object name.
|
||||||
func (s *erasureSets) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
func (s *erasureSets) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
set := s.getHashedSet(object)
|
set := s.getHashedSet(object)
|
||||||
auditObjectErasureSet(ctx, object, set)
|
|
||||||
return set.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
|
return set.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -344,7 +344,7 @@ func (er erasureObjects) cleanupDeletedObjects(ctx context.Context) {
|
|||||||
|
|
||||||
// nsScanner will start scanning buckets and send updated totals as they are traversed.
|
// nsScanner will start scanning buckets and send updated totals as they are traversed.
|
||||||
// Updates are sent on a regular basis and the caller *must* consume them.
|
// Updates are sent on a regular basis and the caller *must* consume them.
|
||||||
func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, bf *bloomFilter, wantCycle uint32, updates chan<- dataUsageCache) error {
|
func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, bf *bloomFilter, wantCycle uint32, updates chan<- dataUsageCache, healScanMode madmin.HealScanMode) error {
|
||||||
if len(buckets) == 0 {
|
if len(buckets) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -490,7 +490,7 @@ func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, bf
|
|||||||
// Calc usage
|
// Calc usage
|
||||||
before := cache.Info.LastUpdate
|
before := cache.Info.LastUpdate
|
||||||
var err error
|
var err error
|
||||||
cache, err = disk.NSScanner(ctx, cache, updates)
|
cache, err = disk.NSScanner(ctx, cache, updates, healScanMode)
|
||||||
cache.Info.BloomFilter = nil
|
cache.Info.BloomFilter = nil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !cache.Info.LastUpdate.IsZero() && cache.Info.LastUpdate.After(before) {
|
if !cache.Info.LastUpdate.IsZero() && cache.Info.LastUpdate.After(before) {
|
||||||
|
|||||||
+6
-4
@@ -235,7 +235,7 @@ func (fs *FSObjects) StorageInfo(ctx context.Context) (StorageInfo, []error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NSScanner returns data usage stats of the current FS deployment
|
// NSScanner returns data usage stats of the current FS deployment
|
||||||
func (fs *FSObjects) NSScanner(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo, wantCycle uint32) error {
|
func (fs *FSObjects) NSScanner(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo, wantCycle uint32, _ madmin.HealScanMode) error {
|
||||||
defer close(updates)
|
defer close(updates)
|
||||||
// Load bucket totals
|
// Load bucket totals
|
||||||
var totalCache dataUsageCache
|
var totalCache dataUsageCache
|
||||||
@@ -396,7 +396,7 @@ func (fs *FSObjects) scanBucket(ctx context.Context, bucket string, cache dataUs
|
|||||||
}
|
}
|
||||||
|
|
||||||
return sizeSummary{totalSize: fi.Size(), versions: 1}, nil
|
return sizeSummary{totalSize: fi.Size(), versions: 1}, nil
|
||||||
})
|
}, 0)
|
||||||
|
|
||||||
return cache, err
|
return cache, err
|
||||||
}
|
}
|
||||||
@@ -1362,13 +1362,15 @@ func (fs *FSObjects) HealFormat(ctx context.Context, dryRun bool) (madmin.HealRe
|
|||||||
|
|
||||||
// HealObject - no-op for fs. Valid only for Erasure.
|
// HealObject - no-op for fs. Valid only for Erasure.
|
||||||
func (fs *FSObjects) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (
|
func (fs *FSObjects) HealObject(ctx context.Context, bucket, object, versionID string, opts madmin.HealOpts) (
|
||||||
res madmin.HealResultItem, err error) {
|
res madmin.HealResultItem, err error,
|
||||||
|
) {
|
||||||
return res, NotImplemented{}
|
return res, NotImplemented{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealBucket - no-op for fs, Valid only for Erasure.
|
// HealBucket - no-op for fs, Valid only for Erasure.
|
||||||
func (fs *FSObjects) HealBucket(ctx context.Context, bucket string, opts madmin.HealOpts) (madmin.HealResultItem,
|
func (fs *FSObjects) HealBucket(ctx context.Context, bucket string, opts madmin.HealOpts) (madmin.HealResultItem,
|
||||||
error) {
|
error,
|
||||||
|
) {
|
||||||
return madmin.HealResultItem{}, NotImplemented{}
|
return madmin.HealResultItem{}, NotImplemented{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,8 @@ package cmd
|
|||||||
|
|
||||||
import "github.com/minio/madmin-go"
|
import "github.com/minio/madmin-go"
|
||||||
|
|
||||||
// GatewayMinioSysTmp prefix is used in Azure/GCS gateway for save metadata sent by Initialize Multipart Upload API.
|
// Gateway name backends
|
||||||
const (
|
const (
|
||||||
GatewayMinioSysTmp = "minio.sys.tmp/"
|
|
||||||
AzureBackendGateway = "azure"
|
|
||||||
GCSBackendGateway = "gcs"
|
|
||||||
HDFSBackendGateway = "hdfs"
|
|
||||||
NASBackendGateway = "nas"
|
NASBackendGateway = "nas"
|
||||||
S3BackendGateway = "s3"
|
S3BackendGateway = "s3"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,5 +49,5 @@ func TestPrintGatewayStartupMessage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
||||||
printGatewayStartupMessage(apiEndpoints, "azure")
|
printGatewayStartupMessage(apiEndpoints, "s3")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func (a GatewayUnsupported) LocalStorageInfo(ctx context.Context) (StorageInfo,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NSScanner - scanner is not implemented for gateway
|
// NSScanner - scanner is not implemented for gateway
|
||||||
func (a GatewayUnsupported) NSScanner(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo, wantCycle uint32) error {
|
func (a GatewayUnsupported) NSScanner(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo, wantCycle uint32, scanMode madmin.HealScanMode) error {
|
||||||
logger.CriticalIf(ctx, errors.New("not implemented"))
|
logger.CriticalIf(ctx, errors.New("not implemented"))
|
||||||
return NotImplemented{}
|
return NotImplemented{}
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,8 @@ func (a GatewayUnsupported) HealObjects(ctx context.Context, bucket, prefix stri
|
|||||||
|
|
||||||
// CopyObject copies a blob from source container to destination container.
|
// CopyObject copies a blob from source container to destination container.
|
||||||
func (a GatewayUnsupported) CopyObject(ctx context.Context, srcBucket string, srcObject string, destBucket string, destObject string,
|
func (a GatewayUnsupported) CopyObject(ctx context.Context, srcBucket string, srcObject string, destBucket string, destObject string,
|
||||||
srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) {
|
srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions,
|
||||||
|
) (objInfo ObjectInfo, err error) {
|
||||||
return objInfo, NotImplemented{}
|
return objInfo, NotImplemented{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,273 +0,0 @@
|
|||||||
/*
|
|
||||||
* MinIO Object Storage (c) 2021 MinIO, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package azure
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
|
||||||
minio "github.com/minio/minio/cmd"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseStorageEndpoint(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
host string
|
|
||||||
accountName string
|
|
||||||
expectedURL string
|
|
||||||
expectedErr error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"", "myaccount", "https://myaccount.blob.core.windows.net", nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"myaccount.blob.core.usgovcloudapi.net", "myaccount", "https://myaccount.blob.core.usgovcloudapi.net", nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"http://localhost:10000", "myaccount", "http://localhost:10000/myaccount", nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
endpointURL, err := parseStorageEndpoint(testCase.host, testCase.accountName)
|
|
||||||
if err != testCase.expectedErr {
|
|
||||||
t.Errorf("Test %d: Expected error %s, got %s", i+1, testCase.expectedErr, err)
|
|
||||||
}
|
|
||||||
if endpointURL.String() != testCase.expectedURL {
|
|
||||||
t.Errorf("Test %d: Expected URL %s, got %s", i+1, testCase.expectedURL, endpointURL.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test canonical metadata.
|
|
||||||
func TestS3MetaToAzureProperties(t *testing.T) {
|
|
||||||
headers := map[string]string{
|
|
||||||
"accept-encoding": "gzip",
|
|
||||||
"content-encoding": "gzip",
|
|
||||||
"cache-control": "age: 3600",
|
|
||||||
"content-disposition": "dummy",
|
|
||||||
"content-length": "10",
|
|
||||||
"content-type": "application/javascript",
|
|
||||||
"X-Amz-Meta-Hdr": "value",
|
|
||||||
"X-Amz-Meta-X_test_key": "value",
|
|
||||||
"X-Amz-Meta-X__test__key": "value",
|
|
||||||
"X-Amz-Meta-X-Test__key": "value",
|
|
||||||
"X-Amz-Meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
|
||||||
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
|
||||||
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
|
||||||
}
|
|
||||||
// Only X-Amz-Meta- prefixed entries will be returned in
|
|
||||||
// Metadata (without the prefix!)
|
|
||||||
expectedHeaders := map[string]string{
|
|
||||||
"Hdr": "value",
|
|
||||||
"X__test__key": "value",
|
|
||||||
"X____test____key": "value",
|
|
||||||
"X_Test____key": "value",
|
|
||||||
"X_Amz_Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
|
||||||
"X_Amz_Matdesc": "{}",
|
|
||||||
"X_Amz_Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
|
||||||
}
|
|
||||||
meta, _, err := s3MetaToAzureProperties(minio.GlobalContext, headers)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test failed, with %s", err)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(map[string]string(meta), expectedHeaders) {
|
|
||||||
t.Fatalf("Test failed, expected %#v, got %#v", expectedHeaders, meta)
|
|
||||||
}
|
|
||||||
headers = map[string]string{
|
|
||||||
"invalid--meta": "value",
|
|
||||||
}
|
|
||||||
_, _, err = s3MetaToAzureProperties(minio.GlobalContext, headers)
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(minio.UnsupportedMetadata); !ok {
|
|
||||||
t.Fatalf("Test failed with unexpected error %s, expected UnsupportedMetadata", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
headers = map[string]string{
|
|
||||||
"content-md5": "Dce7bmCX61zvxzP5QmfelQ==",
|
|
||||||
}
|
|
||||||
_, props, err := s3MetaToAzureProperties(minio.GlobalContext, headers)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test failed, with %s", err)
|
|
||||||
}
|
|
||||||
if base64.StdEncoding.EncodeToString(props.ContentMD5) != headers["content-md5"] {
|
|
||||||
t.Fatalf("Test failed, expected %s, got %s", headers["content-md5"], props.ContentMD5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAzurePropertiesToS3Meta(t *testing.T) {
|
|
||||||
// Just one testcase. Adding more test cases does not add value to the testcase
|
|
||||||
// as azureToS3Metadata() just adds a prefix.
|
|
||||||
metadata := map[string]string{
|
|
||||||
"first_name": "myname",
|
|
||||||
"x_test_key": "value",
|
|
||||||
"x_test__key": "value",
|
|
||||||
"x__test__key": "value",
|
|
||||||
"x____test____key": "value",
|
|
||||||
"x_amz_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
|
||||||
"x_amz_matdesc": "{}",
|
|
||||||
"x_amz_iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
|
||||||
}
|
|
||||||
expectedMeta := map[string]string{
|
|
||||||
"X-Amz-Meta-First-Name": "myname",
|
|
||||||
"X-Amz-Meta-X-Test-Key": "value",
|
|
||||||
"X-Amz-Meta-X-Test_key": "value",
|
|
||||||
"X-Amz-Meta-X_test_key": "value",
|
|
||||||
"X-Amz-Meta-X__test__key": "value",
|
|
||||||
"X-Amz-Meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
|
||||||
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
|
||||||
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
|
||||||
"Cache-Control": "max-age: 3600",
|
|
||||||
"Content-Disposition": "dummy",
|
|
||||||
"Content-Encoding": "gzip",
|
|
||||||
"Content-Length": "10",
|
|
||||||
"Content-MD5": base64.StdEncoding.EncodeToString([]byte("base64-md5")),
|
|
||||||
"Content-Type": "application/javascript",
|
|
||||||
}
|
|
||||||
actualMeta := azurePropertiesToS3Meta(metadata, azblob.BlobHTTPHeaders{
|
|
||||||
CacheControl: "max-age: 3600",
|
|
||||||
ContentDisposition: "dummy",
|
|
||||||
ContentEncoding: "gzip",
|
|
||||||
ContentMD5: []byte("base64-md5"),
|
|
||||||
ContentType: "application/javascript",
|
|
||||||
}, 10)
|
|
||||||
if !reflect.DeepEqual(actualMeta, expectedMeta) {
|
|
||||||
t.Fatalf("Test failed, expected %#v, got %#v", expectedMeta, actualMeta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add tests for azure to object error (top level).
|
|
||||||
func TestAzureToObjectError(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
actualErr error
|
|
||||||
expectedErr error
|
|
||||||
bucket, object string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
nil, nil, "", "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fmt.Errorf("Non azure error"),
|
|
||||||
fmt.Errorf("Non azure error"), "", "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
if err := azureToObjectError(testCase.actualErr, testCase.bucket, testCase.object); err != nil {
|
|
||||||
if err.Error() != testCase.expectedErr.Error() {
|
|
||||||
t.Errorf("Test %d: Expected error %s, got %s", i+1, testCase.expectedErr, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if testCase.expectedErr != nil {
|
|
||||||
t.Errorf("Test %d expected an error but one was not produced", i+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add tests for azure to object error (internal).
|
|
||||||
func TestAzureCodesToObjectError(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
originalErr error
|
|
||||||
actualServiceCode string
|
|
||||||
actualStatusCode int
|
|
||||||
expectedErr error
|
|
||||||
bucket, object string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
nil, "ContainerAlreadyExists", 0,
|
|
||||||
minio.BucketExists{Bucket: "bucket"},
|
|
||||||
"bucket", "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nil, "InvalidResourceName", 0,
|
|
||||||
minio.BucketNameInvalid{Bucket: "bucket."},
|
|
||||||
"bucket.", "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nil, "RequestBodyTooLarge", 0,
|
|
||||||
minio.PartTooBig{},
|
|
||||||
"", "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nil, "InvalidMetadata", 0,
|
|
||||||
minio.UnsupportedMetadata{},
|
|
||||||
"", "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nil, "", http.StatusNotFound,
|
|
||||||
minio.ObjectNotFound{
|
|
||||||
Bucket: "bucket",
|
|
||||||
Object: "object",
|
|
||||||
},
|
|
||||||
"bucket", "object",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nil, "", http.StatusNotFound,
|
|
||||||
minio.BucketNotFound{Bucket: "bucket"},
|
|
||||||
"bucket", "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nil, "", http.StatusBadRequest,
|
|
||||||
minio.BucketNameInvalid{Bucket: "bucket."},
|
|
||||||
"bucket.", "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fmt.Errorf("unhandled azure error"), "", http.StatusForbidden,
|
|
||||||
fmt.Errorf("unhandled azure error"), "", "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
if err := azureCodesToObjectError(testCase.originalErr, testCase.actualServiceCode, testCase.actualStatusCode, testCase.bucket, testCase.object); err != nil {
|
|
||||||
if err.Error() != testCase.expectedErr.Error() {
|
|
||||||
t.Errorf("Test %d: Expected error %s, got %s", i+1, testCase.expectedErr, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if testCase.expectedErr != nil {
|
|
||||||
t.Errorf("Test %d expected an error but one was not produced", i+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckAzureUploadID(t *testing.T) {
|
|
||||||
invalidUploadIDs := []string{
|
|
||||||
"123456789abcdefg",
|
|
||||||
"hello world",
|
|
||||||
"0x1234567890",
|
|
||||||
"1234567890abcdef1234567890abcdef",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, uploadID := range invalidUploadIDs {
|
|
||||||
if err := checkAzureUploadID(minio.GlobalContext, uploadID); err == nil {
|
|
||||||
t.Fatalf("%s: expected: <error>, got: <nil>", uploadID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validUploadIDs := []string{
|
|
||||||
"1234567890abcdef",
|
|
||||||
"1122334455667788",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, uploadID := range validUploadIDs {
|
|
||||||
if err := checkAzureUploadID(minio.GlobalContext, uploadID); err != nil {
|
|
||||||
t.Fatalf("%s: expected: <nil>, got: %s", uploadID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,17 +22,8 @@ import (
|
|||||||
// NAS
|
// NAS
|
||||||
_ "github.com/minio/minio/cmd/gateway/nas"
|
_ "github.com/minio/minio/cmd/gateway/nas"
|
||||||
|
|
||||||
// Azure
|
|
||||||
_ "github.com/minio/minio/cmd/gateway/azure"
|
|
||||||
|
|
||||||
// S3
|
// S3
|
||||||
_ "github.com/minio/minio/cmd/gateway/s3"
|
_ "github.com/minio/minio/cmd/gateway/s3"
|
||||||
|
|
||||||
// HDFS
|
|
||||||
_ "github.com/minio/minio/cmd/gateway/hdfs"
|
|
||||||
|
|
||||||
// GCS (use only if you must, GCS already supports S3 API)
|
|
||||||
_ "github.com/minio/minio/cmd/gateway/gcs"
|
|
||||||
// gateway functionality is frozen, no new gateways are being implemented
|
// gateway functionality is frozen, no new gateways are being implemented
|
||||||
// or considered for upstream inclusion at this point in time. if needed
|
// or considered for upstream inclusion at this point in time. if needed
|
||||||
// please keep a fork of the project.
|
// please keep a fork of the project.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,500 +0,0 @@
|
|||||||
/*
|
|
||||||
* MinIO Object Storage (c) 2021 MinIO, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package gcs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"cloud.google.com/go/storage"
|
|
||||||
"google.golang.org/api/googleapi"
|
|
||||||
|
|
||||||
miniogo "github.com/minio/minio-go/v7"
|
|
||||||
minio "github.com/minio/minio/cmd"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestToGCSPageToken(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
Name string
|
|
||||||
Token string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "A",
|
|
||||||
Token: "CgFB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "AAAAAAAAAA",
|
|
||||||
Token: "CgpBQUFBQUFBQUFB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
||||||
Token: "CmRBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
||||||
Token: "CpEDQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
||||||
Token: "CpIDQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
||||||
Token: "CpMDQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
||||||
Token: "CvQDQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
if toGCSPageToken(testCase.Name) != testCase.Token {
|
|
||||||
t.Errorf("Test %d: Expected %s, got %s", i+1, toGCSPageToken(testCase.Name), testCase.Token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIsValidGCSProjectIDFormat tests isValidGCSProjectIDFormat
|
|
||||||
func TestValidGCSProjectIDFormat(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
ProjectID string
|
|
||||||
Valid bool
|
|
||||||
}{
|
|
||||||
{"", false},
|
|
||||||
{"a", false},
|
|
||||||
{"Abc", false},
|
|
||||||
{"1bcd", false},
|
|
||||||
// 5 chars
|
|
||||||
{"abcdb", false},
|
|
||||||
// 6 chars
|
|
||||||
{"abcdbz", true},
|
|
||||||
// 30 chars
|
|
||||||
{"project-id-1-project-id-more-1", true},
|
|
||||||
// 31 chars
|
|
||||||
{"project-id-1-project-id-more-11", false},
|
|
||||||
{"storage.googleapis.com", false},
|
|
||||||
{"http://storage.googleapis.com", false},
|
|
||||||
{"http://localhost:9000", false},
|
|
||||||
{"project-id-1", true},
|
|
||||||
{"project-id-1988832", true},
|
|
||||||
{"projectid1414", true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
valid := isValidGCSProjectIDFormat(testCase.ProjectID)
|
|
||||||
if valid != testCase.Valid {
|
|
||||||
t.Errorf("Test %d: Expected %v, got %v", i+1, valid, testCase.Valid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for isGCSMarker.
|
|
||||||
func TestIsGCSMarker(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
marker string
|
|
||||||
expected bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
marker: "{minio}gcs123",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
marker: "{mini_no}tgcs123",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
marker: "{minioagainnotgcs123",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
marker: "obj1",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tc := range testCases {
|
|
||||||
if actual := isGCSMarker(tc.marker); actual != tc.expected {
|
|
||||||
t.Errorf("Test %d: marker is %s, expected %v but got %v",
|
|
||||||
i+1, tc.marker, tc.expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for gcsMultipartMetaName.
|
|
||||||
func TestGCSMultipartMetaName(t *testing.T) {
|
|
||||||
uploadID := "a"
|
|
||||||
expected := path.Join(gcsMinioMultipartPathV1, uploadID, gcsMinioMultipartMeta)
|
|
||||||
got := gcsMultipartMetaName(uploadID)
|
|
||||||
if expected != got {
|
|
||||||
t.Errorf("expected: %s, got: %s", expected, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for gcsMultipartDataName.
|
|
||||||
func TestGCSMultipartDataName(t *testing.T) {
|
|
||||||
var (
|
|
||||||
uploadID = "a"
|
|
||||||
etag = "b"
|
|
||||||
partNumber = 1
|
|
||||||
)
|
|
||||||
expected := path.Join(gcsMinioMultipartPathV1, uploadID, fmt.Sprintf("%05d.%s", partNumber, etag))
|
|
||||||
got := gcsMultipartDataName(uploadID, partNumber, etag)
|
|
||||||
if expected != got {
|
|
||||||
t.Errorf("expected: %s, got: %s", expected, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFromMinioClientListBucketResultToV2Info(t *testing.T) {
|
|
||||||
listBucketResult := miniogo.ListBucketResult{
|
|
||||||
IsTruncated: false,
|
|
||||||
Marker: "testMarker",
|
|
||||||
NextMarker: "testMarker2",
|
|
||||||
CommonPrefixes: []miniogo.CommonPrefix{{Prefix: "one"}, {Prefix: "two"}},
|
|
||||||
Contents: []miniogo.ObjectInfo{{Key: "testobj", ContentType: ""}},
|
|
||||||
}
|
|
||||||
|
|
||||||
listBucketV2Info := minio.ListObjectsV2Info{
|
|
||||||
Prefixes: []string{"one", "two"},
|
|
||||||
Objects: []minio.ObjectInfo{{Name: "testobj", Bucket: "testbucket", UserDefined: map[string]string{"Content-Type": ""}}},
|
|
||||||
IsTruncated: false,
|
|
||||||
ContinuationToken: "testMarker",
|
|
||||||
NextContinuationToken: "testMarker2",
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := minio.FromMinioClientListBucketResultToV2Info("testbucket", listBucketResult); !reflect.DeepEqual(got, listBucketV2Info) {
|
|
||||||
t.Errorf("fromMinioClientListBucketResultToV2Info() = %v, want %v", got, listBucketV2Info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for gcsParseProjectID
|
|
||||||
func TestGCSParseProjectID(t *testing.T) {
|
|
||||||
f, err := ioutil.TempFile("", "TestGCSParseProjectID-*")
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
|
|
||||||
contents := `
|
|
||||||
{
|
|
||||||
"type": "service_account",
|
|
||||||
"project_id": "miniotesting"
|
|
||||||
}
|
|
||||||
`
|
|
||||||
f.WriteString(contents)
|
|
||||||
f.Close()
|
|
||||||
projectID, err := gcsParseProjectID(f.Name())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if projectID != "miniotesting" {
|
|
||||||
t.Errorf(`Expected projectID value to be "miniotesting"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = gcsParseProjectID("non-existent"); err == nil {
|
|
||||||
t.Errorf(`Expected to fail but succeeded reading "non-existent"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
contents = `
|
|
||||||
{
|
|
||||||
"type": "service_account",
|
|
||||||
"project_id": "miniotesting"
|
|
||||||
},}
|
|
||||||
`
|
|
||||||
f, err = ioutil.TempFile("", "TestGCSParseProjectID-*")
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
f.WriteString(contents)
|
|
||||||
f.Close()
|
|
||||||
if _, err := gcsParseProjectID(f.Name()); err == nil {
|
|
||||||
t.Errorf(`Expected to fail reading corrupted credentials file`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGCSToObjectError(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
params []string
|
|
||||||
gcsErr error
|
|
||||||
expectedErr error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
[]string{}, nil, nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{}, fmt.Errorf("Not *Error"), fmt.Errorf("Not *Error"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket"},
|
|
||||||
fmt.Errorf("storage: bucket doesn't exist"),
|
|
||||||
minio.BucketNotFound{
|
|
||||||
Bucket: "bucket",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket", "object"},
|
|
||||||
fmt.Errorf("storage: object doesn't exist"),
|
|
||||||
minio.ObjectNotFound{
|
|
||||||
Bucket: "bucket",
|
|
||||||
Object: "object",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket", "object", "uploadID"},
|
|
||||||
fmt.Errorf("storage: object doesn't exist"),
|
|
||||||
minio.InvalidUploadID{
|
|
||||||
UploadID: "uploadID",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{},
|
|
||||||
fmt.Errorf("Unknown error"),
|
|
||||||
fmt.Errorf("Unknown error"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket", "object"},
|
|
||||||
&googleapi.Error{
|
|
||||||
Message: "No list of errors",
|
|
||||||
},
|
|
||||||
&googleapi.Error{
|
|
||||||
Message: "No list of errors",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket", "object"},
|
|
||||||
&googleapi.Error{
|
|
||||||
Errors: []googleapi.ErrorItem{{
|
|
||||||
Reason: "conflict",
|
|
||||||
Message: "You already own this bucket. Please select another name.",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
minio.BucketAlreadyOwnedByYou{
|
|
||||||
Bucket: "bucket",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket", "object"},
|
|
||||||
&googleapi.Error{
|
|
||||||
Errors: []googleapi.ErrorItem{{
|
|
||||||
Reason: "conflict",
|
|
||||||
Message: "Sorry, that name is not available. Please try a different one.",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
minio.BucketAlreadyExists{
|
|
||||||
Bucket: "bucket",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket", "object"},
|
|
||||||
&googleapi.Error{
|
|
||||||
Errors: []googleapi.ErrorItem{{
|
|
||||||
Reason: "conflict",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
minio.BucketNotEmpty{Bucket: "bucket"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket"},
|
|
||||||
&googleapi.Error{
|
|
||||||
Errors: []googleapi.ErrorItem{{
|
|
||||||
Reason: "notFound",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
minio.BucketNotFound{
|
|
||||||
Bucket: "bucket",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket", "object"},
|
|
||||||
&googleapi.Error{
|
|
||||||
Errors: []googleapi.ErrorItem{{
|
|
||||||
Reason: "notFound",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
minio.ObjectNotFound{
|
|
||||||
Bucket: "bucket",
|
|
||||||
Object: "object",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket"},
|
|
||||||
&googleapi.Error{
|
|
||||||
Errors: []googleapi.ErrorItem{{
|
|
||||||
Reason: "invalid",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
minio.BucketNameInvalid{
|
|
||||||
Bucket: "bucket",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket", "object"},
|
|
||||||
&googleapi.Error{
|
|
||||||
Errors: []googleapi.ErrorItem{{
|
|
||||||
Reason: "forbidden",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
minio.PrefixAccessDenied{
|
|
||||||
Bucket: "bucket",
|
|
||||||
Object: "object",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket", "object"},
|
|
||||||
&googleapi.Error{
|
|
||||||
Errors: []googleapi.ErrorItem{{
|
|
||||||
Reason: "keyInvalid",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
minio.PrefixAccessDenied{
|
|
||||||
Bucket: "bucket",
|
|
||||||
Object: "object",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]string{"bucket", "object"},
|
|
||||||
&googleapi.Error{
|
|
||||||
Errors: []googleapi.ErrorItem{{
|
|
||||||
Reason: "required",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
minio.PrefixAccessDenied{
|
|
||||||
Bucket: "bucket",
|
|
||||||
Object: "object",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
actualErr := gcsToObjectError(testCase.gcsErr, testCase.params...)
|
|
||||||
if actualErr != nil {
|
|
||||||
if actualErr.Error() != testCase.expectedErr.Error() {
|
|
||||||
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.expectedErr, actualErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestS3MetaToGCSAttributes(t *testing.T) {
|
|
||||||
headers := map[string]string{
|
|
||||||
"accept-encoding": "gzip",
|
|
||||||
"content-encoding": "gzip",
|
|
||||||
"cache-control": "age: 3600",
|
|
||||||
"content-disposition": "dummy",
|
|
||||||
"content-type": "application/javascript",
|
|
||||||
"Content-Language": "en",
|
|
||||||
"X-Amz-Meta-Hdr": "value",
|
|
||||||
"X-Amz-Meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
|
||||||
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
|
||||||
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
|
||||||
}
|
|
||||||
// Only X-Amz-Meta- prefixed entries will be returned in
|
|
||||||
// Metadata (without the prefix!)
|
|
||||||
expectedHeaders := map[string]string{
|
|
||||||
"x-goog-meta-Hdr": "value",
|
|
||||||
"x-goog-meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
|
||||||
"x-goog-meta-X-Amz-Matdesc": "{}",
|
|
||||||
"x-goog-meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs := storage.ObjectAttrs{}
|
|
||||||
applyMetadataToGCSAttrs(headers, &attrs)
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(attrs.Metadata, expectedHeaders) {
|
|
||||||
t.Fatalf("Test failed, expected %#v, got %#v", expectedHeaders, attrs.Metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
if attrs.CacheControl != headers["cache-control"] {
|
|
||||||
t.Fatalf("Test failed with Cache-Control mistmatch, expected %s, got %s", headers["cache-control"], attrs.CacheControl)
|
|
||||||
}
|
|
||||||
if attrs.ContentDisposition != headers["content-disposition"] {
|
|
||||||
t.Fatalf("Test failed with Content-Disposition mistmatch, expected %s, got %s", headers["content-disposition"], attrs.ContentDisposition)
|
|
||||||
}
|
|
||||||
if attrs.ContentEncoding != headers["content-encoding"] {
|
|
||||||
t.Fatalf("Test failed with Content-Encoding mistmatch, expected %s, got %s", headers["content-encoding"], attrs.ContentEncoding)
|
|
||||||
}
|
|
||||||
if attrs.ContentLanguage != headers["Content-Language"] {
|
|
||||||
t.Fatalf("Test failed with Content-Language mistmatch, expected %s, got %s", headers["Content-Language"], attrs.ContentLanguage)
|
|
||||||
}
|
|
||||||
if attrs.ContentType != headers["content-type"] {
|
|
||||||
t.Fatalf("Test failed with Content-Type mistmatch, expected %s, got %s", headers["content-type"], attrs.ContentType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGCSAttrsToObjectInfo(t *testing.T) {
|
|
||||||
metadata := map[string]string{
|
|
||||||
"x-goog-meta-Hdr": "value",
|
|
||||||
"x-goog-meta-x_amz_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
|
||||||
"x-goog-meta-x-amz-matdesc": "{}",
|
|
||||||
"x-goog-meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
|
||||||
}
|
|
||||||
expectedMeta := map[string]string{
|
|
||||||
"X-Amz-Meta-Hdr": "value",
|
|
||||||
"X-Amz-Meta-X_amz_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
|
||||||
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
|
||||||
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
|
||||||
"Cache-Control": "max-age: 3600",
|
|
||||||
"Content-Disposition": "dummy",
|
|
||||||
"Content-Encoding": "gzip",
|
|
||||||
"Content-Language": "en",
|
|
||||||
"Content-Type": "application/javascript",
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs := storage.ObjectAttrs{
|
|
||||||
Name: "test-obj",
|
|
||||||
Bucket: "test-bucket",
|
|
||||||
Updated: time.Now(),
|
|
||||||
Size: 123,
|
|
||||||
CRC32C: 45312398,
|
|
||||||
CacheControl: "max-age: 3600",
|
|
||||||
ContentDisposition: "dummy",
|
|
||||||
ContentEncoding: "gzip",
|
|
||||||
ContentLanguage: "en",
|
|
||||||
ContentType: "application/javascript",
|
|
||||||
Metadata: metadata,
|
|
||||||
}
|
|
||||||
expectedETag := minio.ToS3ETag(fmt.Sprintf("%d", attrs.CRC32C))
|
|
||||||
|
|
||||||
objInfo := fromGCSAttrsToObjectInfo(&attrs)
|
|
||||||
if !reflect.DeepEqual(objInfo.UserDefined, expectedMeta) {
|
|
||||||
t.Fatalf("Test failed, expected %#v, got %#v", expectedMeta, objInfo.UserDefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
if objInfo.Name != attrs.Name {
|
|
||||||
t.Fatalf("Test failed with Name mistmatch, expected %s, got %s", attrs.Name, objInfo.Name)
|
|
||||||
}
|
|
||||||
if objInfo.Bucket != attrs.Bucket {
|
|
||||||
t.Fatalf("Test failed with Bucket mistmatch, expected %s, got %s", attrs.Bucket, objInfo.Bucket)
|
|
||||||
}
|
|
||||||
if !objInfo.ModTime.Equal(attrs.Updated) {
|
|
||||||
t.Fatalf("Test failed with ModTime mistmatch, expected %s, got %s", attrs.Updated, objInfo.ModTime)
|
|
||||||
}
|
|
||||||
if objInfo.Size != attrs.Size {
|
|
||||||
t.Fatalf("Test failed with Size mistmatch, expected %d, got %d", attrs.Size, objInfo.Size)
|
|
||||||
}
|
|
||||||
if objInfo.ETag != expectedETag {
|
|
||||||
t.Fatalf("Test failed with ETag mistmatch, expected %s, got %s", expectedETag, objInfo.ETag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* MinIO Object Storage (c) 2021 MinIO, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package hdfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7/pkg/s3utils"
|
|
||||||
minio "github.com/minio/minio/cmd"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Minio meta bucket.
|
|
||||||
minioMetaBucket = ".minio.sys"
|
|
||||||
|
|
||||||
// Minio Tmp meta prefix.
|
|
||||||
minioMetaTmpBucket = minioMetaBucket + "/tmp"
|
|
||||||
|
|
||||||
// Minio reserved bucket name.
|
|
||||||
minioReservedBucket = "minio"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ignores all reserved bucket names or invalid bucket names.
|
|
||||||
func isReservedOrInvalidBucket(bucketEntry string, strict bool) bool {
|
|
||||||
bucketEntry = strings.TrimSuffix(bucketEntry, minio.SlashSeparator)
|
|
||||||
if strict {
|
|
||||||
if err := s3utils.CheckValidBucketNameStrict(bucketEntry); err != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := s3utils.CheckValidBucketName(bucketEntry); err != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isMinioMetaBucket(bucketEntry) || isMinioReservedBucket(bucketEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if input bucket is a reserved minio meta bucket '.minio.sys'.
|
|
||||||
func isMinioMetaBucket(bucketName string) bool {
|
|
||||||
return bucketName == minioMetaBucket
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if input bucket is a reserved minio bucket 'minio'.
|
|
||||||
func isMinioReservedBucket(bucketName string) bool {
|
|
||||||
return bucketName == minioReservedBucket
|
|
||||||
}
|
|
||||||
|
|
||||||
// byBucketName is a collection satisfying sort.Interface.
|
|
||||||
type byBucketName []minio.BucketInfo
|
|
||||||
|
|
||||||
func (d byBucketName) Len() int { return len(d) }
|
|
||||||
func (d byBucketName) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
|
|
||||||
func (d byBucketName) Less(i, j int) bool { return d[i].Name < d[j].Name }
|
|
||||||
@@ -1,885 +0,0 @@
|
|||||||
/*
|
|
||||||
* MinIO Object Storage (c) 2021 MinIO, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package hdfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/user"
|
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/colinmarc/hdfs/v2"
|
|
||||||
"github.com/colinmarc/hdfs/v2/hadoopconf"
|
|
||||||
krb "github.com/jcmturner/gokrb5/v8/client"
|
|
||||||
"github.com/jcmturner/gokrb5/v8/config"
|
|
||||||
"github.com/jcmturner/gokrb5/v8/credentials"
|
|
||||||
"github.com/jcmturner/gokrb5/v8/keytab"
|
|
||||||
"github.com/minio/cli"
|
|
||||||
"github.com/minio/madmin-go"
|
|
||||||
"github.com/minio/minio-go/v7/pkg/s3utils"
|
|
||||||
minio "github.com/minio/minio/cmd"
|
|
||||||
"github.com/minio/minio/internal/logger"
|
|
||||||
"github.com/minio/pkg/env"
|
|
||||||
xnet "github.com/minio/pkg/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
hdfsSeparator = minio.SlashSeparator
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
const hdfsGatewayTemplate = `NAME:
|
|
||||||
{{.HelpName}} - {{.Usage}}
|
|
||||||
|
|
||||||
USAGE:
|
|
||||||
{{.HelpName}} {{if .VisibleFlags}}[FLAGS]{{end}} HDFS-NAMENODE [HDFS-NAMENODE...]
|
|
||||||
{{if .VisibleFlags}}
|
|
||||||
FLAGS:
|
|
||||||
{{range .VisibleFlags}}{{.}}
|
|
||||||
{{end}}{{end}}
|
|
||||||
HDFS-NAMENODE:
|
|
||||||
HDFS namenode URI
|
|
||||||
|
|
||||||
EXAMPLES:
|
|
||||||
1. Start minio gateway server for HDFS backend
|
|
||||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ROOT_USER{{.AssignmentOperator}}accesskey
|
|
||||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ROOT_PASSWORD{{.AssignmentOperator}}secretkey
|
|
||||||
{{.Prompt}} {{.HelpName}} hdfs://namenode:8200
|
|
||||||
|
|
||||||
2. Start minio gateway server for HDFS with edge caching enabled
|
|
||||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ROOT_USER{{.AssignmentOperator}}accesskey
|
|
||||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ROOT_PASSWORD{{.AssignmentOperator}}secretkey
|
|
||||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_DRIVES{{.AssignmentOperator}}"/mnt/drive1,/mnt/drive2,/mnt/drive3,/mnt/drive4"
|
|
||||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXCLUDE{{.AssignmentOperator}}"bucket1/*,*.png"
|
|
||||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_QUOTA{{.AssignmentOperator}}90
|
|
||||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_AFTER{{.AssignmentOperator}}3
|
|
||||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_WATERMARK_LOW{{.AssignmentOperator}}75
|
|
||||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_WATERMARK_HIGH{{.AssignmentOperator}}85
|
|
||||||
{{.Prompt}} {{.HelpName}} hdfs://namenode:8200
|
|
||||||
`
|
|
||||||
|
|
||||||
minio.RegisterGatewayCommand(cli.Command{
|
|
||||||
Name: minio.HDFSBackendGateway,
|
|
||||||
Usage: "Hadoop Distributed File System (HDFS)",
|
|
||||||
Action: hdfsGatewayMain,
|
|
||||||
CustomHelpTemplate: hdfsGatewayTemplate,
|
|
||||||
HideHelpCommand: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler for 'minio gateway hdfs' command line.
|
|
||||||
func hdfsGatewayMain(ctx *cli.Context) {
|
|
||||||
// Validate gateway arguments.
|
|
||||||
if ctx.Args().First() == "help" {
|
|
||||||
cli.ShowCommandHelpAndExit(ctx, minio.HDFSBackendGateway, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
minio.StartGateway(ctx, &HDFS{args: ctx.Args()})
|
|
||||||
}
|
|
||||||
|
|
||||||
// HDFS implements Gateway.
|
|
||||||
type HDFS struct {
|
|
||||||
args []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name implements Gateway interface.
|
|
||||||
func (g *HDFS) Name() string {
|
|
||||||
return minio.HDFSBackendGateway
|
|
||||||
}
|
|
||||||
|
|
||||||
func getKerberosClient() (*krb.Client, error) {
|
|
||||||
cfg, err := config.Load(env.Get("KRB5_CONFIG", "/etc/krb5.conf"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
keytabPath := env.Get("KRB5KEYTAB", "")
|
|
||||||
if keytabPath != "" {
|
|
||||||
kt, err := keytab.Load(keytabPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
username := env.Get("KRB5USERNAME", "")
|
|
||||||
realm := env.Get("KRB5REALM", "")
|
|
||||||
if username == "" || realm == "" {
|
|
||||||
return nil, errors.New("empty KRB5USERNAME or KRB5REALM")
|
|
||||||
}
|
|
||||||
|
|
||||||
return krb.NewWithKeytab(username, realm, kt, cfg), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the ccache location from the environment, falling back to the default location.
|
|
||||||
ccachePath := env.Get("KRB5CCNAME", fmt.Sprintf("/tmp/krb5cc_%s", u.Uid))
|
|
||||||
if strings.Contains(ccachePath, ":") {
|
|
||||||
if strings.HasPrefix(ccachePath, "FILE:") {
|
|
||||||
ccachePath = strings.TrimPrefix(ccachePath, "FILE:")
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("unable to use kerberos ccache: %s", ccachePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ccache, err := credentials.LoadCCache(ccachePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return krb.NewFromCCache(ccache, cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGatewayLayer returns hdfs gatewaylayer.
|
|
||||||
func (g *HDFS) NewGatewayLayer(creds madmin.Credentials) (minio.ObjectLayer, error) {
|
|
||||||
dialFunc := (&net.Dialer{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
DualStack: true,
|
|
||||||
}).DialContext
|
|
||||||
|
|
||||||
hconfig, err := hadoopconf.LoadFromEnvironment()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := hdfs.ClientOptionsFromConf(hconfig)
|
|
||||||
opts.NamenodeDialFunc = dialFunc
|
|
||||||
opts.DatanodeDialFunc = dialFunc
|
|
||||||
|
|
||||||
// Not addresses found, load it from command line.
|
|
||||||
var commonPath string
|
|
||||||
if len(opts.Addresses) == 0 {
|
|
||||||
var addresses []string
|
|
||||||
for _, s := range g.args {
|
|
||||||
u, err := xnet.ParseURL(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if u.Scheme != "hdfs" {
|
|
||||||
return nil, fmt.Errorf("unsupported scheme %s, only supports hdfs://", u)
|
|
||||||
}
|
|
||||||
if commonPath != "" && commonPath != u.Path {
|
|
||||||
return nil, fmt.Errorf("all namenode paths should be same %s", g.args)
|
|
||||||
}
|
|
||||||
if commonPath == "" {
|
|
||||||
commonPath = u.Path
|
|
||||||
}
|
|
||||||
addresses = append(addresses, u.Host)
|
|
||||||
}
|
|
||||||
opts.Addresses = addresses
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to lookup local user: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.KerberosClient != nil {
|
|
||||||
opts.KerberosClient, err = getKerberosClient()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to initialize kerberos client: %s", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
opts.User = env.Get("HADOOP_USER_NAME", u.Username)
|
|
||||||
}
|
|
||||||
|
|
||||||
clnt, err := hdfs.NewClient(opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to initialize hdfsClient: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = clnt.MkdirAll(minio.PathJoin(commonPath, hdfsSeparator, minioMetaTmpBucket), os.FileMode(0o755)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &hdfsObjects{clnt: clnt, subPath: commonPath, listPool: minio.NewTreeWalkPool(time.Minute * 30)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) Shutdown(ctx context.Context) error {
|
|
||||||
return n.clnt.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) LocalStorageInfo(ctx context.Context) (si minio.StorageInfo, errs []error) {
|
|
||||||
return n.StorageInfo(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) StorageInfo(ctx context.Context) (si minio.StorageInfo, errs []error) {
|
|
||||||
fsInfo, err := n.clnt.StatFs()
|
|
||||||
if err != nil {
|
|
||||||
return minio.StorageInfo{}, []error{err}
|
|
||||||
}
|
|
||||||
si.Disks = []madmin.Disk{{
|
|
||||||
UsedSpace: fsInfo.Used,
|
|
||||||
}}
|
|
||||||
si.Backend.Type = madmin.Gateway
|
|
||||||
si.Backend.GatewayOnline = true
|
|
||||||
return si, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// hdfsObjects implements gateway for Minio and S3 compatible object storage servers.
|
|
||||||
type hdfsObjects struct {
|
|
||||||
minio.GatewayUnsupported
|
|
||||||
clnt *hdfs.Client
|
|
||||||
subPath string
|
|
||||||
listPool *minio.TreeWalkPool
|
|
||||||
}
|
|
||||||
|
|
||||||
func hdfsToObjectErr(ctx context.Context, err error, params ...string) error {
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
bucket := ""
|
|
||||||
object := ""
|
|
||||||
uploadID := ""
|
|
||||||
switch len(params) {
|
|
||||||
case 3:
|
|
||||||
uploadID = params[2]
|
|
||||||
fallthrough
|
|
||||||
case 2:
|
|
||||||
object = params[1]
|
|
||||||
fallthrough
|
|
||||||
case 1:
|
|
||||||
bucket = params[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case os.IsNotExist(err):
|
|
||||||
if uploadID != "" {
|
|
||||||
return minio.InvalidUploadID{
|
|
||||||
UploadID: uploadID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if object != "" {
|
|
||||||
return minio.ObjectNotFound{Bucket: bucket, Object: object}
|
|
||||||
}
|
|
||||||
return minio.BucketNotFound{Bucket: bucket}
|
|
||||||
case os.IsExist(err):
|
|
||||||
if object != "" {
|
|
||||||
return minio.PrefixAccessDenied{Bucket: bucket, Object: object}
|
|
||||||
}
|
|
||||||
return minio.BucketAlreadyOwnedByYou{Bucket: bucket}
|
|
||||||
case errors.Is(err, syscall.ENOTEMPTY):
|
|
||||||
if object != "" {
|
|
||||||
return minio.PrefixAccessDenied{Bucket: bucket, Object: object}
|
|
||||||
}
|
|
||||||
return minio.BucketNotEmpty{Bucket: bucket}
|
|
||||||
default:
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// hdfsIsValidBucketName verifies whether a bucket name is valid.
|
|
||||||
func hdfsIsValidBucketName(bucket string) bool {
|
|
||||||
return s3utils.CheckValidBucketNameStrict(bucket) == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) hdfsPathJoin(args ...string) string {
|
|
||||||
return minio.PathJoin(append([]string{n.subPath, hdfsSeparator}, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) DeleteBucket(ctx context.Context, bucket string, opts minio.DeleteBucketOptions) error {
|
|
||||||
if !hdfsIsValidBucketName(bucket) {
|
|
||||||
return minio.BucketNameInvalid{Bucket: bucket}
|
|
||||||
}
|
|
||||||
if opts.Force {
|
|
||||||
return hdfsToObjectErr(ctx, n.clnt.RemoveAll(n.hdfsPathJoin(bucket)), bucket)
|
|
||||||
}
|
|
||||||
return hdfsToObjectErr(ctx, n.clnt.Remove(n.hdfsPathJoin(bucket)), bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) MakeBucketWithLocation(ctx context.Context, bucket string, opts minio.BucketOptions) error {
|
|
||||||
if opts.LockEnabled || opts.VersioningEnabled {
|
|
||||||
return minio.NotImplemented{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hdfsIsValidBucketName(bucket) {
|
|
||||||
return minio.BucketNameInvalid{Bucket: bucket}
|
|
||||||
}
|
|
||||||
return hdfsToObjectErr(ctx, n.clnt.Mkdir(n.hdfsPathJoin(bucket), os.FileMode(0o755)), bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.BucketInfo, err error) {
|
|
||||||
fi, err := n.clnt.Stat(n.hdfsPathJoin(bucket))
|
|
||||||
if err != nil {
|
|
||||||
return bi, hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
// As hdfs.Stat() doesn't carry anything other than ModTime(), use ModTime() as CreatedTime.
|
|
||||||
return minio.BucketInfo{
|
|
||||||
Name: bucket,
|
|
||||||
Created: fi.ModTime(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketInfo, err error) {
|
|
||||||
entries, err := n.clnt.ReadDir(n.hdfsPathJoin())
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return nil, hdfsToObjectErr(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
// Ignore all reserved bucket names and invalid bucket names.
|
|
||||||
if isReservedOrInvalidBucket(entry.Name(), false) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
buckets = append(buckets, minio.BucketInfo{
|
|
||||||
Name: entry.Name(),
|
|
||||||
// As hdfs.Stat() doesnt carry CreatedTime, use ModTime() as CreatedTime.
|
|
||||||
Created: entry.ModTime(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort bucket infos by bucket name.
|
|
||||||
sort.Sort(byBucketName(buckets))
|
|
||||||
return buckets, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) isLeafDir(bucket, leafPath string) bool {
|
|
||||||
return n.isObjectDir(context.Background(), bucket, leafPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) isLeaf(bucket, leafPath string) bool {
|
|
||||||
return !strings.HasSuffix(leafPath, hdfsSeparator)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) listDirFactory() minio.ListDirFunc {
|
|
||||||
// listDir - lists all the entries at a given prefix and given entry in the prefix.
|
|
||||||
listDir := func(bucket, prefixDir, prefixEntry string) (emptyDir bool, entries []string, delayIsLeaf bool) {
|
|
||||||
f, err := n.clnt.Open(n.hdfsPathJoin(bucket, prefixDir))
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
logger.LogIf(minio.GlobalContext, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
fis, err := f.Readdir(0)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(minio.GlobalContext, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(fis) == 0 {
|
|
||||||
return true, nil, false
|
|
||||||
}
|
|
||||||
for _, fi := range fis {
|
|
||||||
if fi.IsDir() {
|
|
||||||
entries = append(entries, fi.Name()+hdfsSeparator)
|
|
||||||
} else {
|
|
||||||
entries = append(entries, fi.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entries, delayIsLeaf = minio.FilterListEntries(bucket, prefixDir, entries, prefixEntry, n.isLeaf)
|
|
||||||
return false, entries, delayIsLeaf
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return list factory instance.
|
|
||||||
return listDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListObjects lists all blobs in HDFS bucket filtered by prefix.
|
|
||||||
func (n *hdfsObjects) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (loi minio.ListObjectsInfo, err error) {
|
|
||||||
var mutex sync.Mutex
|
|
||||||
fileInfos := make(map[string]os.FileInfo)
|
|
||||||
targetPath := n.hdfsPathJoin(bucket, prefix)
|
|
||||||
|
|
||||||
var targetFileInfo os.FileInfo
|
|
||||||
|
|
||||||
if targetFileInfo, err = n.populateDirectoryListing(targetPath, fileInfos); err != nil {
|
|
||||||
return loi, hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user is trying to list a single file, bypass the entire directory-walking code below
|
|
||||||
// and just return the single file's information.
|
|
||||||
if !targetFileInfo.IsDir() {
|
|
||||||
return minio.ListObjectsInfo{
|
|
||||||
IsTruncated: false,
|
|
||||||
NextMarker: "",
|
|
||||||
Objects: []minio.ObjectInfo{
|
|
||||||
fileInfoToObjectInfo(bucket, prefix, targetFileInfo),
|
|
||||||
},
|
|
||||||
Prefixes: []string{},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
getObjectInfo := func(ctx context.Context, bucket, entry string) (minio.ObjectInfo, error) {
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
|
|
||||||
filePath := path.Clean(n.hdfsPathJoin(bucket, entry))
|
|
||||||
fi, ok := fileInfos[filePath]
|
|
||||||
|
|
||||||
// If the file info is not known, this may be a recursive listing and filePath is a
|
|
||||||
// child of a sub-directory. In this case, obtain that sub-directory's listing.
|
|
||||||
if !ok {
|
|
||||||
parentPath := path.Dir(filePath)
|
|
||||||
|
|
||||||
if _, err := n.populateDirectoryListing(parentPath, fileInfos); err != nil {
|
|
||||||
return minio.ObjectInfo{}, hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
fi, ok = fileInfos[filePath]
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
err = fmt.Errorf("could not get FileInfo for path '%s'", filePath)
|
|
||||||
return minio.ObjectInfo{}, hdfsToObjectErr(ctx, err, bucket, entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
objectInfo := fileInfoToObjectInfo(bucket, entry, fi)
|
|
||||||
|
|
||||||
delete(fileInfos, filePath)
|
|
||||||
|
|
||||||
return objectInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return minio.ListObjects(ctx, n, bucket, prefix, marker, delimiter, maxKeys, n.listPool, n.listDirFactory(), n.isLeaf, n.isLeafDir, getObjectInfo, getObjectInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileInfoToObjectInfo(bucket string, entry string, fi os.FileInfo) minio.ObjectInfo {
|
|
||||||
return minio.ObjectInfo{
|
|
||||||
Bucket: bucket,
|
|
||||||
Name: entry,
|
|
||||||
ModTime: fi.ModTime(),
|
|
||||||
Size: fi.Size(),
|
|
||||||
IsDir: fi.IsDir(),
|
|
||||||
AccTime: fi.(*hdfs.FileInfo).AccessTime(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lists a path's direct, first-level entries and populates them in the `fileInfos` cache which maps
|
|
||||||
// a path entry to an `os.FileInfo`. It also saves the listed path's `os.FileInfo` in the cache.
|
|
||||||
func (n *hdfsObjects) populateDirectoryListing(filePath string, fileInfos map[string]os.FileInfo) (os.FileInfo, error) {
|
|
||||||
dirReader, err := n.clnt.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dirStat := dirReader.Stat()
|
|
||||||
key := path.Clean(filePath)
|
|
||||||
|
|
||||||
if !dirStat.IsDir() {
|
|
||||||
return dirStat, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInfos[key] = dirStat
|
|
||||||
infos, err := dirReader.Readdir(0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fileInfo := range infos {
|
|
||||||
filePath := minio.PathJoin(filePath, fileInfo.Name())
|
|
||||||
fileInfos[filePath] = fileInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
return dirStat, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteObject deletes a file path if its empty. If it's successfully deleted,
|
|
||||||
// it will recursively move up the tree, deleting empty parent directories
|
|
||||||
// until it finds one with files in it. Returns nil for a non-empty directory.
|
|
||||||
func (n *hdfsObjects) deleteObject(basePath, deletePath string) error {
|
|
||||||
if basePath == deletePath {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to remove path.
|
|
||||||
if err := n.clnt.Remove(deletePath); err != nil {
|
|
||||||
if errors.Is(err, syscall.ENOTEMPTY) {
|
|
||||||
// Ignore errors if the directory is not empty. The server relies on
|
|
||||||
// this functionality, and sometimes uses recursion that should not
|
|
||||||
// error on parent directories.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trailing slash is removed when found to ensure
|
|
||||||
// slashpath.Dir() to work as intended.
|
|
||||||
deletePath = strings.TrimSuffix(deletePath, hdfsSeparator)
|
|
||||||
deletePath = path.Dir(deletePath)
|
|
||||||
|
|
||||||
// Delete parent directory. Errors for parent directories shouldn't trickle down.
|
|
||||||
n.deleteObject(basePath, deletePath)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListObjectsV2 lists all blobs in HDFS bucket filtered by prefix
|
|
||||||
func (n *hdfsObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int,
|
|
||||||
fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, err error) {
|
|
||||||
// fetchOwner is not supported and unused.
|
|
||||||
marker := continuationToken
|
|
||||||
if marker == "" {
|
|
||||||
marker = startAfter
|
|
||||||
}
|
|
||||||
resultV1, err := n.ListObjects(ctx, bucket, prefix, marker, delimiter, maxKeys)
|
|
||||||
if err != nil {
|
|
||||||
return loi, err
|
|
||||||
}
|
|
||||||
return minio.ListObjectsV2Info{
|
|
||||||
Objects: resultV1.Objects,
|
|
||||||
Prefixes: resultV1.Prefixes,
|
|
||||||
ContinuationToken: continuationToken,
|
|
||||||
NextContinuationToken: resultV1.NextMarker,
|
|
||||||
IsTruncated: resultV1.IsTruncated,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) DeleteObject(ctx context.Context, bucket, object string, opts minio.ObjectOptions) (minio.ObjectInfo, error) {
|
|
||||||
err := hdfsToObjectErr(ctx, n.deleteObject(n.hdfsPathJoin(bucket), n.hdfsPathJoin(bucket, object)), bucket, object)
|
|
||||||
return minio.ObjectInfo{
|
|
||||||
Bucket: bucket,
|
|
||||||
Name: object,
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) DeleteObjects(ctx context.Context, bucket string, objects []minio.ObjectToDelete, opts minio.ObjectOptions) ([]minio.DeletedObject, []error) {
|
|
||||||
errs := make([]error, len(objects))
|
|
||||||
dobjects := make([]minio.DeletedObject, len(objects))
|
|
||||||
for idx, object := range objects {
|
|
||||||
_, errs[idx] = n.DeleteObject(ctx, bucket, object.ObjectName, opts)
|
|
||||||
if errs[idx] == nil {
|
|
||||||
dobjects[idx] = minio.DeletedObject{
|
|
||||||
ObjectName: object.ObjectName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dobjects, errs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, opts minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
|
||||||
objInfo, err := n.GetObjectInfo(ctx, bucket, object, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var startOffset, length int64
|
|
||||||
startOffset, length, err = rs.GetOffsetLength(objInfo.Size)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pr, pw := io.Pipe()
|
|
||||||
go func() {
|
|
||||||
nerr := n.getObject(ctx, bucket, object, startOffset, length, pw, objInfo.ETag, opts)
|
|
||||||
pw.CloseWithError(nerr)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Setup cleanup function to cause the above go-routine to
|
|
||||||
// exit in case of partial read
|
|
||||||
pipeCloser := func() { pr.Close() }
|
|
||||||
return minio.NewGetObjectReaderFromReader(pr, objInfo, opts, pipeCloser)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (minio.ObjectInfo, error) {
|
|
||||||
cpSrcDstSame := minio.IsStringEqual(n.hdfsPathJoin(srcBucket, srcObject), n.hdfsPathJoin(dstBucket, dstObject))
|
|
||||||
if cpSrcDstSame {
|
|
||||||
return n.GetObjectInfo(ctx, srcBucket, srcObject, minio.ObjectOptions{})
|
|
||||||
}
|
|
||||||
|
|
||||||
return n.PutObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, minio.ObjectOptions{
|
|
||||||
ServerSideEncryption: dstOpts.ServerSideEncryption,
|
|
||||||
UserDefined: srcInfo.UserDefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) getObject(ctx context.Context, bucket, key string, startOffset, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
|
||||||
if _, err := n.clnt.Stat(n.hdfsPathJoin(bucket)); err != nil {
|
|
||||||
return hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
rd, err := n.clnt.Open(n.hdfsPathJoin(bucket, key))
|
|
||||||
if err != nil {
|
|
||||||
return hdfsToObjectErr(ctx, err, bucket, key)
|
|
||||||
}
|
|
||||||
defer rd.Close()
|
|
||||||
_, err = io.Copy(writer, io.NewSectionReader(rd, startOffset, length))
|
|
||||||
if err == io.ErrClosedPipe {
|
|
||||||
// hdfs library doesn't send EOF correctly, so io.Copy attempts
|
|
||||||
// to write which returns io.ErrClosedPipe - just ignore
|
|
||||||
// this for now.
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return hdfsToObjectErr(ctx, err, bucket, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) isObjectDir(ctx context.Context, bucket, object string) bool {
|
|
||||||
f, err := n.clnt.Open(n.hdfsPathJoin(bucket, object))
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
fis, err := f.Readdir(1)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Readdir returns an io.EOF when len(fis) == 0.
|
|
||||||
return len(fis) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetObjectInfo reads object info and replies back ObjectInfo.
|
|
||||||
func (n *hdfsObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
|
||||||
_, err = n.clnt.Stat(n.hdfsPathJoin(bucket))
|
|
||||||
if err != nil {
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(object, hdfsSeparator) && !n.isObjectDir(ctx, bucket, object) {
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, os.ErrNotExist, bucket, object)
|
|
||||||
}
|
|
||||||
|
|
||||||
fi, err := n.clnt.Stat(n.hdfsPathJoin(bucket, object))
|
|
||||||
if err != nil {
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
|
||||||
}
|
|
||||||
return minio.ObjectInfo{
|
|
||||||
Bucket: bucket,
|
|
||||||
Name: object,
|
|
||||||
ModTime: fi.ModTime(),
|
|
||||||
Size: fi.Size(),
|
|
||||||
IsDir: fi.IsDir(),
|
|
||||||
AccTime: fi.(*hdfs.FileInfo).AccessTime(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) PutObject(ctx context.Context, bucket string, object string, r *minio.PutObjReader, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
|
||||||
_, err = n.clnt.Stat(n.hdfsPathJoin(bucket))
|
|
||||||
if err != nil {
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
name := n.hdfsPathJoin(bucket, object)
|
|
||||||
|
|
||||||
// If its a directory create a prefix {
|
|
||||||
if strings.HasSuffix(object, hdfsSeparator) && r.Size() == 0 {
|
|
||||||
if err = n.clnt.MkdirAll(name, os.FileMode(0o755)); err != nil {
|
|
||||||
n.deleteObject(n.hdfsPathJoin(bucket), name)
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tmpname := n.hdfsPathJoin(minioMetaTmpBucket, minio.MustGetUUID())
|
|
||||||
var w *hdfs.FileWriter
|
|
||||||
w, err = n.clnt.Create(tmpname)
|
|
||||||
if err != nil {
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
|
||||||
}
|
|
||||||
defer n.deleteObject(n.hdfsPathJoin(minioMetaTmpBucket), tmpname)
|
|
||||||
if _, err = io.Copy(w, r); err != nil {
|
|
||||||
w.Close()
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
|
||||||
}
|
|
||||||
dir := path.Dir(name)
|
|
||||||
if dir != "" {
|
|
||||||
if err = n.clnt.MkdirAll(dir, os.FileMode(0o755)); err != nil {
|
|
||||||
w.Close()
|
|
||||||
n.deleteObject(n.hdfsPathJoin(bucket), dir)
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.Close()
|
|
||||||
if err = n.clnt.Rename(tmpname, name); err != nil {
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fi, err := n.clnt.Stat(name)
|
|
||||||
if err != nil {
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
|
||||||
}
|
|
||||||
return minio.ObjectInfo{
|
|
||||||
Bucket: bucket,
|
|
||||||
Name: object,
|
|
||||||
ETag: r.MD5CurrentHexString(),
|
|
||||||
ModTime: fi.ModTime(),
|
|
||||||
Size: fi.Size(),
|
|
||||||
IsDir: fi.IsDir(),
|
|
||||||
AccTime: fi.(*hdfs.FileInfo).AccessTime(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) NewMultipartUpload(ctx context.Context, bucket string, object string, opts minio.ObjectOptions) (uploadID string, err error) {
|
|
||||||
_, err = n.clnt.Stat(n.hdfsPathJoin(bucket))
|
|
||||||
if err != nil {
|
|
||||||
return uploadID, hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadID = minio.MustGetUUID()
|
|
||||||
if err = n.clnt.CreateEmptyFile(n.hdfsPathJoin(minioMetaTmpBucket, uploadID)); err != nil {
|
|
||||||
return uploadID, hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
return uploadID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) ListMultipartUploads(ctx context.Context, bucket string, prefix string, keyMarker string, uploadIDMarker string, delimiter string, maxUploads int) (lmi minio.ListMultipartsInfo, err error) {
|
|
||||||
_, err = n.clnt.Stat(n.hdfsPathJoin(bucket))
|
|
||||||
if err != nil {
|
|
||||||
return lmi, hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// It's decided not to support List Multipart Uploads, hence returning empty result.
|
|
||||||
return lmi, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) checkUploadIDExists(ctx context.Context, bucket, object, uploadID string) (err error) {
|
|
||||||
_, err = n.clnt.Stat(n.hdfsPathJoin(minioMetaTmpBucket, uploadID))
|
|
||||||
if err != nil {
|
|
||||||
return hdfsToObjectErr(ctx, err, bucket, object, uploadID)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMultipartInfo returns multipart info of the uploadId of the object
|
|
||||||
func (n *hdfsObjects) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts minio.ObjectOptions) (result minio.MultipartInfo, err error) {
|
|
||||||
_, err = n.clnt.Stat(n.hdfsPathJoin(bucket))
|
|
||||||
if err != nil {
|
|
||||||
return result, hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = n.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Bucket = bucket
|
|
||||||
result.Object = object
|
|
||||||
result.UploadID = uploadID
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (result minio.ListPartsInfo, err error) {
|
|
||||||
_, err = n.clnt.Stat(n.hdfsPathJoin(bucket))
|
|
||||||
if err != nil {
|
|
||||||
return result, hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = n.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// It's decided not to support List parts, hence returning empty result.
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject, uploadID string, partID int,
|
|
||||||
startOffset int64, length int64, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (minio.PartInfo, error) {
|
|
||||||
return n.PutObjectPart(ctx, dstBucket, dstObject, uploadID, partID, srcInfo.PutObjReader, dstOpts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, r *minio.PutObjReader, opts minio.ObjectOptions) (info minio.PartInfo, err error) {
|
|
||||||
_, err = n.clnt.Stat(n.hdfsPathJoin(bucket))
|
|
||||||
if err != nil {
|
|
||||||
return info, hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
var w *hdfs.FileWriter
|
|
||||||
w, err = n.clnt.Append(n.hdfsPathJoin(minioMetaTmpBucket, uploadID))
|
|
||||||
if err != nil {
|
|
||||||
return info, hdfsToObjectErr(ctx, err, bucket, object, uploadID)
|
|
||||||
}
|
|
||||||
defer w.Close()
|
|
||||||
_, err = io.Copy(w, r.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return info, hdfsToObjectErr(ctx, err, bucket, object, uploadID)
|
|
||||||
}
|
|
||||||
|
|
||||||
info.PartNumber = partID
|
|
||||||
info.ETag = r.MD5CurrentHexString()
|
|
||||||
info.LastModified = minio.UTCNow()
|
|
||||||
info.Size = r.Reader.Size()
|
|
||||||
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, parts []minio.CompletePart, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
|
||||||
_, err = n.clnt.Stat(n.hdfsPathJoin(bucket))
|
|
||||||
if err != nil {
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = n.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
|
|
||||||
return objInfo, err
|
|
||||||
}
|
|
||||||
|
|
||||||
name := n.hdfsPathJoin(bucket, object)
|
|
||||||
dir := path.Dir(name)
|
|
||||||
if dir != "" {
|
|
||||||
if err = n.clnt.MkdirAll(dir, os.FileMode(0o755)); err != nil {
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = n.clnt.Rename(n.hdfsPathJoin(minioMetaTmpBucket, uploadID), name)
|
|
||||||
// Object already exists is an error on HDFS
|
|
||||||
// remove it and then create it again.
|
|
||||||
if os.IsExist(err) {
|
|
||||||
if err = n.clnt.Remove(name); err != nil {
|
|
||||||
if dir != "" {
|
|
||||||
n.deleteObject(n.hdfsPathJoin(bucket), dir)
|
|
||||||
}
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
|
||||||
}
|
|
||||||
if err = n.clnt.Rename(n.hdfsPathJoin(minioMetaTmpBucket, uploadID), name); err != nil {
|
|
||||||
if dir != "" {
|
|
||||||
n.deleteObject(n.hdfsPathJoin(bucket), dir)
|
|
||||||
}
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fi, err := n.clnt.Stat(name)
|
|
||||||
if err != nil {
|
|
||||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate s3 compatible md5sum for complete multipart.
|
|
||||||
s3MD5 := minio.ComputeCompleteMultipartMD5(parts)
|
|
||||||
|
|
||||||
return minio.ObjectInfo{
|
|
||||||
Bucket: bucket,
|
|
||||||
Name: object,
|
|
||||||
ETag: s3MD5,
|
|
||||||
ModTime: fi.ModTime(),
|
|
||||||
Size: fi.Size(),
|
|
||||||
IsDir: fi.IsDir(),
|
|
||||||
AccTime: fi.(*hdfs.FileInfo).AccessTime(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *hdfsObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts minio.ObjectOptions) (err error) {
|
|
||||||
_, err = n.clnt.Stat(n.hdfsPathJoin(bucket))
|
|
||||||
if err != nil {
|
|
||||||
return hdfsToObjectErr(ctx, err, bucket)
|
|
||||||
}
|
|
||||||
return hdfsToObjectErr(ctx, n.clnt.Remove(n.hdfsPathJoin(minioMetaTmpBucket, uploadID)), bucket, object, uploadID)
|
|
||||||
}
|
|
||||||
@@ -364,6 +364,7 @@ func (l *s3EncObjects) GetObjectInfo(ctx context.Context, bucket string, object
|
|||||||
// CopyObject copies an object from source bucket to a destination bucket.
|
// CopyObject copies an object from source bucket to a destination bucket.
|
||||||
func (l *s3EncObjects) CopyObject(ctx context.Context, srcBucket string, srcObject string, dstBucket string, dstObject string, srcInfo minio.ObjectInfo, s, d minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
func (l *s3EncObjects) CopyObject(ctx context.Context, srcBucket string, srcObject string, dstBucket string, dstObject string, srcInfo minio.ObjectInfo, s, d minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||||
cpSrcDstSame := path.Join(srcBucket, srcObject) == path.Join(dstBucket, dstObject)
|
cpSrcDstSame := path.Join(srcBucket, srcObject) == path.Join(dstBucket, dstObject)
|
||||||
|
userDefined := minio.CloneMSS(srcInfo.UserDefined)
|
||||||
if cpSrcDstSame {
|
if cpSrcDstSame {
|
||||||
var gwMeta gwMetaV1
|
var gwMeta gwMetaV1
|
||||||
if s.ServerSideEncryption != nil && d.ServerSideEncryption != nil &&
|
if s.ServerSideEncryption != nil && d.ServerSideEncryption != nil &&
|
||||||
@@ -378,16 +379,16 @@ func (l *s3EncObjects) CopyObject(ctx context.Context, srcBucket string, srcObje
|
|||||||
d.ServerSideEncryption.Marshal(header)
|
d.ServerSideEncryption.Marshal(header)
|
||||||
}
|
}
|
||||||
for k, v := range header {
|
for k, v := range header {
|
||||||
srcInfo.UserDefined[k] = v[0]
|
userDefined[k] = v[0]
|
||||||
}
|
}
|
||||||
gwMeta.Meta = srcInfo.UserDefined
|
gwMeta.Meta = userDefined
|
||||||
if err = l.writeGWMetadata(ctx, dstBucket, getDareMetaPath(dstObject), gwMeta, minio.ObjectOptions{}); err != nil {
|
if err = l.writeGWMetadata(ctx, dstBucket, getDareMetaPath(dstObject), gwMeta, minio.ObjectOptions{}); err != nil {
|
||||||
return objInfo, minio.ErrorRespToObjectError(err)
|
return objInfo, minio.ErrorRespToObjectError(err)
|
||||||
}
|
}
|
||||||
return gwMeta.ToObjectInfo(dstBucket, dstObject), nil
|
return gwMeta.ToObjectInfo(dstBucket, dstObject), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dstOpts := minio.ObjectOptions{ServerSideEncryption: d.ServerSideEncryption, UserDefined: srcInfo.UserDefined}
|
dstOpts := minio.ObjectOptions{ServerSideEncryption: d.ServerSideEncryption, UserDefined: userDefined}
|
||||||
return l.PutObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, dstOpts)
|
return l.PutObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, dstOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,7 +550,8 @@ func (l *s3EncObjects) PutObjectPart(ctx context.Context, bucket string, object
|
|||||||
// CopyObjectPart creates a part in a multipart upload by copying
|
// CopyObjectPart creates a part in a multipart upload by copying
|
||||||
// existing object or a part of it.
|
// existing object or a part of it.
|
||||||
func (l *s3EncObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string,
|
func (l *s3EncObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string,
|
||||||
partID int, startOffset, length int64, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (p minio.PartInfo, err error) {
|
partID int, startOffset, length int64, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions,
|
||||||
|
) (p minio.PartInfo, err error) {
|
||||||
return l.PutObjectPart(ctx, destBucket, destObject, uploadID, partID, srcInfo.PutObjReader, dstOpts)
|
return l.PutObjectPart(ctx, destBucket, destObject, uploadID, partID, srcInfo.PutObjReader, dstOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -502,17 +502,20 @@ func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object str
|
|||||||
// PutObject creates a new object with the incoming data,
|
// PutObject creates a new object with the incoming data,
|
||||||
func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string, r *minio.PutObjReader, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string, r *minio.PutObjReader, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||||
data := r.Reader
|
data := r.Reader
|
||||||
|
|
||||||
|
userDefined := minio.CloneMSS(opts.UserDefined)
|
||||||
|
|
||||||
var tagMap map[string]string
|
var tagMap map[string]string
|
||||||
if tagstr, ok := opts.UserDefined[xhttp.AmzObjectTagging]; ok && tagstr != "" {
|
if tagstr, ok := userDefined[xhttp.AmzObjectTagging]; ok && tagstr != "" {
|
||||||
tagObj, err := tags.ParseObjectTags(tagstr)
|
tagObj, err := tags.ParseObjectTags(tagstr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return objInfo, minio.ErrorRespToObjectError(err, bucket, object)
|
return objInfo, minio.ErrorRespToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
tagMap = tagObj.ToMap()
|
tagMap = tagObj.ToMap()
|
||||||
delete(opts.UserDefined, xhttp.AmzObjectTagging)
|
delete(userDefined, xhttp.AmzObjectTagging)
|
||||||
}
|
}
|
||||||
putOpts := miniogo.PutObjectOptions{
|
putOpts := miniogo.PutObjectOptions{
|
||||||
UserMetadata: opts.UserDefined,
|
UserMetadata: userDefined,
|
||||||
ServerSideEncryption: opts.ServerSideEncryption,
|
ServerSideEncryption: opts.ServerSideEncryption,
|
||||||
UserTags: tagMap,
|
UserTags: tagMap,
|
||||||
// Content-Md5 is needed for buckets with object locking,
|
// Content-Md5 is needed for buckets with object locking,
|
||||||
@@ -529,7 +532,7 @@ func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string,
|
|||||||
ETag: ui.ETag,
|
ETag: ui.ETag,
|
||||||
Size: ui.Size,
|
Size: ui.Size,
|
||||||
Key: object,
|
Key: object,
|
||||||
Metadata: minio.ToMinioClientObjectInfoMetadata(opts.UserDefined),
|
Metadata: minio.ToMinioClientObjectInfoMetadata(userDefined),
|
||||||
}
|
}
|
||||||
|
|
||||||
return minio.FromMinioClientObjectInfo(bucket, oi), nil
|
return minio.FromMinioClientObjectInfo(bucket, oi), nil
|
||||||
@@ -544,8 +547,9 @@ func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject
|
|||||||
// metadata input is already a trickled down value from interpreting x-amz-metadata-directive at
|
// metadata input is already a trickled down value from interpreting x-amz-metadata-directive at
|
||||||
// handler layer. So what we have right now is supposed to be applied on the destination object anyways.
|
// handler layer. So what we have right now is supposed to be applied on the destination object anyways.
|
||||||
// So preserve it by adding "REPLACE" directive to save all the metadata set by CopyObject API.
|
// So preserve it by adding "REPLACE" directive to save all the metadata set by CopyObject API.
|
||||||
srcInfo.UserDefined["x-amz-metadata-directive"] = "REPLACE"
|
userDefined := minio.CloneMSS(srcInfo.UserDefined)
|
||||||
srcInfo.UserDefined["x-amz-copy-source-if-match"] = srcInfo.ETag
|
userDefined["x-amz-metadata-directive"] = "REPLACE"
|
||||||
|
userDefined["x-amz-copy-source-if-match"] = srcInfo.ETag
|
||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
if srcOpts.ServerSideEncryption != nil {
|
if srcOpts.ServerSideEncryption != nil {
|
||||||
encrypt.SSECopy(srcOpts.ServerSideEncryption).Marshal(header)
|
encrypt.SSECopy(srcOpts.ServerSideEncryption).Marshal(header)
|
||||||
@@ -556,10 +560,10 @@ func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range header {
|
for k, v := range header {
|
||||||
srcInfo.UserDefined[k] = v[0]
|
userDefined[k] = v[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = l.Client.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo.UserDefined, miniogo.CopySrcOptions{}, miniogo.PutObjectOptions{}); err != nil {
|
if _, err = l.Client.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, userDefined, miniogo.CopySrcOptions{}, miniogo.PutObjectOptions{}); err != nil {
|
||||||
return objInfo, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
return objInfo, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
return l.GetObjectInfo(ctx, dstBucket, dstObject, dstOpts)
|
return l.GetObjectInfo(ctx, dstBucket, dstObject, dstOpts)
|
||||||
@@ -605,17 +609,18 @@ func (l *s3Objects) ListMultipartUploads(ctx context.Context, bucket string, pre
|
|||||||
// NewMultipartUpload upload object in multiple parts
|
// NewMultipartUpload upload object in multiple parts
|
||||||
func (l *s3Objects) NewMultipartUpload(ctx context.Context, bucket string, object string, o minio.ObjectOptions) (uploadID string, err error) {
|
func (l *s3Objects) NewMultipartUpload(ctx context.Context, bucket string, object string, o minio.ObjectOptions) (uploadID string, err error) {
|
||||||
var tagMap map[string]string
|
var tagMap map[string]string
|
||||||
if tagStr, ok := o.UserDefined[xhttp.AmzObjectTagging]; ok {
|
userDefined := minio.CloneMSS(o.UserDefined)
|
||||||
|
if tagStr, ok := userDefined[xhttp.AmzObjectTagging]; ok {
|
||||||
tagObj, err := tags.Parse(tagStr, true)
|
tagObj, err := tags.Parse(tagStr, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return uploadID, minio.ErrorRespToObjectError(err, bucket, object)
|
return uploadID, minio.ErrorRespToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
tagMap = tagObj.ToMap()
|
tagMap = tagObj.ToMap()
|
||||||
delete(o.UserDefined, xhttp.AmzObjectTagging)
|
delete(userDefined, xhttp.AmzObjectTagging)
|
||||||
}
|
}
|
||||||
// Create PutObject options
|
// Create PutObject options
|
||||||
opts := miniogo.PutObjectOptions{
|
opts := miniogo.PutObjectOptions{
|
||||||
UserMetadata: o.UserDefined,
|
UserMetadata: userDefined,
|
||||||
ServerSideEncryption: o.ServerSideEncryption,
|
ServerSideEncryption: o.ServerSideEncryption,
|
||||||
UserTags: tagMap,
|
UserTags: tagMap,
|
||||||
}
|
}
|
||||||
@@ -640,13 +645,13 @@ func (l *s3Objects) PutObjectPart(ctx context.Context, bucket string, object str
|
|||||||
// CopyObjectPart creates a part in a multipart upload by copying
|
// CopyObjectPart creates a part in a multipart upload by copying
|
||||||
// existing object or a part of it.
|
// existing object or a part of it.
|
||||||
func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string,
|
func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string,
|
||||||
partID int, startOffset, length int64, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (p minio.PartInfo, err error) {
|
partID int, startOffset, length int64, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions,
|
||||||
|
) (p minio.PartInfo, err error) {
|
||||||
if srcOpts.CheckPrecondFn != nil && srcOpts.CheckPrecondFn(srcInfo) {
|
if srcOpts.CheckPrecondFn != nil && srcOpts.CheckPrecondFn(srcInfo) {
|
||||||
return minio.PartInfo{}, minio.PreConditionFailed{}
|
return minio.PartInfo{}, minio.PreConditionFailed{}
|
||||||
}
|
}
|
||||||
srcInfo.UserDefined = map[string]string{
|
userDefined := minio.CloneMSS(srcInfo.UserDefined)
|
||||||
"x-amz-copy-source-if-match": srcInfo.ETag,
|
userDefined["x-amz-copy-source-if-match"] = srcInfo.ETag
|
||||||
}
|
|
||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
if srcOpts.ServerSideEncryption != nil {
|
if srcOpts.ServerSideEncryption != nil {
|
||||||
encrypt.SSECopy(srcOpts.ServerSideEncryption).Marshal(header)
|
encrypt.SSECopy(srcOpts.ServerSideEncryption).Marshal(header)
|
||||||
@@ -656,11 +661,11 @@ func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, de
|
|||||||
dstOpts.ServerSideEncryption.Marshal(header)
|
dstOpts.ServerSideEncryption.Marshal(header)
|
||||||
}
|
}
|
||||||
for k, v := range header {
|
for k, v := range header {
|
||||||
srcInfo.UserDefined[k] = v[0]
|
userDefined[k] = v[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
completePart, err := l.Client.CopyObjectPart(ctx, srcBucket, srcObject, destBucket, destObject,
|
completePart, err := l.Client.CopyObjectPart(ctx, srcBucket, srcObject, destBucket, destObject,
|
||||||
uploadID, partID, startOffset, length, srcInfo.UserDefined)
|
uploadID, partID, startOffset, length, userDefined)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
return p, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-15
@@ -18,6 +18,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
@@ -141,6 +142,14 @@ func setBrowserRedirectHandler(h http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var redirectPrefixes = map[string]struct{}{
|
||||||
|
"favicon-16x16.png": {},
|
||||||
|
"favicon-32x32.png": {},
|
||||||
|
"favicon-96x96.png": {},
|
||||||
|
"index.html": {},
|
||||||
|
minioReservedBucket: {},
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch redirect location if urlPath satisfies certain
|
// Fetch redirect location if urlPath satisfies certain
|
||||||
// criteria. Some special names are considered to be
|
// criteria. Some special names are considered to be
|
||||||
// redirectable, this is purely internal function and
|
// redirectable, this is purely internal function and
|
||||||
@@ -151,24 +160,18 @@ func getRedirectLocation(r *http.Request) *xnet.URL {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, prefix := range []string{
|
|
||||||
"favicon-16x16.png",
|
|
||||||
"favicon-32x32.png",
|
|
||||||
"favicon-96x96.png",
|
|
||||||
"index.html",
|
|
||||||
minioReservedBucket,
|
|
||||||
} {
|
|
||||||
bucket, _ := path2BucketObject(resource)
|
bucket, _ := path2BucketObject(resource)
|
||||||
if path.Clean(bucket) == prefix || resource == slashSeparator {
|
_, redirect := redirectPrefixes[path.Clean(bucket)]
|
||||||
|
if redirect || resource == slashSeparator {
|
||||||
if globalBrowserRedirectURL != nil {
|
if globalBrowserRedirectURL != nil {
|
||||||
return globalBrowserRedirectURL
|
return globalBrowserRedirectURL
|
||||||
}
|
}
|
||||||
hostname, _, _ := net.SplitHostPort(r.Host)
|
xhost, err := xnet.ParseHost(r.Host)
|
||||||
if hostname == "" {
|
if err != nil {
|
||||||
hostname = r.Host
|
return nil
|
||||||
}
|
}
|
||||||
return &xnet.URL{
|
return &xnet.URL{
|
||||||
Host: net.JoinHostPort(hostname, globalMinioConsolePort),
|
Host: net.JoinHostPort(xhost.Name, globalMinioConsolePort),
|
||||||
Scheme: func() string {
|
Scheme: func() string {
|
||||||
scheme := "http"
|
scheme := "http"
|
||||||
if r.TLS != nil {
|
if r.TLS != nil {
|
||||||
@@ -178,7 +181,6 @@ func getRedirectLocation(r *http.Request) *xnet.URL {
|
|||||||
}(),
|
}(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +296,15 @@ const (
|
|||||||
dotComponent = "."
|
dotComponent = "."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func hasBadHost(host string) error {
|
||||||
|
if globalIsCICD && strings.TrimSpace(host) == "" {
|
||||||
|
// under CI/CD test setups ignore empty hosts as invalid hosts
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := xnet.ParseHost(host)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the incoming path has bad path components,
|
// Check if the incoming path has bad path components,
|
||||||
// such as ".." and "."
|
// such as ".." and "."
|
||||||
func hasBadPathComponent(path string) bool {
|
func hasBadPathComponent(path string) bool {
|
||||||
@@ -312,7 +323,11 @@ func hasBadPathComponent(path string) bool {
|
|||||||
// Check if client is sending a malicious request.
|
// Check if client is sending a malicious request.
|
||||||
func hasMultipleAuth(r *http.Request) bool {
|
func hasMultipleAuth(r *http.Request) bool {
|
||||||
authTypeCount := 0
|
authTypeCount := 0
|
||||||
for _, hasValidAuth := range []func(*http.Request) bool{isRequestSignatureV2, isRequestPresignedSignatureV2, isRequestSignatureV4, isRequestPresignedSignatureV4, isRequestJWT, isRequestPostPolicySignatureV4} {
|
for _, hasValidAuth := range []func(*http.Request) bool{
|
||||||
|
isRequestSignatureV2, isRequestPresignedSignatureV2,
|
||||||
|
isRequestSignatureV4, isRequestPresignedSignatureV4,
|
||||||
|
isRequestJWT, isRequestPostPolicySignatureV4,
|
||||||
|
} {
|
||||||
if hasValidAuth(r) {
|
if hasValidAuth(r) {
|
||||||
authTypeCount++
|
authTypeCount++
|
||||||
}
|
}
|
||||||
@@ -324,6 +339,14 @@ func hasMultipleAuth(r *http.Request) bool {
|
|||||||
// any malicious requests.
|
// any malicious requests.
|
||||||
func setRequestValidityHandler(h http.Handler) http.Handler {
|
func setRequestValidityHandler(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := hasBadHost(r.Host); err != nil {
|
||||||
|
invalidReq := errorCodes.ToAPIErr(ErrInvalidRequest)
|
||||||
|
invalidReq.Description = fmt.Sprintf("%s (%s)", invalidReq.Description, err)
|
||||||
|
writeErrorResponse(r.Context(), w, invalidReq, r.URL)
|
||||||
|
atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check for bad components in URL path.
|
// Check for bad components in URL path.
|
||||||
if hasBadPathComponent(r.URL.Path) {
|
if hasBadPathComponent(r.URL.Path) {
|
||||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL)
|
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL)
|
||||||
@@ -341,7 +364,9 @@ func setRequestValidityHandler(h http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasMultipleAuth(r) {
|
if hasMultipleAuth(r) {
|
||||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
invalidReq := errorCodes.ToAPIErr(ErrInvalidRequest)
|
||||||
|
invalidReq.Description = fmt.Sprintf("%s (request has multiple authentication types, please use one)", invalidReq.Description)
|
||||||
|
writeErrorResponse(r.Context(), w, invalidReq, r.URL)
|
||||||
atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
|
atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -45,7 +45,6 @@ func newBgHealSequence() *healSequence {
|
|||||||
hs := madmin.HealOpts{
|
hs := madmin.HealOpts{
|
||||||
// Remove objects that do not have read-quorum
|
// Remove objects that do not have read-quorum
|
||||||
Remove: healDeleteDangling,
|
Remove: healDeleteDangling,
|
||||||
ScanMode: globalHealConfig.ScanMode(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &healSequence{
|
return &healSequence{
|
||||||
@@ -165,7 +164,7 @@ func mustGetHealSequence(ctx context.Context) *healSequence {
|
|||||||
// healErasureSet lists and heals all objects in a specific erasure set
|
// healErasureSet lists and heals all objects in a specific erasure set
|
||||||
func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string, tracker *healingTracker) error {
|
func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string, tracker *healingTracker) error {
|
||||||
bgSeq := mustGetHealSequence(ctx)
|
bgSeq := mustGetHealSequence(ctx)
|
||||||
scanMode := globalHealConfig.ScanMode()
|
scanMode := madmin.HealNormalScan
|
||||||
|
|
||||||
// Make sure to copy since `buckets slice`
|
// Make sure to copy since `buckets slice`
|
||||||
// is modified in place by tracker.
|
// is modified in place by tracker.
|
||||||
|
|||||||
+10
-6
@@ -102,7 +102,7 @@ const (
|
|||||||
GlobalStaleUploadsCleanupInterval = time.Hour * 6 // 6 hrs.
|
GlobalStaleUploadsCleanupInterval = time.Hour * 6 // 6 hrs.
|
||||||
|
|
||||||
// Refresh interval to update in-memory iam config cache.
|
// Refresh interval to update in-memory iam config cache.
|
||||||
globalRefreshIAMInterval = 5 * time.Minute
|
globalRefreshIAMInterval = 10 * time.Minute
|
||||||
|
|
||||||
// Limit of location constraint XML for unauthenticated PUT bucket operations.
|
// Limit of location constraint XML for unauthenticated PUT bucket operations.
|
||||||
maxLocationConstraintSize = 3 * humanize.MiByte
|
maxLocationConstraintSize = 3 * humanize.MiByte
|
||||||
@@ -139,7 +139,7 @@ var (
|
|||||||
// Indicates if the running minio is in gateway mode.
|
// Indicates if the running minio is in gateway mode.
|
||||||
globalIsGateway = false
|
globalIsGateway = false
|
||||||
|
|
||||||
// Name of gateway server, e.g S3, GCS, Azure, etc
|
// Name of gateway server, e.g S3, NAS etc
|
||||||
globalGatewayName = ""
|
globalGatewayName = ""
|
||||||
|
|
||||||
// This flag is set to 'true' by default
|
// This flag is set to 'true' by default
|
||||||
@@ -190,7 +190,7 @@ var (
|
|||||||
globalBucketTargetSys *BucketTargetSys
|
globalBucketTargetSys *BucketTargetSys
|
||||||
// globalAPIConfig controls S3 API requests throttling,
|
// globalAPIConfig controls S3 API requests throttling,
|
||||||
// healthcheck readiness deadlines and cors settings.
|
// healthcheck readiness deadlines and cors settings.
|
||||||
globalAPIConfig = apiConfig{listQuorum: 3}
|
globalAPIConfig = apiConfig{listQuorum: "strict"}
|
||||||
|
|
||||||
globalStorageClass storageclass.Config
|
globalStorageClass storageclass.Config
|
||||||
globalLDAPConfig xldap.Config
|
globalLDAPConfig xldap.Config
|
||||||
@@ -290,9 +290,6 @@ var (
|
|||||||
// Some standard content-types which we strictly dis-allow for compression.
|
// Some standard content-types which we strictly dis-allow for compression.
|
||||||
standardExcludeCompressContentTypes = []string{"video/*", "audio/*", "application/zip", "application/x-gzip", "application/x-zip-compressed", " application/x-compress", "application/x-spoon"}
|
standardExcludeCompressContentTypes = []string{"video/*", "audio/*", "application/zip", "application/x-gzip", "application/x-zip-compressed", " application/x-compress", "application/x-spoon"}
|
||||||
|
|
||||||
// Authorization validators list.
|
|
||||||
globalOpenIDValidators *openid.Validators
|
|
||||||
|
|
||||||
// OPA policy system.
|
// OPA policy system.
|
||||||
globalPolicyOPA *opa.Opa
|
globalPolicyOPA *opa.Opa
|
||||||
|
|
||||||
@@ -345,6 +342,13 @@ var (
|
|||||||
globalIsCICD bool
|
globalIsCICD bool
|
||||||
|
|
||||||
globalRootDiskThreshold uint64
|
globalRootDiskThreshold uint64
|
||||||
|
|
||||||
|
// Used for collecting stats for netperf
|
||||||
|
globalNetPerfMinDuration = time.Second * 10
|
||||||
|
globalNetPerfRX netPerfRX
|
||||||
|
globalObjectPerfBucket = "minio-perf-test-tmp-bucket"
|
||||||
|
globalObjectPerfUserMetadata = "X-Amz-Meta-Minio-Object-Perf" // Clients can set this to bypass S3 API service freeze. Used by object pref tests.
|
||||||
|
|
||||||
// Add new variable global values here.
|
// Add new variable global values here.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -38,7 +38,7 @@ type apiConfig struct {
|
|||||||
requestsDeadline time.Duration
|
requestsDeadline time.Duration
|
||||||
requestsPool chan struct{}
|
requestsPool chan struct{}
|
||||||
clusterDeadline time.Duration
|
clusterDeadline time.Duration
|
||||||
listQuorum int
|
listQuorum string
|
||||||
corsAllowOrigins []string
|
corsAllowOrigins []string
|
||||||
// total drives per erasure set across pools.
|
// total drives per erasure set across pools.
|
||||||
totalDriveCount int
|
totalDriveCount int
|
||||||
@@ -127,7 +127,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cap(t.requestsPool) < apiRequestsMaxPerNode {
|
if cap(t.requestsPool) != apiRequestsMaxPerNode {
|
||||||
// Only replace if needed.
|
// Only replace if needed.
|
||||||
// Existing requests will use the previous limit,
|
// Existing requests will use the previous limit,
|
||||||
// but new requests will use the new limit.
|
// but new requests will use the new limit.
|
||||||
@@ -136,7 +136,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
|
|||||||
t.requestsPool = make(chan struct{}, apiRequestsMaxPerNode)
|
t.requestsPool = make(chan struct{}, apiRequestsMaxPerNode)
|
||||||
}
|
}
|
||||||
t.requestsDeadline = cfg.RequestsDeadline
|
t.requestsDeadline = cfg.RequestsDeadline
|
||||||
t.listQuorum = cfg.GetListQuorum()
|
t.listQuorum = cfg.ListQuorum
|
||||||
if globalReplicationPool != nil &&
|
if globalReplicationPool != nil &&
|
||||||
cfg.ReplicationWorkers != t.replicationWorkers {
|
cfg.ReplicationWorkers != t.replicationWorkers {
|
||||||
globalReplicationPool.ResizeFailedWorkers(cfg.ReplicationFailedWorkers)
|
globalReplicationPool.ResizeFailedWorkers(cfg.ReplicationFailedWorkers)
|
||||||
@@ -170,7 +170,7 @@ func (t *apiConfig) shouldGzipObjects() bool {
|
|||||||
return t.gzipObjects
|
return t.gzipObjects
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *apiConfig) getListQuorum() int {
|
func (t *apiConfig) getListQuorum() string {
|
||||||
t.mu.RLock()
|
t.mu.RLock()
|
||||||
defer t.mu.RUnlock()
|
defer t.mu.RUnlock()
|
||||||
|
|
||||||
@@ -246,12 +246,14 @@ func maxClients(f http.HandlerFunc) http.HandlerFunc {
|
|||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
globalHTTPStats.incS3RequestsIncoming()
|
globalHTTPStats.incS3RequestsIncoming()
|
||||||
|
|
||||||
|
if r.Header.Get(globalObjectPerfUserMetadata) == "" {
|
||||||
if val := globalServiceFreeze.Load(); val != nil {
|
if val := globalServiceFreeze.Load(); val != nil {
|
||||||
if unlock, ok := val.(chan struct{}); ok && unlock != nil {
|
if unlock, ok := val.(chan struct{}); ok && unlock != nil {
|
||||||
// Wait until unfrozen.
|
// Wait until unfrozen.
|
||||||
<-unlock
|
<-unlock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pool, deadline := globalAPIConfig.getRequestsPool()
|
pool, deadline := globalAPIConfig.getRequestsPool()
|
||||||
if pool == nil {
|
if pool == nil {
|
||||||
|
|||||||
+13
-10
@@ -25,7 +25,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -37,6 +36,7 @@ import (
|
|||||||
"github.com/minio/minio/internal/handlers"
|
"github.com/minio/minio/internal/handlers"
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
|
xnet "github.com/minio/pkg/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -237,6 +237,10 @@ func extractReqParams(r *http.Request) map[string]string {
|
|||||||
"sourceIPAddress": handlers.GetSourceIP(r),
|
"sourceIPAddress": handlers.GetSourceIP(r),
|
||||||
// Add more fields here.
|
// Add more fields here.
|
||||||
}
|
}
|
||||||
|
if rangeField := r.Header.Get(xhttp.Range); rangeField != "" {
|
||||||
|
m["range"] = rangeField
|
||||||
|
}
|
||||||
|
|
||||||
if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok {
|
if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok {
|
||||||
m[xhttp.MinIOSourceReplicationRequest] = ""
|
m[xhttp.MinIOSourceReplicationRequest] = ""
|
||||||
}
|
}
|
||||||
@@ -395,26 +399,25 @@ func getResource(path string, host string, domains []string) (string, error) {
|
|||||||
if len(domains) == 0 {
|
if len(domains) == 0 {
|
||||||
return path, nil
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If virtual-host-style is enabled construct the "resource" properly.
|
// If virtual-host-style is enabled construct the "resource" properly.
|
||||||
if strings.Contains(host, ":") {
|
xhost, err := xnet.ParseHost(host)
|
||||||
// In bucket.mydomain.com:9000, strip out :9000
|
if err != nil {
|
||||||
var err error
|
|
||||||
if host, _, err = net.SplitHostPort(host); err != nil {
|
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("host", host)
|
reqInfo := (&logger.ReqInfo{}).AppendTags("host", host)
|
||||||
reqInfo.AppendTags("path", path)
|
reqInfo.AppendTags("path", path)
|
||||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
if host == minioReservedBucket+"."+domain {
|
if xhost.Name == minioReservedBucket+"."+domain {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !strings.HasSuffix(host, "."+domain) {
|
if !strings.HasSuffix(xhost.Name, "."+domain) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bucket := strings.TrimSuffix(host, "."+domain)
|
bucket := strings.TrimSuffix(xhost.Name, "."+domain)
|
||||||
return SlashSeparator + pathJoin(bucket, path), nil
|
return SlashSeparator + pathJoin(bucket, path), nil
|
||||||
}
|
}
|
||||||
return path, nil
|
return path, nil
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user