mirror of
https://github.com/pgsty/minio.git
synced 2026-08-14 10:43:15 +03:00
Compare commits
352 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41688a936b | |||
| b2db8123ec | |||
| b330c2c57e | |||
| 231c5cf6de | |||
| 7214a0160a | |||
| 375b79f11b | |||
| 661068d1a2 | |||
| 3da1869d5e | |||
| 000a7aa094 | |||
| 7cedc5369d | |||
| e5ecd20d44 | |||
| 53aaa5d2a5 | |||
| cccf2de129 | |||
| 9d39fb3604 | |||
| 4a007e3767 | |||
| 95814359bd | |||
| de6c286258 | |||
| d0ae69087c | |||
| 5e15b0b844 | |||
| 7ea026ff1d | |||
| 6e0575a53d | |||
| fb9be81fab | |||
| 60791d6dd1 | |||
| eba423bb9d | |||
| 301de169e9 | |||
| 1868c7016d | |||
| 0c71ce3398 | |||
| bc285cf0dd | |||
| 209a3ee7af | |||
| 7d19ab9f62 | |||
| 3f6d624c7b | |||
| c138272d63 | |||
| 7dbfea1353 | |||
| c121d27f31 | |||
| 43c19a6b82 | |||
| 6542bc4a03 | |||
| bede525dc9 | |||
| e45c90060f | |||
| 7d79c723e5 | |||
| 5f6d6c3b70 | |||
| d15042470e | |||
| f1f414ca59 | |||
| cdf4815a6b | |||
| fade056244 | |||
| a546047c95 | |||
| ea210319ce | |||
| 2896e780ae | |||
| eaafb23535 | |||
| baa30f4289 | |||
| 2164984d2b | |||
| 189c861835 | |||
| 6656fa3066 | |||
| 0cc2ed04f5 | |||
| b11adfa5cd | |||
| 9baeda781a | |||
| bd032d13ff | |||
| 730775bb4e | |||
| d31eaddba3 | |||
| 3202f78f0f | |||
| 6de410a0aa | |||
| a3f41c7049 | |||
| 1847f17f50 | |||
| 1bc32215b9 | |||
| 96009975d6 | |||
| de9b391db3 | |||
| a62572fb86 | |||
| 011a2c0b78 | |||
| daf4418cbb | |||
| 0f8df3d340 | |||
| 9cac385aec | |||
| 814ddc0923 | |||
| 247795dd36 | |||
| dfadf70a7f | |||
| d348ec0f6c | |||
| b730bd1396 | |||
| 56e0c6adf8 | |||
| f44a960dcd | |||
| 6c1bbf918d | |||
| 48e614b167 | |||
| fe8d33452b | |||
| c19ece6921 | |||
| 216fa57b88 | |||
| a9558ae248 | |||
| ee9077db7d | |||
| 9c85928740 | |||
| af0309371e | |||
| ead3c186a6 | |||
| 6ac48a65cb | |||
| 2ecf5ba1de | |||
| 94f1a1dea3 | |||
| c045ae15e7 | |||
| 1756b7c6ff | |||
| e25ace2151 | |||
| a8e5a86fa0 | |||
| f8edc233ab | |||
| 337c2a7cb4 | |||
| 2d735144b9 | |||
| 0113035237 | |||
| 52a1d248b2 | |||
| b5ed42c845 | |||
| d9e7cadacf | |||
| 36e88cbd50 | |||
| 6d76efb9bb | |||
| a1de9cec58 | |||
| 6885c72f32 | |||
| 0f1389e992 | |||
| 5bf3eeaa77 | |||
| 9dda1fd624 | |||
| 2dc46cb153 | |||
| 0674c0075e | |||
| 518ef670da | |||
| 0dd626ec67 | |||
| 53f4c0fdc0 | |||
| 7e3ea77fdf | |||
| 7290d23b26 | |||
| 24f20eb1bd | |||
| 4c9de098b0 | |||
| a2ccba69e5 | |||
| 8eb99d3a87 | |||
| 3773874cd3 | |||
| 6c62b1a2ea | |||
| b768645fde | |||
| 7b58dcb28c | |||
| fea4a1e68e | |||
| a9e83dd42c | |||
| 145f501a21 | |||
| 9b3b04ecec | |||
| 3e063cca5c | |||
| 27d716c663 | |||
| fbd15cb7b7 | |||
| f7c91eff54 | |||
| a6bdc086a2 | |||
| 1242dd951a | |||
| d1c8e9f31b | |||
| 83ccae6c8b | |||
| 086be07bf5 | |||
| 28f9c477a8 | |||
| 09571d03a5 | |||
| 71ce63f79c | |||
| 5205c9591f | |||
| 9a547dcbfb | |||
| 27632ca6ec | |||
| c7470e6e6e | |||
| d090a17ed0 | |||
| c2529260e7 | |||
| da87188ff8 | |||
| d099039f5d | |||
| 5dd9cf4398 | |||
| ab77b216d1 | |||
| b37a02cddf | |||
| c3c3e9087b | |||
| 7ad6bc955f | |||
| 7a5271ad96 | |||
| 1b122526aa | |||
| a3b266761e | |||
| 498389123e | |||
| bc61417284 | |||
| 97d952e61c | |||
| 073aac3d92 | |||
| eff4127efd | |||
| b1c0c32ba6 | |||
| f14bf25cb9 | |||
| f216670814 | |||
| 6ecc98fddb | |||
| 4843affd0e | |||
| 558785a4bb | |||
| 60d415bb8a | |||
| 45e22cf8aa | |||
| e4900b99d7 | |||
| 20766069a8 | |||
| e8160c9fae | |||
| 957ecb1b64 | |||
| ac5061df2c | |||
| cddb2714ef | |||
| d7d9cac20b | |||
| 6817c5ea58 | |||
| 4cd6ca02c7 | |||
| a5efcbab51 | |||
| 85be7b39ac | |||
| ebf3dda449 | |||
| 582953260b | |||
| 2d1ea86fc6 | |||
| 322385f1b6 | |||
| 1b38aed05f | |||
| a69c98e394 | |||
| 282c9f790a | |||
| 2eeb0e6a0b | |||
| 3ff5bf2369 | |||
| 69ee28a082 | |||
| d02deff3d7 | |||
| 3e78ea8acc | |||
| b54c0f0ef3 | |||
| c79358c67e | |||
| 75107d7698 | |||
| c4464e36c8 | |||
| d92db198d1 | |||
| 8bae956df6 | |||
| 7758524703 | |||
| c82fa2c829 | |||
| a51280fd20 | |||
| bd437c1c17 | |||
| 69fb68ef0b | |||
| 787dbaff36 | |||
| c50ae1fdbe | |||
| bde0f444db | |||
| 6a8298b137 | |||
| f19cbfad5c | |||
| 78f2183e70 | |||
| 5c11a46412 | |||
| 8a94aebdb8 | |||
| ec11e99667 | |||
| 37d066b563 | |||
| bfec5fe200 | |||
| 525287f4b6 | |||
| 9054ce73b2 | |||
| d079adc167 | |||
| a9d401ac10 | |||
| 1fa65c7f2f | |||
| cc9b63eb51 | |||
| 7e12eab3ad | |||
| fa685d7d9c | |||
| 4314ee1670 | |||
| 7d636a7c13 | |||
| bf9d51cf14 | |||
| 29e0727b58 | |||
| c434dff0a4 | |||
| b2a8cb4aba | |||
| b412a222ae | |||
| 79bcb705bf | |||
| 9f81d014f1 | |||
| 3184205519 | |||
| 7c919329e8 | |||
| db41953618 | |||
| cea078a593 | |||
| f44cfb2863 | |||
| 1b45be0d60 | |||
| 6bb693488c | |||
| e20e08d700 | |||
| ac07df2985 | |||
| 2054ca5c9a | |||
| e51e465543 | |||
| 2bbc6a83e8 | |||
| a78731a3ba | |||
| f4e779c964 | |||
| a973402821 | |||
| 44decbeae0 | |||
| 3ea1be3c52 | |||
| 2642e12d14 | |||
| e7276b7b9b | |||
| e375341c33 | |||
| 2c20716f37 | |||
| 928f5b0564 | |||
| 91f21ddc47 | |||
| 43a3778b45 | |||
| b9b1bfefe7 | |||
| 05cda35b14 | |||
| 2155e74951 | |||
| 4714958e99 | |||
| c6e62b9175 | |||
| ab66b23194 | |||
| 73f9d8a636 | |||
| 541a778d7b | |||
| d49f2ec19c | |||
| 8dd63a462f | |||
| 9902c9baaa | |||
| 7de29e6e6b | |||
| 336460f67e | |||
| 95e89f1712 | |||
| 886ae15464 | |||
| d8af244708 | |||
| c8243706b4 | |||
| 30707659b5 | |||
| 90c365a174 | |||
| 7b732b566f | |||
| ba52a925f9 | |||
| fdda5f98c6 | |||
| fa4d627b57 | |||
| 2c3e34f001 | |||
| 7f8f1ad4e3 | |||
| 6f992134a2 | |||
| 0c80bf45d0 | |||
| 2777956581 | |||
| b207520d98 | |||
| 2196fd9cd5 | |||
| ef6304c5c2 | |||
| 6b984410d5 | |||
| 813e0fc1a8 | |||
| 38cf263409 | |||
| 6f6a2214fc | |||
| 791821d590 | |||
| 9a951da881 | |||
| e7a0be5bd3 | |||
| ff932ca2a0 | |||
| 818d3bcaf5 | |||
| 45b1c66195 | |||
| da04cb91ce | |||
| cfc9cfd84a | |||
| ea18e51f4d | |||
| 3d3beb6a9d | |||
| 27b8f18cce | |||
| bf545dc320 | |||
| f001e99fcd | |||
| b4bfdc92cc | |||
| ae654831aa | |||
| 1ffa983a9d | |||
| ecf1566266 | |||
| c5b87f93dd | |||
| b1a2169dcc | |||
| d45a1808f2 | |||
| db2155551a | |||
| 09d35d3b4c | |||
| 5b9342d35c | |||
| 8d98662633 | |||
| 7fdeb44372 | |||
| 59dced8237 | |||
| 496f4a7dc7 | |||
| 8b880a246a | |||
| eeb5942b6b | |||
| 7ec904d67b | |||
| c9212819af | |||
| 10fd53d6bb | |||
| 3fea1d5e35 | |||
| 35ecc04223 | |||
| 3ca9f5ffa3 | |||
| 2e9fed1a14 | |||
| 6b92f3fd99 | |||
| 06e30b5aa1 | |||
| 603cf2a8bb | |||
| a54cdb9587 | |||
| ed4bd20a7c | |||
| cfd12914e1 | |||
| c55aeaf814 | |||
| fdf65aa9b9 | |||
| 69b2aacf5a | |||
| 0af62d35a0 | |||
| 7c32f3f554 | |||
| cec8cdb35e | |||
| 5ab9cc029d | |||
| 667f42515a | |||
| 3614cb7a8b | |||
| b809c84338 | |||
| 33edb072a3 | |||
| 6a00eb10bf | |||
| 792ee48d2c | |||
| 52873ac3a3 | |||
| 88ae0f1196 | |||
| 23a0415eb7 | |||
| 75a0661213 | |||
| a1c7c9ea73 | |||
| 7f19a9a617 | |||
| 2f2c7d91a8 | |||
| 9ad1c2d07d |
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: MinIO Community Support
|
||||||
|
url: https://slack.min.io
|
||||||
|
about: Please ask and answer questions here.
|
||||||
|
- name: MinIO SUBNET Support
|
||||||
|
url: https://min.io/pricing
|
||||||
|
about: Join this for Enterprise Support.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app
|
||||||
|
|
||||||
|
# Number of days of inactivity before a closed issue or pull request is locked
|
||||||
|
daysUntilLock: 365
|
||||||
|
|
||||||
|
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||||
|
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||||
|
skipCreatedBefore: false
|
||||||
|
|
||||||
|
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||||
|
exemptLabels: []
|
||||||
|
|
||||||
|
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||||
|
lockLabel: false
|
||||||
|
|
||||||
|
# Comment to post before locking. Set to `false` to disable
|
||||||
|
lockComment: >-
|
||||||
|
|
||||||
|
This thread has been automatically locked since there has not been
|
||||||
|
any recent activity after it was closed. Please open a new issue for
|
||||||
|
related bugs.
|
||||||
|
|
||||||
|
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||||
|
setLockReason: true
|
||||||
|
|
||||||
|
# Limit to only `issues` or `pulls`
|
||||||
|
only: issues
|
||||||
|
|
||||||
|
# Optionally, specify configuration settings just for `issues` or `pulls`
|
||||||
|
# issues:
|
||||||
|
# exemptLabels:
|
||||||
|
# - help-wanted
|
||||||
|
# lockLabel: outdated
|
||||||
|
|
||||||
|
# pulls:
|
||||||
|
# daysUntilLock: 30
|
||||||
|
|
||||||
|
# Repository to extend settings from
|
||||||
|
# _extends: repo
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Configuration for probot-stale - https://github.com/probot/stale
|
||||||
|
|
||||||
|
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||||
|
daysUntilStale: 90
|
||||||
|
|
||||||
|
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||||
|
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||||
|
daysUntilClose: 30
|
||||||
|
|
||||||
|
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||||
|
onlyLabels: []
|
||||||
|
|
||||||
|
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||||
|
exemptLabels:
|
||||||
|
- "security"
|
||||||
|
- "pending discussion"
|
||||||
|
|
||||||
|
# Set to true to ignore issues in a project (defaults to false)
|
||||||
|
exemptProjects: false
|
||||||
|
|
||||||
|
# Set to true to ignore issues in a milestone (defaults to false)
|
||||||
|
exemptMilestones: false
|
||||||
|
|
||||||
|
# Set to true to ignore issues with an assignee (defaults to false)
|
||||||
|
exemptAssignees: false
|
||||||
|
|
||||||
|
# Label to use when marking as stale
|
||||||
|
staleLabel: stale
|
||||||
|
|
||||||
|
# Comment to post when marking as stale. Set to `false` to disable
|
||||||
|
markComment: >-
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed after 21 days if no further activity
|
||||||
|
occurs. Thank you for your contributions.
|
||||||
|
# Comment to post when removing the stale label.
|
||||||
|
# unmarkComment: >
|
||||||
|
# Your comment here.
|
||||||
|
|
||||||
|
# Comment to post when closing a stale Issue or Pull Request.
|
||||||
|
# closeComment: >
|
||||||
|
# Your comment here.
|
||||||
|
|
||||||
|
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||||
|
limitPerRun: 1
|
||||||
|
|
||||||
|
# Limit to only `issues` or `pulls`
|
||||||
|
# only: issues
|
||||||
|
|
||||||
|
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
|
||||||
|
# pulls:
|
||||||
|
# daysUntilStale: 30
|
||||||
|
# markComment: >
|
||||||
|
# This pull request has been automatically marked as stale because it has not had
|
||||||
|
# recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
|
# for your contributions.
|
||||||
|
|
||||||
|
# issues:
|
||||||
|
# exemptLabels:
|
||||||
|
# - confirmed
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
name: Go
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Test on Go ${{ matrix.go-version }} and ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.13.x]
|
||||||
|
os: [ubuntu-latest, windows-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: '12'
|
||||||
|
- uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
- name: Build on ${{ matrix.os }}
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GO111MODULE: on
|
||||||
|
MINIO_CI_CD: 1
|
||||||
|
run: |
|
||||||
|
go build --ldflags="-s -w" -o %GOPATH%\bin\minio.exe
|
||||||
|
go test -v --timeout 50m ./...
|
||||||
|
- name: Build on ${{ matrix.os }}
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GO111MODULE: on
|
||||||
|
MINIO_CI_CD: 1
|
||||||
|
run: |
|
||||||
|
sudo apt-get install devscripts shellcheck
|
||||||
|
make
|
||||||
|
diff -au <(gofmt -s -d cmd) <(printf "")
|
||||||
|
diff -au <(gofmt -s -d pkg) <(printf "")
|
||||||
|
make test-race
|
||||||
|
make crosscompile
|
||||||
|
make verify
|
||||||
|
make verify-healing
|
||||||
|
cd browser && npm install && npm run test && cd ..
|
||||||
|
bash -c 'shopt -s globstar; shellcheck mint/**/*.sh'
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
linters-settings:
|
||||||
|
golint:
|
||||||
|
min-confidence: 0
|
||||||
|
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- typecheck
|
||||||
|
- goimports
|
||||||
|
- misspell
|
||||||
|
- govet
|
||||||
|
- golint
|
||||||
|
- ineffassign
|
||||||
|
- gosimple
|
||||||
|
- deadcode
|
||||||
|
- structcheck
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-use-default: false
|
||||||
|
exclude:
|
||||||
|
- should have a package comment
|
||||||
|
- error strings should not be capitalized or end with punctuation or a newline
|
||||||
|
service:
|
||||||
|
golangci-lint-version: 1.20.0 # use the fixed version to not introduce new linters unexpectedly
|
||||||
-58
@@ -1,58 +0,0 @@
|
|||||||
go_import_path: github.com/minio/minio
|
|
||||||
|
|
||||||
language: go
|
|
||||||
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
packages:
|
|
||||||
- shellcheck
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
# this ensures PRs based on a local branch are not built twice
|
|
||||||
# the downside is that a PR targeting a different branch is not built
|
|
||||||
# but as a workaround you can add the branch to this list
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: linux
|
|
||||||
dist: trusty
|
|
||||||
sudo: required
|
|
||||||
env:
|
|
||||||
- ARCH=x86_64
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
- GO111MODULE=on
|
|
||||||
- SIMPLE_CI=1
|
|
||||||
go: 1.13.x
|
|
||||||
script:
|
|
||||||
- make
|
|
||||||
- diff -au <(gofmt -s -d cmd) <(printf "")
|
|
||||||
- diff -au <(gofmt -s -d pkg) <(printf "")
|
|
||||||
- make test-race
|
|
||||||
- make crosscompile
|
|
||||||
- make verify
|
|
||||||
- cd browser && npm install && npm run test && cd ..
|
|
||||||
- bash -c 'shopt -s globstar; shellcheck mint/**/*.sh'
|
|
||||||
|
|
||||||
- os: windows
|
|
||||||
env:
|
|
||||||
- ARCH=x86_64
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
- GO111MODULE=on
|
|
||||||
- SIMPLE_CI=1
|
|
||||||
go: 1.13.x
|
|
||||||
script:
|
|
||||||
- go build --ldflags="$(go run buildscripts/gen-ldflags.go)" -o %GOPATH%\bin\minio.exe
|
|
||||||
- for d in $(go list ./... | grep -v browser); do go test -v --timeout 20m "$d"; done
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
# Add an IPv6 config - see the corresponding Travis issue
|
|
||||||
# https://github.com/travis-ci/travis-ci/issues/8361
|
|
||||||
- if [[ "${TRAVIS_OS_NAME}" == "linux" ]]; then sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; fi
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then nvm install stable ; fi
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# MinIO Contribution Guide [](https://slack.min.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/)
|
# MinIO Contribution Guide [](https://slack.min.io) [](https://hub.docker.com/r/minio/minio/)
|
||||||
|
|
||||||
``MinIO`` community welcomes your contribution. To make the process as seamless as possible, we recommend you read this contribution guide.
|
``MinIO`` community welcomes your contribution. To make the process as seamless as possible, we recommend you read this contribution guide.
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -22,7 +22,8 @@ ENV MINIO_ACCESS_KEY_FILE=access_key \
|
|||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|
||||||
COPY --from=0 /go/bin/minio /usr/bin/minio
|
COPY --from=0 /go/bin/minio /usr/bin/minio
|
||||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
COPY --from=0 /go/minio/CREDITS /third_party/
|
||||||
|
COPY --from=0 /go/minio/dockerscripts/docker-entrypoint.sh /usr/bin/
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
apk add --no-cache ca-certificates 'curl>7.61.0' 'su-exec>=0.2' && \
|
apk add --no-cache ca-certificates 'curl>7.61.0' 'su-exec>=0.2' && \
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
FROM golang:1.13-alpine as builder
|
||||||
|
|
||||||
|
WORKDIR /home
|
||||||
|
|
||||||
|
ENV GOPATH /go
|
||||||
|
ENV CGO_ENABLED 0
|
||||||
|
ENV GO111MODULE on
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apk add --no-cache git 'curl>7.61.0' && \
|
||||||
|
git clone https://github.com/minio/minio && \
|
||||||
|
curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .
|
||||||
|
|
||||||
|
FROM arm32v7/alpine:3.10
|
||||||
|
|
||||||
|
LABEL maintainer="MinIO Inc <dev@min.io>"
|
||||||
|
|
||||||
|
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
||||||
|
COPY CREDITS /third_party/
|
||||||
|
COPY --from=builder /home/qemu-arm-static /usr/bin/qemu-arm-static
|
||||||
|
|
||||||
|
ENV MINIO_UPDATE off
|
||||||
|
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||||
|
MINIO_SECRET_KEY_FILE=secret_key \
|
||||||
|
MINIO_KMS_MASTER_KEY_FILE=kms_master_key \
|
||||||
|
MINIO_SSE_MASTER_KEY_FILE=sse_master_key
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apk add --no-cache ca-certificates 'curl>7.61.0' 'su-exec>=0.2' && \
|
||||||
|
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||||
|
curl https://dl.min.io/server/minio/release/linux-arm/minio > /usr/bin/minio && \
|
||||||
|
chmod +x /usr/bin/minio && \
|
||||||
|
chmod +x /usr/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
CMD ["minio"]
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
FROM golang:1.13-alpine as builder
|
||||||
|
|
||||||
|
WORKDIR /home
|
||||||
|
|
||||||
|
ENV GOPATH /go
|
||||||
|
ENV CGO_ENABLED 0
|
||||||
|
ENV GO111MODULE on
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apk add --no-cache git 'curl>7.61.0' && \
|
||||||
|
git clone https://github.com/minio/minio && \
|
||||||
|
curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .
|
||||||
|
|
||||||
|
FROM arm64v8/alpine:3.10
|
||||||
|
|
||||||
|
LABEL maintainer="MinIO Inc <dev@min.io>"
|
||||||
|
|
||||||
|
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
||||||
|
COPY CREDITS /third_party/
|
||||||
|
COPY --from=builder /home/qemu-arm-static /usr/bin/qemu-arm-static
|
||||||
|
|
||||||
|
ENV MINIO_UPDATE off
|
||||||
|
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||||
|
MINIO_SECRET_KEY_FILE=secret_key \
|
||||||
|
MINIO_KMS_MASTER_KEY_FILE=kms_master_key \
|
||||||
|
MINIO_SSE_MASTER_KEY_FILE=sse_master_key
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apk add --no-cache ca-certificates 'curl>7.61.0' 'su-exec>=0.2' && \
|
||||||
|
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||||
|
curl https://dl.min.io/server/minio/release/linux-arm64/minio > /usr/bin/minio && \
|
||||||
|
chmod +x /usr/bin/minio && \
|
||||||
|
chmod +x /usr/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
CMD ["minio"]
|
||||||
@@ -4,6 +4,7 @@ LABEL maintainer="MinIO Inc <dev@min.io>"
|
|||||||
|
|
||||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
||||||
COPY minio /usr/bin/
|
COPY minio /usr/bin/
|
||||||
|
COPY CREDITS /third_party/
|
||||||
|
|
||||||
ENV MINIO_UPDATE off
|
ENV MINIO_UPDATE off
|
||||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
FROM ubuntu
|
||||||
|
|
||||||
|
LABEL maintainer="MinIO Inc <dev@min.io>"
|
||||||
|
|
||||||
|
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-install-suggests \
|
||||||
|
git golang make npm && \
|
||||||
|
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV PATH=$PATH:/root/go/bin
|
||||||
|
|
||||||
|
RUN go get github.com/go-bindata/go-bindata/go-bindata && \
|
||||||
|
go get github.com/elazarl/go-bindata-assetfs/go-bindata-assetfs
|
||||||
|
|
||||||
+2
-8
@@ -1,17 +1,11 @@
|
|||||||
FROM ubuntu:16.04
|
FROM ubuntu:18.04
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|
||||||
ENV GOROOT /usr/local/go
|
ENV GOROOT /usr/local/go
|
||||||
|
ENV GOPATH /usr/local/gopath
|
||||||
ENV GOPATH /usr/local
|
|
||||||
|
|
||||||
ENV PATH $GOPATH/bin:$GOROOT/bin:$PATH
|
ENV PATH $GOPATH/bin:$GOROOT/bin:$PATH
|
||||||
|
|
||||||
ENV MINT_ROOT_DIR /mint
|
ENV MINT_ROOT_DIR /mint
|
||||||
|
|
||||||
COPY mint /mint
|
COPY mint /mint
|
||||||
|
|
||||||
RUN apt-get --yes update && apt-get --yes upgrade && \
|
RUN apt-get --yes update && apt-get --yes upgrade && \
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ FROM alpine:3.10
|
|||||||
LABEL maintainer="MinIO Inc <dev@min.io>"
|
LABEL maintainer="MinIO Inc <dev@min.io>"
|
||||||
|
|
||||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
||||||
|
COPY CREDITS /third_party/
|
||||||
|
|
||||||
ENV MINIO_UPDATE off
|
ENV MINIO_UPDATE off
|
||||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
#-------------------------------------------------------------
|
|
||||||
# Stage 1: Build and Unit tests
|
|
||||||
#-------------------------------------------------------------
|
|
||||||
FROM golang:1.13
|
|
||||||
|
|
||||||
COPY . /go/src/github.com/minio/minio
|
|
||||||
WORKDIR /go/src/github.com/minio/minio
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y jq
|
|
||||||
ENV GO111MODULE=on
|
|
||||||
ENV SIMPLE_CI 1
|
|
||||||
|
|
||||||
RUN git config --global http.cookiefile /gitcookie/.gitcookie
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get -y install sudo
|
|
||||||
RUN touch /etc/sudoers
|
|
||||||
|
|
||||||
RUN echo "root ALL=(ALL) ALL" >> /etc/sudoers
|
|
||||||
RUN echo "ci ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
|
||||||
RUN echo "Defaults env_reset" >> /etc/sudoers
|
|
||||||
RUN echo 'Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go:/usr/local/go/bin"' >> /etc/sudoers
|
|
||||||
|
|
||||||
RUN mkdir -p /home/ci/.cache
|
|
||||||
|
|
||||||
RUN groupadd -g 999 ci && \
|
|
||||||
useradd -r -u 999 -g ci ci && \
|
|
||||||
chown -R ci:ci /go /home/ci && \
|
|
||||||
chmod -R a+rw /go
|
|
||||||
|
|
||||||
USER ci
|
|
||||||
|
|
||||||
# -- tests --
|
|
||||||
RUN make
|
|
||||||
RUN bash -c 'diff -au <(gofmt -s -d cmd) <(printf "")'
|
|
||||||
RUN bash -c 'diff -au <(gofmt -s -d pkg) <(printf "")'
|
|
||||||
RUN make test-race
|
|
||||||
RUN make crosscompile
|
|
||||||
RUN make verify
|
|
||||||
|
|
||||||
## -- add healing tests
|
|
||||||
RUN make verify-healing
|
|
||||||
|
|
||||||
#-------------------------------------------------------------
|
|
||||||
# Stage 2: Test Frontend
|
|
||||||
#-------------------------------------------------------------
|
|
||||||
FROM node:10.15-stretch-slim
|
|
||||||
|
|
||||||
ENV SIMPLE_CI 1
|
|
||||||
|
|
||||||
COPY browser /minio/browser
|
|
||||||
WORKDIR /minio/browser
|
|
||||||
|
|
||||||
RUN yarn
|
|
||||||
RUN yarn test
|
|
||||||
|
|
||||||
#-------------------------------------------------------------
|
|
||||||
# Stage 3: Run Gateway Tests
|
|
||||||
#-------------------------------------------------------------
|
|
||||||
FROM ubuntu:16.04
|
|
||||||
|
|
||||||
COPY --from=0 /go/src/github.com/minio/minio/minio /usr/bin/minio
|
|
||||||
COPY buildscripts/gateway-tests.sh /usr/bin/gateway-tests.sh
|
|
||||||
COPY mint /mint
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
|
||||||
ENV LANG C.UTF-8
|
|
||||||
ENV GOROOT /usr/local/go
|
|
||||||
ENV GOPATH /usr/local
|
|
||||||
ENV PATH $GOPATH/bin:$GOROOT/bin:$PATH
|
|
||||||
ENV SIMPLE_CI 1
|
|
||||||
ENV MINT_ROOT_DIR /mint
|
|
||||||
|
|
||||||
RUN apt-get --yes update && apt-get --yes upgrade && \
|
|
||||||
apt-get --yes --quiet install wget jq curl git dnsmasq && \
|
|
||||||
cd /mint && /mint/release.sh
|
|
||||||
|
|
||||||
WORKDIR /mint
|
|
||||||
|
|
||||||
RUN /usr/bin/gateway-tests.sh
|
|
||||||
@@ -18,22 +18,12 @@ checks:
|
|||||||
|
|
||||||
getdeps:
|
getdeps:
|
||||||
@mkdir -p ${GOPATH}/bin
|
@mkdir -p ${GOPATH}/bin
|
||||||
@which golint 1>/dev/null || (echo "Installing golint" && GO111MODULE=off go get -u golang.org/x/lint/golint)
|
@which golangci-lint 1>/dev/null || (echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.27.0)
|
||||||
ifeq ($(GOARCH),s390x)
|
|
||||||
@which staticcheck 1>/dev/null || (echo "Installing staticcheck" && GO111MODULE=off go get honnef.co/go/tools/cmd/staticcheck)
|
|
||||||
else
|
|
||||||
@which staticcheck 1>/dev/null || (echo "Installing staticcheck" && wget --quiet https://github.com/dominikh/go-tools/releases/download/2019.2.3/staticcheck_${GOOS}_${GOARCH}.tar.gz && tar xf staticcheck_${GOOS}_${GOARCH}.tar.gz && mv staticcheck/staticcheck ${GOPATH}/bin/staticcheck && chmod +x ${GOPATH}/bin/staticcheck && rm -f staticcheck_${GOOS}_${GOARCH}.tar.gz && rm -rf staticcheck)
|
|
||||||
endif
|
|
||||||
@which misspell 1>/dev/null || (echo "Installing misspell" && GO111MODULE=off go get -u github.com/client9/misspell/cmd/misspell)
|
|
||||||
|
|
||||||
crosscompile:
|
crosscompile:
|
||||||
@(env bash $(PWD)/buildscripts/cross-compile.sh)
|
@(env bash $(PWD)/buildscripts/cross-compile.sh)
|
||||||
|
|
||||||
verifiers: getdeps vet fmt lint staticcheck spelling
|
verifiers: getdeps fmt lint
|
||||||
|
|
||||||
vet:
|
|
||||||
@echo "Running $@ check"
|
|
||||||
@GO111MODULE=on go vet github.com/minio/minio/...
|
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
@echo "Running $@ check"
|
@echo "Running $@ check"
|
||||||
@@ -42,21 +32,8 @@ fmt:
|
|||||||
|
|
||||||
lint:
|
lint:
|
||||||
@echo "Running $@ check"
|
@echo "Running $@ check"
|
||||||
@GO111MODULE=on ${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/cmd/...
|
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
|
||||||
@GO111MODULE=on ${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/pkg/...
|
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --timeout=5m --config ./.golangci.yml
|
||||||
|
|
||||||
staticcheck:
|
|
||||||
@echo "Running $@ check"
|
|
||||||
@GO111MODULE=on ${GOPATH}/bin/staticcheck github.com/minio/minio/cmd/...
|
|
||||||
@GO111MODULE=on ${GOPATH}/bin/staticcheck github.com/minio/minio/pkg/...
|
|
||||||
|
|
||||||
spelling:
|
|
||||||
@echo "Running $@ check"
|
|
||||||
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find cmd/`
|
|
||||||
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find pkg/`
|
|
||||||
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find docs/`
|
|
||||||
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find buildscripts/`
|
|
||||||
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find dockerscripts/`
|
|
||||||
|
|
||||||
# Builds minio, runs the verifiers then runs the tests.
|
# Builds minio, runs the verifiers then runs the tests.
|
||||||
check: test
|
check: test
|
||||||
@@ -71,19 +48,19 @@ test-race: verifiers build
|
|||||||
# Verify minio binary
|
# Verify minio binary
|
||||||
verify:
|
verify:
|
||||||
@echo "Verifying build with race"
|
@echo "Verifying build with race"
|
||||||
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
||||||
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||||
|
|
||||||
# Verify healing of disks with minio binary
|
# Verify healing of disks with minio binary
|
||||||
verify-healing:
|
verify-healing:
|
||||||
@echo "Verify healing build with race"
|
@echo "Verify healing build with race"
|
||||||
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
||||||
@(env bash $(PWD)/buildscripts/verify-healing.sh)
|
@(env bash $(PWD)/buildscripts/verify-healing.sh)
|
||||||
|
|
||||||
# Builds minio locally.
|
# Builds minio locally.
|
||||||
build: checks
|
build: checks
|
||||||
@echo "Building minio binary to './minio'"
|
@echo "Building minio binary to './minio'"
|
||||||
@GO111MODULE=on CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
@GO111MODULE=on CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
||||||
|
|
||||||
docker: build
|
docker: build
|
||||||
@docker build -t $(TAG) . -f Dockerfile.dev
|
@docker build -t $(TAG) . -f Dockerfile.dev
|
||||||
@@ -101,3 +78,4 @@ clean:
|
|||||||
@rm -rvf minio
|
@rm -rvf minio
|
||||||
@rm -rvf build
|
@rm -rvf build
|
||||||
@rm -rvf release
|
@rm -rvf release
|
||||||
|
@rm -rvf .verify*
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# MinIO Quickstart Guide
|
# MinIO Quickstart Guide
|
||||||
[](https://slack.min.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/)
|
[](https://slack.min.io) [](https://hub.docker.com/r/minio/minio/)
|
||||||
|
|
||||||
[](https://min.io)
|
[](https://min.io)
|
||||||
|
|
||||||
MinIO is High Performance Object Storage released under Apache License v2.0. It is API compatible with Amazon S3 cloud storage service. Using MinIO build high performance infrastructure for machine learning, analytics and application data workloads.
|
MinIO is a High Performance Object Storage released under Apache License v2.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads.
|
||||||
|
|
||||||
## Docker Container
|
## Docker Container
|
||||||
### Stable
|
### Stable
|
||||||
@@ -22,7 +22,7 @@ docker run -p 9000:9000 minio/minio:edge server /data
|
|||||||
|
|
||||||
## macOS
|
## macOS
|
||||||
### Homebrew (recommended)
|
### Homebrew (recommended)
|
||||||
Install minio packages using [Homebrew](http://brew.sh/)
|
Install minio packages using [Homebrew](https://brew.sh/)
|
||||||
```sh
|
```sh
|
||||||
brew install minio/stable/minio
|
brew install minio/stable/minio
|
||||||
minio server /data
|
minio server /data
|
||||||
@@ -185,11 +185,5 @@ mc admin update <minio alias, e.g., myminio>
|
|||||||
## Contribute to MinIO Project
|
## Contribute to MinIO Project
|
||||||
Please follow MinIO [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
|
Please follow MinIO [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
|
||||||
|
|
||||||
## Caveats
|
|
||||||
MinIO in its default mode doesn't use MD5Sum checkums of incoming streams unless requested by the client in `Content-Md5` header for validation. This may lead to incompatibility with rare S3 clients like `s3ql` which unfortunately do not set `Content-Md5` but depend on hex MD5Sum for the stream to be calculated by the server. MinIO considers this as a bug in `s3ql` and should be fixed on the client side because MD5Sum is a poor way to checksum and validate the authenticity of the objects. Although MinIO provides a workaround until client applications are fixed use `--compat` option instead to start the server.
|
|
||||||
```sh
|
|
||||||
./minio --compat server /data
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fminio%2Fminio?ref=badge_large)
|
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fminio%2Fminio?ref=badge_large)
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# MinIO Quickstart Guide [](https://slack.min.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/)
|
# MinIO Quickstart Guide [](https://slack.min.io) [](https://hub.docker.com/r/minio/minio/)
|
||||||
|
|
||||||
MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
|
MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
|
||||||
|
|
||||||
|
|||||||
+109
-2
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
``MinIO Browser`` provides minimal set of UI to manage buckets and objects on ``minio`` server. ``MinIO Browser`` is written in javascript and released under [Apache 2.0 License](./LICENSE).
|
``MinIO Browser`` provides minimal set of UI to manage buckets and objects on ``minio`` server. ``MinIO Browser`` is written in javascript and released under [Apache 2.0 License](./LICENSE).
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Install node
|
### Install node
|
||||||
@@ -11,6 +12,11 @@ exec -l $SHELL
|
|||||||
nvm install stable
|
nvm install stable
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Install node dependencies
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
### Install `go-bindata` and `go-bindata-assetfs`
|
### Install `go-bindata` and `go-bindata-assetfs`
|
||||||
|
|
||||||
If you do not have a working Golang environment, please follow [Install Golang](https://golang.org/doc/install)
|
If you do not have a working Golang environment, please follow [Install Golang](https://golang.org/doc/install)
|
||||||
@@ -30,13 +36,16 @@ npm run release
|
|||||||
|
|
||||||
This generates ui-assets.go in the current directory. Now do `make` in the parent directory to build the minio binary with the newly generated ``ui-assets.go``
|
This generates ui-assets.go in the current directory. Now do `make` in the parent directory to build the minio binary with the newly generated ``ui-assets.go``
|
||||||
|
|
||||||
|
|
||||||
|
## Run MinIO Browser with live reload
|
||||||
|
|
||||||
### Run MinIO Browser with live reload
|
### Run MinIO Browser with live reload
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:8080/minio/](http://localhost:8080/minio/) in your browser to play with the application
|
Open [http://localhost:8080/minio/](http://localhost:8080/minio/) in your browser to play with the application.
|
||||||
|
|
||||||
### Run MinIO Browser with live reload on custom port
|
### Run MinIO Browser with live reload on custom port
|
||||||
|
|
||||||
@@ -70,4 +79,102 @@ index 3ccdaba..9496c56 100644
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:8888/minio/](http://localhost:8888/minio/) in your browser to play with the application
|
Open [http://localhost:8888/minio/](http://localhost:8888/minio/) in your browser to play with the application.
|
||||||
|
|
||||||
|
### Run MinIO Browser with live reload on any IP
|
||||||
|
|
||||||
|
Edit `browser/webpack.config.js`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
diff --git a/browser/webpack.config.js b/browser/webpack.config.js
|
||||||
|
index 8bdbba53..139f6049 100644
|
||||||
|
--- a/browser/webpack.config.js
|
||||||
|
+++ b/browser/webpack.config.js
|
||||||
|
@@ -71,6 +71,7 @@ var exports = {
|
||||||
|
historyApiFallback: {
|
||||||
|
index: '/minio/'
|
||||||
|
},
|
||||||
|
+ host: '0.0.0.0',
|
||||||
|
proxy: {
|
||||||
|
'/minio/webrpc': {
|
||||||
|
target: 'http://localhost:9000',
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://IP:8080/minio/](http://IP:8080/minio/) in your browser to play with the application.
|
||||||
|
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
|
||||||
|
## Docker development environment
|
||||||
|
|
||||||
|
This approach will download the sources on your machine such that you are able to use your IDE or editor of choice.
|
||||||
|
A Docker container will be used in order to provide a controlled build environment without messing with your host system.
|
||||||
|
|
||||||
|
### Prepare host system
|
||||||
|
|
||||||
|
Install [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) and [Docker](https://docs.docker.com/get-docker/).
|
||||||
|
|
||||||
|
### Development within container
|
||||||
|
|
||||||
|
Prepare and build container
|
||||||
|
```
|
||||||
|
git clone git@github.com:minio/minio.git
|
||||||
|
cd minio
|
||||||
|
docker build -t minio-dev -f Dockerfile.dev.browser .
|
||||||
|
```
|
||||||
|
|
||||||
|
Run container, build and run core
|
||||||
|
```sh
|
||||||
|
docker run -it --rm --name minio-dev -v "$PWD":/minio minio-dev
|
||||||
|
|
||||||
|
cd /minio/browser
|
||||||
|
npm install
|
||||||
|
npm run release
|
||||||
|
cd /minio
|
||||||
|
make
|
||||||
|
./minio server /data
|
||||||
|
```
|
||||||
|
Note `Endpoint` IP (the one which is _not_ `127.0.0.1`), `AccessKey` and `SecretKey` (both default to `minioadmin`) in order to enter them in the browser later.
|
||||||
|
|
||||||
|
|
||||||
|
Open another terminal.
|
||||||
|
Connect to container
|
||||||
|
```sh
|
||||||
|
docker exec -it minio-dev bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply patch to allow access from outside container
|
||||||
|
```sh
|
||||||
|
cd /minio
|
||||||
|
git apply --ignore-whitespace <<EOF
|
||||||
|
diff --git a/browser/webpack.config.js b/browser/webpack.config.js
|
||||||
|
index 8bdbba53..139f6049 100644
|
||||||
|
--- a/browser/webpack.config.js
|
||||||
|
+++ b/browser/webpack.config.js
|
||||||
|
@@ -71,6 +71,7 @@ var exports = {
|
||||||
|
historyApiFallback: {
|
||||||
|
index: '/minio/'
|
||||||
|
},
|
||||||
|
+ host: '0.0.0.0',
|
||||||
|
proxy: {
|
||||||
|
'/minio/webrpc': {
|
||||||
|
target: 'http://localhost:9000',
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Build and run frontend with auto-reload
|
||||||
|
```sh
|
||||||
|
cd /minio/browser
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://IP:8080/minio/](http://IP:8080/minio/) in your browser to play with the application.
|
||||||
|
|
||||||
|
|||||||
@@ -88,11 +88,6 @@ export class ChangePasswordModal extends React.Component {
|
|||||||
|
|
||||||
canChangePassword() {
|
canChangePassword() {
|
||||||
const { serverInfo } = this.props
|
const { serverInfo } = this.props
|
||||||
// Password change is not allowed in WORM mode
|
|
||||||
if (serverInfo.info.isWorm) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Password change is not allowed for temporary users(STS)
|
// Password change is not allowed for temporary users(STS)
|
||||||
if(serverInfo.userInfo.isTempUser) {
|
if(serverInfo.userInfo.isTempUser) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -64,17 +64,6 @@ describe("ChangePasswordModal", () => {
|
|||||||
shallow(<ChangePasswordModal serverInfo={serverInfo} />)
|
shallow(<ChangePasswordModal serverInfo={serverInfo} />)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not allow changing password when isWorm is true", () => {
|
|
||||||
const newServerInfo = { ...serverInfo, info: { isWorm: true } }
|
|
||||||
const wrapper = shallow(<ChangePasswordModal serverInfo={newServerInfo} />)
|
|
||||||
expect(
|
|
||||||
wrapper
|
|
||||||
.find("ModalBody")
|
|
||||||
.childAt(0)
|
|
||||||
.text()
|
|
||||||
).toBe("Credentials of this user cannot be updated through MinIO Browser.")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should not allow changing password when not IAM user", () => {
|
it("should not allow changing password when not IAM user", () => {
|
||||||
const newServerInfo = {
|
const newServerInfo = {
|
||||||
...serverInfo,
|
...serverInfo,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
|
* MinIO Cloud Storage (C) 2020 MinIO, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -14,24 +14,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package cpu
|
import { createSelector } from "reselect"
|
||||||
|
|
||||||
import (
|
export const getServerInfo = state => state.browser.serverInfo
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
export const hasServerPublicDomain = createSelector(
|
||||||
|
getServerInfo,
|
||||||
|
serverInfo => Boolean(serverInfo.info && serverInfo.info.domains && serverInfo.info.domains.length),
|
||||||
)
|
)
|
||||||
|
|
||||||
func newCounter() (counter, error) {
|
|
||||||
return counter{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c counter) now() time.Time {
|
|
||||||
var ts syscall.Timespec
|
|
||||||
// Retrieve Per-process CPU-time clock
|
|
||||||
syscall.Syscall(syscall.SYS_CLOCK_GETTIME, unix.CLOCK_PROCESS_CPUTIME_ID, uintptr(unsafe.Pointer(&ts)), 0)
|
|
||||||
sec, nsec := ts.Unix()
|
|
||||||
return time.Unix(sec, nsec)
|
|
||||||
}
|
|
||||||
+72
-34
@@ -14,30 +14,67 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import mimedb from 'mime-types'
|
import mimedb from "mime-types"
|
||||||
|
|
||||||
const isFolder = (name, contentType) => {
|
const isFolder = (name, contentType) => {
|
||||||
if (name.endsWith('/')) return true
|
if (name.endsWith("/")) return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPdf = (name, contentType) => {
|
const isPdf = (name, contentType) => {
|
||||||
if (contentType === 'application/pdf') return true
|
if (contentType === "application/pdf") return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const isImage = (name, contentType) => {
|
||||||
|
if (
|
||||||
|
contentType === "image/jpeg" ||
|
||||||
|
contentType === "image/gif" ||
|
||||||
|
contentType === "image/x-icon" ||
|
||||||
|
contentType === "image/png" ||
|
||||||
|
contentType === "image/svg+xml" ||
|
||||||
|
contentType === "image/tiff" ||
|
||||||
|
contentType === "image/webp"
|
||||||
|
)
|
||||||
|
return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const isZip = (name, contentType) => {
|
const isZip = (name, contentType) => {
|
||||||
if (!contentType || !contentType.includes('/')) return false
|
if (!contentType || !contentType.includes("/")) return false
|
||||||
if (contentType.split('/')[1].includes('zip')) return true
|
if (contentType.split("/")[1].includes("zip")) return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCode = (name, contentType) => {
|
const isCode = (name, contentType) => {
|
||||||
const codeExt = ['c', 'cpp', 'go', 'py', 'java', 'rb', 'js', 'pl', 'fs',
|
const codeExt = [
|
||||||
'php', 'css', 'less', 'scss', 'coffee', 'net', 'html',
|
"c",
|
||||||
'rs', 'exs', 'scala', 'hs', 'clj', 'el', 'scm', 'lisp',
|
"cpp",
|
||||||
'asp', 'aspx']
|
"go",
|
||||||
const ext = name.split('.').reverse()[0]
|
"py",
|
||||||
|
"java",
|
||||||
|
"rb",
|
||||||
|
"js",
|
||||||
|
"pl",
|
||||||
|
"fs",
|
||||||
|
"php",
|
||||||
|
"css",
|
||||||
|
"less",
|
||||||
|
"scss",
|
||||||
|
"coffee",
|
||||||
|
"net",
|
||||||
|
"html",
|
||||||
|
"rs",
|
||||||
|
"exs",
|
||||||
|
"scala",
|
||||||
|
"hs",
|
||||||
|
"clj",
|
||||||
|
"el",
|
||||||
|
"scm",
|
||||||
|
"lisp",
|
||||||
|
"asp",
|
||||||
|
"aspx",
|
||||||
|
]
|
||||||
|
const ext = name.split(".").reverse()[0]
|
||||||
for (var i in codeExt) {
|
for (var i in codeExt) {
|
||||||
if (ext === codeExt[i]) return true
|
if (ext === codeExt[i]) return true
|
||||||
}
|
}
|
||||||
@@ -45,9 +82,9 @@ const isCode = (name, contentType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isExcel = (name, contentType) => {
|
const isExcel = (name, contentType) => {
|
||||||
if (!contentType || !contentType.includes('/')) return false
|
if (!contentType || !contentType.includes("/")) return false
|
||||||
const types = ['excel', 'spreadsheet']
|
const types = ["excel", "spreadsheet"]
|
||||||
const subType = contentType.split('/')[1]
|
const subType = contentType.split("/")[1]
|
||||||
for (var i in types) {
|
for (var i in types) {
|
||||||
if (subType.includes(types[i])) return true
|
if (subType.includes(types[i])) return true
|
||||||
}
|
}
|
||||||
@@ -55,9 +92,9 @@ const isExcel = (name, contentType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isDoc = (name, contentType) => {
|
const isDoc = (name, contentType) => {
|
||||||
if (!contentType || !contentType.includes('/')) return false
|
if (!contentType || !contentType.includes("/")) return false
|
||||||
const types = ['word', '.document']
|
const types = ["word", ".document"]
|
||||||
const subType = contentType.split('/')[1]
|
const subType = contentType.split("/")[1]
|
||||||
for (var i in types) {
|
for (var i in types) {
|
||||||
if (subType.includes(types[i])) return true
|
if (subType.includes(types[i])) return true
|
||||||
}
|
}
|
||||||
@@ -65,9 +102,9 @@ const isDoc = (name, contentType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isPresentation = (name, contentType) => {
|
const isPresentation = (name, contentType) => {
|
||||||
if (!contentType || !contentType.includes('/')) return false
|
if (!contentType || !contentType.includes("/")) return false
|
||||||
var types = ['powerpoint', 'presentation']
|
var types = ["powerpoint", "presentation"]
|
||||||
const subType = contentType.split('/')[1]
|
const subType = contentType.split("/")[1]
|
||||||
for (var i in types) {
|
for (var i in types) {
|
||||||
if (subType.includes(types[i])) return true
|
if (subType.includes(types[i])) return true
|
||||||
}
|
}
|
||||||
@@ -76,31 +113,32 @@ const isPresentation = (name, contentType) => {
|
|||||||
|
|
||||||
const typeToIcon = (type) => {
|
const typeToIcon = (type) => {
|
||||||
return (name, contentType) => {
|
return (name, contentType) => {
|
||||||
if (!contentType || !contentType.includes('/')) return false
|
if (!contentType || !contentType.includes("/")) return false
|
||||||
if (contentType.split('/')[0] === type) return true
|
if (contentType.split("/")[0] === type) return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDataType = (name, contentType) => {
|
export const getDataType = (name, contentType) => {
|
||||||
if (contentType === "") {
|
if (contentType === "") {
|
||||||
contentType = mimedb.lookup(name) || 'application/octet-stream'
|
contentType = mimedb.lookup(name) || "application/octet-stream"
|
||||||
}
|
}
|
||||||
const check = [
|
const check = [
|
||||||
['folder', isFolder],
|
["folder", isFolder],
|
||||||
['code', isCode],
|
["code", isCode],
|
||||||
['audio', typeToIcon('audio')],
|
["audio", typeToIcon("audio")],
|
||||||
['image', typeToIcon('image')],
|
["image", typeToIcon("image")],
|
||||||
['video', typeToIcon('video')],
|
["video", typeToIcon("video")],
|
||||||
['text', typeToIcon('text')],
|
["text", typeToIcon("text")],
|
||||||
['pdf', isPdf],
|
["pdf", isPdf],
|
||||||
['zip', isZip],
|
["image", isImage],
|
||||||
['excel', isExcel],
|
["zip", isZip],
|
||||||
['doc', isDoc],
|
["excel", isExcel],
|
||||||
['presentation', isPresentation]
|
["doc", isDoc],
|
||||||
|
["presentation", isPresentation],
|
||||||
]
|
]
|
||||||
for (var i in check) {
|
for (var i in check) {
|
||||||
if (check[i][1](name, contentType)) return check[i][0]
|
if (check[i][1](name, contentType)) return check[i][0]
|
||||||
}
|
}
|
||||||
return 'other'
|
return "other"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,18 +19,22 @@ import { connect } from "react-redux"
|
|||||||
import { Dropdown } from "react-bootstrap"
|
import { Dropdown } from "react-bootstrap"
|
||||||
import ShareObjectModal from "./ShareObjectModal"
|
import ShareObjectModal from "./ShareObjectModal"
|
||||||
import DeleteObjectConfirmModal from "./DeleteObjectConfirmModal"
|
import DeleteObjectConfirmModal from "./DeleteObjectConfirmModal"
|
||||||
|
import PreviewObjectModal from "./PreviewObjectModal"
|
||||||
|
|
||||||
import * as objectsActions from "./actions"
|
import * as objectsActions from "./actions"
|
||||||
|
import { getDataType } from "../mime.js"
|
||||||
import {
|
import {
|
||||||
SHARE_OBJECT_EXPIRY_DAYS,
|
SHARE_OBJECT_EXPIRY_DAYS,
|
||||||
SHARE_OBJECT_EXPIRY_HOURS,
|
SHARE_OBJECT_EXPIRY_HOURS,
|
||||||
SHARE_OBJECT_EXPIRY_MINUTES
|
SHARE_OBJECT_EXPIRY_MINUTES,
|
||||||
} from "../constants"
|
} from "../constants"
|
||||||
|
|
||||||
export class ObjectActions extends React.Component {
|
export class ObjectActions extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
showDeleteConfirmation: false
|
showDeleteConfirmation: false,
|
||||||
|
showPreview: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shareObject(e) {
|
shareObject(e) {
|
||||||
@@ -43,6 +47,11 @@ export class ObjectActions extends React.Component {
|
|||||||
SHARE_OBJECT_EXPIRY_MINUTES
|
SHARE_OBJECT_EXPIRY_MINUTES
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
handleDownload(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const { object, downloadObject } = this.props
|
||||||
|
downloadObject(object.name)
|
||||||
|
}
|
||||||
deleteObject() {
|
deleteObject() {
|
||||||
const { object, deleteObject } = this.props
|
const { object, deleteObject } = this.props
|
||||||
deleteObject(object.name)
|
deleteObject(object.name)
|
||||||
@@ -53,7 +62,20 @@ export class ObjectActions extends React.Component {
|
|||||||
}
|
}
|
||||||
hideDeleteConfirmModal() {
|
hideDeleteConfirmModal() {
|
||||||
this.setState({
|
this.setState({
|
||||||
showDeleteConfirmation: false
|
showDeleteConfirmation: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
getObjectURL(objectname, callback) {
|
||||||
|
const { getObjectURL } = this.props
|
||||||
|
getObjectURL(objectname, callback)
|
||||||
|
}
|
||||||
|
showPreviewModal(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.setState({ showPreview: true })
|
||||||
|
}
|
||||||
|
hidePreviewModal() {
|
||||||
|
this.setState({
|
||||||
|
showPreview: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
@@ -65,26 +87,54 @@ export class ObjectActions extends React.Component {
|
|||||||
<a
|
<a
|
||||||
href=""
|
href=""
|
||||||
className="fiad-action"
|
className="fiad-action"
|
||||||
|
title="Share"
|
||||||
onClick={this.shareObject.bind(this)}
|
onClick={this.shareObject.bind(this)}
|
||||||
>
|
>
|
||||||
<i className="fas fa-share-alt" />
|
<i className="fas fa-share-alt" />
|
||||||
</a>
|
</a>
|
||||||
|
{getDataType(object.name, object.contentType) == "image" && (
|
||||||
<a
|
<a
|
||||||
href=""
|
href=""
|
||||||
className="fiad-action"
|
className="fiad-action"
|
||||||
|
title="Preview"
|
||||||
|
onClick={this.showPreviewModal.bind(this)}
|
||||||
|
>
|
||||||
|
<i className="far fa-file-image" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
className="fiad-action"
|
||||||
|
title="Download"
|
||||||
|
onClick={this.handleDownload.bind(this)}
|
||||||
|
>
|
||||||
|
<i className="fas fa-cloud-download-alt" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
className="fiad-action"
|
||||||
|
title="Delete"
|
||||||
onClick={this.showDeleteConfirmModal.bind(this)}
|
onClick={this.showDeleteConfirmModal.bind(this)}
|
||||||
>
|
>
|
||||||
<i className="fas fa-trash-alt" />
|
<i className="fas fa-trash-alt" />
|
||||||
</a>
|
</a>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
{(showShareObjectModal && shareObjectName === object.name) &&
|
{showShareObjectModal && shareObjectName === object.name && (
|
||||||
<ShareObjectModal object={object} />}
|
<ShareObjectModal object={object} />
|
||||||
|
)}
|
||||||
{this.state.showDeleteConfirmation && (
|
{this.state.showDeleteConfirmation && (
|
||||||
<DeleteObjectConfirmModal
|
<DeleteObjectConfirmModal
|
||||||
deleteObject={this.deleteObject.bind(this)}
|
deleteObject={this.deleteObject.bind(this)}
|
||||||
hideDeleteConfirmModal={this.hideDeleteConfirmModal.bind(this)}
|
hideDeleteConfirmModal={this.hideDeleteConfirmModal.bind(this)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{this.state.showPreview && (
|
||||||
|
<PreviewObjectModal
|
||||||
|
object={object}
|
||||||
|
hidePreviewModal={this.hidePreviewModal.bind(this)}
|
||||||
|
getObjectURL={this.getObjectURL.bind(this)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -94,15 +144,18 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
return {
|
return {
|
||||||
object: ownProps.object,
|
object: ownProps.object,
|
||||||
showShareObjectModal: state.objects.shareObject.show,
|
showShareObjectModal: state.objects.shareObject.show,
|
||||||
shareObjectName: state.objects.shareObject.object
|
shareObjectName: state.objects.shareObject.object,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = (dispatch) => {
|
||||||
return {
|
return {
|
||||||
|
downloadObject: object => dispatch(objectsActions.downloadObject(object)),
|
||||||
shareObject: (object, days, hours, minutes) =>
|
shareObject: (object, days, hours, minutes) =>
|
||||||
dispatch(objectsActions.shareObject(object, days, hours, minutes)),
|
dispatch(objectsActions.shareObject(object, days, hours, minutes)),
|
||||||
deleteObject: object => dispatch(objectsActions.deleteObject(object))
|
deleteObject: (object) => dispatch(objectsActions.deleteObject(object)),
|
||||||
|
getObjectURL: (object, callback) =>
|
||||||
|
dispatch(objectsActions.getObjectURL(object, callback)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage (C) 2020 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react"
|
||||||
|
import { connect } from "react-redux"
|
||||||
|
import { Dropdown } from "react-bootstrap"
|
||||||
|
import DeleteObjectConfirmModal from "./DeleteObjectConfirmModal"
|
||||||
|
import * as actions from "./actions"
|
||||||
|
|
||||||
|
export class PrefixActions extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
showDeleteConfirmation: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleDownload(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const { object, downloadPrefix } = this.props
|
||||||
|
downloadPrefix(object.name)
|
||||||
|
}
|
||||||
|
deleteObject() {
|
||||||
|
const { object, deleteObject } = this.props
|
||||||
|
deleteObject(object.name)
|
||||||
|
}
|
||||||
|
showDeleteConfirmModal(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.setState({ showDeleteConfirmation: true })
|
||||||
|
}
|
||||||
|
hideDeleteConfirmModal() {
|
||||||
|
this.setState({
|
||||||
|
showDeleteConfirmation: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const { object, showShareObjectModal, shareObjectName } = this.props
|
||||||
|
return (
|
||||||
|
<Dropdown id={`obj-actions-${object.name}`}>
|
||||||
|
<Dropdown.Toggle noCaret className="fia-toggle" />
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
className="fiad-action"
|
||||||
|
title="Download as zip"
|
||||||
|
onClick={this.handleDownload.bind(this)}
|
||||||
|
>
|
||||||
|
<i className="fas fa-cloud-download-alt" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
className="fiad-action"
|
||||||
|
title="Delete"
|
||||||
|
onClick={this.showDeleteConfirmModal.bind(this)}
|
||||||
|
>
|
||||||
|
<i className="fas fa-trash-alt" />
|
||||||
|
</a>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
{this.state.showDeleteConfirmation && (
|
||||||
|
<DeleteObjectConfirmModal
|
||||||
|
deleteObject={this.deleteObject.bind(this)}
|
||||||
|
hideDeleteConfirmModal={this.hideDeleteConfirmModal.bind(this)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state, ownProps) => {
|
||||||
|
return {
|
||||||
|
object: ownProps.object,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => {
|
||||||
|
return {
|
||||||
|
downloadPrefix: object => dispatch(actions.downloadPrefix(object)),
|
||||||
|
deleteObject: (object) => dispatch(actions.deleteObject(object)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PrefixActions)
|
||||||
@@ -17,22 +17,32 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { connect } from "react-redux"
|
import { connect } from "react-redux"
|
||||||
import ObjectItem from "./ObjectItem"
|
import ObjectItem from "./ObjectItem"
|
||||||
|
import PrefixActions from "./PrefixActions"
|
||||||
import * as actionsObjects from "./actions"
|
import * as actionsObjects from "./actions"
|
||||||
|
import { getCheckedList } from "./selectors"
|
||||||
|
|
||||||
export const PrefixContainer = ({ object, currentPrefix, selectPrefix }) => {
|
export const PrefixContainer = ({
|
||||||
|
object,
|
||||||
|
currentPrefix,
|
||||||
|
checkedObjectsCount,
|
||||||
|
selectPrefix
|
||||||
|
}) => {
|
||||||
const props = {
|
const props = {
|
||||||
name: object.name,
|
name: object.name,
|
||||||
contentType: object.contentType,
|
contentType: object.contentType,
|
||||||
onClick: () => selectPrefix(`${currentPrefix}${object.name}`)
|
onClick: () => selectPrefix(`${currentPrefix}${object.name}`)
|
||||||
}
|
}
|
||||||
|
if (checkedObjectsCount == 0) {
|
||||||
|
props.actionButtons = <PrefixActions object={object} />
|
||||||
|
}
|
||||||
return <ObjectItem {...props} />
|
return <ObjectItem {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
return {
|
return {
|
||||||
object: ownProps.object,
|
object: ownProps.object,
|
||||||
currentPrefix: state.objects.currentPrefix
|
currentPrefix: state.objects.currentPrefix,
|
||||||
|
checkedObjectsCount: getCheckedList(state).length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage (C) 2020 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react"
|
||||||
|
import { Modal, ModalHeader, ModalBody } from "react-bootstrap"
|
||||||
|
|
||||||
|
class PreviewObjectModal extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
url: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.getObjectURL(this.props.object.name, (url) => {
|
||||||
|
this.setState({
|
||||||
|
url: url,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { hidePreviewModal } = this.props
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show={true}
|
||||||
|
animation={false}
|
||||||
|
onHide={hidePreviewModal}
|
||||||
|
bsSize="large"
|
||||||
|
>
|
||||||
|
<ModalHeader>Preview</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<div className="input-group">
|
||||||
|
{this.state.url && (
|
||||||
|
<img
|
||||||
|
alt="Image broken"
|
||||||
|
src={this.state.url}
|
||||||
|
style={{ display: "block", width: "100%" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<div className="modal-footer">
|
||||||
|
{
|
||||||
|
<button className="btn btn-link" onClick={hidePreviewModal}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default PreviewObjectModal
|
||||||
@@ -77,7 +77,8 @@ export class ShareObjectModal extends React.Component {
|
|||||||
hideShareObject()
|
hideShareObject()
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const { shareObjectDetails, shareObject, hideShareObject } = this.props
|
const { shareObjectDetails, hideShareObject } = this.props
|
||||||
|
const url = `${window.location.protocol}//${shareObjectDetails.url}`
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
show={true}
|
show={true}
|
||||||
@@ -93,10 +94,11 @@ export class ShareObjectModal extends React.Component {
|
|||||||
type="text"
|
type="text"
|
||||||
ref={node => (this.copyTextInput = node)}
|
ref={node => (this.copyTextInput = node)}
|
||||||
readOnly="readOnly"
|
readOnly="readOnly"
|
||||||
value={window.location.protocol + "//" + shareObjectDetails.url}
|
value={url}
|
||||||
onClick={() => this.copyTextInput.select()}
|
onClick={() => this.copyTextInput.select()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{shareObjectDetails.showExpiryDate && (
|
||||||
<div
|
<div
|
||||||
className="input-group"
|
className="input-group"
|
||||||
style={{ display: web.LoggedIn() ? "block" : "none" }}
|
style={{ display: web.LoggedIn() ? "block" : "none" }}
|
||||||
@@ -174,10 +176,11 @@ export class ShareObjectModal extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
text={window.location.protocol + "//" + shareObjectDetails.url}
|
text={url}
|
||||||
onCopy={this.onUrlCopied.bind(this)}
|
onCopy={this.onUrlCopied.bind(this)}
|
||||||
>
|
>
|
||||||
<button className="btn btn-success">Copy Link</button>
|
<button className="btn btn-success">Copy Link</button>
|
||||||
|
|||||||
@@ -66,6 +66,63 @@ describe("ObjectActions", () => {
|
|||||||
expect(deleteObject).toHaveBeenCalledWith("obj1")
|
expect(deleteObject).toHaveBeenCalledWith("obj1")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it("should call downloadObject when single object is selected and download button is clicked", () => {
|
||||||
|
const downloadObject = jest.fn()
|
||||||
|
const wrapper = shallow(
|
||||||
|
<ObjectActions
|
||||||
|
object={{ name: "obj1" }}
|
||||||
|
currentPrefix={"pre1/"}
|
||||||
|
downloadObject={downloadObject} />
|
||||||
|
)
|
||||||
|
wrapper
|
||||||
|
.find("a")
|
||||||
|
.at(1)
|
||||||
|
.simulate("click", { preventDefault: jest.fn() })
|
||||||
|
expect(downloadObject).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it("should show PreviewObjectModal when preview action is clicked", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<ObjectActions
|
||||||
|
object={{ name: "obj1", contentType: "image/jpeg"}}
|
||||||
|
currentPrefix={"pre1/"} />
|
||||||
|
)
|
||||||
|
wrapper
|
||||||
|
.find("a")
|
||||||
|
.at(1)
|
||||||
|
.simulate("click", { preventDefault: jest.fn() })
|
||||||
|
expect(wrapper.state("showPreview")).toBeTruthy()
|
||||||
|
expect(wrapper.find("PreviewObjectModal").length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should hide PreviewObjectModal when cancel button is clicked", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<ObjectActions
|
||||||
|
object={{ name: "obj1" , contentType: "image/jpeg"}}
|
||||||
|
currentPrefix={"pre1/"} />
|
||||||
|
)
|
||||||
|
wrapper
|
||||||
|
.find("a")
|
||||||
|
.at(1)
|
||||||
|
.simulate("click", { preventDefault: jest.fn() })
|
||||||
|
wrapper.find("PreviewObjectModal").prop("hidePreviewModal")()
|
||||||
|
wrapper.update()
|
||||||
|
expect(wrapper.state("showPreview")).toBeFalsy()
|
||||||
|
expect(wrapper.find("PreviewObjectModal").length).toBe(0)
|
||||||
|
})
|
||||||
|
it("should not show PreviewObjectModal when preview action is clicked if object is not an image", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<ObjectActions
|
||||||
|
object={{ name: "obj1"}}
|
||||||
|
currentPrefix={"pre1/"} />
|
||||||
|
)
|
||||||
|
expect(wrapper
|
||||||
|
.find("a")
|
||||||
|
.length).toBe(3) // find only the other 2
|
||||||
|
})
|
||||||
|
|
||||||
it("should call shareObject with object and expiry", () => {
|
it("should call shareObject with object and expiry", () => {
|
||||||
const shareObject = jest.fn()
|
const shareObject = jest.fn()
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage (C) 2018 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react"
|
||||||
|
import { shallow } from "enzyme"
|
||||||
|
import { PrefixActions } from "../PrefixActions"
|
||||||
|
|
||||||
|
describe("PrefixActions", () => {
|
||||||
|
it("should render without crashing", () => {
|
||||||
|
shallow(<PrefixActions object={{ name: "abc/" }} currentPrefix={"pre1/"} />)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should show DeleteObjectConfirmModal when delete action is clicked", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<PrefixActions object={{ name: "abc/" }} currentPrefix={"pre1/"} />
|
||||||
|
)
|
||||||
|
wrapper
|
||||||
|
.find("a")
|
||||||
|
.last()
|
||||||
|
.simulate("click", { preventDefault: jest.fn() })
|
||||||
|
expect(wrapper.state("showDeleteConfirmation")).toBeTruthy()
|
||||||
|
expect(wrapper.find("DeleteObjectConfirmModal").length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should hide DeleteObjectConfirmModal when Cancel button is clicked", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<PrefixActions object={{ name: "abc/" }} currentPrefix={"pre1/"} />
|
||||||
|
)
|
||||||
|
wrapper
|
||||||
|
.find("a")
|
||||||
|
.last()
|
||||||
|
.simulate("click", { preventDefault: jest.fn() })
|
||||||
|
wrapper.find("DeleteObjectConfirmModal").prop("hideDeleteConfirmModal")()
|
||||||
|
wrapper.update()
|
||||||
|
expect(wrapper.state("showDeleteConfirmation")).toBeFalsy()
|
||||||
|
expect(wrapper.find("DeleteObjectConfirmModal").length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call deleteObject with object name", () => {
|
||||||
|
const deleteObject = jest.fn()
|
||||||
|
const wrapper = shallow(
|
||||||
|
<PrefixActions
|
||||||
|
object={{ name: "abc/" }}
|
||||||
|
currentPrefix={"pre1/"}
|
||||||
|
deleteObject={deleteObject}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
wrapper
|
||||||
|
.find("a")
|
||||||
|
.last()
|
||||||
|
.simulate("click", { preventDefault: jest.fn() })
|
||||||
|
wrapper.find("DeleteObjectConfirmModal").prop("deleteObject")()
|
||||||
|
expect(deleteObject).toHaveBeenCalledWith("abc/")
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it("should call downloadPrefix when single object is selected and download button is clicked", () => {
|
||||||
|
const downloadPrefix = jest.fn()
|
||||||
|
const wrapper = shallow(
|
||||||
|
<PrefixActions
|
||||||
|
object={{ name: "abc/" }}
|
||||||
|
currentPrefix={"pre1/"}
|
||||||
|
downloadPrefix={downloadPrefix} />
|
||||||
|
)
|
||||||
|
wrapper
|
||||||
|
.find("a")
|
||||||
|
.first()
|
||||||
|
.simulate("click", { preventDefault: jest.fn() })
|
||||||
|
expect(downloadPrefix).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -41,4 +41,22 @@ describe("PrefixContainer", () => {
|
|||||||
wrapper.find("Connect(ObjectItem)").prop("onClick")()
|
wrapper.find("Connect(ObjectItem)").prop("onClick")()
|
||||||
expect(selectPrefix).toHaveBeenCalledWith("xyz/abc/")
|
expect(selectPrefix).toHaveBeenCalledWith("xyz/abc/")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should pass actions to ObjectItem", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<PrefixContainer object={{ name: "abc/" }} checkedObjectsCount={0} />
|
||||||
|
)
|
||||||
|
expect(wrapper.find("Connect(ObjectItem)").prop("actionButtons")).not.toBe(
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should pass empty actions to ObjectItem when checkedObjectCount is more than 0", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<PrefixContainer object={{ name: "abc/" }} checkedObjectsCount={1} />
|
||||||
|
)
|
||||||
|
expect(wrapper.find("Connect(ObjectItem)").prop("actionButtons")).toBe(
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ describe("ShareObjectModal", () => {
|
|||||||
shallow(
|
shallow(
|
||||||
<ShareObjectModal
|
<ShareObjectModal
|
||||||
object={{ name: "obj1" }}
|
object={{ name: "obj1" }}
|
||||||
shareObjectDetails={{ show: true, object: "obj1", url: "test" }}
|
shareObjectDetails={{ show: true, object: "obj1", url: "test", showExpiryDate: true }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -44,7 +44,7 @@ describe("ShareObjectModal", () => {
|
|||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<ShareObjectModal
|
<ShareObjectModal
|
||||||
object={{ name: "obj1" }}
|
object={{ name: "obj1" }}
|
||||||
shareObjectDetails={{ show: true, object: "obj1", url: "test" }}
|
shareObjectDetails={{ show: true, object: "obj1", url: "test", showExpiryDate: true }}
|
||||||
hideShareObject={hideShareObject}
|
hideShareObject={hideShareObject}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -59,7 +59,7 @@ describe("ShareObjectModal", () => {
|
|||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<ShareObjectModal
|
<ShareObjectModal
|
||||||
object={{ name: "obj1" }}
|
object={{ name: "obj1" }}
|
||||||
shareObjectDetails={{ show: true, object: "obj1", url: "test" }}
|
shareObjectDetails={{ show: true, object: "obj1", url: "test", showExpiryDate: true }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
expect(
|
expect(
|
||||||
@@ -76,7 +76,7 @@ describe("ShareObjectModal", () => {
|
|||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<ShareObjectModal
|
<ShareObjectModal
|
||||||
object={{ name: "obj1" }}
|
object={{ name: "obj1" }}
|
||||||
shareObjectDetails={{ show: true, object: "obj1", url: "test" }}
|
shareObjectDetails={{ show: true, object: "obj1", url: "test", showExpiryDate: true }}
|
||||||
hideShareObject={hideShareObject}
|
hideShareObject={hideShareObject}
|
||||||
showCopyAlert={showCopyAlert}
|
showCopyAlert={showCopyAlert}
|
||||||
/>
|
/>
|
||||||
@@ -89,8 +89,15 @@ describe("ShareObjectModal", () => {
|
|||||||
describe("Update expiry values", () => {
|
describe("Update expiry values", () => {
|
||||||
const props = {
|
const props = {
|
||||||
object: { name: "obj1" },
|
object: { name: "obj1" },
|
||||||
shareObjectDetails: { show: true, object: "obj1", url: "test" }
|
shareObjectDetails: { show: true, object: "obj1", url: "test", showExpiryDate: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it("should not show expiry values if shared with public link", () => {
|
||||||
|
const shareObjectDetails = { show: true, object: "obj1", url: "test", showExpiryDate: false }
|
||||||
|
const wrapper = shallow(<ShareObjectModal {...props} shareObjectDetails={shareObjectDetails} />)
|
||||||
|
expect(wrapper.find('.set-expire').exists()).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
it("should have default expiry values", () => {
|
it("should have default expiry values", () => {
|
||||||
const wrapper = shallow(<ShareObjectModal {...props} />)
|
const wrapper = shallow(<ShareObjectModal {...props} />)
|
||||||
expect(wrapper.state("expiry")).toEqual({
|
expect(wrapper.state("expiry")).toEqual({
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ jest.mock("../../web", () => ({
|
|||||||
.mockReturnValueOnce(false)
|
.mockReturnValueOnce(false)
|
||||||
.mockReturnValueOnce(true)
|
.mockReturnValueOnce(true)
|
||||||
.mockReturnValueOnce(true)
|
.mockReturnValueOnce(true)
|
||||||
|
.mockReturnValueOnce(true)
|
||||||
.mockReturnValueOnce(false),
|
.mockReturnValueOnce(false),
|
||||||
ListObjects: jest.fn(({ bucketName }) => {
|
ListObjects: jest.fn(({ bucketName }) => {
|
||||||
if (bucketName === "test-deny") {
|
if (bucketName === "test-deny") {
|
||||||
@@ -70,6 +71,16 @@ jest.mock("../../web", () => ({
|
|||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
return Promise.resolve({ token: "test" })
|
return Promise.resolve({ token: "test" })
|
||||||
})
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve({ token: "test" })
|
||||||
|
}),
|
||||||
|
GetBucketPolicy: jest.fn(({ bucketName, prefix }) => {
|
||||||
|
if (!bucketName) {
|
||||||
|
return Promise.reject({ message: "Invalid bucket" })
|
||||||
|
}
|
||||||
|
if (bucketName === 'test-public') return Promise.resolve({ policy: 'readonly' })
|
||||||
|
return Promise.resolve({})
|
||||||
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const middlewares = [thunk]
|
const middlewares = [thunk]
|
||||||
@@ -295,7 +306,8 @@ describe("Objects actions", () => {
|
|||||||
type: "objects/SET_SHARE_OBJECT",
|
type: "objects/SET_SHARE_OBJECT",
|
||||||
show: true,
|
show: true,
|
||||||
object: "b.txt",
|
object: "b.txt",
|
||||||
url: "test"
|
url: "test",
|
||||||
|
showExpiryDate: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
store.dispatch(actionsObjects.showShareObject("b.txt", "test"))
|
store.dispatch(actionsObjects.showShareObject("b.txt", "test"))
|
||||||
@@ -321,14 +333,16 @@ describe("Objects actions", () => {
|
|||||||
it("creates objects/SET_SHARE_OBJECT when object is shared", () => {
|
it("creates objects/SET_SHARE_OBJECT when object is shared", () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
buckets: { currentBucket: "bk1" },
|
buckets: { currentBucket: "bk1" },
|
||||||
objects: { currentPrefix: "pre1/" }
|
objects: { currentPrefix: "pre1/" },
|
||||||
|
browser: { serverInfo: {} },
|
||||||
})
|
})
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{
|
{
|
||||||
type: "objects/SET_SHARE_OBJECT",
|
type: "objects/SET_SHARE_OBJECT",
|
||||||
show: true,
|
show: true,
|
||||||
object: "a.txt",
|
object: "a.txt",
|
||||||
url: "https://test.com/bk1/pre1/b.txt"
|
url: "https://test.com/bk1/pre1/b.txt",
|
||||||
|
showExpiryDate: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "alert/SET",
|
type: "alert/SET",
|
||||||
@@ -347,10 +361,42 @@ describe("Objects actions", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("creates objects/SET_SHARE_OBJECT when object is shared with public link", () => {
|
||||||
|
const store = mockStore({
|
||||||
|
buckets: { currentBucket: "test-public" },
|
||||||
|
objects: { currentPrefix: "pre1/" },
|
||||||
|
browser: { serverInfo: { info: { domains: ['public.com'] }} },
|
||||||
|
})
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: "objects/SET_SHARE_OBJECT",
|
||||||
|
show: true,
|
||||||
|
object: "a.txt",
|
||||||
|
url: "public.com/test-public/pre1/a.txt",
|
||||||
|
showExpiryDate: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "alert/SET",
|
||||||
|
alert: {
|
||||||
|
type: "success",
|
||||||
|
message: "Object shared.",
|
||||||
|
id: alertActions.alertId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return store
|
||||||
|
.dispatch(actionsObjects.shareObject("a.txt", 1, 0, 0))
|
||||||
|
.then(() => {
|
||||||
|
const actions = store.getActions()
|
||||||
|
expect(actions).toEqual(expectedActions)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("creates alert/SET when shareObject is failed", () => {
|
it("creates alert/SET when shareObject is failed", () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
buckets: { currentBucket: "" },
|
buckets: { currentBucket: "" },
|
||||||
objects: { currentPrefix: "pre1/" }
|
objects: { currentPrefix: "pre1/" },
|
||||||
|
browser: { serverInfo: {} },
|
||||||
})
|
})
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{
|
{
|
||||||
@@ -442,6 +488,34 @@ describe("Objects actions", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should download prefix", () => {
|
||||||
|
const open = jest.fn()
|
||||||
|
const send = jest.fn()
|
||||||
|
const xhrMockClass = () => ({
|
||||||
|
open: open,
|
||||||
|
send: send
|
||||||
|
})
|
||||||
|
window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass)
|
||||||
|
|
||||||
|
const store = mockStore({
|
||||||
|
buckets: { currentBucket: "bk1" },
|
||||||
|
objects: { currentPrefix: "pre1/" }
|
||||||
|
})
|
||||||
|
return store.dispatch(actionsObjects.downloadPrefix("pre2/")).then(() => {
|
||||||
|
const requestUrl = `${
|
||||||
|
location.origin
|
||||||
|
}${minioBrowserPrefix}/zip?token=test`
|
||||||
|
expect(open).toHaveBeenCalledWith("POST", requestUrl, true)
|
||||||
|
expect(send).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify({
|
||||||
|
bucketName: "bk1",
|
||||||
|
prefix: "pre1/",
|
||||||
|
objects: ["pre2/"]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("creates objects/CHECKED_LIST_ADD action", () => {
|
it("creates objects/CHECKED_LIST_ADD action", () => {
|
||||||
const store = mockStore()
|
const store = mockStore()
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
|
|||||||
@@ -19,20 +19,20 @@ import history from "../history"
|
|||||||
import {
|
import {
|
||||||
sortObjectsByName,
|
sortObjectsByName,
|
||||||
sortObjectsBySize,
|
sortObjectsBySize,
|
||||||
sortObjectsByDate
|
sortObjectsByDate,
|
||||||
} from "../utils"
|
} from "../utils"
|
||||||
import { getCurrentBucket } from "../buckets/selectors"
|
import { getCurrentBucket } from "../buckets/selectors"
|
||||||
import { getCurrentPrefix, getCheckedList } from "./selectors"
|
import { getCurrentPrefix, getCheckedList } from "./selectors"
|
||||||
import * as alertActions from "../alert/actions"
|
import * as alertActions from "../alert/actions"
|
||||||
import * as bucketActions from "../buckets/actions"
|
|
||||||
import {
|
import {
|
||||||
minioBrowserPrefix,
|
minioBrowserPrefix,
|
||||||
SORT_BY_NAME,
|
SORT_BY_NAME,
|
||||||
SORT_BY_SIZE,
|
SORT_BY_SIZE,
|
||||||
SORT_BY_LAST_MODIFIED,
|
SORT_BY_LAST_MODIFIED,
|
||||||
SORT_ORDER_ASC,
|
SORT_ORDER_ASC,
|
||||||
SORT_ORDER_DESC
|
SORT_ORDER_DESC,
|
||||||
} from "../constants"
|
} from "../constants"
|
||||||
|
import { getServerInfo, hasServerPublicDomain } from '../browser/selectors'
|
||||||
|
|
||||||
export const SET_LIST = "objects/SET_LIST"
|
export const SET_LIST = "objects/SET_LIST"
|
||||||
export const RESET_LIST = "objects/RESET_LIST"
|
export const RESET_LIST = "objects/RESET_LIST"
|
||||||
@@ -48,35 +48,35 @@ export const CHECKED_LIST_REMOVE = "objects/CHECKED_LIST_REMOVE"
|
|||||||
export const CHECKED_LIST_RESET = "objects/CHECKED_LIST_RESET"
|
export const CHECKED_LIST_RESET = "objects/CHECKED_LIST_RESET"
|
||||||
export const SET_LIST_LOADING = "objects/SET_LIST_LOADING"
|
export const SET_LIST_LOADING = "objects/SET_LIST_LOADING"
|
||||||
|
|
||||||
export const setList = objects => ({
|
export const setList = (objects) => ({
|
||||||
type: SET_LIST,
|
type: SET_LIST,
|
||||||
objects
|
objects,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const resetList = () => ({
|
export const resetList = () => ({
|
||||||
type: RESET_LIST
|
type: RESET_LIST,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const setListLoading = listLoading => ({
|
export const setListLoading = (listLoading) => ({
|
||||||
type: SET_LIST_LOADING,
|
type: SET_LIST_LOADING,
|
||||||
listLoading
|
listLoading,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const fetchObjects = () => {
|
export const fetchObjects = () => {
|
||||||
return function(dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
dispatch(resetList())
|
dispatch(resetList())
|
||||||
const {
|
const {
|
||||||
buckets: { currentBucket },
|
buckets: { currentBucket },
|
||||||
objects: { currentPrefix }
|
objects: { currentPrefix },
|
||||||
} = getState()
|
} = getState()
|
||||||
if (currentBucket) {
|
if (currentBucket) {
|
||||||
dispatch(setListLoading(true))
|
dispatch(setListLoading(true))
|
||||||
return web
|
return web
|
||||||
.ListObjects({
|
.ListObjects({
|
||||||
bucketName: currentBucket,
|
bucketName: currentBucket,
|
||||||
prefix: currentPrefix
|
prefix: currentPrefix,
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then((res) => {
|
||||||
// we need to check if the bucket name and prefix are the same as
|
// we need to check if the bucket name and prefix are the same as
|
||||||
// when the request was made before updating the displayed objects
|
// when the request was made before updating the displayed objects
|
||||||
if (
|
if (
|
||||||
@@ -85,10 +85,10 @@ export const fetchObjects = () => {
|
|||||||
) {
|
) {
|
||||||
let objects = []
|
let objects = []
|
||||||
if (res.objects) {
|
if (res.objects) {
|
||||||
objects = res.objects.map(object => {
|
objects = res.objects.map((object) => {
|
||||||
return {
|
return {
|
||||||
...object,
|
...object,
|
||||||
name: object.name.replace(currentPrefix, "")
|
name: object.name.replace(currentPrefix, ""),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -104,13 +104,13 @@ export const fetchObjects = () => {
|
|||||||
dispatch(setListLoading(false))
|
dispatch(setListLoading(false))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
if (web.LoggedIn()) {
|
if (web.LoggedIn()) {
|
||||||
dispatch(
|
dispatch(
|
||||||
alertActions.set({
|
alertActions.set({
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: err.message,
|
message: err.message,
|
||||||
autoClear: true
|
autoClear: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
dispatch(resetList())
|
dispatch(resetList())
|
||||||
@@ -123,8 +123,8 @@ export const fetchObjects = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sortObjects = sortBy => {
|
export const sortObjects = (sortBy) => {
|
||||||
return function(dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
const { objects } = getState()
|
const { objects } = getState()
|
||||||
let sortOrder = SORT_ORDER_ASC
|
let sortOrder = SORT_ORDER_ASC
|
||||||
// Reverse sort order if the list is already sorted on same field
|
// Reverse sort order if the list is already sorted on same field
|
||||||
@@ -149,18 +149,18 @@ const sortObjectsList = (list, sortBy, sortOrder) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setSortBy = sortBy => ({
|
export const setSortBy = (sortBy) => ({
|
||||||
type: SET_SORT_BY,
|
type: SET_SORT_BY,
|
||||||
sortBy
|
sortBy,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const setSortOrder = sortOrder => ({
|
export const setSortOrder = (sortOrder) => ({
|
||||||
type: SET_SORT_ORDER,
|
type: SET_SORT_ORDER,
|
||||||
sortOrder
|
sortOrder,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const selectPrefix = prefix => {
|
export const selectPrefix = (prefix) => {
|
||||||
return function(dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
dispatch(setCurrentPrefix(prefix))
|
dispatch(setCurrentPrefix(prefix))
|
||||||
dispatch(fetchObjects())
|
dispatch(fetchObjects())
|
||||||
dispatch(resetCheckedList())
|
dispatch(resetCheckedList())
|
||||||
@@ -169,49 +169,49 @@ export const selectPrefix = prefix => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setCurrentPrefix = prefix => {
|
export const setCurrentPrefix = (prefix) => {
|
||||||
return {
|
return {
|
||||||
type: SET_CURRENT_PREFIX,
|
type: SET_CURRENT_PREFIX,
|
||||||
prefix
|
prefix,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setPrefixWritable = prefixWritable => ({
|
export const setPrefixWritable = (prefixWritable) => ({
|
||||||
type: SET_PREFIX_WRITABLE,
|
type: SET_PREFIX_WRITABLE,
|
||||||
prefixWritable
|
prefixWritable,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deleteObject = object => {
|
export const deleteObject = (object) => {
|
||||||
return function(dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
const currentBucket = getCurrentBucket(getState())
|
const currentBucket = getCurrentBucket(getState())
|
||||||
const currentPrefix = getCurrentPrefix(getState())
|
const currentPrefix = getCurrentPrefix(getState())
|
||||||
const objectName = `${currentPrefix}${object}`
|
const objectName = `${currentPrefix}${object}`
|
||||||
return web
|
return web
|
||||||
.RemoveObject({
|
.RemoveObject({
|
||||||
bucketName: currentBucket,
|
bucketName: currentBucket,
|
||||||
objects: [objectName]
|
objects: [objectName],
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(removeObject(object))
|
dispatch(removeObject(object))
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
alertActions.set({
|
alertActions.set({
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: e.message
|
message: e.message,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const removeObject = object => ({
|
export const removeObject = (object) => ({
|
||||||
type: REMOVE,
|
type: REMOVE,
|
||||||
object
|
object,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deleteCheckedObjects = () => {
|
export const deleteCheckedObjects = () => {
|
||||||
return function(dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
const checkedObjects = getCheckedList(getState())
|
const checkedObjects = getCheckedList(getState())
|
||||||
for (let i = 0; i < checkedObjects.length; i++) {
|
for (let i = 0; i < checkedObjects.length; i++) {
|
||||||
dispatch(deleteObject(checkedObjects[i]))
|
dispatch(deleteObject(checkedObjects[i]))
|
||||||
@@ -221,12 +221,28 @@ export const deleteCheckedObjects = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const shareObject = (object, days, hours, minutes) => {
|
export const shareObject = (object, days, hours, minutes) => {
|
||||||
return function(dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
|
const hasServerDomain = hasServerPublicDomain(getState())
|
||||||
const currentBucket = getCurrentBucket(getState())
|
const currentBucket = getCurrentBucket(getState())
|
||||||
const currentPrefix = getCurrentPrefix(getState())
|
const currentPrefix = getCurrentPrefix(getState())
|
||||||
const objectName = `${currentPrefix}${object}`
|
const objectName = `${currentPrefix}${object}`
|
||||||
const expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
|
const expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
|
||||||
if (web.LoggedIn()) {
|
if (web.LoggedIn()) {
|
||||||
|
return web
|
||||||
|
.GetBucketPolicy({ bucketName: currentBucket, prefix: currentPrefix })
|
||||||
|
.catch(() => ({ policy: null }))
|
||||||
|
.then(({ policy }) => {
|
||||||
|
if (hasServerDomain && ['readonly', 'readwrite'].includes(policy)) {
|
||||||
|
const domain = getServerInfo(getState()).info.domains[0]
|
||||||
|
const url = `${domain}/${currentBucket}/${encodeURI(objectName)}`
|
||||||
|
dispatch(showShareObject(object, url, false))
|
||||||
|
dispatch(
|
||||||
|
alertActions.set({
|
||||||
|
type: "success",
|
||||||
|
message: "Object shared."
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
return web
|
return web
|
||||||
.PresignedGet({
|
.PresignedGet({
|
||||||
host: location.host,
|
host: location.host,
|
||||||
@@ -234,20 +250,23 @@ export const shareObject = (object, days, hours, minutes) => {
|
|||||||
object: objectName,
|
object: objectName,
|
||||||
expiry: expiry
|
expiry: expiry
|
||||||
})
|
})
|
||||||
.then(obj => {
|
}
|
||||||
|
})
|
||||||
|
.then((obj) => {
|
||||||
|
if (!obj) return
|
||||||
dispatch(showShareObject(object, obj.url))
|
dispatch(showShareObject(object, obj.url))
|
||||||
dispatch(
|
dispatch(
|
||||||
alertActions.set({
|
alertActions.set({
|
||||||
type: "success",
|
type: "success",
|
||||||
message: `Object shared. Expires in ${days} days ${hours} hours ${minutes} minutes`
|
message: `Object shared. Expires in ${days} days ${hours} hours ${minutes} minutes`,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
alertActions.set({
|
alertActions.set({
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: err.message
|
message: err.message,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -265,29 +284,29 @@ export const shareObject = (object, days, hours, minutes) => {
|
|||||||
dispatch(
|
dispatch(
|
||||||
alertActions.set({
|
alertActions.set({
|
||||||
type: "success",
|
type: "success",
|
||||||
message: `Object shared.`
|
message: `Object shared.`,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showShareObject = (object, url) => ({
|
export const showShareObject = (object, url, showExpiryDate = true) => ({
|
||||||
type: SET_SHARE_OBJECT,
|
type: SET_SHARE_OBJECT,
|
||||||
show: true,
|
show: true,
|
||||||
object,
|
object,
|
||||||
url
|
url,
|
||||||
|
showExpiryDate,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const hideShareObject = (object, url) => ({
|
export const hideShareObject = (object, url) => ({
|
||||||
type: SET_SHARE_OBJECT,
|
type: SET_SHARE_OBJECT,
|
||||||
show: false,
|
show: false,
|
||||||
object: "",
|
object: "",
|
||||||
url: ""
|
url: "",
|
||||||
})
|
})
|
||||||
|
export const getObjectURL = (object, callback) => {
|
||||||
export const downloadObject = object => {
|
return function (dispatch, getState) {
|
||||||
return function(dispatch, getState) {
|
|
||||||
const currentBucket = getCurrentBucket(getState())
|
const currentBucket = getCurrentBucket(getState())
|
||||||
const currentPrefix = getCurrentPrefix(getState())
|
const currentPrefix = getCurrentPrefix(getState())
|
||||||
const objectName = `${currentPrefix}${object}`
|
const objectName = `${currentPrefix}${object}`
|
||||||
@@ -295,78 +314,119 @@ export const downloadObject = object => {
|
|||||||
if (web.LoggedIn()) {
|
if (web.LoggedIn()) {
|
||||||
return web
|
return web
|
||||||
.CreateURLToken()
|
.CreateURLToken()
|
||||||
.then(res => {
|
.then((res) => {
|
||||||
const url = `${
|
const url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=${res.token}`
|
||||||
window.location.origin
|
callback(url)
|
||||||
}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=${
|
|
||||||
res.token
|
|
||||||
}`
|
|
||||||
window.location = url
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
alertActions.set({
|
alertActions.set({
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: err.message
|
message: err.message,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const url = `${
|
const url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=`
|
||||||
window.location.origin
|
callback(url)
|
||||||
}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=`
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const downloadObject = (object) => {
|
||||||
|
return function (dispatch, getState) {
|
||||||
|
const currentBucket = getCurrentBucket(getState())
|
||||||
|
const currentPrefix = getCurrentPrefix(getState())
|
||||||
|
const objectName = `${currentPrefix}${object}`
|
||||||
|
const encObjectName = encodeURI(objectName)
|
||||||
|
if (web.LoggedIn()) {
|
||||||
|
return web
|
||||||
|
.CreateURLToken()
|
||||||
|
.then((res) => {
|
||||||
|
const url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=${res.token}`
|
||||||
|
window.location = url
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
dispatch(
|
||||||
|
alertActions.set({
|
||||||
|
type: "danger",
|
||||||
|
message: err.message,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=`
|
||||||
window.location = url
|
window.location = url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkObject = object => ({
|
export const downloadPrefix = (object) => {
|
||||||
|
return function (dispatch, getState) {
|
||||||
|
return downloadObjects(
|
||||||
|
getCurrentBucket(getState()),
|
||||||
|
getCurrentPrefix(getState()),
|
||||||
|
[object],
|
||||||
|
`${object.slice(0, -1)}.zip`,
|
||||||
|
dispatch
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const checkObject = (object) => ({
|
||||||
type: CHECKED_LIST_ADD,
|
type: CHECKED_LIST_ADD,
|
||||||
object
|
object,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const uncheckObject = object => ({
|
export const uncheckObject = (object) => ({
|
||||||
type: CHECKED_LIST_REMOVE,
|
type: CHECKED_LIST_REMOVE,
|
||||||
object
|
object,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const resetCheckedList = () => ({
|
export const resetCheckedList = () => ({
|
||||||
type: CHECKED_LIST_RESET
|
type: CHECKED_LIST_RESET,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const downloadCheckedObjects = () => {
|
export const downloadCheckedObjects = () => {
|
||||||
return function(dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
const state = getState()
|
return downloadObjects(
|
||||||
const req = {
|
getCurrentBucket(getState()),
|
||||||
bucketName: getCurrentBucket(state),
|
getCurrentPrefix(getState()),
|
||||||
prefix: getCurrentPrefix(state),
|
getCheckedList(getState()),
|
||||||
objects: getCheckedList(state)
|
null,
|
||||||
}
|
dispatch
|
||||||
if (!web.LoggedIn()) {
|
|
||||||
const requestUrl = location.origin + "/minio/zip?token="
|
|
||||||
downloadZip(requestUrl, req, dispatch)
|
|
||||||
} else {
|
|
||||||
return web
|
|
||||||
.CreateURLToken()
|
|
||||||
.then(res => {
|
|
||||||
const requestUrl = `${
|
|
||||||
location.origin
|
|
||||||
}${minioBrowserPrefix}/zip?token=${res.token}`
|
|
||||||
downloadZip(requestUrl, req, dispatch)
|
|
||||||
})
|
|
||||||
.catch(err =>
|
|
||||||
dispatch(
|
|
||||||
alertActions.set({
|
|
||||||
type: "danger",
|
|
||||||
message: err.message
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadZip = (url, req, dispatch) => {
|
const downloadObjects = (bucketName, prefix, objects, filename, dispatch) => {
|
||||||
|
const req = {
|
||||||
|
bucketName: bucketName,
|
||||||
|
prefix: prefix,
|
||||||
|
objects: objects,
|
||||||
|
}
|
||||||
|
if (web.LoggedIn()) {
|
||||||
|
return web
|
||||||
|
.CreateURLToken()
|
||||||
|
.then((res) => {
|
||||||
|
const requestUrl = `${location.origin}${minioBrowserPrefix}/zip?token=${res.token}`
|
||||||
|
downloadZip(requestUrl, req, filename, dispatch)
|
||||||
|
})
|
||||||
|
.catch((err) =>
|
||||||
|
dispatch(
|
||||||
|
alertActions.set({
|
||||||
|
type: "danger",
|
||||||
|
message: err.message,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const requestUrl = `${location.origin}${minioBrowserPrefix}/zip?token=`
|
||||||
|
downloadZip(requestUrl, req, filename, dispatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadZip = (url, req, filename, dispatch) => {
|
||||||
var anchor = document.createElement("a")
|
var anchor = document.createElement("a")
|
||||||
document.body.appendChild(anchor)
|
document.body.appendChild(anchor)
|
||||||
|
|
||||||
@@ -374,17 +434,17 @@ const downloadZip = (url, req, dispatch) => {
|
|||||||
xhr.open("POST", url, true)
|
xhr.open("POST", url, true)
|
||||||
xhr.responseType = "blob"
|
xhr.responseType = "blob"
|
||||||
|
|
||||||
xhr.onload = function(e) {
|
xhr.onload = function (e) {
|
||||||
if (this.status == 200) {
|
if (this.status == 200) {
|
||||||
dispatch(resetCheckedList())
|
dispatch(resetCheckedList())
|
||||||
var blob = new Blob([this.response], {
|
var blob = new Blob([this.response], {
|
||||||
type: "octet/stream"
|
type: "octet/stream",
|
||||||
})
|
})
|
||||||
var blobUrl = window.URL.createObjectURL(blob)
|
var blobUrl = window.URL.createObjectURL(blob)
|
||||||
var separator = req.prefix.length > 1 ? "-" : ""
|
var separator = req.prefix.length > 1 ? "-" : ""
|
||||||
|
|
||||||
anchor.href = blobUrl
|
anchor.href = blobUrl
|
||||||
anchor.download =
|
anchor.download = filename ||
|
||||||
req.bucketName + separator + req.prefix.slice(0, -1) + ".zip"
|
req.bucketName + separator + req.prefix.slice(0, -1) + ".zip"
|
||||||
|
|
||||||
anchor.click()
|
anchor.click()
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ export default (
|
|||||||
shareObject: {
|
shareObject: {
|
||||||
show: action.show,
|
show: action.show,
|
||||||
object: action.object,
|
object: action.object,
|
||||||
url: action.url
|
url: action.url,
|
||||||
|
showExpiryDate: action.showExpiryDate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case actionsObjects.CHECKED_LIST_ADD:
|
case actionsObjects.CHECKED_LIST_ADD:
|
||||||
|
|||||||
Generated
+7041
-3580
File diff suppressed because it is too large
Load Diff
+47
-48
@@ -28,71 +28,70 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/minio/minio",
|
"homepage": "https://github.com/minio/minio",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"async": "^1.5.2",
|
"async": "^3.2.0",
|
||||||
"babel-cli": "^6.26.0",
|
"babel-cli": "^6.26.0",
|
||||||
"babel-core": "^6.26.3",
|
"babel-core": "^6.26.3",
|
||||||
"babel-jest": "^22.1.0",
|
"babel-jest": "^23.6.0",
|
||||||
"babel-loader": "^7.1.2",
|
"babel-loader": "^7.1.2",
|
||||||
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
|
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
"babel-polyfill": "^6.23.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"babel-preset-es2015": "^6.14.0",
|
"babel-preset-es2015": "^6.14.0",
|
||||||
"babel-preset-react": "^6.11.1",
|
"babel-preset-react": "^6.11.1",
|
||||||
"babel-register": "^6.26.0",
|
"babel-register": "^6.26.0",
|
||||||
"copy-webpack-plugin": "^4.6.0",
|
"copy-webpack-plugin": "^6.0.1",
|
||||||
"css-loader": "^0.23.1",
|
"css-loader": "^3.5.3",
|
||||||
"enzyme": "^3.10.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-adapter-react-16": "^1.1.1",
|
"enzyme-adapter-react-16": "^1.15.2",
|
||||||
"esformatter": "^0.10.0",
|
"esformatter": "^0.11.3",
|
||||||
"esformatter-jsx": "^7.4.1",
|
"esformatter-jsx": "^8.0.1",
|
||||||
"esformatter-jsx-ignore": "^1.0.6",
|
"esformatter-jsx-ignore": "^1.0.6",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^4.3.0",
|
||||||
"jest": "^22.1.4",
|
"jest": "^23.6.0",
|
||||||
"jest-enzyme": "^4.0.2",
|
"jest-enzyme": "^7.1.2",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.7",
|
||||||
"less": "^3.9.0",
|
"less": "^3.11.1",
|
||||||
"less-loader": "^4.1.0",
|
"less-loader": "^6.1.0",
|
||||||
"purgecss-webpack-plugin": "^1.4.0",
|
"purgecss-webpack-plugin": "^2.2.0",
|
||||||
"style-loader": "^0.13.1",
|
"style-loader": "^1.2.1",
|
||||||
"url-loader": "^0.5.7",
|
"url-loader": "^4.1.0",
|
||||||
"webpack-cli": "^3.2.0",
|
"webpack-cli": "^3.3.11",
|
||||||
"webpack-dev-server": "^3.1.14"
|
"webpack-dev-server": "^3.11.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.10.0",
|
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||||
"bootstrap": "^3.4.1",
|
"bootstrap": "^3.4.1",
|
||||||
"classnames": "^2.2.3",
|
"classnames": "^2.2.6",
|
||||||
"core-js": "^3.2.1",
|
"core-js": "^3.6.5",
|
||||||
"expect": "^1.20.2",
|
"expect": "^26.0.1",
|
||||||
"glob-all": "^3.1.0",
|
"glob-all": "^3.2.1",
|
||||||
"history": "^4.7.2",
|
"history": "^4.10.1",
|
||||||
"humanize": "0.0.9",
|
"humanize": "0.0.9",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"json-loader": "^0.5.4",
|
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"local-storage-fallback": "^4.0.2",
|
"local-storage-fallback": "^4.1.1",
|
||||||
"material-design-iconic-font": "^2.2.0",
|
"material-design-iconic-font": "^2.2.0",
|
||||||
"mime-db": "^1.25.0",
|
"mime-db": "^1.44.0",
|
||||||
"mime-types": "^2.1.13",
|
"mime-types": "^2.1.27",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.26.0",
|
||||||
"query-string": "^6.8.2",
|
"query-string": "^6.12.1",
|
||||||
"react": "^16.2.0",
|
"react": "^16.13.1",
|
||||||
"react-addons-test-utils": "^0.14.8",
|
"react-addons-test-utils": "^15.6.2",
|
||||||
"react-bootstrap": "^0.32.0",
|
"react-bootstrap": "^0.32.4",
|
||||||
"react-copy-to-clipboard": "^5.0.1",
|
"react-copy-to-clipboard": "^5.0.2",
|
||||||
"react-custom-scrollbars": "^4.2.1",
|
"react-custom-scrollbars": "^4.2.1",
|
||||||
"react-dom": "^16.2.0",
|
"react-dom": "^16.13.1",
|
||||||
"react-dropzone": "^4.2.3",
|
"react-dropzone": "^4.3.0",
|
||||||
"react-infinite-scroller": "^1.0.6",
|
"react-infinite-scroller": "^1.2.4",
|
||||||
"react-onclickout": "^2.0.8",
|
"react-onclickout": "^2.0.8",
|
||||||
"react-redux": "^5.0.6",
|
"react-redux": "^5.1.2",
|
||||||
"react-router-dom": "^4.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"redux": "^3.7.2",
|
"redux": "^4.0.5",
|
||||||
"redux-mock-store": "^1.5.1",
|
"redux-mock-store": "^1.5.4",
|
||||||
"redux-thunk": "^2.2.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"reselect": "^3.0.1",
|
"reselect": "^4.0.0",
|
||||||
"superagent": "^3.8.2",
|
"superagent": "^5.2.2",
|
||||||
"superagent-es6-promise": "^1.0.0",
|
"superagent-es6-promise": "^1.0.0",
|
||||||
"webpack": "^4.28.3"
|
"webpack": "^4.43.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
checks = ["all", "-ST1005", "-ST1000", "-SA4000", "-SA9004", "-SA1019", "-SA1008", "-U1000", "-ST1003", "-ST1018"]
|
||||||
+52
-48
File diff suppressed because one or more lines are too long
@@ -92,7 +92,7 @@ var exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin({patterns: [
|
||||||
{from: 'app/css/loader.css'},
|
{from: 'app/css/loader.css'},
|
||||||
{from: 'app/img/browsers/chrome.png'},
|
{from: 'app/img/browsers/chrome.png'},
|
||||||
{from: 'app/img/browsers/firefox.png'},
|
{from: 'app/img/browsers/firefox.png'},
|
||||||
@@ -102,7 +102,7 @@ var exports = {
|
|||||||
{from: 'app/img/favicon/favicon-32x32.png'},
|
{from: 'app/img/favicon/favicon-32x32.png'},
|
||||||
{from: 'app/img/favicon/favicon-96x96.png'},
|
{from: 'app/img/favicon/favicon-96x96.png'},
|
||||||
{from: 'app/index.html'}
|
{from: 'app/index.html'}
|
||||||
]),
|
]}),
|
||||||
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
|
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
|
||||||
new PurgecssPlugin({
|
new PurgecssPlugin({
|
||||||
paths: glob.sync([
|
paths: glob.sync([
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ var exports = {
|
|||||||
fs:'empty'
|
fs:'empty'
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin({patterns: [
|
||||||
{from: 'app/css/loader.css'},
|
{from: 'app/css/loader.css'},
|
||||||
{from: 'app/img/browsers/chrome.png'},
|
{from: 'app/img/browsers/chrome.png'},
|
||||||
{from: 'app/img/browsers/firefox.png'},
|
{from: 'app/img/browsers/firefox.png'},
|
||||||
@@ -77,7 +77,7 @@ var exports = {
|
|||||||
{from: 'app/img/favicon/favicon-32x32.png'},
|
{from: 'app/img/favicon/favicon-32x32.png'},
|
||||||
{from: 'app/img/favicon/favicon-96x96.png'},
|
{from: 'app/img/favicon/favicon-96x96.png'},
|
||||||
{from: 'app/index.html'}
|
{from: 'app/index.html'}
|
||||||
]),
|
]}),
|
||||||
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
|
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
|
||||||
new PurgecssPlugin({
|
new PurgecssPlugin({
|
||||||
paths: glob.sync([
|
paths: glob.sync([
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ function _build() {
|
|||||||
package=$(go list -f '{{.ImportPath}}')
|
package=$(go list -f '{{.ImportPath}}')
|
||||||
printf -- "--> %15s:%s\n" "${osarch}" "${package}"
|
printf -- "--> %15s:%s\n" "${osarch}" "${package}"
|
||||||
|
|
||||||
# Go build to build the binary.
|
# go build -trimpath to build the binary.
|
||||||
export GOOS=$os
|
export GOOS=$os
|
||||||
export GOARCH=$arch
|
export GOARCH=$arch
|
||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
go build -tags kqueue -o /dev/null
|
go build -trimpath -tags kqueue -o /dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ function main()
|
|||||||
gw_pid="$(start_minio_gateway_s3)"
|
gw_pid="$(start_minio_gateway_s3)"
|
||||||
|
|
||||||
SERVER_ENDPOINT=127.0.0.1:24240 ENABLE_HTTPS=0 ACCESS_KEY=minio \
|
SERVER_ENDPOINT=127.0.0.1:24240 ENABLE_HTTPS=0 ACCESS_KEY=minio \
|
||||||
SECRET_KEY=minio123 MINT_MODE="full" /mint/entrypoint.sh
|
SECRET_KEY=minio123 MINT_MODE="full" /mint/entrypoint.sh \
|
||||||
|
aws-sdk-go aws-sdk-java aws-sdk-php aws-sdk-ruby awscli \
|
||||||
|
healthcheck mc minio-dotnet minio-js \
|
||||||
|
minio-py s3cmd s3select security
|
||||||
rv=$?
|
rv=$?
|
||||||
|
|
||||||
kill "$sr_pid"
|
kill "$sr_pid"
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
for d in $(go list ./... | grep -v browser); do
|
for d in $(go list ./... | grep -v browser); do
|
||||||
CGO_ENABLED=1 go test -v -race --timeout 20m "$d"
|
CGO_ENABLED=1 go test -v -race --timeout 50m "$d"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -154,34 +154,6 @@ function run_test_erasure_sets() {
|
|||||||
return "$rv"
|
return "$rv"
|
||||||
}
|
}
|
||||||
|
|
||||||
function run_test_dist_erasure_sets_ipv6()
|
|
||||||
{
|
|
||||||
minio_pids=( $(start_minio_dist_erasure_sets_ipv6) )
|
|
||||||
|
|
||||||
export SERVER_ENDPOINT="[::1]:9000"
|
|
||||||
|
|
||||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
|
||||||
rv=$?
|
|
||||||
|
|
||||||
for pid in "${minio_pids[@]}"; do
|
|
||||||
kill "$pid"
|
|
||||||
done
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
if [ "$rv" -ne 0 ]; then
|
|
||||||
for i in $(seq 0 9); do
|
|
||||||
echo "server$i log:"
|
|
||||||
cat "$WORK_DIR/dist-minio-v6-900$i.log"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
for i in $(seq 0 9); do
|
|
||||||
rm -f "$WORK_DIR/dist-minio-v6-900$i.log"
|
|
||||||
done
|
|
||||||
|
|
||||||
return "$rv"
|
|
||||||
}
|
|
||||||
|
|
||||||
function run_test_zone_erasure_sets()
|
function run_test_zone_erasure_sets()
|
||||||
{
|
{
|
||||||
minio_pids=( $(start_minio_zone_erasure_sets) )
|
minio_pids=( $(start_minio_zone_erasure_sets) )
|
||||||
|
|||||||
@@ -30,33 +30,49 @@ MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server )
|
|||||||
|
|
||||||
function start_minio_3_node() {
|
function start_minio_3_node() {
|
||||||
declare -a minio_pids
|
declare -a minio_pids
|
||||||
|
declare -a ARGS
|
||||||
export MINIO_ACCESS_KEY=minio
|
export MINIO_ACCESS_KEY=minio
|
||||||
export MINIO_SECRET_KEY=minio123
|
export MINIO_SECRET_KEY=minio123
|
||||||
|
|
||||||
|
start_port=$(shuf -i 10000-65000 -n 1)
|
||||||
for i in $(seq 1 3); do
|
for i in $(seq 1 3); do
|
||||||
ARGS+=("http://127.0.0.1:$[8000+$i]${WORK_DIR}/$i/1/ http://127.0.0.1:$[8000+$i]${WORK_DIR}/$i/2/ http://127.0.0.1:$[8000+$i]${WORK_DIR}/$i/3/ http://127.0.0.1:$[8000+$i]${WORK_DIR}/$i/4/ http://127.0.0.1:$[8000+$i]${WORK_DIR}/$i/5/ http://127.0.0.1:$[8000+$i]${WORK_DIR}/$i/6/")
|
ARGS+=("http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/1/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/2/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/3/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/4/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/5/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/6/")
|
||||||
done
|
done
|
||||||
|
|
||||||
"${MINIO[@]}" --address ":8001" ${ARGS[@]} > "${WORK_DIR}/dist-minio-8001.log" 2>&1 &
|
"${MINIO[@]}" --address ":$[$start_port+1]" ${ARGS[@]} > "${WORK_DIR}/dist-minio-server1.log" 2>&1 &
|
||||||
minio_pids[0]=$!
|
minio_pids[0]=$!
|
||||||
|
disown "${minio_pids[0]}"
|
||||||
|
|
||||||
"${MINIO[@]}" --address ":8002" ${ARGS[@]} > "${WORK_DIR}/dist-minio-8002.log" 2>&1 &
|
"${MINIO[@]}" --address ":$[$start_port+2]" ${ARGS[@]} > "${WORK_DIR}/dist-minio-server2.log" 2>&1 &
|
||||||
minio_pids[1]=$!
|
minio_pids[1]=$!
|
||||||
|
disown "${minio_pids[1]}"
|
||||||
|
|
||||||
"${MINIO[@]}" --address ":8003" ${ARGS[@]} > "${WORK_DIR}/dist-minio-8003.log" 2>&1 &
|
"${MINIO[@]}" --address ":$[$start_port+3]" ${ARGS[@]} > "${WORK_DIR}/dist-minio-server3.log" 2>&1 &
|
||||||
minio_pids[2]=$!
|
minio_pids[2]=$!
|
||||||
|
disown "${minio_pids[2]}"
|
||||||
|
|
||||||
sleep "$1"
|
sleep "$1"
|
||||||
echo "${minio_pids[@]}"
|
for pid in "${minio_pids[@]}"; do
|
||||||
|
if ! kill "$pid"; then
|
||||||
|
for i in $(seq 1 3); do
|
||||||
|
echo "server$i log:"
|
||||||
|
cat "${WORK_DIR}/dist-minio-server$i.log"
|
||||||
|
done
|
||||||
|
echo "FAILED"
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# forcibly killing, to proceed further properly.
|
||||||
|
kill -9 "$pid"
|
||||||
|
sleep 1 # wait 1sec per pid
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function check_online() {
|
function check_online() {
|
||||||
for i in $(seq 1 3); do
|
if grep -q 'Server switching to safe mode' ${WORK_DIR}/dist-minio-*.log; then
|
||||||
if grep -q 'Server switching to safe mode' ${WORK_DIR}/dist-minio-$[8000+$i].log; then
|
|
||||||
echo "1"
|
echo "1"
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function purge()
|
function purge()
|
||||||
@@ -75,50 +91,21 @@ function __init__()
|
|||||||
}
|
}
|
||||||
|
|
||||||
function perform_test() {
|
function perform_test() {
|
||||||
minio_pids=( $(start_minio_3_node 60) )
|
start_minio_3_node 60
|
||||||
for pid in "${minio_pids[@]}"; do
|
|
||||||
if ! kill "$pid"; then
|
|
||||||
for i in $(seq 1 3); do
|
|
||||||
echo "server$i log:"
|
|
||||||
cat "${WORK_DIR}/dist-minio-$[8000+$i].log"
|
|
||||||
done
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# forcibly killing, to proceed further properly.
|
|
||||||
kill -9 "$pid"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Testing Distributed Erasure setup healing of drives"
|
echo "Testing Distributed Erasure setup healing of drives"
|
||||||
echo "Remove the contents of the disks belonging to '${1}' erasure set"
|
echo "Remove the contents of the disks belonging to '${1}' erasure set"
|
||||||
|
|
||||||
rm -rf ${WORK_DIR}/${1}/*/
|
rm -rf ${WORK_DIR}/${1}/*/
|
||||||
|
|
||||||
minio_pids=( $(start_minio_3_node 60) )
|
start_minio_3_node 60
|
||||||
for pid in "${minio_pids[@]}"; do
|
|
||||||
if ! kill "$pid"; then
|
|
||||||
for i in $(seq 1 3); do
|
|
||||||
echo "server$i log:"
|
|
||||||
cat "${WORK_DIR}/dist-minio-$[8000+$i].log"
|
|
||||||
done
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# forcibly killing, to proceed further properly.
|
|
||||||
# if the previous kill is taking time.
|
|
||||||
kill -9 "$pid"
|
|
||||||
done
|
|
||||||
|
|
||||||
rv=$(check_online)
|
rv=$(check_online)
|
||||||
if [ "$rv" == "1" ]; then
|
if [ "$rv" == "1" ]; then
|
||||||
for pid in "${minio_pids[@]}"; do
|
pkill -9 minio
|
||||||
kill -9 "$pid"
|
|
||||||
done
|
|
||||||
for i in $(seq 1 3); do
|
for i in $(seq 1 3); do
|
||||||
echo "server$i log:"
|
echo "server$i log:"
|
||||||
cat "${WORK_DIR}/dist-minio-$[8000+$i].log"
|
cat "${WORK_DIR}/dist-minio-server$i.log"
|
||||||
done
|
done
|
||||||
echo "FAILED"
|
echo "FAILED"
|
||||||
purge "$WORK_DIR"
|
purge "$WORK_DIR"
|
||||||
|
|||||||
+7
-6
@@ -100,18 +100,18 @@ func (api objectAPIHandlers) PutBucketACLHandler(w http.ResponseWriter, r *http.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(acl.AccessControlList.Grants) == 0 {
|
if len(acl.AccessControlList.Grants) == 0 {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if acl.AccessControlList.Grants[0].Permission != "FULL_CONTROL" {
|
if acl.AccessControlList.Grants[0].Permission != "FULL_CONTROL" {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if aclHeader != "" && aclHeader != "private" {
|
if aclHeader != "" && aclHeader != "private" {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +159,7 @@ func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.
|
|||||||
},
|
},
|
||||||
Permission: "FULL_CONTROL",
|
Permission: "FULL_CONTROL",
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := xml.NewEncoder(w).Encode(acl); err != nil {
|
if err := xml.NewEncoder(w).Encode(acl); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
@@ -214,18 +215,18 @@ func (api objectAPIHandlers) PutObjectACLHandler(w http.ResponseWriter, r *http.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(acl.AccessControlList.Grants) == 0 {
|
if len(acl.AccessControlList.Grants) == 0 {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if acl.AccessControlList.Grants[0].Permission != "FULL_CONTROL" {
|
if acl.AccessControlList.Grants[0].Permission != "FULL_CONTROL" {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if aclHeader != "" && aclHeader != "private" {
|
if aclHeader != "" && aclHeader != "private" {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,50 +35,47 @@ import (
|
|||||||
"github.com/minio/minio/cmd/config/storageclass"
|
"github.com/minio/minio/cmd/config/storageclass"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/auth"
|
||||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func validateAdminReqConfigKV(ctx context.Context, w http.ResponseWriter, r *http.Request) ObjectLayer {
|
func validateAdminReqConfigKV(ctx context.Context, w http.ResponseWriter, r *http.Request) (auth.Credentials, ObjectLayer) {
|
||||||
// Get current object layer instance.
|
// Get current object layer instance.
|
||||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
return nil
|
return auth.Credentials{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate request signature.
|
// Validate request signature.
|
||||||
_, adminAPIErr := checkAdminRequestAuthType(ctx, r, iampolicy.ConfigUpdateAdminAction, "")
|
cred, adminAPIErr := checkAdminRequestAuthType(ctx, r, iampolicy.ConfigUpdateAdminAction, "")
|
||||||
if adminAPIErr != ErrNone {
|
if adminAPIErr != ErrNone {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
|
||||||
return nil
|
return cred, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectAPI
|
return cred, objectAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
// DelConfigKVHandler - DELETE /minio/admin/v2/del-config-kv
|
// DelConfigKVHandler - DELETE /minio/admin/v3/del-config-kv
|
||||||
func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "DelConfigKVHandler")
|
ctx := newContext(r, w, "DeleteConfigKV")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
defer logger.AuditLog(w, r, "DeleteConfigKV", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deny if WORM is enabled
|
|
||||||
if globalWORMEnabled {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||||
// More than maxConfigSize bytes were available
|
// More than maxConfigSize bytes were available
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
password := globalActiveCred.SecretKey
|
password := cred.SecretKey
|
||||||
kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err, logger.Application)
|
logger.LogIf(ctx, err, logger.Application)
|
||||||
@@ -103,28 +100,24 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfigKVHandler - PUT /minio/admin/v2/set-config-kv
|
// SetConfigKVHandler - PUT /minio/admin/v3/set-config-kv
|
||||||
func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "SetConfigKVHandler")
|
ctx := newContext(r, w, "SetConfigKV")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
defer logger.AuditLog(w, r, "SetConfigKV", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deny if WORM is enabled
|
|
||||||
if globalWORMEnabled {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||||
// More than maxConfigSize bytes were available
|
// More than maxConfigSize bytes were available
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
password := globalActiveCred.SecretKey
|
password := cred.SecretKey
|
||||||
kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err, logger.Application)
|
logger.LogIf(ctx, err, logger.Application)
|
||||||
@@ -162,17 +155,19 @@ func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
// Make sure to write backend is encrypted
|
// Make sure to write backend is encrypted
|
||||||
if globalConfigEncrypted {
|
if globalConfigEncrypted {
|
||||||
saveConfig(context.Background(), objectAPI, backendEncryptedFile, backendEncryptedMigrationComplete)
|
saveConfig(GlobalContext, objectAPI, backendEncryptedFile, backendEncryptedMigrationComplete)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfigKVHandler - GET /minio/admin/v2/get-config-kv?key={key}
|
// GetConfigKVHandler - GET /minio/admin/v3/get-config-kv?key={key}
|
||||||
func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetConfigKVHandler")
|
ctx := newContext(r, w, "GetConfigKV")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
defer logger.AuditLog(w, r, "GetConfigKV", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -195,7 +190,7 @@ func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
password := globalActiveCred.SecretKey
|
password := cred.SecretKey
|
||||||
econfigData, err := madmin.EncryptData(password, buf.Bytes())
|
econfigData, err := madmin.EncryptData(password, buf.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
@@ -206,9 +201,11 @@ func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ClearConfigHistoryKVHandler")
|
ctx := newContext(r, w, "ClearConfigHistoryKV")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
defer logger.AuditLog(w, r, "ClearConfigHistoryKV", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
_, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -241,9 +238,11 @@ func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *
|
|||||||
|
|
||||||
// RestoreConfigHistoryKVHandler - restores a config with KV settings for the given KV id.
|
// RestoreConfigHistoryKVHandler - restores a config with KV settings for the given KV id.
|
||||||
func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "RestoreConfigHistoryKVHandler")
|
ctx := newContext(r, w, "RestoreConfigHistoryKV")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
defer logger.AuditLog(w, r, "RestoreConfigHistoryKV", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
_, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -287,9 +286,11 @@ func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r
|
|||||||
|
|
||||||
// ListConfigHistoryKVHandler - lists all the KV ids.
|
// ListConfigHistoryKVHandler - lists all the KV ids.
|
||||||
func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListConfigHistoryKVHandler")
|
ctx := newContext(r, w, "ListConfigHistoryKV")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
defer logger.AuditLog(w, r, "ListConfigHistoryKV", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -313,7 +314,7 @@ func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
password := globalActiveCred.SecretKey
|
password := cred.SecretKey
|
||||||
econfigData, err := madmin.EncryptData(password, data)
|
econfigData, err := madmin.EncryptData(password, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
@@ -323,11 +324,13 @@ func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *h
|
|||||||
writeSuccessResponseJSON(w, econfigData)
|
writeSuccessResponseJSON(w, econfigData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HelpConfigKVHandler - GET /minio/admin/v2/help-config-kv?subSys={subSys}&key={key}
|
// HelpConfigKVHandler - GET /minio/admin/v3/help-config-kv?subSys={subSys}&key={key}
|
||||||
func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "HelpConfigKVHandler")
|
ctx := newContext(r, w, "HelpConfigKV")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
defer logger.AuditLog(w, r, "HelpHistoryKV", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
_, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -349,28 +352,24 @@ func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Req
|
|||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfigHandler - PUT /minio/admin/v2/config
|
// SetConfigHandler - PUT /minio/admin/v3/config
|
||||||
func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "SetConfigHandler")
|
ctx := newContext(r, w, "SetConfig")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
defer logger.AuditLog(w, r, "SetConfig", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deny if WORM is enabled
|
|
||||||
if globalWORMEnabled {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||||
// More than maxConfigSize bytes were available
|
// More than maxConfigSize bytes were available
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
password := globalActiveCred.SecretKey
|
password := cred.SecretKey
|
||||||
kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err, logger.Application)
|
logger.LogIf(ctx, err, logger.Application)
|
||||||
@@ -403,18 +402,20 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
// Make sure to write backend is encrypted
|
// Make sure to write backend is encrypted
|
||||||
if globalConfigEncrypted {
|
if globalConfigEncrypted {
|
||||||
saveConfig(context.Background(), objectAPI, backendEncryptedFile, backendEncryptedMigrationComplete)
|
saveConfig(GlobalContext, objectAPI, backendEncryptedFile, backendEncryptedMigrationComplete)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfigHandler - GET /minio/admin/v2/config
|
// GetConfigHandler - GET /minio/admin/v3/config
|
||||||
// Get config.json of this minio setup.
|
// Get config.json of this minio setup.
|
||||||
func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetConfigHandler")
|
ctx := newContext(r, w, "GetConfig")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
defer logger.AuditLog(w, r, "GetConfig", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -471,7 +472,7 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
password := globalActiveCred.SecretKey
|
password := cred.SecretKey
|
||||||
econfigData, err := madmin.EncryptData(password, []byte(s.String()))
|
econfigData, err := madmin.EncryptData(password, []byte(s.String()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
|||||||
+422
-60
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
|
* MinIO Cloud Storage, (C) 2019-2020 MinIO, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -51,21 +51,17 @@ func validateAdminUsersReq(ctx context.Context, w http.ResponseWriter, r *http.R
|
|||||||
return objectAPI, cred
|
return objectAPI, cred
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveUser - DELETE /minio/admin/v2/remove-user?accessKey=<access_key>
|
// RemoveUser - DELETE /minio/admin/v3/remove-user?accessKey=<access_key>
|
||||||
func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "RemoveUser")
|
ctx := newContext(r, w, "RemoveUser")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "RemoveUser", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.DeleteUserAdminAction)
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.DeleteUserAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deny if WORM is enabled
|
|
||||||
if globalWORMEnabled {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
accessKey := vars["accessKey"]
|
accessKey := vars["accessKey"]
|
||||||
|
|
||||||
@@ -93,15 +89,19 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUsers - GET /minio/admin/v2/list-users
|
// ListUsers - GET /minio/admin/v3/list-users
|
||||||
func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListUsers")
|
ctx := newContext(r, w, "ListUsers")
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.ListUsersAdminAction)
|
defer logger.AuditLog(w, r, "ListUsers", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.ListUsersAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
password := cred.SecretKey
|
||||||
|
|
||||||
allCredentials, err := globalIAMSys.ListUsers()
|
allCredentials, err := globalIAMSys.ListUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
@@ -114,7 +114,6 @@ func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
password := globalActiveCred.SecretKey
|
|
||||||
econfigData, err := madmin.EncryptData(password, data)
|
econfigData, err := madmin.EncryptData(password, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
@@ -124,10 +123,12 @@ func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeSuccessResponseJSON(w, econfigData)
|
writeSuccessResponseJSON(w, econfigData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserInfo - GET /minio/admin/v2/user-info
|
// GetUserInfo - GET /minio/admin/v3/user-info
|
||||||
func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetUserInfo")
|
ctx := newContext(r, w, "GetUserInfo")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "GetUserInfo", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetUserAdminAction)
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetUserAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -151,10 +152,12 @@ func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeSuccessResponseJSON(w, data)
|
writeSuccessResponseJSON(w, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateGroupMembers - PUT /minio/admin/v2/update-group-members
|
// UpdateGroupMembers - PUT /minio/admin/v3/update-group-members
|
||||||
func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "UpdateGroupMembers")
|
ctx := newContext(r, w, "UpdateGroupMembers")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "UpdateGroupMembers", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.AddUserToGroupAdminAction)
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.AddUserToGroupAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -194,10 +197,12 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGroup - /minio/admin/v2/group?group=mygroup1
|
// GetGroup - /minio/admin/v3/group?group=mygroup1
|
||||||
func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetGroup")
|
ctx := newContext(r, w, "GetGroup")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "GetGroup", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetGroupAdminAction)
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetGroupAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -221,10 +226,12 @@ func (a adminAPIHandlers) GetGroup(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeSuccessResponseJSON(w, body)
|
writeSuccessResponseJSON(w, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListGroups - GET /minio/admin/v2/groups
|
// ListGroups - GET /minio/admin/v3/groups
|
||||||
func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListGroups")
|
ctx := newContext(r, w, "ListGroups")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "ListGroups", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.ListGroupsAdminAction)
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.ListGroupsAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -245,10 +252,12 @@ func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeSuccessResponseJSON(w, body)
|
writeSuccessResponseJSON(w, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetGroupStatus - PUT /minio/admin/v2/set-group-status?group=mygroup1&status=enabled
|
// SetGroupStatus - PUT /minio/admin/v3/set-group-status?group=mygroup1&status=enabled
|
||||||
func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "SetGroupStatus")
|
ctx := newContext(r, w, "SetGroupStatus")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "SetGroupStatus", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.EnableGroupAdminAction)
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.EnableGroupAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -280,21 +289,17 @@ func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetUserStatus - PUT /minio/admin/v2/set-user-status?accessKey=<access_key>&status=[enabled|disabled]
|
// SetUserStatus - PUT /minio/admin/v3/set-user-status?accessKey=<access_key>&status=[enabled|disabled]
|
||||||
func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "SetUserStatus")
|
ctx := newContext(r, w, "SetUserStatus")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "SetUserStatus", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.EnableUserAdminAction)
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.EnableUserAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deny if WORM is enabled
|
|
||||||
if globalWORMEnabled {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
accessKey := vars["accessKey"]
|
accessKey := vars["accessKey"]
|
||||||
status := vars["status"]
|
status := vars["status"]
|
||||||
@@ -319,21 +324,17 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddUser - PUT /minio/admin/v2/add-user?accessKey=<access_key>
|
// AddUser - PUT /minio/admin/v3/add-user?accessKey=<access_key>
|
||||||
func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "AddUser")
|
ctx := newContext(r, w, "AddUser")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "AddUser", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.CreateUserAdminAction)
|
objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.CreateUserAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deny if WORM is enabled
|
|
||||||
if globalWORMEnabled {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
accessKey := vars["accessKey"]
|
accessKey := vars["accessKey"]
|
||||||
|
|
||||||
@@ -378,16 +379,323 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InfoCannedPolicy - GET /minio/admin/v2/info-canned-policy?name={policyName}
|
// AddServiceAccount - PUT /minio/admin/v3/add-service-account
|
||||||
func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "InfoCannedPolicy")
|
ctx := newContext(r, w, "AddServiceAccount")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "AddServiceAccount", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||||
|
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cred, _, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password := cred.SecretKey
|
||||||
|
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var createReq madmin.AddServiceAccountReq
|
||||||
|
if err = json.Unmarshal(reqBytes, &createReq); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disallow creating service accounts by root user.
|
||||||
|
if owner {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parentUser := cred.AccessKey
|
||||||
|
if cred.ParentUser != "" {
|
||||||
|
parentUser = cred.ParentUser
|
||||||
|
}
|
||||||
|
|
||||||
|
newCred, err := globalIAMSys.NewServiceAccount(ctx, parentUser, createReq.Policy)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify all other Minio peers to reload user the service account
|
||||||
|
for _, nerr := range globalNotificationSys.LoadServiceAccount(newCred.AccessKey) {
|
||||||
|
if nerr.Err != nil {
|
||||||
|
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||||
|
logger.LogIf(ctx, nerr.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var createResp = madmin.AddServiceAccountResp{
|
||||||
|
Credentials: auth.Credentials{
|
||||||
|
AccessKey: newCred.AccessKey,
|
||||||
|
SecretKey: newCred.SecretKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(createResp)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedData, err := madmin.EncryptData(password, data)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, encryptedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListServiceAccounts - GET /minio/admin/v3/list-service-accounts
|
||||||
|
func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "ListServiceAccounts")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "ListServiceAccounts", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||||
|
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cred, _, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disallow creating service accounts by root user.
|
||||||
|
if owner {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parentUser := cred.AccessKey
|
||||||
|
if cred.ParentUser != "" {
|
||||||
|
parentUser = cred.ParentUser
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, parentUser)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var listResp = madmin.ListServiceAccountsResp{
|
||||||
|
Accounts: serviceAccounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(listResp)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, encryptedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteServiceAccount - DELETE /minio/admin/v3/delete-service-account
|
||||||
|
func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "DeleteServiceAccount")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "DeleteServiceAccount", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||||
|
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cred, _, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disallow creating service accounts by root user.
|
||||||
|
if owner {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceAccount := mux.Vars(r)["accessKey"]
|
||||||
|
if serviceAccount == "" {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := globalIAMSys.GetServiceAccountParent(ctx, serviceAccount)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parentUser := cred.AccessKey
|
||||||
|
if cred.ParentUser != "" {
|
||||||
|
parentUser = cred.ParentUser
|
||||||
|
}
|
||||||
|
|
||||||
|
if parentUser != user || user == "" {
|
||||||
|
// The service account belongs to another user but return not
|
||||||
|
// found error to mitigate brute force attacks. or the
|
||||||
|
// serviceAccount doesn't exist.
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServiceAccountNotFound), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = globalIAMSys.DeleteServiceAccount(ctx, serviceAccount)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessNoContent(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountUsageInfoHandler returns usage
|
||||||
|
func (a adminAPIHandlers) AccountUsageInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "AccountUsageInfo")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "AccountUsageInfo", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||||
|
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set prefix value for "s3:prefix" policy conditionals.
|
||||||
|
r.Header.Set("prefix", "")
|
||||||
|
|
||||||
|
// Set delimiter value for "s3:delimiter" policy conditionals.
|
||||||
|
r.Header.Set("delimiter", SlashSeparator)
|
||||||
|
|
||||||
|
isAllowedAccess := func(bucketName string) (rd, wr bool) {
|
||||||
|
// Use the following trick to filter in place
|
||||||
|
// https://github.com/golang/go/wiki/SliceTricks#filter-in-place
|
||||||
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Action: iampolicy.ListBucketAction,
|
||||||
|
BucketName: bucketName,
|
||||||
|
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
||||||
|
IsOwner: owner,
|
||||||
|
ObjectName: "",
|
||||||
|
Claims: claims,
|
||||||
|
}) {
|
||||||
|
rd = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Action: iampolicy.PutObjectAction,
|
||||||
|
BucketName: bucketName,
|
||||||
|
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
||||||
|
IsOwner: owner,
|
||||||
|
ObjectName: "",
|
||||||
|
Claims: claims,
|
||||||
|
}) {
|
||||||
|
wr = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return rd, wr
|
||||||
|
}
|
||||||
|
|
||||||
|
buckets, err := objectAPI.ListBuckets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the latest calculated data usage
|
||||||
|
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
||||||
|
if err != nil {
|
||||||
|
// log the error, continue with the accounting response
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountName := cred.AccessKey
|
||||||
|
if cred.ParentUser != "" {
|
||||||
|
accountName = cred.ParentUser
|
||||||
|
}
|
||||||
|
|
||||||
|
acctInfo := madmin.AccountUsageInfo{
|
||||||
|
AccountName: accountName,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bucket := range buckets {
|
||||||
|
rd, wr := isAllowedAccess(bucket.Name)
|
||||||
|
if rd || wr {
|
||||||
|
var size uint64
|
||||||
|
// Fetch the data usage of the current bucket
|
||||||
|
if !dataUsageInfo.LastUpdate.IsZero() {
|
||||||
|
size = dataUsageInfo.BucketsUsage[bucket.Name].Size
|
||||||
|
}
|
||||||
|
acctInfo.Buckets = append(acctInfo.Buckets, madmin.BucketUsageInfo{
|
||||||
|
Name: bucket.Name,
|
||||||
|
Created: bucket.Created,
|
||||||
|
Size: size,
|
||||||
|
Access: madmin.AccountAccess{
|
||||||
|
Read: rd,
|
||||||
|
Write: wr,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usageInfoJSON, err := json.Marshal(acctInfo)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, usageInfoJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoCannedPolicyV2 - GET /minio/admin/v2/info-canned-policy?name={policyName}
|
||||||
|
func (a adminAPIHandlers) InfoCannedPolicyV2(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "InfoCannedPolicyV2")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "InfoCannedPolicyV2", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetPolicyAdminAction)
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetPolicyAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := globalIAMSys.InfoPolicy(mux.Vars(r)["name"])
|
policy, err := globalIAMSys.InfoPolicy(mux.Vars(r)["name"])
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(policy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -397,9 +705,32 @@ func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Reques
|
|||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListCannedPolicies - GET /minio/admin/v2/list-canned-policies
|
// InfoCannedPolicy - GET /minio/admin/v3/info-canned-policy?name={policyName}
|
||||||
func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListCannedPolicies")
|
ctx := newContext(r, w, "InfoCannedPolicy")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "InfoCannedPolicy", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetPolicyAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
policy, err := globalIAMSys.InfoPolicy(mux.Vars(r)["name"])
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(policy)
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCannedPoliciesV2 - GET /minio/admin/v2/list-canned-policies
|
||||||
|
func (a adminAPIHandlers) ListCannedPoliciesV2(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "ListCannedPoliciesV2")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "ListCannedPoliciesV2", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.ListUserPoliciesAdminAction)
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.ListUserPoliciesAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
@@ -412,7 +743,16 @@ func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = json.NewEncoder(w).Encode(policies); err != nil {
|
policyMap := make(map[string][]byte, len(policies))
|
||||||
|
for k, p := range policies {
|
||||||
|
var err error
|
||||||
|
policyMap[k], err = json.Marshal(p)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = json.NewEncoder(w).Encode(policyMap); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -420,10 +760,46 @@ func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Requ
|
|||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveCannedPolicy - DELETE /minio/admin/v2/remove-canned-policy?name=<policy_name>
|
// ListCannedPolicies - GET /minio/admin/v3/list-canned-policies
|
||||||
|
func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "ListCannedPolicies")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "ListCannedPolicies", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.ListUserPoliciesAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
policies, err := globalIAMSys.ListPolicies()
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPolicies = make(map[string]iampolicy.Policy)
|
||||||
|
for name, p := range policies {
|
||||||
|
_, err = json.Marshal(p)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newPolicies[name] = p
|
||||||
|
}
|
||||||
|
if err = json.NewEncoder(w).Encode(newPolicies); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveCannedPolicy - DELETE /minio/admin/v3/remove-canned-policy?name=<policy_name>
|
||||||
func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "RemoveCannedPolicy")
|
ctx := newContext(r, w, "RemoveCannedPolicy")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "RemoveCannedPolicy", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.DeletePolicyAdminAction)
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.DeletePolicyAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -432,12 +808,6 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
|
|||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
policyName := vars["name"]
|
policyName := vars["name"]
|
||||||
|
|
||||||
// Deny if WORM is enabled
|
|
||||||
if globalWORMEnabled {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := globalIAMSys.DeletePolicy(policyName); err != nil {
|
if err := globalIAMSys.DeletePolicy(policyName); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -452,10 +822,12 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddCannedPolicy - PUT /minio/admin/v2/add-canned-policy?name=<policy_name>
|
// AddCannedPolicy - PUT /minio/admin/v3/add-canned-policy?name=<policy_name>
|
||||||
func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "AddCannedPolicy")
|
ctx := newContext(r, w, "AddCannedPolicy")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "AddCannedPolicy", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.CreatePolicyAdminAction)
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.CreatePolicyAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -464,12 +836,6 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
|||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
policyName := vars["name"]
|
policyName := vars["name"]
|
||||||
|
|
||||||
// Deny if WORM is enabled
|
|
||||||
if globalWORMEnabled {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), 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)
|
||||||
@@ -508,10 +874,12 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPolicyForUserOrGroup - PUT /minio/admin/v2/set-policy?policy=xxx&user-or-group=?[&is-group]
|
// SetPolicyForUserOrGroup - PUT /minio/admin/v3/set-policy?policy=xxx&user-or-group=?[&is-group]
|
||||||
func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "SetPolicyForUserOrGroup")
|
ctx := newContext(r, w, "SetPolicyForUserOrGroup")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "SetPolicyForUserOrGroup", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.AttachPolicyAdminAction)
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.AttachPolicyAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -522,15 +890,9 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
|
|||||||
entityName := vars["userOrGroup"]
|
entityName := vars["userOrGroup"]
|
||||||
isGroup := vars["isGroup"] == "true"
|
isGroup := vars["isGroup"] == "true"
|
||||||
|
|
||||||
// Deny if WORM is enabled
|
|
||||||
if globalWORMEnabled {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isGroup {
|
if !isGroup {
|
||||||
ok, err := globalIAMSys.IsTempUser(entityName)
|
ok, err := globalIAMSys.IsTempUser(entityName)
|
||||||
if err != nil {
|
if err != nil && err != errNoSuchUser {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+277
-330
@@ -34,7 +34,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
humanize "github.com/dustin/go-humanize"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/config"
|
"github.com/minio/minio/cmd/config"
|
||||||
@@ -42,20 +41,17 @@ import (
|
|||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/cmd/logger/message/log"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/cpu"
|
|
||||||
"github.com/minio/minio/pkg/event/target"
|
|
||||||
"github.com/minio/minio/pkg/handlers"
|
"github.com/minio/minio/pkg/handlers"
|
||||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
"github.com/minio/minio/pkg/mem"
|
|
||||||
xnet "github.com/minio/minio/pkg/net"
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
trace "github.com/minio/minio/pkg/trace"
|
trace "github.com/minio/minio/pkg/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxEConfigJSONSize = 262272
|
maxEConfigJSONSize = 262272
|
||||||
defaultNetPerfSize = 100 * humanize.MiByte
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Type-safe query params.
|
// Type-safe query params.
|
||||||
@@ -98,12 +94,14 @@ func updateServer(updateURL, sha256Hex string, latestReleaseTime time.Time) (us
|
|||||||
return us, nil
|
return us, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerUpdateHandler - POST /minio/admin/v2/update?updateURL={updateURL}
|
// ServerUpdateHandler - POST /minio/admin/v3/update?updateURL={updateURL}
|
||||||
// ----------
|
// ----------
|
||||||
// updates all minio servers and restarts them gracefully.
|
// updates all minio servers and restarts them gracefully.
|
||||||
func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ServerUpdate")
|
ctx := newContext(r, w, "ServerUpdate")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "ServerUpdate", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerUpdateAdminAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerUpdateAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -141,9 +139,9 @@ func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
u.Path = path.Dir(u.Path) + "minio.exe"
|
u.Path = path.Dir(u.Path) + SlashSeparator + "minio.exe"
|
||||||
} else {
|
} else {
|
||||||
u.Path = path.Dir(u.Path) + "minio"
|
u.Path = path.Dir(u.Path) + SlashSeparator + "minio"
|
||||||
}
|
}
|
||||||
|
|
||||||
updateURL = u.String()
|
updateURL = u.String()
|
||||||
@@ -177,20 +175,17 @@ func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceActionHandler - POST /minio/admin/v2/service?action={action}
|
// ServiceHandler - POST /minio/admin/v3/service?action={action}
|
||||||
// ----------
|
// ----------
|
||||||
// restarts/stops minio server gracefully. In a distributed setup,
|
// restarts/stops minio server gracefully. In a distributed setup,
|
||||||
func (a adminAPIHandlers) ServiceActionHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) ServiceHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ServiceAction")
|
ctx := newContext(r, w, "Service")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "Service", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
action := vars["action"]
|
action := vars["action"]
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, "")
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var serviceSig serviceSignal
|
var serviceSig serviceSignal
|
||||||
switch madmin.ServiceAction(action) {
|
switch madmin.ServiceAction(action) {
|
||||||
case madmin.ServiceActionRestart:
|
case madmin.ServiceActionRestart:
|
||||||
@@ -203,6 +198,16 @@ func (a adminAPIHandlers) ServiceActionHandler(w http.ResponseWriter, r *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var objectAPI ObjectLayer
|
||||||
|
if serviceSig == serviceRestart {
|
||||||
|
objectAPI, _ = validateAdminReq(ctx, w, r, iampolicy.ServiceRestartAdminAction)
|
||||||
|
} else {
|
||||||
|
objectAPI, _ = validateAdminReq(ctx, w, r, iampolicy.ServiceStopAdminAction)
|
||||||
|
}
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Notify all other MinIO peers signal service.
|
// Notify all other MinIO peers signal service.
|
||||||
for _, nerr := range globalNotificationSys.SignalService(serviceSig) {
|
for _, nerr := range globalNotificationSys.SignalService(serviceSig) {
|
||||||
if nerr.Err != nil {
|
if nerr.Err != nil {
|
||||||
@@ -266,17 +271,21 @@ type ServerInfo struct {
|
|||||||
Data *ServerInfoData `json:"data"`
|
Data *ServerInfoData `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StorageInfoHandler - GET /minio/admin/v2/storageinfo
|
// StorageInfoHandler - GET /minio/admin/v3/storageinfo
|
||||||
// ----------
|
// ----------
|
||||||
// Get server information
|
// Get server information
|
||||||
func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "StorageInfo")
|
ctx := newContext(r, w, "StorageInfo")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "StorageInfo", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.StorageInfoAdminAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.StorageInfoAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
storageInfo := objectAPI.StorageInfo(ctx, false)
|
// ignores any errors here.
|
||||||
|
storageInfo, _ := objectAPI.StorageInfo(ctx, false)
|
||||||
|
|
||||||
// Marshal API response
|
// Marshal API response
|
||||||
jsonBytes, err := json.Marshal(storageInfo)
|
jsonBytes, err := json.Marshal(storageInfo)
|
||||||
@@ -291,11 +300,14 @@ func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DataUsageInfoHandler - GET /minio/admin/v2/datausage
|
// DataUsageInfoHandler - GET /minio/admin/v3/datausage
|
||||||
// ----------
|
// ----------
|
||||||
// Get server/cluster data usage info
|
// Get server/cluster data usage info
|
||||||
func (a adminAPIHandlers) DataUsageInfoHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) DataUsageInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "DataUsageInfo")
|
ctx := newContext(r, w, "DataUsageInfo")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "DataUsageInfo", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DataUsageInfoAdminAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DataUsageInfoAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -316,216 +328,6 @@ func (a adminAPIHandlers) DataUsageInfoHandler(w http.ResponseWriter, r *http.Re
|
|||||||
writeSuccessResponseJSON(w, dataUsageInfoJSON)
|
writeSuccessResponseJSON(w, dataUsageInfoJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a adminAPIHandlers) AccountingUsageInfoHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "AccountingUsageInfo")
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AccountingUsageInfoAdminAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountingUsageInfo = make(map[string]madmin.BucketAccountingUsage)
|
|
||||||
|
|
||||||
buckets, err := objectAPI.ListBuckets(ctx)
|
|
||||||
if err != nil {
|
|
||||||
// writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
users, err := globalIAMSys.ListUsers()
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the latest calculated data usage
|
|
||||||
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate for each bucket, which users are allowed to access to it
|
|
||||||
for _, bucket := range buckets {
|
|
||||||
bucketUsageInfo := madmin.BucketAccountingUsage{}
|
|
||||||
|
|
||||||
// Fetch the data usage of the current bucket
|
|
||||||
if !dataUsageInfo.LastUpdate.IsZero() && dataUsageInfo.BucketsSizes != nil {
|
|
||||||
bucketUsageInfo.Size = dataUsageInfo.BucketsSizes[bucket.Name]
|
|
||||||
}
|
|
||||||
|
|
||||||
for user := range users {
|
|
||||||
rd, wr, custom := globalIAMSys.GetAccountAccess(user, bucket.Name)
|
|
||||||
if rd || wr || custom {
|
|
||||||
bucketUsageInfo.AccessList = append(bucketUsageInfo.AccessList, madmin.AccountAccess{
|
|
||||||
AccountName: user,
|
|
||||||
Read: rd,
|
|
||||||
Write: wr,
|
|
||||||
Custom: custom,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
accountingUsageInfo[bucket.Name] = bucketUsageInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
usageInfoJSON, err := json.Marshal(accountingUsageInfo)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
writeSuccessResponseJSON(w, usageInfoJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerCPULoadInfo holds informantion about cpu utilization
|
|
||||||
// of one minio node. It also reports any errors if encountered
|
|
||||||
// while trying to reach this server.
|
|
||||||
type ServerCPULoadInfo struct {
|
|
||||||
Addr string `json:"addr"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
Load []cpu.Load `json:"load"`
|
|
||||||
HistoricLoad []cpu.Load `json:"historicLoad"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerMemUsageInfo holds informantion about memory utilization
|
|
||||||
// of one minio node. It also reports any errors if encountered
|
|
||||||
// while trying to reach this server.
|
|
||||||
type ServerMemUsageInfo struct {
|
|
||||||
Addr string `json:"addr"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
Usage []mem.Usage `json:"usage"`
|
|
||||||
HistoricUsage []mem.Usage `json:"historicUsage"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerNetReadPerfInfo network read performance information.
|
|
||||||
type ServerNetReadPerfInfo struct {
|
|
||||||
Addr string `json:"addr"`
|
|
||||||
ReadThroughput uint64 `json:"readThroughput"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PerfInfoHandler - GET /minio/admin/v2/performance?perfType={perfType}
|
|
||||||
// ----------
|
|
||||||
// Get all performance information based on input type
|
|
||||||
// Supported types = drive
|
|
||||||
func (a adminAPIHandlers) PerfInfoHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "PerfInfo")
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.PerfInfoAdminAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
switch perfType := vars["perfType"]; perfType {
|
|
||||||
case "net":
|
|
||||||
var size int64 = defaultNetPerfSize
|
|
||||||
if sizeStr, found := vars["size"]; found {
|
|
||||||
var err error
|
|
||||||
if size, err = strconv.ParseInt(sizeStr, 10, 64); err != nil || size < 0 {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !globalIsDistXL {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
addr := r.Host
|
|
||||||
if globalIsDistXL {
|
|
||||||
addr = GetLocalPeer(globalEndpoints)
|
|
||||||
}
|
|
||||||
|
|
||||||
infos := map[string][]ServerNetReadPerfInfo{}
|
|
||||||
infos[addr] = globalNotificationSys.NetReadPerfInfo(size)
|
|
||||||
for peer, info := range globalNotificationSys.CollectNetPerfInfo(size) {
|
|
||||||
infos[peer] = info
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal API response
|
|
||||||
jsonBytes, err := json.Marshal(infos)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reply with performance information (across nodes in a
|
|
||||||
// distributed setup) as json.
|
|
||||||
writeSuccessResponseJSON(w, jsonBytes)
|
|
||||||
|
|
||||||
case "drive":
|
|
||||||
// Drive Perf is only implemented for Erasure coded backends
|
|
||||||
if !globalIsXL {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var size int64 = madmin.DefaultDrivePerfSize
|
|
||||||
if sizeStr, found := vars["size"]; found {
|
|
||||||
var err error
|
|
||||||
if size, err = strconv.ParseInt(sizeStr, 10, 64); err != nil || size <= 0 {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Get drive performance details from local server's drive(s)
|
|
||||||
dp := getLocalDrivesPerf(globalEndpoints, size, r)
|
|
||||||
|
|
||||||
// Notify all other MinIO peers to report drive performance numbers
|
|
||||||
dps := globalNotificationSys.DrivePerfInfo(size)
|
|
||||||
dps = append(dps, dp)
|
|
||||||
|
|
||||||
// Marshal API response
|
|
||||||
jsonBytes, err := json.Marshal(dps)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reply with performance information (across nodes in a
|
|
||||||
// distributed setup) as json.
|
|
||||||
writeSuccessResponseJSON(w, jsonBytes)
|
|
||||||
case "cpu":
|
|
||||||
// Get CPU load details from local server's cpu(s)
|
|
||||||
cpu := getLocalCPULoad(globalEndpoints, r)
|
|
||||||
// Notify all other MinIO peers to report cpu load numbers
|
|
||||||
cpus := globalNotificationSys.CPULoadInfo()
|
|
||||||
cpus = append(cpus, cpu)
|
|
||||||
|
|
||||||
// Marshal API response
|
|
||||||
jsonBytes, err := json.Marshal(cpus)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reply with cpu load information (across nodes in a
|
|
||||||
// distributed setup) as json.
|
|
||||||
writeSuccessResponseJSON(w, jsonBytes)
|
|
||||||
case "mem":
|
|
||||||
// Get mem usage details from local server(s)
|
|
||||||
m := getLocalMemUsage(globalEndpoints, r)
|
|
||||||
// Notify all other MinIO peers to report mem usage numbers
|
|
||||||
mems := globalNotificationSys.MemUsageInfo()
|
|
||||||
mems = append(mems, m)
|
|
||||||
|
|
||||||
// Marshal API response
|
|
||||||
jsonBytes, err := json.Marshal(mems)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reply with mem usage information (across nodes in a
|
|
||||||
// distributed setup) as json.
|
|
||||||
writeSuccessResponseJSON(w, jsonBytes)
|
|
||||||
default:
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLockEntry(l lockRequesterInfo, resource, server string) *madmin.LockEntry {
|
func newLockEntry(l lockRequesterInfo, resource, server string) *madmin.LockEntry {
|
||||||
entry := &madmin.LockEntry{
|
entry := &madmin.LockEntry{
|
||||||
Timestamp: l.Timestamp,
|
Timestamp: l.Timestamp,
|
||||||
@@ -582,6 +384,8 @@ type PeerLocks struct {
|
|||||||
func (a adminAPIHandlers) TopLocksHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) TopLocksHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "TopLocks")
|
ctx := newContext(r, w, "TopLocks")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "TopLocks", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.TopLocksAdminAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.TopLocksAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -621,12 +425,14 @@ type StartProfilingResult struct {
|
|||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartProfilingHandler - POST /minio/admin/v2/profiling/start?profilerType={profilerType}
|
// StartProfilingHandler - POST /minio/admin/v3/profiling/start?profilerType={profilerType}
|
||||||
// ----------
|
// ----------
|
||||||
// Enable server profiling
|
// Enable server profiling
|
||||||
func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "StartProfiling")
|
ctx := newContext(r, w, "StartProfiling")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "StartProfiling", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ProfilingAdminAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ProfilingAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -717,12 +523,14 @@ func (f dummyFileInfo) ModTime() time.Time { return f.modTime }
|
|||||||
func (f dummyFileInfo) IsDir() bool { return f.isDir }
|
func (f dummyFileInfo) IsDir() bool { return f.isDir }
|
||||||
func (f dummyFileInfo) Sys() interface{} { return f.sys }
|
func (f dummyFileInfo) Sys() interface{} { return f.sys }
|
||||||
|
|
||||||
// DownloadProfilingHandler - POST /minio/admin/v2/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
|
||||||
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")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "DownloadProfiling", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ProfilingAdminAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ProfilingAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -789,7 +597,7 @@ func extractHealInitParams(vars map[string]string, qParms url.Values, r io.Reade
|
|||||||
if hip.clientToken == "" {
|
if hip.clientToken == "" {
|
||||||
jerr := json.NewDecoder(r).Decode(&hip.hs)
|
jerr := json.NewDecoder(r).Decode(&hip.hs)
|
||||||
if jerr != nil {
|
if jerr != nil {
|
||||||
logger.LogIf(context.Background(), jerr, logger.Application)
|
logger.LogIf(GlobalContext, jerr, logger.Application)
|
||||||
err = ErrRequestBodyParse
|
err = ErrRequestBodyParse
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -799,7 +607,7 @@ func extractHealInitParams(vars map[string]string, qParms url.Values, r io.Reade
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// HealHandler - POST /minio/admin/v2/heal/
|
// HealHandler - POST /minio/admin/v3/heal/
|
||||||
// -----------
|
// -----------
|
||||||
// Start heal processing and return heal status items.
|
// Start heal processing and return heal status items.
|
||||||
//
|
//
|
||||||
@@ -815,6 +623,8 @@ func extractHealInitParams(vars map[string]string, qParms url.Values, r io.Reade
|
|||||||
func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "Heal")
|
ctx := newContext(r, w, "Heal")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "Heal", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealAdminAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -840,13 +650,15 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Define a closure to start sending whitespace to client
|
// Define a closure to start sending whitespace to client
|
||||||
// after 10s unless a response item comes in
|
// after 10s unless a response item comes in
|
||||||
keepConnLive := func(w http.ResponseWriter, respCh chan healResp) {
|
keepConnLive := func(w http.ResponseWriter, r *http.Request, respCh chan healResp) {
|
||||||
ticker := time.NewTicker(time.Second * 10)
|
ticker := time.NewTicker(time.Second * 10)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
started := false
|
started := false
|
||||||
forLoop:
|
forLoop:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
case <-r.Context().Done():
|
||||||
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if !started {
|
if !started {
|
||||||
// Start writing response to client
|
// Start writing response to client
|
||||||
@@ -896,8 +708,8 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// find number of disks in the setup
|
// find number of disks in the setup, ignore any errors here.
|
||||||
info := objectAPI.StorageInfo(ctx, false)
|
info, _ := objectAPI.StorageInfo(ctx, false)
|
||||||
numDisks := info.Backend.OfflineDisks.Sum() + info.Backend.OnlineDisks.Sum()
|
numDisks := info.Backend.OfflineDisks.Sum() + info.Backend.OnlineDisks.Sum()
|
||||||
|
|
||||||
healPath := pathJoin(hip.bucket, hip.objPrefix)
|
healPath := pathJoin(hip.bucket, hip.objPrefix)
|
||||||
@@ -942,7 +754,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
respCh <- hr
|
respCh <- hr
|
||||||
}()
|
}()
|
||||||
case hip.clientToken == "":
|
case hip.clientToken == "":
|
||||||
nh := newHealSequence(hip.bucket, hip.objPrefix, handlers.GetSourceIP(r), numDisks, hip.hs, hip.forceStart)
|
nh := newHealSequence(GlobalContext, hip.bucket, hip.objPrefix, handlers.GetSourceIP(r), numDisks, hip.hs, hip.forceStart)
|
||||||
go func() {
|
go func() {
|
||||||
respBytes, apiErr, errMsg := globalAllHealState.LaunchNewHealSequence(nh)
|
respBytes, apiErr, errMsg := globalAllHealState.LaunchNewHealSequence(nh)
|
||||||
hr := healResp{respBytes, apiErr, errMsg}
|
hr := healResp{respBytes, apiErr, errMsg}
|
||||||
@@ -953,12 +765,14 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Due to the force-starting functionality, the Launch
|
// Due to the force-starting functionality, the Launch
|
||||||
// call above can take a long time - to keep the
|
// call above can take a long time - to keep the
|
||||||
// connection alive, we start sending whitespace
|
// connection alive, we start sending whitespace
|
||||||
keepConnLive(w, respCh)
|
keepConnLive(w, r, respCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "HealBackgroundStatus")
|
ctx := newContext(r, w, "HealBackgroundStatus")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "HealBackgroundStatus", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealAdminAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -982,13 +796,24 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Aggregate healing result
|
// Aggregate healing result
|
||||||
var aggregatedHealStateResult = madmin.BgHealState{}
|
var aggregatedHealStateResult = madmin.BgHealState{
|
||||||
for _, state := range bgHealStates {
|
ScannedItemsCount: bgHealStates[0].ScannedItemsCount,
|
||||||
aggregatedHealStateResult.ScannedItemsCount += state.ScannedItemsCount
|
LastHealActivity: bgHealStates[0].LastHealActivity,
|
||||||
if aggregatedHealStateResult.LastHealActivity.Before(state.LastHealActivity) {
|
NextHealRound: bgHealStates[0].NextHealRound,
|
||||||
aggregatedHealStateResult.LastHealActivity = state.LastHealActivity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bgHealStates = bgHealStates[1:]
|
||||||
|
|
||||||
|
for _, state := range bgHealStates {
|
||||||
|
aggregatedHealStateResult.ScannedItemsCount += state.ScannedItemsCount
|
||||||
|
if !state.LastHealActivity.IsZero() && aggregatedHealStateResult.LastHealActivity.Before(state.LastHealActivity) {
|
||||||
|
aggregatedHealStateResult.LastHealActivity = state.LastHealActivity
|
||||||
|
// The node which has the last heal activity means its
|
||||||
|
// is the node that is orchestrating self healing operations,
|
||||||
|
// which also means it is the same node which decides when
|
||||||
|
// the next self healing operation will be done.
|
||||||
|
aggregatedHealStateResult.NextHealRound = state.NextHealRound
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(aggregatedHealStateResult); err != nil {
|
if err := json.NewEncoder(w).Encode(aggregatedHealStateResult); err != nil {
|
||||||
@@ -1074,14 +899,14 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
|||||||
HTTPStatusCode: e.StatusCode,
|
HTTPStatusCode: e.StatusCode,
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if err == errConfigNotFound {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "XMinioConfigError",
|
Code: "XMinioConfigError",
|
||||||
Description: err.Error(),
|
Description: err.Error(),
|
||||||
HTTPStatusCode: http.StatusNotFound,
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
apiErr = errorCodes.ToAPIErr(toAdminAPIErrCode(ctx, err))
|
apiErr = errorCodes.ToAPIErrWithErr(toAdminAPIErrCode(ctx, err), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return apiErr
|
return apiErr
|
||||||
@@ -1104,11 +929,12 @@ func mustTrace(entry interface{}, trcAll, errOnly bool) bool {
|
|||||||
return trace
|
return trace
|
||||||
}
|
}
|
||||||
|
|
||||||
// TraceHandler - POST /minio/admin/v2/trace
|
// TraceHandler - POST /minio/admin/v3/trace
|
||||||
// ----------
|
// ----------
|
||||||
// The handler sends http trace to the connected HTTP client.
|
// The handler sends http trace to the connected HTTP client.
|
||||||
func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "HTTPTrace")
|
ctx := newContext(r, w, "HTTPTrace")
|
||||||
|
|
||||||
trcAll := r.URL.Query().Get("all") == "true"
|
trcAll := r.URL.Query().Get("all") == "true"
|
||||||
trcErr := r.URL.Query().Get("err") == "true"
|
trcErr := r.URL.Query().Get("err") == "true"
|
||||||
|
|
||||||
@@ -1121,16 +947,13 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
w.Header().Set(xhttp.ContentType, "text/event-stream")
|
w.Header().Set(xhttp.ContentType, "text/event-stream")
|
||||||
|
|
||||||
doneCh := make(chan struct{})
|
|
||||||
defer close(doneCh)
|
|
||||||
|
|
||||||
// Trace Publisher and peer-trace-client uses nonblocking send and hence does not wait for slow receivers.
|
// Trace Publisher and peer-trace-client uses nonblocking send and hence does not wait for slow receivers.
|
||||||
// Use buffered channel to take care of burst sends or slow w.Write()
|
// Use buffered channel to take care of burst sends or slow w.Write()
|
||||||
traceCh := make(chan interface{}, 4000)
|
traceCh := make(chan interface{}, 4000)
|
||||||
|
|
||||||
peers := getRestClients(globalEndpoints)
|
peers := newPeerRestClients(globalEndpoints)
|
||||||
|
|
||||||
globalHTTPTrace.Subscribe(traceCh, doneCh, func(entry interface{}) bool {
|
globalHTTPTrace.Subscribe(traceCh, ctx.Done(), func(entry interface{}) bool {
|
||||||
return mustTrace(entry, trcAll, trcErr)
|
return mustTrace(entry, trcAll, trcErr)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1138,7 +961,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if peer == nil {
|
if peer == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
peer.Trace(traceCh, doneCh, trcAll, trcErr)
|
peer.Trace(traceCh, ctx.Done(), trcAll, trcErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
|
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
|
||||||
@@ -1157,7 +980,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
case <-GlobalServiceDoneCh:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1167,6 +990,8 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ConsoleLog")
|
ctx := newContext(r, w, "ConsoleLog")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "ConsoleLog", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConsoleLogAdminAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConsoleLogAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -1191,20 +1016,18 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
w.Header().Add("Connection", "close")
|
w.Header().Add("Connection", "close")
|
||||||
w.Header().Set(xhttp.ContentType, "text/event-stream")
|
w.Header().Set(xhttp.ContentType, "text/event-stream")
|
||||||
|
|
||||||
doneCh := make(chan struct{})
|
|
||||||
defer close(doneCh)
|
|
||||||
logCh := make(chan interface{}, 4000)
|
logCh := make(chan interface{}, 4000)
|
||||||
|
|
||||||
peers := getRestClients(globalEndpoints)
|
peers := newPeerRestClients(globalEndpoints)
|
||||||
|
|
||||||
globalConsoleSys.Subscribe(logCh, doneCh, node, limitLines, logKind, nil)
|
globalConsoleSys.Subscribe(logCh, ctx.Done(), node, limitLines, logKind, nil)
|
||||||
|
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
if peer == nil {
|
if peer == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if node == "" || strings.EqualFold(peer.host.Name, node) {
|
if node == "" || strings.EqualFold(peer.host.Name, node) {
|
||||||
peer.ConsoleLog(logCh, doneCh)
|
peer.ConsoleLog(logCh, ctx.Done())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1216,8 +1039,8 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case entry := <-logCh:
|
case entry := <-logCh:
|
||||||
log := entry.(madmin.LogInfo)
|
log, ok := entry.(log.Info)
|
||||||
if log.SendLog(node, logKind) {
|
if ok && log.SendLog(node, logKind) {
|
||||||
if err := enc.Encode(log); err != nil {
|
if err := enc.Encode(log); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1228,15 +1051,17 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
case <-GlobalServiceDoneCh:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMSKeyStatusHandler - GET /minio/admin/v2/kms/key/status?key-id=<master-key-id>
|
// KMSKeyStatusHandler - GET /minio/admin/v3/kms/key/status?key-id=<master-key-id>
|
||||||
func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "KMSKeyStatusHandler")
|
ctx := newContext(r, w, "KMSKeyStatus")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "KMSKeyStatus", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSKeyStatusAdminAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSKeyStatusAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
@@ -1303,68 +1128,190 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
|
|||||||
writeSuccessResponseJSON(w, resp)
|
writeSuccessResponseJSON(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerHardwareInfoHandler - GET /minio/admin/v2/hardwareinfo?Type={hwType}
|
// OBDInfoHandler - GET /minio/admin/v3/obdinfo
|
||||||
// ----------
|
// ----------
|
||||||
// Get all hardware information based on input type
|
// Get server on-board diagnostics
|
||||||
// Supported types = cpu
|
func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
func (a adminAPIHandlers) ServerHardwareInfoHandler(w http.ResponseWriter, r *http.Request) {
|
ctx := newContext(r, w, "OBDInfo")
|
||||||
ctx := newContext(r, w, "HardwareInfo")
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerHardwareInfoAdminAction)
|
defer logger.AuditLog(w, r, "OBDInfo", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.OBDInfoAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
hardware := vars[madmin.HARDWARE]
|
obdInfo := madmin.OBDInfo{}
|
||||||
|
obdInfoCh := make(chan madmin.OBDInfo)
|
||||||
|
|
||||||
switch madmin.HardwareType(hardware) {
|
enc := json.NewEncoder(w)
|
||||||
case madmin.CPU:
|
partialWrite := func(oinfo madmin.OBDInfo) {
|
||||||
// Get CPU hardware details from local server's cpu(s)
|
obdInfoCh <- oinfo
|
||||||
cpu := getLocalCPUInfo(globalEndpoints, r)
|
}
|
||||||
// Notify all other MinIO peers to report cpu hardware
|
|
||||||
cpus := globalNotificationSys.CPUInfo()
|
|
||||||
cpus = append(cpus, cpu)
|
|
||||||
|
|
||||||
// Marshal API response
|
setCommonHeaders(w)
|
||||||
jsonBytes, err := json.Marshal(cpus)
|
w.Header().Set(xhttp.ContentType, "text/event-stream")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
errResp := func(err error) {
|
||||||
|
errorResponse := getAPIErrorResponse(ctx, toAdminAPIErr(ctx, err), r.URL.String(),
|
||||||
|
w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
|
||||||
|
encodedErrorResponse := encodeResponse(errorResponse)
|
||||||
|
obdInfo.Error = string(encodedErrorResponse)
|
||||||
|
logger.LogIf(ctx, enc.Encode(obdInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline := 3600 * time.Second
|
||||||
|
if dstr := r.URL.Query().Get("deadline"); dstr != "" {
|
||||||
|
var err error
|
||||||
|
deadline, err = time.ParseDuration(dstr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
errResp(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reply with cpu hardware information (across nodes in a
|
deadlinedCtx, cancel := context.WithTimeout(ctx, deadline)
|
||||||
// distributed setup) as json.
|
|
||||||
writeSuccessResponseJSON(w, jsonBytes)
|
|
||||||
|
|
||||||
case madmin.NETWORK:
|
defer cancel()
|
||||||
// Get Network hardware details from local server's network(s)
|
|
||||||
network := getLocalNetworkInfo(globalEndpoints, r)
|
|
||||||
// Notify all other MinIO peers to report network hardware
|
|
||||||
networks := globalNotificationSys.NetworkInfo()
|
|
||||||
networks = append(networks, network)
|
|
||||||
|
|
||||||
// Marshal API response
|
nsLock := objectAPI.NewNSLock(deadlinedCtx, minioMetaBucket, "obd-in-progress")
|
||||||
jsonBytes, err := json.Marshal(networks)
|
if err := nsLock.GetLock(newDynamicTimeout(deadline, deadline)); err != nil { // returns a locked lock
|
||||||
if err != nil {
|
errResp(err)
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer nsLock.Unlock()
|
||||||
|
|
||||||
// Reply with cpu network information (across nodes in a
|
go func() {
|
||||||
// distributed setup) as json.
|
defer close(obdInfoCh)
|
||||||
writeSuccessResponseJSON(w, jsonBytes)
|
|
||||||
|
|
||||||
default:
|
if cpu, ok := vars["syscpu"]; ok && cpu == "true" {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
cpuInfo := getLocalCPUOBDInfo(deadlinedCtx, r)
|
||||||
|
|
||||||
|
obdInfo.Sys.CPUInfo = append(obdInfo.Sys.CPUInfo, cpuInfo)
|
||||||
|
obdInfo.Sys.CPUInfo = append(obdInfo.Sys.CPUInfo, globalNotificationSys.CPUOBDInfo(deadlinedCtx)...)
|
||||||
|
partialWrite(obdInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if diskHw, ok := vars["sysdiskhw"]; ok && diskHw == "true" {
|
||||||
|
diskHwInfo := getLocalDiskHwOBD(deadlinedCtx, r)
|
||||||
|
|
||||||
|
obdInfo.Sys.DiskHwInfo = append(obdInfo.Sys.DiskHwInfo, diskHwInfo)
|
||||||
|
obdInfo.Sys.DiskHwInfo = append(obdInfo.Sys.DiskHwInfo, globalNotificationSys.DiskHwOBDInfo(deadlinedCtx)...)
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if osInfo, ok := vars["sysosinfo"]; ok && osInfo == "true" {
|
||||||
|
osInfo := getLocalOsInfoOBD(deadlinedCtx, r)
|
||||||
|
|
||||||
|
obdInfo.Sys.OsInfo = append(obdInfo.Sys.OsInfo, osInfo)
|
||||||
|
obdInfo.Sys.OsInfo = append(obdInfo.Sys.OsInfo, globalNotificationSys.OsOBDInfo(deadlinedCtx)...)
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mem, ok := vars["sysmem"]; ok && mem == "true" {
|
||||||
|
memInfo := getLocalMemOBD(deadlinedCtx, r)
|
||||||
|
|
||||||
|
obdInfo.Sys.MemInfo = append(obdInfo.Sys.MemInfo, memInfo)
|
||||||
|
obdInfo.Sys.MemInfo = append(obdInfo.Sys.MemInfo, globalNotificationSys.MemOBDInfo(deadlinedCtx)...)
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if proc, ok := vars["sysprocess"]; ok && proc == "true" {
|
||||||
|
procInfo := getLocalProcOBD(deadlinedCtx, r)
|
||||||
|
|
||||||
|
obdInfo.Sys.ProcInfo = append(obdInfo.Sys.ProcInfo, procInfo)
|
||||||
|
obdInfo.Sys.ProcInfo = append(obdInfo.Sys.ProcInfo, globalNotificationSys.ProcOBDInfo(deadlinedCtx)...)
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config, ok := vars["minioconfig"]; ok && config == "true" {
|
||||||
|
cfg, err := readServerConfig(ctx, objectAPI)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
obdInfo.Minio.Config = cfg
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if drive, ok := vars["perfdrive"]; ok && drive == "true" {
|
||||||
|
// Get drive obd details from local server's drive(s)
|
||||||
|
driveOBDSerial := getLocalDrivesOBD(deadlinedCtx, false, globalEndpoints, r)
|
||||||
|
driveOBDParallel := getLocalDrivesOBD(deadlinedCtx, true, globalEndpoints, r)
|
||||||
|
|
||||||
|
errStr := ""
|
||||||
|
if driveOBDSerial.Error != "" {
|
||||||
|
errStr = "serial: " + driveOBDSerial.Error
|
||||||
|
}
|
||||||
|
if driveOBDParallel.Error != "" {
|
||||||
|
errStr = errStr + " parallel: " + driveOBDParallel.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
driveOBD := madmin.ServerDrivesOBDInfo{
|
||||||
|
Addr: driveOBDSerial.Addr,
|
||||||
|
Serial: driveOBDSerial.Serial,
|
||||||
|
Parallel: driveOBDParallel.Parallel,
|
||||||
|
Error: errStr,
|
||||||
|
}
|
||||||
|
obdInfo.Perf.DriveInfo = append(obdInfo.Perf.DriveInfo, driveOBD)
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
|
||||||
|
// Notify all other MinIO peers to report drive obd numbers
|
||||||
|
driveOBDs := globalNotificationSys.DriveOBDInfoChan(deadlinedCtx)
|
||||||
|
for obd := range driveOBDs {
|
||||||
|
obdInfo.Perf.DriveInfo = append(obdInfo.Perf.DriveInfo, obd)
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
}
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if net, ok := vars["perfnet"]; ok && net == "true" && globalIsDistXL {
|
||||||
|
obdInfo.Perf.Net = append(obdInfo.Perf.Net, globalNotificationSys.NetOBDInfo(deadlinedCtx))
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
|
||||||
|
netOBDs := globalNotificationSys.DispatchNetOBDChan(deadlinedCtx)
|
||||||
|
for obd := range netOBDs {
|
||||||
|
obdInfo.Perf.Net = append(obdInfo.Perf.Net, obd)
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
}
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
|
||||||
|
obdInfo.Perf.NetParallel = globalNotificationSys.NetOBDParallelInfo(deadlinedCtx)
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case oinfo, ok := <-obdInfoCh:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.LogIf(ctx, enc.Encode(oinfo))
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
case <-ticker.C:
|
||||||
|
if _, err := w.Write([]byte(" ")); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
case <-deadlinedCtx.Done():
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerInfoHandler - GET /minio/admin/v2/info
|
// ServerInfoHandler - GET /minio/admin/v3/info
|
||||||
// ----------
|
// ----------
|
||||||
// Get server information
|
// Get server information
|
||||||
func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ServerInfo")
|
ctx := newContext(r, w, "ServerInfo")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "ServerInfo", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerInfoAdminAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerInfoAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
@@ -1383,11 +1330,10 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
buckets = madmin.Buckets{Count: dataUsageInfo.BucketsCount}
|
buckets = madmin.Buckets{Count: dataUsageInfo.BucketsCount}
|
||||||
objects = madmin.Objects{Count: dataUsageInfo.ObjectsCount}
|
objects = madmin.Objects{Count: dataUsageInfo.ObjectsTotalCount}
|
||||||
usage = madmin.Usage{Size: dataUsageInfo.ObjectsTotalSize}
|
usage = madmin.Usage{Size: dataUsageInfo.ObjectsTotalSize}
|
||||||
}
|
}
|
||||||
|
|
||||||
infoMsg := madmin.InfoMessage{}
|
|
||||||
vault := fetchVaultStatus(cfg)
|
vault := fetchVaultStatus(cfg)
|
||||||
|
|
||||||
ldap := madmin.LDAP{}
|
ldap := madmin.LDAP{}
|
||||||
@@ -1398,10 +1344,10 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
} else if ldapConn == nil {
|
} else if ldapConn == nil {
|
||||||
ldap.Status = "Not Configured"
|
ldap.Status = "Not Configured"
|
||||||
} else {
|
} else {
|
||||||
|
// Close ldap connection to avoid leaks.
|
||||||
|
ldapConn.Close()
|
||||||
ldap.Status = "online"
|
ldap.Status = "online"
|
||||||
}
|
}
|
||||||
// Close ldap connection to avoid leaks.
|
|
||||||
defer ldapConn.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log, audit := fetchLoggerInfo(cfg)
|
log, audit := fetchLoggerInfo(cfg)
|
||||||
@@ -1409,8 +1355,8 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
// Get the notification target info
|
// Get the notification target info
|
||||||
notifyTarget := fetchLambdaInfo(cfg)
|
notifyTarget := fetchLambdaInfo(cfg)
|
||||||
|
|
||||||
// Fetching the Storage information
|
// Fetching the Storage information, ignore any errors.
|
||||||
storageInfo := objectAPI.StorageInfo(ctx, false)
|
storageInfo, _ := objectAPI.StorageInfo(ctx, false)
|
||||||
|
|
||||||
var OnDisks int
|
var OnDisks int
|
||||||
var OffDisks int
|
var OffDisks int
|
||||||
@@ -1425,7 +1371,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
OffDisks += v
|
OffDisks += v
|
||||||
}
|
}
|
||||||
|
|
||||||
backend = madmin.XlBackend{
|
backend = madmin.XLBackend{
|
||||||
Type: madmin.ErasureType,
|
Type: madmin.ErasureType,
|
||||||
OnlineDisks: OnDisks,
|
OnlineDisks: OnDisks,
|
||||||
OfflineDisks: OffDisks,
|
OfflineDisks: OffDisks,
|
||||||
@@ -1435,7 +1381,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
RRSCParity: storageInfo.Backend.RRSCParity,
|
RRSCParity: storageInfo.Backend.RRSCParity,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
backend = madmin.FsBackend{
|
backend = madmin.FSBackend{
|
||||||
Type: madmin.FsType,
|
Type: madmin.FsType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1492,11 +1438,11 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
Notifications: notifyTarget,
|
Notifications: notifyTarget,
|
||||||
}
|
}
|
||||||
|
|
||||||
infoMsg = madmin.InfoMessage{
|
infoMsg := madmin.InfoMessage{
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Region: globalServerRegion,
|
Region: globalServerRegion,
|
||||||
SQSARN: globalNotificationSys.GetARNList(),
|
SQSARN: globalNotificationSys.GetARNList(false),
|
||||||
DeploymentID: globalDeploymentID,
|
DeploymentID: globalDeploymentID,
|
||||||
Buckets: buckets,
|
Buckets: buckets,
|
||||||
Objects: objects,
|
Objects: objects,
|
||||||
@@ -1520,11 +1466,15 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
func fetchLambdaInfo(cfg config.Config) []map[string][]madmin.TargetIDStatus {
|
func fetchLambdaInfo(cfg config.Config) []map[string][]madmin.TargetIDStatus {
|
||||||
|
|
||||||
// Fetch the targets
|
// Fetch the configured targets
|
||||||
targetList, err := notify.RegisterNotificationTargets(cfg, GlobalServiceDoneCh, NewCustomHTTPTransport(), nil, true)
|
tr := NewGatewayHTTPTransport()
|
||||||
if err != nil {
|
defer tr.CloseIdleConnections()
|
||||||
|
targetList, err := notify.FetchRegisteredTargets(cfg, GlobalContext.Done(), tr, true, false)
|
||||||
|
if err != nil && err != notify.ErrTargetsOffline {
|
||||||
|
logger.LogIf(GlobalContext, err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
lambdaMap := make(map[string][]madmin.TargetIDStatus)
|
lambdaMap := make(map[string][]madmin.TargetIDStatus)
|
||||||
|
|
||||||
for targetID, target := range targetList.TargetMap() {
|
for targetID, target := range targetList.TargetMap() {
|
||||||
@@ -1569,7 +1519,6 @@ func fetchVaultStatus(cfg config.Config) madmin.Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := checkConnection(kmsInfo.Endpoint, 15*time.Second); err != nil {
|
if err := checkConnection(kmsInfo.Endpoint, 15*time.Second); err != nil {
|
||||||
|
|
||||||
vault.Status = "offline"
|
vault.Status = "offline"
|
||||||
} else {
|
} else {
|
||||||
vault.Status = "online"
|
vault.Status = "online"
|
||||||
@@ -1637,25 +1586,23 @@ func fetchLoggerInfo(cfg config.Config) ([]madmin.Logger, []madmin.Audit) {
|
|||||||
|
|
||||||
// checkConnection - ping an endpoint , return err in case of no connection
|
// checkConnection - ping an endpoint , return err in case of no connection
|
||||||
func checkConnection(endpointStr string, timeout time.Duration) error {
|
func checkConnection(endpointStr string, timeout time.Duration) error {
|
||||||
u, pErr := xnet.ParseURL(endpointStr)
|
tr := newCustomHTTPTransport(&tls.Config{RootCAs: globalRootCAs}, timeout)()
|
||||||
if pErr != nil {
|
defer tr.CloseIdleConnections()
|
||||||
return pErr
|
|
||||||
|
ctx, cancel := context.WithTimeout(GlobalContext, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodHead, endpointStr, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tr := newCustomHTTPTransport(
|
client := &http.Client{Transport: tr}
|
||||||
&tls.Config{RootCAs: globalRootCAs},
|
resp, err := client.Do(req.WithContext(ctx))
|
||||||
timeout,
|
if err != nil {
|
||||||
0, /* Default value */
|
return err
|
||||||
)()
|
|
||||||
|
|
||||||
if dErr := u.DialHTTP(tr); dErr != nil {
|
|
||||||
if urlErr, ok := dErr.(*url.Error); ok {
|
|
||||||
// To treat "connection refused" errors as un reachable endpoint.
|
|
||||||
if target.IsConnRefusedErr(urlErr.Err) {
|
|
||||||
return errors.New("endpoint unreachable, please check your endpoint")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dErr
|
|
||||||
}
|
}
|
||||||
|
defer xhttp.DrainBody(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-23
@@ -43,7 +43,8 @@ type adminXLTestBed struct {
|
|||||||
|
|
||||||
// prepareAdminXLTestBed - helper function that setups a single-node
|
// prepareAdminXLTestBed - helper function that setups a single-node
|
||||||
// XL backend for admin-handler tests.
|
// XL backend for admin-handler tests.
|
||||||
func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
func prepareAdminXLTestBed(ctx context.Context) (*adminXLTestBed, error) {
|
||||||
|
|
||||||
// reset global variables to start afresh.
|
// reset global variables to start afresh.
|
||||||
resetTestGlobals()
|
resetTestGlobals()
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
|||||||
globalIsXL = true
|
globalIsXL = true
|
||||||
|
|
||||||
// Initializing objectLayer for HealFormatHandler.
|
// Initializing objectLayer for HealFormatHandler.
|
||||||
objLayer, xlDirs, xlErr := initTestXLObjLayer()
|
objLayer, xlDirs, xlErr := initTestXLObjLayer(ctx)
|
||||||
if xlErr != nil {
|
if xlErr != nil {
|
||||||
return nil, xlErr
|
return nil, xlErr
|
||||||
}
|
}
|
||||||
@@ -67,21 +68,9 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
|||||||
|
|
||||||
globalEndpoints = mustGetZoneEndpoints(xlDirs...)
|
globalEndpoints = mustGetZoneEndpoints(xlDirs...)
|
||||||
|
|
||||||
globalConfigSys = NewConfigSys()
|
newAllSubsystems()
|
||||||
|
|
||||||
globalIAMSys = NewIAMSys()
|
initAllSubsystems(objLayer)
|
||||||
globalIAMSys.Init(objLayer)
|
|
||||||
|
|
||||||
buckets, err := objLayer.ListBuckets(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
globalPolicySys = NewPolicySys()
|
|
||||||
globalPolicySys.Init(buckets, objLayer)
|
|
||||||
|
|
||||||
globalNotificationSys = NewNotificationSys(globalEndpoints)
|
|
||||||
globalNotificationSys.Init(buckets, objLayer)
|
|
||||||
|
|
||||||
// Setup admin mgmt REST API handlers.
|
// Setup admin mgmt REST API handlers.
|
||||||
adminRouter := mux.NewRouter()
|
adminRouter := mux.NewRouter()
|
||||||
@@ -103,20 +92,20 @@ func (atb *adminXLTestBed) TearDown() {
|
|||||||
|
|
||||||
// initTestObjLayer - Helper function to initialize an XL-based object
|
// initTestObjLayer - Helper function to initialize an XL-based object
|
||||||
// layer and set globalObjectAPI.
|
// layer and set globalObjectAPI.
|
||||||
func initTestXLObjLayer() (ObjectLayer, []string, error) {
|
func initTestXLObjLayer(ctx context.Context) (ObjectLayer, []string, error) {
|
||||||
xlDirs, err := getRandomDisks(16)
|
xlDirs, err := getRandomDisks(16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
endpoints := mustGetNewEndpoints(xlDirs...)
|
endpoints := mustGetNewEndpoints(xlDirs...)
|
||||||
format, err := waitForFormatXL(true, endpoints, 1, 1, 16, "")
|
storageDisks, format, err := waitForFormatXL(true, endpoints, 1, 1, 16, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
removeRoots(xlDirs)
|
removeRoots(xlDirs)
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
globalPolicySys = NewPolicySys()
|
globalPolicySys = NewPolicySys()
|
||||||
objLayer, err := newXLSets(endpoints, format, 1, 16)
|
objLayer, err := newXLSets(ctx, endpoints, storageDisks, format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -191,7 +180,10 @@ func getServiceCmdRequest(cmd cmdType, cred auth.Credentials) (*http.Request, er
|
|||||||
// testServicesCmdHandler - parametrizes service subcommand tests on
|
// testServicesCmdHandler - parametrizes service subcommand tests on
|
||||||
// cmdType value.
|
// cmdType value.
|
||||||
func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
||||||
adminTestBed, err := prepareAdminXLTestBed()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
adminTestBed, err := prepareAdminXLTestBed(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
||||||
}
|
}
|
||||||
@@ -259,7 +251,10 @@ func buildAdminRequest(queryVal url.Values, method, path string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminServerInfo(t *testing.T) {
|
func TestAdminServerInfo(t *testing.T) {
|
||||||
adminTestBed, err := prepareAdminXLTestBed()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
adminTestBed, err := prepareAdminXLTestBed(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
||||||
}
|
}
|
||||||
@@ -314,12 +309,12 @@ func TestToAdminAPIErrCode(t *testing.T) {
|
|||||||
// 3. Non-admin API specific error.
|
// 3. Non-admin API specific error.
|
||||||
{
|
{
|
||||||
err: errDiskNotFound,
|
err: errDiskNotFound,
|
||||||
expectedAPIErr: toAPIErrorCode(context.Background(), errDiskNotFound),
|
expectedAPIErr: toAPIErrorCode(GlobalContext, errDiskNotFound),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range testCases {
|
for i, test := range testCases {
|
||||||
actualErr := toAdminAPIErrCode(context.Background(), test.err)
|
actualErr := toAdminAPIErrCode(GlobalContext, test.err)
|
||||||
if actualErr != test.expectedAPIErr {
|
if actualErr != test.expectedAPIErr {
|
||||||
t.Errorf("Test %d: Expected %v but received %v",
|
t.Errorf("Test %d: Expected %v but received %v",
|
||||||
i+1, test.expectedAPIErr, actualErr)
|
i+1, test.expectedAPIErr, actualErr)
|
||||||
|
|||||||
+203
-114
@@ -61,7 +61,6 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
errHealIdleTimeout = fmt.Errorf("healing results were not consumed for too long")
|
errHealIdleTimeout = fmt.Errorf("healing results were not consumed for too long")
|
||||||
errHealPushStopNDiscard = fmt.Errorf("heal push stopped due to heal stop signal")
|
|
||||||
errHealStopSignalled = fmt.Errorf("heal stop signaled")
|
errHealStopSignalled = fmt.Errorf("heal stop signaled")
|
||||||
|
|
||||||
errFnHealFromAPIErr = func(ctx context.Context, err error) error {
|
errFnHealFromAPIErr = func(ctx context.Context, err error) error {
|
||||||
@@ -73,10 +72,6 @@ var (
|
|||||||
|
|
||||||
// healSequenceStatus - accumulated status of the heal sequence
|
// healSequenceStatus - accumulated status of the heal sequence
|
||||||
type healSequenceStatus struct {
|
type healSequenceStatus struct {
|
||||||
// lock to update this structure as it is concurrently
|
|
||||||
// accessed
|
|
||||||
updateLock *sync.RWMutex
|
|
||||||
|
|
||||||
// summary and detail for failures
|
// summary and detail for failures
|
||||||
Summary healStatusSummary `json:"Summary"`
|
Summary healStatusSummary `json:"Summary"`
|
||||||
FailureDetail string `json:"Detail,omitempty"`
|
FailureDetail string `json:"Detail,omitempty"`
|
||||||
@@ -106,19 +101,17 @@ func initHealState() *allHealState {
|
|||||||
healSeqMap: make(map[string]*healSequence),
|
healSeqMap: make(map[string]*healSequence),
|
||||||
}
|
}
|
||||||
|
|
||||||
go healState.periodicHealSeqsClean()
|
go healState.periodicHealSeqsClean(GlobalContext)
|
||||||
|
|
||||||
return healState
|
return healState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ahs *allHealState) periodicHealSeqsClean() {
|
func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) {
|
||||||
// Launch clean-up routine to remove this heal sequence (after
|
// Launch clean-up routine to remove this heal sequence (after
|
||||||
// it ends) from the global state after timeout has elapsed.
|
// it ends) from the global state after timeout has elapsed.
|
||||||
ticker := time.NewTicker(time.Minute * 5)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-time.After(time.Minute * 5):
|
||||||
now := UTCNow()
|
now := UTCNow()
|
||||||
ahs.Lock()
|
ahs.Lock()
|
||||||
for path, h := range ahs.healSeqMap {
|
for path, h := range ahs.healSeqMap {
|
||||||
@@ -127,7 +120,7 @@ func (ahs *allHealState) periodicHealSeqsClean() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ahs.Unlock()
|
ahs.Unlock()
|
||||||
case <-GlobalServiceDoneCh:
|
case <-ctx.Done():
|
||||||
// server could be restarting - need
|
// server could be restarting - need
|
||||||
// to exit immediately
|
// to exit immediately
|
||||||
return
|
return
|
||||||
@@ -183,7 +176,7 @@ func (ahs *allHealState) stopHealSequence(path string) ([]byte, APIError) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
b, err := json.Marshal(&hsp)
|
b, err := json.Marshal(&hsp)
|
||||||
return b, toAdminAPIErr(context.Background(), err)
|
return b, toAdminAPIErr(GlobalContext, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LaunchNewHealSequence - launches a background routine that performs
|
// LaunchNewHealSequence - launches a background routine that performs
|
||||||
@@ -202,9 +195,7 @@ func (ahs *allHealState) LaunchNewHealSequence(h *healSequence) (
|
|||||||
existsAndLive := false
|
existsAndLive := false
|
||||||
he, exists := ahs.getHealSequence(h.path)
|
he, exists := ahs.getHealSequence(h.path)
|
||||||
if exists {
|
if exists {
|
||||||
if !he.hasEnded() || len(he.currentStatus.Items) > 0 {
|
existsAndLive = !he.hasEnded()
|
||||||
existsAndLive = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if existsAndLive {
|
if existsAndLive {
|
||||||
@@ -230,8 +221,7 @@ func (ahs *allHealState) LaunchNewHealSequence(h *healSequence) (
|
|||||||
// Check if new heal sequence to be started overlaps with any
|
// Check if new heal sequence to be started overlaps with any
|
||||||
// existing, running sequence
|
// existing, running sequence
|
||||||
for k, hSeq := range ahs.healSeqMap {
|
for k, hSeq := range ahs.healSeqMap {
|
||||||
if !hSeq.hasEnded() && (strings.HasPrefix(k, h.path) ||
|
if !hSeq.hasEnded() && (HasPrefix(k, h.path) || HasPrefix(h.path, k)) {
|
||||||
strings.HasPrefix(h.path, k)) {
|
|
||||||
|
|
||||||
errMsg = "The provided heal sequence path overlaps with an existing " +
|
errMsg = "The provided heal sequence path overlaps with an existing " +
|
||||||
fmt.Sprintf("heal path: %s", k)
|
fmt.Sprintf("heal path: %s", k)
|
||||||
@@ -277,8 +267,8 @@ func (ahs *allHealState) PopHealStatusJSON(path string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Take lock to access and update the heal-sequence
|
// Take lock to access and update the heal-sequence
|
||||||
h.currentStatus.updateLock.Lock()
|
h.mutex.Lock()
|
||||||
defer h.currentStatus.updateLock.Unlock()
|
defer h.mutex.Unlock()
|
||||||
|
|
||||||
numItems := len(h.currentStatus.Items)
|
numItems := len(h.currentStatus.Items)
|
||||||
|
|
||||||
@@ -289,23 +279,27 @@ func (ahs *allHealState) PopHealStatusJSON(path string,
|
|||||||
lastResultIndex = h.currentStatus.Items[numItems-1].ResultIndex
|
lastResultIndex = h.currentStatus.Items[numItems-1].ResultIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
// After sending status to client, and before relinquishing
|
h.lastSentResultIndex = lastResultIndex
|
||||||
// the updateLock, reset Item to nil and record the result
|
|
||||||
// index sent to the client.
|
|
||||||
defer func(i int64) {
|
|
||||||
h.lastSentResultIndex = i
|
|
||||||
h.currentStatus.Items = nil
|
|
||||||
}(lastResultIndex)
|
|
||||||
|
|
||||||
jbytes, err := json.Marshal(h.currentStatus)
|
jbytes, err := json.Marshal(h.currentStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
h.currentStatus.Items = nil
|
||||||
|
|
||||||
logger.LogIf(h.ctx, err)
|
logger.LogIf(h.ctx, err)
|
||||||
return nil, ErrInternalError
|
return nil, ErrInternalError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.currentStatus.Items = nil
|
||||||
|
|
||||||
return jbytes, ErrNone
|
return jbytes, ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// healSource denotes single entity and heal option.
|
||||||
|
type healSource struct {
|
||||||
|
path string // entity path (format, buckets, objects) to heal
|
||||||
|
opts madmin.HealOpts // optional heal option overrides default setting
|
||||||
|
}
|
||||||
|
|
||||||
// healSequence - state for each heal sequence initiated on the
|
// healSequence - state for each heal sequence initiated on the
|
||||||
// server.
|
// server.
|
||||||
type healSequence struct {
|
type healSequence struct {
|
||||||
@@ -315,12 +309,13 @@ type healSequence struct {
|
|||||||
// path is just pathJoin(bucket, objPrefix)
|
// path is just pathJoin(bucket, objPrefix)
|
||||||
path string
|
path string
|
||||||
|
|
||||||
// List of entities (format, buckets, objects) to heal
|
// A channel of entities (format, buckets, objects) to heal
|
||||||
sourceCh chan string
|
sourceCh chan healSource
|
||||||
|
|
||||||
// Report healing progress, false if this is a background
|
// A channel of entities with heal result
|
||||||
// healing since currently there is no entity which will
|
respCh chan healResult
|
||||||
// receive realtime healing status
|
|
||||||
|
// Report healing progress
|
||||||
reportProgress bool
|
reportProgress bool
|
||||||
|
|
||||||
// time at which heal sequence was started
|
// time at which heal sequence was started
|
||||||
@@ -345,33 +340,42 @@ type healSequence struct {
|
|||||||
// completed
|
// completed
|
||||||
traverseAndHealDoneCh chan error
|
traverseAndHealDoneCh chan error
|
||||||
|
|
||||||
// channel to signal heal sequence to stop (e.g. from the
|
// canceler to cancel heal sequence.
|
||||||
// heal-stop API)
|
cancelCtx context.CancelFunc
|
||||||
stopSignalCh chan struct{}
|
|
||||||
|
|
||||||
// the last result index sent to client
|
// the last result index sent to client
|
||||||
lastSentResultIndex int64
|
lastSentResultIndex int64
|
||||||
|
|
||||||
// Number of total items scanned
|
// Number of total items scanned against item type
|
||||||
scannedItemsCount int64
|
scannedItemsMap map[madmin.HealItemType]int64
|
||||||
|
|
||||||
|
// Number of total items healed against item type
|
||||||
|
healedItemsMap map[madmin.HealItemType]int64
|
||||||
|
|
||||||
|
// Number of total items where healing failed against endpoint and drive state
|
||||||
|
healFailedItemsMap map[string]int64
|
||||||
|
|
||||||
// The time of the last scan/heal activity
|
// The time of the last scan/heal activity
|
||||||
lastHealActivity time.Time
|
lastHealActivity time.Time
|
||||||
|
|
||||||
// Holds the request-info for logging
|
// Holds the request-info for logging
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
|
// used to lock this structure as it is concurrently accessed
|
||||||
|
mutex sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHealSequence - creates healSettings, assumes bucket and
|
// NewHealSequence - creates healSettings, assumes bucket and
|
||||||
// objPrefix are already validated.
|
// objPrefix are already validated.
|
||||||
func newHealSequence(bucket, objPrefix, clientAddr string,
|
func newHealSequence(ctx context.Context, bucket, objPrefix, clientAddr string,
|
||||||
numDisks int, hs madmin.HealOpts, forceStart bool) *healSequence {
|
numDisks int, hs madmin.HealOpts, forceStart bool) *healSequence {
|
||||||
|
|
||||||
reqInfo := &logger.ReqInfo{RemoteHost: clientAddr, API: "Heal", BucketName: bucket}
|
reqInfo := &logger.ReqInfo{RemoteHost: clientAddr, API: "Heal", BucketName: bucket}
|
||||||
reqInfo.AppendTags("prefix", objPrefix)
|
reqInfo.AppendTags("prefix", objPrefix)
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
ctx, cancel := context.WithCancel(logger.SetReqInfo(ctx, reqInfo))
|
||||||
|
|
||||||
return &healSequence{
|
return &healSequence{
|
||||||
|
respCh: make(chan healResult),
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
objPrefix: objPrefix,
|
objPrefix: objPrefix,
|
||||||
path: pathJoin(bucket, objPrefix),
|
path: pathJoin(bucket, objPrefix),
|
||||||
@@ -385,19 +389,92 @@ func newHealSequence(bucket, objPrefix, clientAddr string,
|
|||||||
Summary: healNotStartedStatus,
|
Summary: healNotStartedStatus,
|
||||||
HealSettings: hs,
|
HealSettings: hs,
|
||||||
NumDisks: numDisks,
|
NumDisks: numDisks,
|
||||||
updateLock: &sync.RWMutex{},
|
|
||||||
},
|
},
|
||||||
traverseAndHealDoneCh: make(chan error),
|
traverseAndHealDoneCh: make(chan error),
|
||||||
stopSignalCh: make(chan struct{}),
|
cancelCtx: cancel,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
|
scannedItemsMap: make(map[madmin.HealItemType]int64),
|
||||||
|
healedItemsMap: make(map[madmin.HealItemType]int64),
|
||||||
|
healFailedItemsMap: make(map[string]int64),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resetHealStatusCounters - reset the healSequence status counters between
|
||||||
|
// each monthly background heal scanning activity.
|
||||||
|
// This is used only in case of Background healing scenario, where
|
||||||
|
// we use a single long running healSequence which reactively heals
|
||||||
|
// objects passed to the SourceCh.
|
||||||
|
func (h *healSequence) resetHealStatusCounters() {
|
||||||
|
h.mutex.Lock()
|
||||||
|
defer h.mutex.Unlock()
|
||||||
|
|
||||||
|
h.currentStatus.Items = []madmin.HealResultItem{}
|
||||||
|
h.lastSentResultIndex = 0
|
||||||
|
h.scannedItemsMap = make(map[madmin.HealItemType]int64)
|
||||||
|
h.healedItemsMap = make(map[madmin.HealItemType]int64)
|
||||||
|
h.healFailedItemsMap = make(map[string]int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getScannedItemsCount - returns a count of all scanned items
|
||||||
|
func (h *healSequence) getScannedItemsCount() int64 {
|
||||||
|
var count int64
|
||||||
|
h.mutex.RLock()
|
||||||
|
defer h.mutex.RUnlock()
|
||||||
|
|
||||||
|
for _, v := range h.scannedItemsMap {
|
||||||
|
count = count + v
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// getScannedItemsMap - returns map of all scanned items against type
|
||||||
|
func (h *healSequence) getScannedItemsMap() map[madmin.HealItemType]int64 {
|
||||||
|
h.mutex.RLock()
|
||||||
|
defer h.mutex.RUnlock()
|
||||||
|
|
||||||
|
// Make a copy before returning the value
|
||||||
|
retMap := make(map[madmin.HealItemType]int64, len(h.scannedItemsMap))
|
||||||
|
for k, v := range h.scannedItemsMap {
|
||||||
|
retMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return retMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHealedItemsMap - returns the map of all healed items against type
|
||||||
|
func (h *healSequence) getHealedItemsMap() map[madmin.HealItemType]int64 {
|
||||||
|
h.mutex.RLock()
|
||||||
|
defer h.mutex.RUnlock()
|
||||||
|
|
||||||
|
// Make a copy before returning the value
|
||||||
|
retMap := make(map[madmin.HealItemType]int64, len(h.healedItemsMap))
|
||||||
|
for k, v := range h.healedItemsMap {
|
||||||
|
retMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return retMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// gethealFailedItemsMap - returns map of all items where heal failed against
|
||||||
|
// drive endpoint and status
|
||||||
|
func (h *healSequence) gethealFailedItemsMap() map[string]int64 {
|
||||||
|
h.mutex.RLock()
|
||||||
|
defer h.mutex.RUnlock()
|
||||||
|
|
||||||
|
// Make a copy before returning the value
|
||||||
|
retMap := make(map[string]int64, len(h.healFailedItemsMap))
|
||||||
|
for k, v := range h.healFailedItemsMap {
|
||||||
|
retMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return retMap
|
||||||
|
}
|
||||||
|
|
||||||
// isQuitting - determines if the heal sequence is quitting (due to an
|
// isQuitting - determines if the heal sequence is quitting (due to an
|
||||||
// external signal)
|
// external signal)
|
||||||
func (h *healSequence) isQuitting() bool {
|
func (h *healSequence) isQuitting() bool {
|
||||||
select {
|
select {
|
||||||
case <-h.stopSignalCh:
|
case <-h.ctx.Done():
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -406,19 +483,15 @@ func (h *healSequence) isQuitting() bool {
|
|||||||
|
|
||||||
// check if the heal sequence has ended
|
// check if the heal sequence has ended
|
||||||
func (h *healSequence) hasEnded() bool {
|
func (h *healSequence) hasEnded() bool {
|
||||||
h.currentStatus.updateLock.RLock()
|
h.mutex.RLock()
|
||||||
summary := h.currentStatus.Summary
|
ended := len(h.currentStatus.Items) == 0 || h.currentStatus.Summary == healStoppedStatus || h.currentStatus.Summary == healFinishedStatus
|
||||||
h.currentStatus.updateLock.RUnlock()
|
h.mutex.RUnlock()
|
||||||
return summary == healStoppedStatus || summary == healFinishedStatus
|
return ended
|
||||||
}
|
}
|
||||||
|
|
||||||
// stops the heal sequence - safe to call multiple times.
|
// stops the heal sequence - safe to call multiple times.
|
||||||
func (h *healSequence) stop() {
|
func (h *healSequence) stop() {
|
||||||
select {
|
h.cancelCtx()
|
||||||
case <-h.stopSignalCh:
|
|
||||||
default:
|
|
||||||
close(h.stopSignalCh)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pushHealResultItem - pushes a heal result item for consumption in
|
// pushHealResultItem - pushes a heal result item for consumption in
|
||||||
@@ -445,29 +518,27 @@ func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
|||||||
|
|
||||||
var itemsLen int
|
var itemsLen int
|
||||||
for {
|
for {
|
||||||
h.currentStatus.updateLock.Lock()
|
h.mutex.Lock()
|
||||||
itemsLen = len(h.currentStatus.Items)
|
itemsLen = len(h.currentStatus.Items)
|
||||||
if itemsLen == maxUnconsumedHealResultItems {
|
if itemsLen == maxUnconsumedHealResultItems {
|
||||||
// unlock and wait to check again if we can push
|
|
||||||
h.currentStatus.updateLock.Unlock()
|
|
||||||
|
|
||||||
// wait for a second, or quit if an external
|
// wait for a second, or quit if an external
|
||||||
// stop signal is received or the
|
// stop signal is received or the
|
||||||
// unconsumedTimer fires.
|
// unconsumedTimer fires.
|
||||||
select {
|
select {
|
||||||
// Check after a second
|
// Check after a second
|
||||||
case <-time.After(time.Second):
|
case <-time.After(time.Second):
|
||||||
|
h.mutex.Unlock()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
case <-h.stopSignalCh:
|
case <-h.ctx.Done():
|
||||||
|
h.mutex.Unlock()
|
||||||
// discard result and return.
|
// discard result and return.
|
||||||
return errHealPushStopNDiscard
|
return errHealStopSignalled
|
||||||
|
|
||||||
// Timeout if no results consumed for too
|
// Timeout if no results consumed for too long.
|
||||||
// long.
|
|
||||||
case <-unconsumedTimer.C:
|
case <-unconsumedTimer.C:
|
||||||
|
h.mutex.Unlock()
|
||||||
return errHealIdleTimeout
|
return errHealIdleTimeout
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -484,13 +555,7 @@ func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
|||||||
h.currentStatus.Items = append(h.currentStatus.Items, r)
|
h.currentStatus.Items = append(h.currentStatus.Items, r)
|
||||||
|
|
||||||
// release lock
|
// release lock
|
||||||
h.currentStatus.updateLock.Unlock()
|
h.mutex.Unlock()
|
||||||
|
|
||||||
// This is a "safe" point for the heal sequence to quit if
|
|
||||||
// signaled externally.
|
|
||||||
if h.isQuitting() {
|
|
||||||
return errHealStopSignalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -504,10 +569,10 @@ func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
|||||||
// the heal-sequence.
|
// the heal-sequence.
|
||||||
func (h *healSequence) healSequenceStart() {
|
func (h *healSequence) healSequenceStart() {
|
||||||
// Set status as running
|
// Set status as running
|
||||||
h.currentStatus.updateLock.Lock()
|
h.mutex.Lock()
|
||||||
h.currentStatus.Summary = healRunningStatus
|
h.currentStatus.Summary = healRunningStatus
|
||||||
h.currentStatus.StartTime = UTCNow()
|
h.currentStatus.StartTime = UTCNow()
|
||||||
h.currentStatus.updateLock.Unlock()
|
h.mutex.Unlock()
|
||||||
|
|
||||||
if h.sourceCh == nil {
|
if h.sourceCh == nil {
|
||||||
go h.traverseAndHeal()
|
go h.traverseAndHeal()
|
||||||
@@ -517,25 +582,27 @@ func (h *healSequence) healSequenceStart() {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case err, ok := <-h.traverseAndHealDoneCh:
|
case err, ok := <-h.traverseAndHealDoneCh:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.mutex.Lock()
|
||||||
h.endTime = UTCNow()
|
h.endTime = UTCNow()
|
||||||
h.currentStatus.updateLock.Lock()
|
|
||||||
defer h.currentStatus.updateLock.Unlock()
|
|
||||||
// Heal traversal is complete.
|
// Heal traversal is complete.
|
||||||
if ok {
|
if err == nil {
|
||||||
|
// heal traversal succeeded.
|
||||||
|
h.currentStatus.Summary = healFinishedStatus
|
||||||
|
} else {
|
||||||
// heal traversal had an error.
|
// heal traversal had an error.
|
||||||
h.currentStatus.Summary = healStoppedStatus
|
h.currentStatus.Summary = healStoppedStatus
|
||||||
h.currentStatus.FailureDetail = err.Error()
|
h.currentStatus.FailureDetail = err.Error()
|
||||||
} else {
|
|
||||||
// heal traversal succeeded.
|
|
||||||
h.currentStatus.Summary = healFinishedStatus
|
|
||||||
}
|
}
|
||||||
|
h.mutex.Unlock()
|
||||||
case <-h.stopSignalCh:
|
case <-h.ctx.Done():
|
||||||
|
h.mutex.Lock()
|
||||||
h.endTime = UTCNow()
|
h.endTime = UTCNow()
|
||||||
h.currentStatus.updateLock.Lock()
|
|
||||||
h.currentStatus.Summary = healStoppedStatus
|
h.currentStatus.Summary = healStoppedStatus
|
||||||
h.currentStatus.FailureDetail = errHealStopSignalled.Error()
|
h.currentStatus.FailureDetail = errHealStopSignalled.Error()
|
||||||
h.currentStatus.updateLock.Unlock()
|
h.mutex.Unlock()
|
||||||
|
|
||||||
// drain traverse channel so the traversal
|
// drain traverse channel so the traversal
|
||||||
// go-routine does not leak.
|
// go-routine does not leak.
|
||||||
@@ -548,16 +615,48 @@ func (h *healSequence) healSequenceStart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *healSequence) queueHealTask(path string, healType madmin.HealItemType) error {
|
func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItemType) error {
|
||||||
var respCh = make(chan healResult)
|
|
||||||
defer close(respCh)
|
|
||||||
// Send heal request
|
// Send heal request
|
||||||
globalBackgroundHealRoutine.queueHealTask(healTask{path: path, responseCh: respCh, opts: h.settings})
|
task := healTask{
|
||||||
// Wait for answer and push result to the client
|
path: source.path,
|
||||||
res := <-respCh
|
opts: h.settings,
|
||||||
|
responseCh: h.respCh,
|
||||||
|
}
|
||||||
|
if !source.opts.Equal(h.settings) {
|
||||||
|
task.opts = source.opts
|
||||||
|
}
|
||||||
|
globalBackgroundHealRoutine.queueHealTask(task)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case res := <-h.respCh:
|
||||||
if !h.reportProgress {
|
if !h.reportProgress {
|
||||||
|
// Object might have been deleted, by the time heal
|
||||||
|
// was attempted, we should ignore this object and
|
||||||
|
// return success.
|
||||||
|
if isErrObjectNotFound(res.err) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mutex.Lock()
|
||||||
|
defer h.mutex.Unlock()
|
||||||
|
|
||||||
|
// Progress is not reported in case of background heal processing.
|
||||||
|
// Instead we increment relevant counter based on the heal result
|
||||||
|
// for prometheus reporting.
|
||||||
|
if res.err != nil {
|
||||||
|
for _, d := range res.result.After.Drives {
|
||||||
|
// For failed items we report the endpoint and drive state
|
||||||
|
// This will help users take corrective actions for drives
|
||||||
|
h.healFailedItemsMap[d.Endpoint+","+d.State]++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only object type reported for successful healing
|
||||||
|
h.healedItemsMap[res.result.Type]++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report caller of any failure
|
||||||
|
return res.err
|
||||||
|
}
|
||||||
res.result.Type = healType
|
res.result.Type = healType
|
||||||
if res.err != nil {
|
if res.err != nil {
|
||||||
// Object might have been deleted, by the time heal
|
// Object might have been deleted, by the time heal
|
||||||
@@ -572,11 +671,12 @@ func (h *healSequence) queueHealTask(path string, healType madmin.HealItemType)
|
|||||||
res.result.Detail = res.err.Error()
|
res.result.Detail = res.err.Error()
|
||||||
}
|
}
|
||||||
return h.pushHealResultItem(res.result)
|
return h.pushHealResultItem(res.result)
|
||||||
|
case <-h.ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *healSequence) healItemsFromSourceCh() error {
|
func (h *healSequence) healItemsFromSourceCh() error {
|
||||||
h.lastHealActivity = UTCNow()
|
|
||||||
|
|
||||||
bucketsOnly := true // heal buckets only, not objects.
|
bucketsOnly := true // heal buckets only, not objects.
|
||||||
if err := h.healItems(bucketsOnly); err != nil {
|
if err := h.healItems(bucketsOnly); err != nil {
|
||||||
logger.LogIf(h.ctx, err)
|
logger.LogIf(h.ctx, err)
|
||||||
@@ -584,28 +684,29 @@ func (h *healSequence) healItemsFromSourceCh() error {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case path := <-h.sourceCh:
|
case source, ok := <-h.sourceCh:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var itemType madmin.HealItemType
|
var itemType madmin.HealItemType
|
||||||
switch {
|
switch {
|
||||||
case path == nopHeal:
|
case source.path == nopHeal:
|
||||||
continue
|
continue
|
||||||
case path == SlashSeparator:
|
case source.path == SlashSeparator:
|
||||||
itemType = madmin.HealItemMetadata
|
itemType = madmin.HealItemMetadata
|
||||||
case !strings.Contains(path, SlashSeparator):
|
case !strings.Contains(source.path, SlashSeparator):
|
||||||
itemType = madmin.HealItemBucket
|
itemType = madmin.HealItemBucket
|
||||||
default:
|
default:
|
||||||
itemType = madmin.HealItemObject
|
itemType = madmin.HealItemObject
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.queueHealTask(path, itemType); err != nil {
|
if err := h.queueHealTask(source, itemType); err != nil {
|
||||||
logger.LogIf(h.ctx, err)
|
logger.LogIf(h.ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
h.scannedItemsCount++
|
h.scannedItemsMap[itemType]++
|
||||||
h.lastHealActivity = UTCNow()
|
h.lastHealActivity = UTCNow()
|
||||||
case <-h.traverseAndHealDoneCh:
|
case <-h.ctx.Done():
|
||||||
return nil
|
|
||||||
case <-GlobalServiceDoneCh:
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -613,7 +714,6 @@ func (h *healSequence) healItemsFromSourceCh() error {
|
|||||||
|
|
||||||
func (h *healSequence) healFromSourceCh() {
|
func (h *healSequence) healFromSourceCh() {
|
||||||
h.healItemsFromSourceCh()
|
h.healItemsFromSourceCh()
|
||||||
close(h.traverseAndHealDoneCh)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *healSequence) healItems(bucketsOnly bool) error {
|
func (h *healSequence) healItems(bucketsOnly bool) error {
|
||||||
@@ -632,11 +732,6 @@ func (h *healSequence) healItems(bucketsOnly bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start healing the background ops prefix.
|
|
||||||
if err := h.healMinioSysMeta(backgroundOpsMetaPrefix)(); err != nil {
|
|
||||||
logger.LogIf(h.ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Heal buckets and objects
|
// Heal buckets and objects
|
||||||
return h.healBuckets(bucketsOnly)
|
return h.healBuckets(bucketsOnly)
|
||||||
}
|
}
|
||||||
@@ -650,13 +745,7 @@ func (h *healSequence) healItems(bucketsOnly bool) error {
|
|||||||
// two objects.
|
// two objects.
|
||||||
func (h *healSequence) traverseAndHeal() {
|
func (h *healSequence) traverseAndHeal() {
|
||||||
bucketsOnly := false // Heals buckets and objects also.
|
bucketsOnly := false // Heals buckets and objects also.
|
||||||
if err := h.healItems(bucketsOnly); err != nil {
|
h.traverseAndHealDoneCh <- h.healItems(bucketsOnly)
|
||||||
if h.isQuitting() {
|
|
||||||
err = errHealStopSignalled
|
|
||||||
}
|
|
||||||
h.traverseAndHealDoneCh <- err
|
|
||||||
}
|
|
||||||
|
|
||||||
close(h.traverseAndHealDoneCh)
|
close(h.traverseAndHealDoneCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,12 +762,12 @@ func (h *healSequence) healMinioSysMeta(metaPrefix string) func() error {
|
|||||||
// NOTE: Healing on meta is run regardless
|
// NOTE: Healing on meta is run regardless
|
||||||
// of any bucket being selected, this is to ensure that
|
// of any bucket being selected, this is to ensure that
|
||||||
// meta are always upto date and correct.
|
// meta are always upto date and correct.
|
||||||
return objectAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, func(bucket string, object string) error {
|
return objectAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, h.settings, func(bucket string, object string) error {
|
||||||
if h.isQuitting() {
|
if h.isQuitting() {
|
||||||
return errHealStopSignalled
|
return errHealStopSignalled
|
||||||
}
|
}
|
||||||
|
|
||||||
herr := h.queueHealTask(pathJoin(bucket, object), madmin.HealItemBucketMetadata)
|
herr := h.queueHealTask(healSource{path: pathJoin(bucket, object)}, madmin.HealItemBucketMetadata)
|
||||||
// Object might have been deleted, by the time heal
|
// Object might have been deleted, by the time heal
|
||||||
// was attempted we ignore this object an move on.
|
// was attempted we ignore this object an move on.
|
||||||
if isErrObjectNotFound(herr) {
|
if isErrObjectNotFound(herr) {
|
||||||
@@ -702,7 +791,7 @@ func (h *healSequence) healDiskFormat() error {
|
|||||||
return errServerNotInitialized
|
return errServerNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.queueHealTask(SlashSeparator, madmin.HealItemMetadata)
|
return h.queueHealTask(healSource{path: SlashSeparator}, madmin.HealItemMetadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// healBuckets - check for all buckets heal or just particular bucket.
|
// healBuckets - check for all buckets heal or just particular bucket.
|
||||||
@@ -744,7 +833,7 @@ func (h *healSequence) healBucket(bucket string, bucketsOnly bool) error {
|
|||||||
return errServerNotInitialized
|
return errServerNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.queueHealTask(bucket, madmin.HealItemBucket); err != nil {
|
if err := h.queueHealTask(healSource{path: bucket}, madmin.HealItemBucket); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,7 +856,7 @@ func (h *healSequence) healBucket(bucket string, bucketsOnly bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := objectAPI.HealObjects(h.ctx, bucket, h.objPrefix, h.healObject); err != nil {
|
if err := objectAPI.HealObjects(h.ctx, bucket, h.objPrefix, h.settings, h.healObject); err != nil {
|
||||||
return errFnHealFromAPIErr(h.ctx, err)
|
return errFnHealFromAPIErr(h.ctx, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -785,5 +874,5 @@ func (h *healSequence) healObject(bucket, object string) error {
|
|||||||
return errHealStopSignalled
|
return errHealStopSignalled
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.queueHealTask(pathJoin(bucket, object), madmin.HealItemObject)
|
return h.queueHealTask(healSource{path: pathJoin(bucket, object)}, madmin.HealItemObject)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/minio/cmd/config"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/env"
|
||||||
|
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bucketQuotaConfigFile = "quota.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PutBucketQuotaConfigHandler - PUT Bucket quota configuration.
|
||||||
|
// ----------
|
||||||
|
// Places a quota configuration on the specified bucket. The quota
|
||||||
|
// specified in the quota configuration will be applied by default
|
||||||
|
// to enforce total quota for the specified bucket.
|
||||||
|
func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "PutBucketQuotaConfig")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "PutBucketQuotaConfig", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SetBucketQuotaAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
|
// Turn off quota commands if data usage info is unavailable.
|
||||||
|
if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOff {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminBucketQuotaDisabled), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = parseBucketQuota(bucket, data); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = globalBucketMetadataSys.Update(bucket, bucketQuotaConfigFile, data); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write success response.
|
||||||
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBucketQuotaConfigHandler - gets bucket quota configuration
|
||||||
|
func (a adminAPIHandlers) GetBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "GetBucketQuotaConfig")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "GetBucketQuotaConfig", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetBucketQuotaAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucket := vars["bucket"]
|
||||||
|
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := globalBucketMetadataSys.GetQuotaConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write success response.
|
||||||
|
writeSuccessResponseJSON(w, configData)
|
||||||
|
}
|
||||||
+104
-50
@@ -20,13 +20,17 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/minio/cmd/config"
|
||||||
|
"github.com/minio/minio/pkg/env"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
adminPathPrefix = minioReservedBucketPath + "/admin"
|
adminPathPrefix = minioReservedBucketPath + "/admin"
|
||||||
|
adminAPIVersionV2 = madmin.AdminAPIVersionV2
|
||||||
adminAPIVersion = madmin.AdminAPIVersion
|
adminAPIVersion = madmin.AdminAPIVersion
|
||||||
adminAPIVersionPrefix = SlashSeparator + madmin.AdminAPIVersion
|
adminAPIVersionPrefix = SlashSeparator + adminAPIVersion
|
||||||
|
adminAPIVersionV2Prefix = SlashSeparator + adminAPIVersionV2
|
||||||
)
|
)
|
||||||
|
|
||||||
// adminAPIHandlers provides HTTP handlers for MinIO admin API.
|
// adminAPIHandlers provides HTTP handlers for MinIO admin API.
|
||||||
@@ -41,126 +45,176 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
|
|||||||
|
|
||||||
/// Service operations
|
/// Service operations
|
||||||
|
|
||||||
|
adminVersions := []string{
|
||||||
|
adminAPIVersionPrefix,
|
||||||
|
adminAPIVersionV2Prefix,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, adminVersion := range adminVersions {
|
||||||
// Restart and stop MinIO service.
|
// Restart and stop MinIO service.
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix+"/service").HandlerFunc(httpTraceAll(adminAPI.ServiceActionHandler)).Queries("action", "{action:.*}")
|
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/service").HandlerFunc(httpTraceAll(adminAPI.ServiceHandler)).Queries("action", "{action:.*}")
|
||||||
// Update MinIO servers.
|
// Update MinIO servers.
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix+"/update").HandlerFunc(httpTraceAll(adminAPI.ServerUpdateHandler)).Queries("updateURL", "{updateURL:.*}")
|
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update").HandlerFunc(httpTraceAll(adminAPI.ServerUpdateHandler)).Queries("updateURL", "{updateURL:.*}")
|
||||||
|
|
||||||
// Info operations
|
// Info operations
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
|
||||||
// Harware Info operations
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/hardware").HandlerFunc(httpTraceAll(adminAPI.ServerHardwareInfoHandler)).Queries("hwType", "{hwType:.*}")
|
|
||||||
|
|
||||||
// StorageInfo operations
|
// StorageInfo operations
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/storageinfo").HandlerFunc(httpTraceAll(adminAPI.StorageInfoHandler))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/storageinfo").HandlerFunc(httpTraceAll(adminAPI.StorageInfoHandler))
|
||||||
// DataUsageInfo operations
|
// DataUsageInfo operations
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/datausageinfo").HandlerFunc(httpTraceAll(adminAPI.DataUsageInfoHandler))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/datausageinfo").HandlerFunc(httpTraceAll(adminAPI.DataUsageInfoHandler))
|
||||||
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/accountingusageinfo").HandlerFunc(httpTraceAll(adminAPI.AccountingUsageInfoHandler))
|
|
||||||
|
|
||||||
if globalIsDistXL || globalIsXL {
|
if globalIsDistXL || globalIsXL {
|
||||||
/// Heal operations
|
/// Heal operations
|
||||||
|
|
||||||
// Heal processing endpoint.
|
// Heal processing endpoint.
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix + "/heal/").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix + "/heal/{bucket}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix + "/heal/{bucket}/{prefix:.*}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}/{prefix:.*}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||||
|
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix + "/background-heal/status").HandlerFunc(httpTraceAll(adminAPI.BackgroundHealStatusHandler))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/background-heal/status").HandlerFunc(httpTraceAll(adminAPI.BackgroundHealStatusHandler))
|
||||||
|
|
||||||
/// Health operations
|
/// Health operations
|
||||||
|
|
||||||
}
|
}
|
||||||
// Performance command - return performance details based on input type
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/performance").HandlerFunc(httpTraceAll(adminAPI.PerfInfoHandler)).Queries("perfType", "{perfType:.*}")
|
|
||||||
|
|
||||||
// Profiling operations
|
// Profiling operations
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix+"/profiling/start").HandlerFunc(httpTraceAll(adminAPI.StartProfilingHandler)).
|
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/profiling/start").HandlerFunc(httpTraceAll(adminAPI.StartProfilingHandler)).
|
||||||
Queries("profilerType", "{profilerType:.*}")
|
Queries("profilerType", "{profilerType:.*}")
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/profiling/download").HandlerFunc(httpTraceAll(adminAPI.DownloadProfilingHandler))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/profiling/download").HandlerFunc(httpTraceAll(adminAPI.DownloadProfilingHandler))
|
||||||
|
|
||||||
// Config KV operations.
|
// Config KV operations.
|
||||||
if enableConfigOps {
|
if enableConfigOps {
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/get-config-kv").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKVHandler)).Queries("key", "{key:.*}")
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-config-kv").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKVHandler)).Queries("key", "{key:.*}")
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix + "/set-config-kv").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKVHandler))
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/set-config-kv").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKVHandler))
|
||||||
adminRouter.Methods(http.MethodDelete).Path(adminAPIVersionPrefix + "/del-config-kv").HandlerFunc(httpTraceHdrs(adminAPI.DelConfigKVHandler))
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/del-config-kv").HandlerFunc(httpTraceHdrs(adminAPI.DelConfigKVHandler))
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/help-config-kv").HandlerFunc(httpTraceAll(adminAPI.HelpConfigKVHandler)).Queries("subSys", "{subSys:.*}", "key", "{key:.*}")
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/list-config-history-kv").HandlerFunc(httpTraceAll(adminAPI.ListConfigHistoryKVHandler)).Queries("count", "{count:[0-9]+}")
|
|
||||||
adminRouter.Methods(http.MethodDelete).Path(adminAPIVersionPrefix+"/clear-config-history-kv").HandlerFunc(httpTraceHdrs(adminAPI.ClearConfigHistoryKVHandler)).Queries("restoreId", "{restoreId:.*}")
|
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix+"/restore-config-history-kv").HandlerFunc(httpTraceHdrs(adminAPI.RestoreConfigHistoryKVHandler)).Queries("restoreId", "{restoreId:.*}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Config operations
|
// Enable config help in all modes.
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/help-config-kv").HandlerFunc(httpTraceAll(adminAPI.HelpConfigKVHandler)).Queries("subSys", "{subSys:.*}", "key", "{key:.*}")
|
||||||
|
|
||||||
|
// Config KV history operations.
|
||||||
|
if enableConfigOps {
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-config-history-kv").HandlerFunc(httpTraceAll(adminAPI.ListConfigHistoryKVHandler)).Queries("count", "{count:[0-9]+}")
|
||||||
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/clear-config-history-kv").HandlerFunc(httpTraceHdrs(adminAPI.ClearConfigHistoryKVHandler)).Queries("restoreId", "{restoreId:.*}")
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/restore-config-history-kv").HandlerFunc(httpTraceHdrs(adminAPI.RestoreConfigHistoryKVHandler)).Queries("restoreId", "{restoreId:.*}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Config import/export bulk operations
|
||||||
if enableConfigOps {
|
if enableConfigOps {
|
||||||
// Get config
|
// Get config
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
||||||
// Set config
|
// Set config
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix + "/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
|
||||||
}
|
}
|
||||||
|
|
||||||
if enableIAMOps {
|
if enableIAMOps {
|
||||||
// -- IAM APIs --
|
// -- IAM APIs --
|
||||||
|
|
||||||
// Add policy IAM
|
// Add policy IAM
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix+"/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name",
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}")
|
||||||
"{name:.*}")
|
|
||||||
|
|
||||||
// Add user IAM
|
// Add user IAM
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix+"/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
|
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix+"/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)).
|
|
||||||
Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
|
|
||||||
|
|
||||||
// Info policy IAM
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountusageinfo").HandlerFunc(httpTraceAll(adminAPI.AccountUsageInfoHandler))
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}")
|
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
|
||||||
|
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)).Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
|
||||||
|
|
||||||
|
// Service accounts ops
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/add-service-account").HandlerFunc(httpTraceHdrs(adminAPI.AddServiceAccount))
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-service-accounts").HandlerFunc(httpTraceHdrs(adminAPI.ListServiceAccounts))
|
||||||
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/delete-service-account").HandlerFunc(httpTraceHdrs(adminAPI.DeleteServiceAccount)).Queries("accessKey", "{accessKey:.*}")
|
||||||
|
|
||||||
|
if adminVersion == adminAPIVersionV2Prefix {
|
||||||
|
// Info policy IAM v2
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicyV2)).Queries("name", "{name:.*}")
|
||||||
|
|
||||||
|
// List policies v2
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPoliciesV2))
|
||||||
|
} else {
|
||||||
|
// Info policy IAM latest
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}")
|
||||||
|
|
||||||
|
// List policies latest
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
|
||||||
|
}
|
||||||
|
|
||||||
// Remove policy IAM
|
// Remove policy IAM
|
||||||
adminRouter.Methods(http.MethodDelete).Path(adminAPIVersionPrefix+"/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
|
||||||
|
|
||||||
// Set user or group policy
|
// Set user or group policy
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix+"/set-user-or-group-policy").
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-user-or-group-policy").
|
||||||
HandlerFunc(httpTraceHdrs(adminAPI.SetPolicyForUserOrGroup)).
|
HandlerFunc(httpTraceHdrs(adminAPI.SetPolicyForUserOrGroup)).
|
||||||
Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}")
|
Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}")
|
||||||
|
|
||||||
// Remove user IAM
|
// Remove user IAM
|
||||||
adminRouter.Methods(http.MethodDelete).Path(adminAPIVersionPrefix+"/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
|
||||||
|
|
||||||
// List users
|
// List users
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))
|
||||||
|
|
||||||
// User info
|
// User info
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/user-info").HandlerFunc(httpTraceHdrs(adminAPI.GetUserInfo)).Queries("accessKey", "{accessKey:.*}")
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/user-info").HandlerFunc(httpTraceHdrs(adminAPI.GetUserInfo)).Queries("accessKey", "{accessKey:.*}")
|
||||||
|
|
||||||
// Add/Remove members from group
|
// Add/Remove members from group
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix + "/update-group-members").HandlerFunc(httpTraceHdrs(adminAPI.UpdateGroupMembers))
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/update-group-members").HandlerFunc(httpTraceHdrs(adminAPI.UpdateGroupMembers))
|
||||||
|
|
||||||
// Get Group
|
// Get Group
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/group").HandlerFunc(httpTraceHdrs(adminAPI.GetGroup)).Queries("group", "{group:.*}")
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/group").HandlerFunc(httpTraceHdrs(adminAPI.GetGroup)).Queries("group", "{group:.*}")
|
||||||
|
|
||||||
// List Groups
|
// List Groups
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/groups").HandlerFunc(httpTraceHdrs(adminAPI.ListGroups))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/groups").HandlerFunc(httpTraceHdrs(adminAPI.ListGroups))
|
||||||
|
|
||||||
// Set Group Status
|
// Set Group Status
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix+"/set-group-status").HandlerFunc(httpTraceHdrs(adminAPI.SetGroupStatus)).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(httpTraceHdrs(adminAPI.SetGroupStatus)).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
|
||||||
|
}
|
||||||
|
|
||||||
// List policies
|
// Quota operations
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
|
if globalIsXL || globalIsDistXL {
|
||||||
|
if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOn {
|
||||||
|
// GetBucketQuotaConfig
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
|
||||||
|
httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}")
|
||||||
|
// PutBucketQuotaConfig
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc(
|
||||||
|
httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Top APIs --
|
// -- Top APIs --
|
||||||
// Top locks
|
// Top locks
|
||||||
if globalIsDistXL {
|
if globalIsDistXL {
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/top/locks").HandlerFunc(httpTraceHdrs(adminAPI.TopLocksHandler))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/top/locks").HandlerFunc(httpTraceHdrs(adminAPI.TopLocksHandler))
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP Trace
|
// HTTP Trace
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/trace").HandlerFunc(adminAPI.TraceHandler)
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/trace").HandlerFunc(adminAPI.TraceHandler)
|
||||||
|
|
||||||
// Console Logs
|
// Console Logs
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/log").HandlerFunc(httpTraceAll(adminAPI.ConsoleLogHandler))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/log").HandlerFunc(httpTraceAll(adminAPI.ConsoleLogHandler))
|
||||||
|
|
||||||
// -- KMS APIs --
|
// -- KMS APIs --
|
||||||
//
|
//
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/kms/key/status").HandlerFunc(httpTraceAll(adminAPI.KMSKeyStatusHandler))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/kms/key/status").HandlerFunc(httpTraceAll(adminAPI.KMSKeyStatusHandler))
|
||||||
|
|
||||||
|
if !globalIsGateway {
|
||||||
|
// -- OBD API --
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/obdinfo").
|
||||||
|
HandlerFunc(httpTraceHdrs(adminAPI.OBDInfoHandler)).
|
||||||
|
Queries("perfdrive", "{perfdrive:true|false}",
|
||||||
|
"perfnet", "{perfnet:true|false}",
|
||||||
|
"minioinfo", "{minioinfo:true|false}",
|
||||||
|
"minioconfig", "{minioconfig:true|false}",
|
||||||
|
"syscpu", "{syscpu:true|false}",
|
||||||
|
"sysdiskhw", "{sysdiskhw:true|false}",
|
||||||
|
"sysosinfo", "{sysosinfo:true|false}",
|
||||||
|
"sysmem", "{sysmem:true|false}",
|
||||||
|
"sysprocess", "{sysprocess:true|false}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If none of the routes match add default error handler routes
|
// If none of the routes match add default error handler routes
|
||||||
adminRouter.NotFoundHandler = http.HandlerFunc(httpTraceAll(errorResponseHandler))
|
adminRouter.NotFoundHandler = http.HandlerFunc(httpTraceAll(errorResponseHandler))
|
||||||
|
|||||||
+1
-163
@@ -17,176 +17,14 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v6/pkg/set"
|
|
||||||
"github.com/minio/minio/pkg/cpu"
|
|
||||||
"github.com/minio/minio/pkg/disk"
|
"github.com/minio/minio/pkg/disk"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
"github.com/minio/minio/pkg/mem"
|
|
||||||
|
|
||||||
cpuhw "github.com/shirou/gopsutil/cpu"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// getLocalMemUsage - returns ServerMemUsageInfo for all zones, endpoints.
|
// getLocalServerProperty - returns madmin.ServerProperties for only the
|
||||||
func getLocalMemUsage(endpointZones EndpointZones, r *http.Request) ServerMemUsageInfo {
|
|
||||||
var memUsages []mem.Usage
|
|
||||||
var historicUsages []mem.Usage
|
|
||||||
seenHosts := set.NewStringSet()
|
|
||||||
for _, ep := range endpointZones {
|
|
||||||
for _, endpoint := range ep.Endpoints {
|
|
||||||
if seenHosts.Contains(endpoint.Host) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seenHosts.Add(endpoint.Host)
|
|
||||||
|
|
||||||
// Only proceed for local endpoints
|
|
||||||
if endpoint.IsLocal {
|
|
||||||
memUsages = append(memUsages, mem.GetUsage())
|
|
||||||
historicUsages = append(historicUsages, mem.GetHistoricUsage())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addr := r.Host
|
|
||||||
if globalIsDistXL {
|
|
||||||
addr = GetLocalPeer(endpointZones)
|
|
||||||
}
|
|
||||||
return ServerMemUsageInfo{
|
|
||||||
Addr: addr,
|
|
||||||
Usage: memUsages,
|
|
||||||
HistoricUsage: historicUsages,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getLocalCPULoad - returns ServerCPULoadInfo for all zones, endpoints.
|
|
||||||
func getLocalCPULoad(endpointZones EndpointZones, r *http.Request) ServerCPULoadInfo {
|
|
||||||
var cpuLoads []cpu.Load
|
|
||||||
var historicLoads []cpu.Load
|
|
||||||
seenHosts := set.NewStringSet()
|
|
||||||
for _, ep := range endpointZones {
|
|
||||||
for _, endpoint := range ep.Endpoints {
|
|
||||||
if seenHosts.Contains(endpoint.Host) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seenHosts.Add(endpoint.Host)
|
|
||||||
|
|
||||||
// Only proceed for local endpoints
|
|
||||||
if endpoint.IsLocal {
|
|
||||||
cpuLoads = append(cpuLoads, cpu.GetLoad())
|
|
||||||
historicLoads = append(historicLoads, cpu.GetHistoricLoad())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addr := r.Host
|
|
||||||
if globalIsDistXL {
|
|
||||||
addr = GetLocalPeer(endpointZones)
|
|
||||||
}
|
|
||||||
return ServerCPULoadInfo{
|
|
||||||
Addr: addr,
|
|
||||||
Load: cpuLoads,
|
|
||||||
HistoricLoad: historicLoads,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getLocalDrivesPerf - returns ServerDrivesPerfInfo for all zones, endpoints.
|
|
||||||
func getLocalDrivesPerf(endpointZones EndpointZones, size int64, r *http.Request) madmin.ServerDrivesPerfInfo {
|
|
||||||
var dps []disk.Performance
|
|
||||||
for _, ep := range endpointZones {
|
|
||||||
for _, endpoint := range ep.Endpoints {
|
|
||||||
// Only proceed for local endpoints
|
|
||||||
if endpoint.IsLocal {
|
|
||||||
if _, err := os.Stat(endpoint.Path); err != nil {
|
|
||||||
// Since this drive is not available, add relevant details and proceed
|
|
||||||
dps = append(dps, disk.Performance{Path: endpoint.Path, Error: err.Error()})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dp := disk.GetPerformance(pathJoin(endpoint.Path, minioMetaTmpBucket, mustGetUUID()), size)
|
|
||||||
dp.Path = endpoint.Path
|
|
||||||
dps = append(dps, dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addr := r.Host
|
|
||||||
if globalIsDistXL {
|
|
||||||
addr = GetLocalPeer(endpointZones)
|
|
||||||
}
|
|
||||||
return madmin.ServerDrivesPerfInfo{
|
|
||||||
Addr: addr,
|
|
||||||
Perf: dps,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getLocalCPUInfo - returns ServerCPUHardwareInfo for all zones, endpoints.
|
|
||||||
func getLocalCPUInfo(endpointZones EndpointZones, r *http.Request) madmin.ServerCPUHardwareInfo {
|
|
||||||
var cpuHardwares []cpuhw.InfoStat
|
|
||||||
seenHosts := set.NewStringSet()
|
|
||||||
for _, ep := range endpointZones {
|
|
||||||
for _, endpoint := range ep.Endpoints {
|
|
||||||
if seenHosts.Contains(endpoint.Host) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Add to the list of visited hosts
|
|
||||||
seenHosts.Add(endpoint.Host)
|
|
||||||
// Only proceed for local endpoints
|
|
||||||
if endpoint.IsLocal {
|
|
||||||
cpuHardware, err := cpuhw.Info()
|
|
||||||
if err != nil {
|
|
||||||
return madmin.ServerCPUHardwareInfo{
|
|
||||||
Error: err.Error(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cpuHardwares = append(cpuHardwares, cpuHardware...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addr := r.Host
|
|
||||||
if globalIsDistXL {
|
|
||||||
addr = GetLocalPeer(endpointZones)
|
|
||||||
}
|
|
||||||
|
|
||||||
return madmin.ServerCPUHardwareInfo{
|
|
||||||
Addr: addr,
|
|
||||||
CPUInfo: cpuHardwares,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getLocalNetworkInfo - returns ServerNetworkHardwareInfo for all zones, endpoints.
|
|
||||||
func getLocalNetworkInfo(endpointZones EndpointZones, r *http.Request) madmin.ServerNetworkHardwareInfo {
|
|
||||||
var networkHardwares []net.Interface
|
|
||||||
seenHosts := set.NewStringSet()
|
|
||||||
for _, ep := range endpointZones {
|
|
||||||
for _, endpoint := range ep.Endpoints {
|
|
||||||
if seenHosts.Contains(endpoint.Host) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Add to the list of visited hosts
|
|
||||||
seenHosts.Add(endpoint.Host)
|
|
||||||
// Only proceed for local endpoints
|
|
||||||
if endpoint.IsLocal {
|
|
||||||
networkHardware, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
return madmin.ServerNetworkHardwareInfo{
|
|
||||||
Error: err.Error(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
networkHardwares = append(networkHardwares, networkHardware...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addr := r.Host
|
|
||||||
if globalIsDistXL {
|
|
||||||
addr = GetLocalPeer(endpointZones)
|
|
||||||
}
|
|
||||||
|
|
||||||
return madmin.ServerNetworkHardwareInfo{
|
|
||||||
Addr: addr,
|
|
||||||
NetworkInfo: networkHardwares,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getLocalServerProperty - returns ServerDrivesPerfInfo for only the
|
|
||||||
// local endpoints from given list of endpoints
|
// local endpoints from given list of endpoints
|
||||||
func getLocalServerProperty(endpointZones EndpointZones, r *http.Request) madmin.ServerProperties {
|
func getLocalServerProperty(endpointZones EndpointZones, r *http.Request) madmin.ServerProperties {
|
||||||
var disks []madmin.Disk
|
var disks []madmin.Disk
|
||||||
|
|||||||
+102
-19
@@ -25,17 +25,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
|
||||||
"google.golang.org/api/googleapi"
|
"google.golang.org/api/googleapi"
|
||||||
|
|
||||||
minio "github.com/minio/minio-go/v6"
|
minio "github.com/minio/minio-go/v6"
|
||||||
|
"github.com/minio/minio-go/v6/pkg/tags"
|
||||||
"github.com/minio/minio/cmd/config/etcd/dns"
|
"github.com/minio/minio/cmd/config/etcd/dns"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/bucket/lifecycle"
|
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||||
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
||||||
"github.com/minio/minio/pkg/bucket/object/tagging"
|
|
||||||
"github.com/minio/minio/pkg/bucket/policy"
|
"github.com/minio/minio/pkg/bucket/policy"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
@@ -101,6 +100,9 @@ const (
|
|||||||
ErrNoSuchBucketLifecycle
|
ErrNoSuchBucketLifecycle
|
||||||
ErrNoSuchLifecycleConfiguration
|
ErrNoSuchLifecycleConfiguration
|
||||||
ErrNoSuchBucketSSEConfig
|
ErrNoSuchBucketSSEConfig
|
||||||
|
ErrNoSuchCORSConfiguration
|
||||||
|
ErrNoSuchWebsiteConfiguration
|
||||||
|
ErrReplicationConfigurationNotFoundError
|
||||||
ErrNoSuchKey
|
ErrNoSuchKey
|
||||||
ErrNoSuchUpload
|
ErrNoSuchUpload
|
||||||
ErrNoSuchVersion
|
ErrNoSuchVersion
|
||||||
@@ -122,7 +124,8 @@ const (
|
|||||||
ErrMissingCredTag
|
ErrMissingCredTag
|
||||||
ErrCredMalformed
|
ErrCredMalformed
|
||||||
ErrInvalidRegion
|
ErrInvalidRegion
|
||||||
ErrInvalidService
|
ErrInvalidServiceS3
|
||||||
|
ErrInvalidServiceSTS
|
||||||
ErrInvalidRequestVersion
|
ErrInvalidRequestVersion
|
||||||
ErrMissingSignTag
|
ErrMissingSignTag
|
||||||
ErrMissingSignHeadersTag
|
ErrMissingSignHeadersTag
|
||||||
@@ -150,12 +153,14 @@ const (
|
|||||||
ErrBadRequest
|
ErrBadRequest
|
||||||
ErrKeyTooLongError
|
ErrKeyTooLongError
|
||||||
ErrInvalidBucketObjectLockConfiguration
|
ErrInvalidBucketObjectLockConfiguration
|
||||||
|
ErrObjectLockConfigurationNotFound
|
||||||
ErrObjectLockConfigurationNotAllowed
|
ErrObjectLockConfigurationNotAllowed
|
||||||
ErrNoSuchObjectLockConfiguration
|
ErrNoSuchObjectLockConfiguration
|
||||||
ErrObjectLocked
|
ErrObjectLocked
|
||||||
ErrInvalidRetentionDate
|
ErrInvalidRetentionDate
|
||||||
ErrPastObjectLockRetainDate
|
ErrPastObjectLockRetainDate
|
||||||
ErrUnknownWORMModeDirective
|
ErrUnknownWORMModeDirective
|
||||||
|
ErrBucketTaggingNotFound
|
||||||
ErrObjectLockInvalidHeaders
|
ErrObjectLockInvalidHeaders
|
||||||
ErrInvalidTagDirective
|
ErrInvalidTagDirective
|
||||||
// Add new error codes here.
|
// Add new error codes here.
|
||||||
@@ -210,6 +215,7 @@ const (
|
|||||||
ErrInvalidResourceName
|
ErrInvalidResourceName
|
||||||
ErrServerNotInitialized
|
ErrServerNotInitialized
|
||||||
ErrOperationTimedOut
|
ErrOperationTimedOut
|
||||||
|
ErrOperationMaxedOut
|
||||||
ErrInvalidRequest
|
ErrInvalidRequest
|
||||||
// MinIO storage class error codes
|
// MinIO storage class error codes
|
||||||
ErrInvalidStorageClass
|
ErrInvalidStorageClass
|
||||||
@@ -233,6 +239,10 @@ const (
|
|||||||
ErrAdminCredentialsMismatch
|
ErrAdminCredentialsMismatch
|
||||||
ErrInsecureClientRequest
|
ErrInsecureClientRequest
|
||||||
ErrObjectTampered
|
ErrObjectTampered
|
||||||
|
// Bucket Quota error codes
|
||||||
|
ErrAdminBucketQuotaExceeded
|
||||||
|
ErrAdminNoSuchQuotaConfiguration
|
||||||
|
ErrAdminBucketQuotaDisabled
|
||||||
|
|
||||||
ErrHealNotImplemented
|
ErrHealNotImplemented
|
||||||
ErrHealNoSuchProcess
|
ErrHealNoSuchProcess
|
||||||
@@ -332,19 +342,28 @@ const (
|
|||||||
ErrAdminProfilerNotEnabled
|
ErrAdminProfilerNotEnabled
|
||||||
ErrInvalidDecompressedSize
|
ErrInvalidDecompressedSize
|
||||||
ErrAddUserInvalidArgument
|
ErrAddUserInvalidArgument
|
||||||
|
ErrAdminAccountNotEligible
|
||||||
|
ErrServiceAccountNotFound
|
||||||
ErrPostPolicyConditionInvalidFormat
|
ErrPostPolicyConditionInvalidFormat
|
||||||
)
|
)
|
||||||
|
|
||||||
type errorCodeMap map[APIErrorCode]APIError
|
type errorCodeMap map[APIErrorCode]APIError
|
||||||
|
|
||||||
func (e errorCodeMap) ToAPIErr(errCode APIErrorCode) APIError {
|
func (e errorCodeMap) ToAPIErrWithErr(errCode APIErrorCode, err error) APIError {
|
||||||
apiErr, ok := e[errCode]
|
apiErr, ok := e[errCode]
|
||||||
if !ok {
|
if !ok {
|
||||||
return e[ErrInternalError]
|
apiErr = e[ErrInternalError]
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
|
||||||
}
|
}
|
||||||
return apiErr
|
return apiErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e errorCodeMap) ToAPIErr(errCode APIErrorCode) APIError {
|
||||||
|
return e.ToAPIErrWithErr(errCode, nil)
|
||||||
|
}
|
||||||
|
|
||||||
// error code to APIError structure, these fields carry respective
|
// error code to APIError structure, these fields carry respective
|
||||||
// descriptions for all the error responses.
|
// descriptions for all the error responses.
|
||||||
var errorCodes = errorCodeMap{
|
var errorCodes = errorCodeMap{
|
||||||
@@ -440,7 +459,7 @@ var errorCodes = errorCodeMap{
|
|||||||
},
|
},
|
||||||
ErrInvalidAccessKeyID: {
|
ErrInvalidAccessKeyID: {
|
||||||
Code: "InvalidAccessKeyId",
|
Code: "InvalidAccessKeyId",
|
||||||
Description: "The access key ID you provided does not exist in our records.",
|
Description: "The Access Key Id you provided does not exist in our records.",
|
||||||
HTTPStatusCode: http.StatusForbidden,
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
ErrInvalidBucketName: {
|
ErrInvalidBucketName: {
|
||||||
@@ -652,9 +671,14 @@ var errorCodes = errorCodeMap{
|
|||||||
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
||||||
// right Description: "Error parsing the X-Amz-Credential parameter; incorrect service \"s4\". This endpoint belongs to \"s3\".".
|
// right Description: "Error parsing the X-Amz-Credential parameter; incorrect service \"s4\". This endpoint belongs to \"s3\".".
|
||||||
// Need changes to make sure variable messages can be constructed.
|
// Need changes to make sure variable messages can be constructed.
|
||||||
ErrInvalidService: {
|
ErrInvalidServiceS3: {
|
||||||
Code: "AuthorizationQueryParametersError",
|
Code: "AuthorizationParametersError",
|
||||||
Description: "Error parsing the X-Amz-Credential parameter; incorrect service. This endpoint belongs to \"s3\".",
|
Description: "Error parsing the Credential/X-Amz-Credential parameter; incorrect service. This endpoint belongs to \"s3\".",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidServiceSTS: {
|
||||||
|
Code: "AuthorizationParametersError",
|
||||||
|
Description: "Error parsing the Credential parameter; incorrect service. This endpoint belongs to \"sts\".",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
||||||
@@ -757,11 +781,36 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Bucket is missing ObjectLockConfiguration",
|
Description: "Bucket is missing ObjectLockConfiguration",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrBucketTaggingNotFound: {
|
||||||
|
Code: "NoSuchTagSet",
|
||||||
|
Description: "The TagSet does not exist",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
ErrObjectLockConfigurationNotFound: {
|
||||||
|
Code: "ObjectLockConfigurationNotFoundError",
|
||||||
|
Description: "Object Lock configuration does not exist for this bucket",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
ErrObjectLockConfigurationNotAllowed: {
|
ErrObjectLockConfigurationNotAllowed: {
|
||||||
Code: "InvalidBucketState",
|
Code: "InvalidBucketState",
|
||||||
Description: "Object Lock configuration cannot be enabled on existing buckets.",
|
Description: "Object Lock configuration cannot be enabled on existing buckets",
|
||||||
HTTPStatusCode: http.StatusConflict,
|
HTTPStatusCode: http.StatusConflict,
|
||||||
},
|
},
|
||||||
|
ErrNoSuchCORSConfiguration: {
|
||||||
|
Code: "NoSuchCORSConfiguration",
|
||||||
|
Description: "The CORS configuration does not exist",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
ErrNoSuchWebsiteConfiguration: {
|
||||||
|
Code: "NoSuchWebsiteConfiguration",
|
||||||
|
Description: "The specified bucket does not have a website configuration",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
ErrReplicationConfigurationNotFoundError: {
|
||||||
|
Code: "ReplicationConfigurationNotFoundError",
|
||||||
|
Description: "The replication configuration was not found",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
ErrNoSuchObjectLockConfiguration: {
|
ErrNoSuchObjectLockConfiguration: {
|
||||||
Code: "NoSuchObjectLockConfiguration",
|
Code: "NoSuchObjectLockConfiguration",
|
||||||
Description: "The specified object does not have a ObjectLock configuration",
|
Description: "The specified object does not have a ObjectLock configuration",
|
||||||
@@ -1068,6 +1117,21 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Credentials in config mismatch with server environment variables",
|
Description: "Credentials in config mismatch with server environment variables",
|
||||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
},
|
},
|
||||||
|
ErrAdminBucketQuotaExceeded: {
|
||||||
|
Code: "XMinioAdminBucketQuotaExceeded",
|
||||||
|
Description: "Bucket quota exceeded",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrAdminNoSuchQuotaConfiguration: {
|
||||||
|
Code: "XMinioAdminNoSuchQuotaConfiguration",
|
||||||
|
Description: "The quota configuration does not exist",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
ErrAdminBucketQuotaDisabled: {
|
||||||
|
Code: "XMinioAdminBucketQuotaDisabled",
|
||||||
|
Description: "Quota specified but disk usage crawl is disabled on MinIO server",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrInsecureClientRequest: {
|
ErrInsecureClientRequest: {
|
||||||
Code: "XMinioInsecureClientRequest",
|
Code: "XMinioInsecureClientRequest",
|
||||||
Description: "Cannot respond to plain-text request from TLS-encrypted server",
|
Description: "Cannot respond to plain-text request from TLS-encrypted server",
|
||||||
@@ -1078,6 +1142,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "A timeout occurred while trying to lock a resource",
|
Description: "A timeout occurred while trying to lock a resource",
|
||||||
HTTPStatusCode: http.StatusRequestTimeout,
|
HTTPStatusCode: http.StatusRequestTimeout,
|
||||||
},
|
},
|
||||||
|
ErrOperationMaxedOut: {
|
||||||
|
Code: "XMinioServerTimedOut",
|
||||||
|
Description: "A timeout exceeded while waiting to proceed with the request",
|
||||||
|
HTTPStatusCode: http.StatusRequestTimeout,
|
||||||
|
},
|
||||||
ErrUnsupportedMetadata: {
|
ErrUnsupportedMetadata: {
|
||||||
Code: "InvalidArgument",
|
Code: "InvalidArgument",
|
||||||
Description: "Your metadata headers are not supported.",
|
Description: "Your metadata headers are not supported.",
|
||||||
@@ -1573,6 +1642,16 @@ 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.StatusConflict,
|
HTTPStatusCode: http.StatusConflict,
|
||||||
},
|
},
|
||||||
|
ErrAdminAccountNotEligible: {
|
||||||
|
Code: "XMinioInvalidIAMCredentials",
|
||||||
|
Description: "The administrator key is not eligible for this operation",
|
||||||
|
HTTPStatusCode: http.StatusConflict,
|
||||||
|
},
|
||||||
|
ErrServiceAccountNotFound: {
|
||||||
|
Code: "XMinioInvalidIAMCredentials",
|
||||||
|
Description: "The specified service account is not found",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
ErrPostPolicyConditionInvalidFormat: {
|
ErrPostPolicyConditionInvalidFormat: {
|
||||||
Code: "PostPolicyInvalidKeyName",
|
Code: "PostPolicyInvalidKeyName",
|
||||||
Description: "Invalid according to Policy: Policy Condition failed",
|
Description: "Invalid according to Policy: Policy Condition failed",
|
||||||
@@ -1588,7 +1667,7 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return ErrNone
|
return ErrNone
|
||||||
}
|
}
|
||||||
// Verify if the underlying error is signature mismatch.
|
|
||||||
switch err {
|
switch err {
|
||||||
case errInvalidArgument:
|
case errInvalidArgument:
|
||||||
apiErr = ErrAdminInvalidArgument
|
apiErr = ErrAdminInvalidArgument
|
||||||
@@ -1641,7 +1720,7 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrKMSNotConfigured
|
apiErr = ErrKMSNotConfigured
|
||||||
case crypto.ErrKMSAuthLogin:
|
case crypto.ErrKMSAuthLogin:
|
||||||
apiErr = ErrKMSAuthFailure
|
apiErr = ErrKMSAuthFailure
|
||||||
case errOperationTimedOut, context.Canceled, context.DeadlineExceeded:
|
case context.Canceled, context.DeadlineExceeded:
|
||||||
apiErr = ErrOperationTimedOut
|
apiErr = ErrOperationTimedOut
|
||||||
case errDiskNotFound:
|
case errDiskNotFound:
|
||||||
apiErr = ErrSlowDown
|
apiErr = ErrSlowDown
|
||||||
@@ -1747,6 +1826,14 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrNoSuchLifecycleConfiguration
|
apiErr = ErrNoSuchLifecycleConfiguration
|
||||||
case BucketSSEConfigNotFound:
|
case BucketSSEConfigNotFound:
|
||||||
apiErr = ErrNoSuchBucketSSEConfig
|
apiErr = ErrNoSuchBucketSSEConfig
|
||||||
|
case BucketTaggingNotFound:
|
||||||
|
apiErr = ErrBucketTaggingNotFound
|
||||||
|
case BucketObjectLockConfigNotFound:
|
||||||
|
apiErr = ErrObjectLockConfigurationNotFound
|
||||||
|
case BucketQuotaConfigNotFound:
|
||||||
|
apiErr = ErrAdminNoSuchQuotaConfiguration
|
||||||
|
case BucketQuotaExceeded:
|
||||||
|
apiErr = ErrAdminBucketQuotaExceeded
|
||||||
case *event.ErrInvalidEventName:
|
case *event.ErrInvalidEventName:
|
||||||
apiErr = ErrEventNotification
|
apiErr = ErrEventNotification
|
||||||
case *event.ErrInvalidARN:
|
case *event.ErrInvalidARN:
|
||||||
@@ -1769,6 +1856,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrOverlappingFilterNotification
|
apiErr = ErrOverlappingFilterNotification
|
||||||
case *event.ErrUnsupportedConfiguration:
|
case *event.ErrUnsupportedConfiguration:
|
||||||
apiErr = ErrUnsupportedNotification
|
apiErr = ErrUnsupportedNotification
|
||||||
|
case OperationTimedOut:
|
||||||
|
apiErr = ErrOperationTimedOut
|
||||||
case BackendDown:
|
case BackendDown:
|
||||||
apiErr = ErrBackendDown
|
apiErr = ErrBackendDown
|
||||||
case ObjectNameTooLong:
|
case ObjectNameTooLong:
|
||||||
@@ -1835,7 +1924,7 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
Description: e.Error(),
|
Description: e.Error(),
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
}
|
}
|
||||||
case tagging.Error:
|
case tags.Error:
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: e.Code(),
|
Code: e.Code(),
|
||||||
Description: e.Error(),
|
Description: e.Error(),
|
||||||
@@ -1877,12 +1966,6 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
Description: e.Error(),
|
Description: e.Error(),
|
||||||
HTTPStatusCode: e.Response().StatusCode,
|
HTTPStatusCode: e.Response().StatusCode,
|
||||||
}
|
}
|
||||||
case oss.ServiceError:
|
|
||||||
apiErr = APIError{
|
|
||||||
Code: e.Code,
|
|
||||||
Description: e.Message,
|
|
||||||
HTTPStatusCode: e.StatusCode,
|
|
||||||
}
|
|
||||||
// Add more Gateway SDKs here if any in future.
|
// Add more Gateway SDKs here if any in future.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-15
@@ -24,6 +24,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
@@ -81,6 +82,10 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
|||||||
w.Header()[xhttp.ETag] = []string{"\"" + objInfo.ETag + "\""}
|
w.Header()[xhttp.ETag] = []string{"\"" + objInfo.ETag + "\""}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.Contains(objInfo.ETag, "-") && len(objInfo.Parts) > 0 {
|
||||||
|
w.Header()[xhttp.AmzMpPartsCount] = []string{strconv.Itoa(len(objInfo.Parts))}
|
||||||
|
}
|
||||||
|
|
||||||
if objInfo.ContentType != "" {
|
if objInfo.ContentType != "" {
|
||||||
w.Header().Set(xhttp.ContentType, objInfo.ContentType)
|
w.Header().Set(xhttp.ContentType, objInfo.ContentType)
|
||||||
}
|
}
|
||||||
@@ -92,6 +97,7 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
|||||||
if !objInfo.Expires.IsZero() {
|
if !objInfo.Expires.IsZero() {
|
||||||
w.Header().Set(xhttp.Expires, objInfo.Expires.UTC().Format(http.TimeFormat))
|
w.Header().Set(xhttp.Expires, objInfo.Expires.UTC().Format(http.TimeFormat))
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalCacheConfig.Enabled {
|
if globalCacheConfig.Enabled {
|
||||||
w.Header().Set(xhttp.XCache, objInfo.CacheStatus.String())
|
w.Header().Set(xhttp.XCache, objInfo.CacheStatus.String())
|
||||||
w.Header().Set(xhttp.XCacheLookup, objInfo.CacheLookupStatus.String())
|
w.Header().Set(xhttp.XCacheLookup, objInfo.CacheLookupStatus.String())
|
||||||
@@ -100,35 +106,35 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
|||||||
// Set tag count if object has tags
|
// Set tag count if object has tags
|
||||||
tags, _ := url.ParseQuery(objInfo.UserTags)
|
tags, _ := url.ParseQuery(objInfo.UserTags)
|
||||||
tagCount := len(tags)
|
tagCount := len(tags)
|
||||||
if tagCount != 0 {
|
if tagCount > 0 {
|
||||||
w.Header().Set(xhttp.AmzTagCount, strconv.Itoa(tagCount))
|
w.Header()[xhttp.AmzTagCount] = []string{strconv.Itoa(tagCount)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set all other user defined metadata.
|
// Set all other user defined metadata.
|
||||||
for k, v := range objInfo.UserDefined {
|
for k, v := range objInfo.UserDefined {
|
||||||
if HasPrefix(k, ReservedMetadataPrefix) {
|
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||||
// Do not need to send any internal metadata
|
// Do not need to send any internal metadata
|
||||||
// values to client.
|
// values to client.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
var isSet bool
|
||||||
|
for _, userMetadataPrefix := range userMetadataKeyPrefixes {
|
||||||
|
if !strings.HasPrefix(k, userMetadataPrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
w.Header()[strings.ToLower(k)] = []string{v}
|
||||||
|
isSet = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !isSet {
|
||||||
w.Header().Set(k, v)
|
w.Header().Set(k, v)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var totalObjectSize int64
|
totalObjectSize, err := objInfo.GetActualSize()
|
||||||
switch {
|
|
||||||
case crypto.IsEncrypted(objInfo.UserDefined):
|
|
||||||
totalObjectSize, err = objInfo.DecryptedSize()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case objInfo.IsCompressed():
|
|
||||||
totalObjectSize = objInfo.GetActualSize()
|
|
||||||
if totalObjectSize < 0 {
|
|
||||||
return errInvalidDecompressedSize
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
totalObjectSize = objInfo.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
// for providing ranged content
|
// for providing ranged content
|
||||||
start, rangeLen, err := rs.GetOffsetLength(totalObjectSize)
|
start, rangeLen, err := rs.GetOffsetLength(totalObjectSize)
|
||||||
|
|||||||
+29
-24
@@ -33,8 +33,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
timeFormatAMZLong = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
|
// RFC3339 a subset of the ISO8601 timestamp format. e.g 2014-04-29T18:30:38Z
|
||||||
maxObjectList = 10000 // Limit number of objects in a listObjectsResponse.
|
iso8601TimeFormat = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
|
||||||
|
maxObjectList = 50000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
|
||||||
|
maxDeleteList = 10000 // Limit number of objects deleted in a delete call.
|
||||||
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
|
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
|
||||||
maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
|
maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
|
||||||
)
|
)
|
||||||
@@ -400,7 +402,7 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
|
|||||||
for _, bucket := range buckets {
|
for _, bucket := range buckets {
|
||||||
var listbucket = Bucket{}
|
var listbucket = Bucket{}
|
||||||
listbucket.Name = bucket.Name
|
listbucket.Name = bucket.Name
|
||||||
listbucket.CreationDate = bucket.Created.UTC().Format(timeFormatAMZLong)
|
listbucket.CreationDate = bucket.Created.UTC().Format(iso8601TimeFormat)
|
||||||
listbuckets = append(listbuckets, listbucket)
|
listbuckets = append(listbuckets, listbucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,20 +426,25 @@ func generateListVersionsResponse(bucket, prefix, marker, delimiter, encodingTyp
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
content.Key = s3EncodeName(object.Name, encodingType)
|
content.Key = s3EncodeName(object.Name, encodingType)
|
||||||
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZLong)
|
content.LastModified = object.ModTime.UTC().Format(iso8601TimeFormat)
|
||||||
if object.ETag != "" {
|
if object.ETag != "" {
|
||||||
content.ETag = "\"" + object.ETag + "\""
|
content.ETag = "\"" + object.ETag + "\""
|
||||||
}
|
}
|
||||||
content.Size = object.Size
|
content.Size = object.Size
|
||||||
|
if object.StorageClass != "" {
|
||||||
content.StorageClass = object.StorageClass
|
content.StorageClass = object.StorageClass
|
||||||
|
} else {
|
||||||
|
content.StorageClass = globalMinioDefaultStorageClass
|
||||||
|
}
|
||||||
|
|
||||||
content.Owner = owner
|
content.Owner = owner
|
||||||
content.VersionID = "null"
|
content.VersionID = "null"
|
||||||
content.IsLatest = true
|
content.IsLatest = true
|
||||||
versions = append(versions, content)
|
versions = append(versions, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
data.Name = bucket
|
data.Name = bucket
|
||||||
data.Versions = versions
|
data.Versions = versions
|
||||||
|
|
||||||
data.EncodingType = encodingType
|
data.EncodingType = encodingType
|
||||||
data.Prefix = s3EncodeName(prefix, encodingType)
|
data.Prefix = s3EncodeName(prefix, encodingType)
|
||||||
data.KeyMarker = s3EncodeName(marker, encodingType)
|
data.KeyMarker = s3EncodeName(marker, encodingType)
|
||||||
@@ -470,12 +477,16 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingTy
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
content.Key = s3EncodeName(object.Name, encodingType)
|
content.Key = s3EncodeName(object.Name, encodingType)
|
||||||
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZLong)
|
content.LastModified = object.ModTime.UTC().Format(iso8601TimeFormat)
|
||||||
if object.ETag != "" {
|
if object.ETag != "" {
|
||||||
content.ETag = "\"" + object.ETag + "\""
|
content.ETag = "\"" + object.ETag + "\""
|
||||||
}
|
}
|
||||||
content.Size = object.Size
|
content.Size = object.Size
|
||||||
|
if object.StorageClass != "" {
|
||||||
content.StorageClass = object.StorageClass
|
content.StorageClass = object.StorageClass
|
||||||
|
} else {
|
||||||
|
content.StorageClass = globalMinioDefaultStorageClass
|
||||||
|
}
|
||||||
content.Owner = owner
|
content.Owner = owner
|
||||||
contents = append(contents, content)
|
contents = append(contents, content)
|
||||||
}
|
}
|
||||||
@@ -516,17 +527,21 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
content.Key = s3EncodeName(object.Name, encodingType)
|
content.Key = s3EncodeName(object.Name, encodingType)
|
||||||
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZLong)
|
content.LastModified = object.ModTime.UTC().Format(iso8601TimeFormat)
|
||||||
if object.ETag != "" {
|
if object.ETag != "" {
|
||||||
content.ETag = "\"" + object.ETag + "\""
|
content.ETag = "\"" + object.ETag + "\""
|
||||||
}
|
}
|
||||||
content.Size = object.Size
|
content.Size = object.Size
|
||||||
|
if object.StorageClass != "" {
|
||||||
content.StorageClass = object.StorageClass
|
content.StorageClass = object.StorageClass
|
||||||
|
} else {
|
||||||
|
content.StorageClass = globalMinioDefaultStorageClass
|
||||||
|
}
|
||||||
content.Owner = owner
|
content.Owner = owner
|
||||||
if metadata {
|
if metadata {
|
||||||
content.UserMetadata = make(StringMap)
|
content.UserMetadata = make(StringMap)
|
||||||
for k, v := range CleanMinioInternalMetadataKeys(object.UserDefined) {
|
for k, v := range CleanMinioInternalMetadataKeys(object.UserDefined) {
|
||||||
if HasPrefix(k, ReservedMetadataPrefix) {
|
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||||
// Do not need to send any internal metadata
|
// Do not need to send any internal metadata
|
||||||
// values to client.
|
// values to client.
|
||||||
continue
|
continue
|
||||||
@@ -561,7 +576,7 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
|||||||
func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse {
|
func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse {
|
||||||
return CopyObjectResponse{
|
return CopyObjectResponse{
|
||||||
ETag: "\"" + etag + "\"",
|
ETag: "\"" + etag + "\"",
|
||||||
LastModified: lastModified.UTC().Format(timeFormatAMZLong),
|
LastModified: lastModified.UTC().Format(iso8601TimeFormat),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,7 +584,7 @@ func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectR
|
|||||||
func generateCopyObjectPartResponse(etag string, lastModified time.Time) CopyObjectPartResponse {
|
func generateCopyObjectPartResponse(etag string, lastModified time.Time) CopyObjectPartResponse {
|
||||||
return CopyObjectPartResponse{
|
return CopyObjectPartResponse{
|
||||||
ETag: "\"" + etag + "\"",
|
ETag: "\"" + etag + "\"",
|
||||||
LastModified: lastModified.UTC().Format(timeFormatAMZLong),
|
LastModified: lastModified.UTC().Format(iso8601TimeFormat),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,7 +603,8 @@ func generateCompleteMultpartUploadResponse(bucket, key, location, etag string)
|
|||||||
Location: location,
|
Location: location,
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Key: key,
|
Key: key,
|
||||||
ETag: etag,
|
// AWS S3 quotes the ETag in XML, make sure we are compatible here.
|
||||||
|
ETag: "\"" + etag + "\"",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,7 +629,7 @@ func generateListPartsResponse(partsInfo ListPartsInfo, encodingType string) Lis
|
|||||||
newPart.PartNumber = part.PartNumber
|
newPart.PartNumber = part.PartNumber
|
||||||
newPart.ETag = "\"" + part.ETag + "\""
|
newPart.ETag = "\"" + part.ETag + "\""
|
||||||
newPart.Size = part.Size
|
newPart.Size = part.Size
|
||||||
newPart.LastModified = part.LastModified.UTC().Format(timeFormatAMZLong)
|
newPart.LastModified = part.LastModified.UTC().Format(iso8601TimeFormat)
|
||||||
listPartsResponse.Parts[index] = newPart
|
listPartsResponse.Parts[index] = newPart
|
||||||
}
|
}
|
||||||
return listPartsResponse
|
return listPartsResponse
|
||||||
@@ -643,7 +659,7 @@ func generateListMultipartUploadsResponse(bucket string, multipartsInfo ListMult
|
|||||||
newUpload := Upload{}
|
newUpload := Upload{}
|
||||||
newUpload.UploadID = upload.UploadID
|
newUpload.UploadID = upload.UploadID
|
||||||
newUpload.Key = s3EncodeName(upload.Object, encodingType)
|
newUpload.Key = s3EncodeName(upload.Object, encodingType)
|
||||||
newUpload.Initiated = upload.Initiated.UTC().Format(timeFormatAMZLong)
|
newUpload.Initiated = upload.Initiated.UTC().Format(iso8601TimeFormat)
|
||||||
listMultipartUploadsResponse.Uploads[index] = newUpload
|
listMultipartUploadsResponse.Uploads[index] = newUpload
|
||||||
}
|
}
|
||||||
return listMultipartUploadsResponse
|
return listMultipartUploadsResponse
|
||||||
@@ -753,17 +769,6 @@ func writeErrorResponseJSON(ctx context.Context, w http.ResponseWriter, err APIE
|
|||||||
writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeJSON)
|
writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeVersionMismatchResponse - writes custom error responses for version mismatches.
|
|
||||||
func writeVersionMismatchResponse(ctx context.Context, w http.ResponseWriter, err APIError, reqURL *url.URL, isJSON bool) {
|
|
||||||
if isJSON {
|
|
||||||
// Generate error response.
|
|
||||||
errorResponse := getAPIErrorResponse(ctx, err, reqURL.String(), w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
|
|
||||||
writeResponse(w, err.HTTPStatusCode, encodeResponseJSON(errorResponse), mimeJSON)
|
|
||||||
} else {
|
|
||||||
writeResponse(w, err.HTTPStatusCode, []byte(err.Description), mimeNone)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeCustomErrorResponseJSON - similar to writeErrorResponseJSON,
|
// writeCustomErrorResponseJSON - similar to writeErrorResponseJSON,
|
||||||
// but accepts the error message directly (this allows messages to be
|
// but accepts the error message directly (this allows messages to be
|
||||||
// dynamically generated.)
|
// dynamically generated.)
|
||||||
|
|||||||
+128
-62
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* MinIO Cloud Storage, (C) 2016 MinIO, Inc.
|
* MinIO Cloud Storage, (C) 2016-2020 MinIO, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -90,139 +90,205 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
|
|||||||
for _, bucket := range routers {
|
for _, bucket := range routers {
|
||||||
// Object operations
|
// Object operations
|
||||||
// HeadObject
|
// HeadObject
|
||||||
bucket.Methods(http.MethodHead).Path("/{object:.+}").HandlerFunc(collectAPIStats("headobject", httpTraceAll(api.HeadObjectHandler)))
|
bucket.Methods(http.MethodHead).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("headobject", httpTraceAll(api.HeadObjectHandler))))
|
||||||
// CopyObjectPart
|
// CopyObjectPart
|
||||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(collectAPIStats("copyobjectpart", httpTraceAll(api.CopyObjectPartHandler))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(maxClients(collectAPIStats("copyobjectpart", httpTraceAll(api.CopyObjectPartHandler)))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||||
// PutObjectPart
|
// PutObjectPart
|
||||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjectpart", httpTraceHdrs(api.PutObjectPartHandler))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putobjectpart", httpTraceHdrs(api.PutObjectPartHandler)))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||||
// ListObjectParts
|
// ListObjectParts
|
||||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("listobjectparts", httpTraceAll(api.ListObjectPartsHandler))).Queries("uploadId", "{uploadId:.*}")
|
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("listobjectparts", httpTraceAll(api.ListObjectPartsHandler)))).Queries("uploadId", "{uploadId:.*}")
|
||||||
// CompleteMultipartUpload
|
// CompleteMultipartUpload
|
||||||
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("completemutipartupload", httpTraceAll(api.CompleteMultipartUploadHandler))).Queries("uploadId", "{uploadId:.*}")
|
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("completemutipartupload", httpTraceAll(api.CompleteMultipartUploadHandler)))).Queries("uploadId", "{uploadId:.*}")
|
||||||
// NewMultipartUpload
|
// NewMultipartUpload
|
||||||
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("newmultipartupload", httpTraceAll(api.NewMultipartUploadHandler))).Queries("uploads", "")
|
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("newmultipartupload", httpTraceAll(api.NewMultipartUploadHandler)))).Queries("uploads", "")
|
||||||
// AbortMultipartUpload
|
// AbortMultipartUpload
|
||||||
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("abortmultipartupload", httpTraceAll(api.AbortMultipartUploadHandler))).Queries("uploadId", "{uploadId:.*}")
|
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("abortmultipartupload", httpTraceAll(api.AbortMultipartUploadHandler)))).Queries("uploadId", "{uploadId:.*}")
|
||||||
// GetObjectACL - this is a dummy call.
|
// GetObjectACL - this is a dummy call.
|
||||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectacl", httpTraceHdrs(api.GetObjectACLHandler))).Queries("acl", "")
|
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getobjectacl", httpTraceHdrs(api.GetObjectACLHandler)))).Queries("acl", "")
|
||||||
// PutObjectACL - this is a dummy call.
|
// PutObjectACL - this is a dummy call.
|
||||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjectacl", httpTraceHdrs(api.PutObjectACLHandler))).Queries("acl", "")
|
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putobjectacl", httpTraceHdrs(api.PutObjectACLHandler)))).Queries("acl", "")
|
||||||
// GetObjectTagging
|
// GetObjectTagging
|
||||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjecttagging", httpTraceHdrs(api.GetObjectTaggingHandler))).Queries("tagging", "")
|
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getobjecttagging", httpTraceHdrs(api.GetObjectTaggingHandler)))).Queries("tagging", "")
|
||||||
// PutObjectTagging
|
// PutObjectTagging
|
||||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjecttagging", httpTraceHdrs(api.PutObjectTaggingHandler))).Queries("tagging", "")
|
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putobjecttagging", httpTraceHdrs(api.PutObjectTaggingHandler)))).Queries("tagging", "")
|
||||||
// DeleteObjectTagging
|
// DeleteObjectTagging
|
||||||
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("deleteobjecttagging", httpTraceHdrs(api.DeleteObjectTaggingHandler))).Queries("tagging", "")
|
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("deleteobjecttagging", httpTraceHdrs(api.DeleteObjectTaggingHandler)))).Queries("tagging", "")
|
||||||
// SelectObjectContent
|
// SelectObjectContent
|
||||||
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("selectobjectcontent", httpTraceHdrs(api.SelectObjectContentHandler))).Queries("select", "").Queries("select-type", "2")
|
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("selectobjectcontent", httpTraceHdrs(api.SelectObjectContentHandler)))).Queries("select", "").Queries("select-type", "2")
|
||||||
// GetObjectRetention
|
// GetObjectRetention
|
||||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectretention", httpTraceAll(api.GetObjectRetentionHandler))).Queries("retention", "")
|
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getobjectretention", httpTraceAll(api.GetObjectRetentionHandler)))).Queries("retention", "")
|
||||||
// GetObjectLegalHold
|
// GetObjectLegalHold
|
||||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectlegalhold", httpTraceAll(api.GetObjectLegalHoldHandler))).Queries("legal-hold", "")
|
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getobjectlegalhold", httpTraceAll(api.GetObjectLegalHoldHandler)))).Queries("legal-hold", "")
|
||||||
// GetObject
|
// GetObject
|
||||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobject", httpTraceHdrs(api.GetObjectHandler)))
|
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getobject", httpTraceHdrs(api.GetObjectHandler))))
|
||||||
// CopyObject
|
// CopyObject
|
||||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(collectAPIStats("copyobject", httpTraceAll(api.CopyObjectHandler)))
|
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(maxClients(collectAPIStats("copyobject", httpTraceAll(api.CopyObjectHandler))))
|
||||||
// PutObjectRetention
|
// PutObjectRetention
|
||||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjectretention", httpTraceAll(api.PutObjectRetentionHandler))).Queries("retention", "")
|
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putobjectretention", httpTraceAll(api.PutObjectRetentionHandler)))).Queries("retention", "")
|
||||||
// PutObjectLegalHold
|
// PutObjectLegalHold
|
||||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjectlegalhold", httpTraceAll(api.PutObjectLegalHoldHandler))).Queries("legal-hold", "")
|
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putobjectlegalhold", httpTraceAll(api.PutObjectLegalHoldHandler)))).Queries("legal-hold", "")
|
||||||
|
|
||||||
// PutObject
|
// PutObject
|
||||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobject", httpTraceHdrs(api.PutObjectHandler)))
|
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putobject", httpTraceHdrs(api.PutObjectHandler))))
|
||||||
// DeleteObject
|
// DeleteObject
|
||||||
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("deleteobject", httpTraceAll(api.DeleteObjectHandler)))
|
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("deleteobject", httpTraceAll(api.DeleteObjectHandler))))
|
||||||
|
|
||||||
/// Bucket operations
|
/// Bucket operations
|
||||||
// GetBucketLocation
|
// GetBucketLocation
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketlocation", httpTraceAll(api.GetBucketLocationHandler))).Queries("location", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketlocation", httpTraceAll(api.GetBucketLocationHandler)))).Queries("location", "")
|
||||||
// GetBucketPolicy
|
// GetBucketPolicy
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketpolicy", httpTraceAll(api.GetBucketPolicyHandler))).Queries("policy", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketpolicy", httpTraceAll(api.GetBucketPolicyHandler)))).Queries("policy", "")
|
||||||
// GetBucketLifecycle
|
// GetBucketLifecycle
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketlifecycle", httpTraceAll(api.GetBucketLifecycleHandler))).Queries("lifecycle", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketlifecycle", httpTraceAll(api.GetBucketLifecycleHandler)))).Queries("lifecycle", "")
|
||||||
// GetBucketEncryption
|
// GetBucketEncryption
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketencryption", httpTraceAll(api.GetBucketEncryptionHandler))).Queries("encryption", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketencryption", httpTraceAll(api.GetBucketEncryptionHandler)))).Queries("encryption", "")
|
||||||
|
|
||||||
// Dummy Bucket Calls
|
// Dummy Bucket Calls
|
||||||
// GetBucketACL -- this is a dummy call.
|
// GetBucketACL -- this is a dummy call.
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketacl", httpTraceAll(api.GetBucketACLHandler))).Queries("acl", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketacl", httpTraceAll(api.GetBucketACLHandler)))).Queries("acl", "")
|
||||||
// PutBucketACL -- this is a dummy call.
|
// PutBucketACL -- this is a dummy call.
|
||||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketacl", httpTraceAll(api.PutBucketACLHandler))).Queries("acl", "")
|
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putbucketacl", httpTraceAll(api.PutBucketACLHandler)))).Queries("acl", "")
|
||||||
// GetBucketCors - this is a dummy call.
|
// GetBucketCors - this is a dummy call.
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketcors", httpTraceAll(api.GetBucketCorsHandler))).Queries("cors", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketcors", httpTraceAll(api.GetBucketCorsHandler)))).Queries("cors", "")
|
||||||
// GetBucketWebsiteHandler - this is a dummy call.
|
// GetBucketWebsiteHandler - this is a dummy call.
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketwebsite", httpTraceAll(api.GetBucketWebsiteHandler))).Queries("website", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketwebsite", httpTraceAll(api.GetBucketWebsiteHandler)))).Queries("website", "")
|
||||||
// GetBucketAccelerateHandler - this is a dummy call.
|
// GetBucketAccelerateHandler - this is a dummy call.
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketaccelerate", httpTraceAll(api.GetBucketAccelerateHandler))).Queries("accelerate", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketaccelerate", httpTraceAll(api.GetBucketAccelerateHandler)))).Queries("accelerate", "")
|
||||||
// GetBucketRequestPaymentHandler - this is a dummy call.
|
// GetBucketRequestPaymentHandler - this is a dummy call.
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketrequestpayment", httpTraceAll(api.GetBucketRequestPaymentHandler))).Queries("requestPayment", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketrequestpayment", httpTraceAll(api.GetBucketRequestPaymentHandler)))).Queries("requestPayment", "")
|
||||||
// GetBucketLoggingHandler - this is a dummy call.
|
// GetBucketLoggingHandler - this is a dummy call.
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketlogging", httpTraceAll(api.GetBucketLoggingHandler))).Queries("logging", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketlogging", httpTraceAll(api.GetBucketLoggingHandler)))).Queries("logging", "")
|
||||||
// GetBucketLifecycleHandler - this is a dummy call.
|
// GetBucketLifecycleHandler - this is a dummy call.
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketlifecycle", httpTraceAll(api.GetBucketLifecycleHandler))).Queries("lifecycle", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketlifecycle", httpTraceAll(api.GetBucketLifecycleHandler)))).Queries("lifecycle", "")
|
||||||
// GetBucketReplicationHandler - this is a dummy call.
|
// GetBucketReplicationHandler - this is a dummy call.
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketreplication", httpTraceAll(api.GetBucketReplicationHandler))).Queries("replication", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
// GetBucketTaggingHandler - this is a dummy call.
|
maxClients(collectAPIStats("getbucketreplication", httpTraceAll(api.GetBucketReplicationHandler)))).Queries("replication", "")
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbuckettagging", httpTraceAll(api.GetBucketTaggingHandler))).Queries("tagging", "")
|
// GetBucketTaggingHandler
|
||||||
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbuckettagging", httpTraceAll(api.GetBucketTaggingHandler)))).Queries("tagging", "")
|
||||||
//DeleteBucketWebsiteHandler
|
//DeleteBucketWebsiteHandler
|
||||||
bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucketwebsite", httpTraceAll(api.DeleteBucketWebsiteHandler))).Queries("website", "")
|
bucket.Methods(http.MethodDelete).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("deletebucketwebsite", httpTraceAll(api.DeleteBucketWebsiteHandler)))).Queries("website", "")
|
||||||
// DeleteBucketTaggingHandler
|
// DeleteBucketTaggingHandler
|
||||||
bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebuckettagging", httpTraceAll(api.DeleteBucketTaggingHandler))).Queries("tagging", "")
|
bucket.Methods(http.MethodDelete).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("deletebuckettagging", httpTraceAll(api.DeleteBucketTaggingHandler)))).Queries("tagging", "")
|
||||||
|
|
||||||
// GetBucketObjectLockConfig
|
// GetBucketObjectLockConfig
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketobjectlockconfiguration", httpTraceAll(api.GetBucketObjectLockConfigHandler))).Queries("object-lock", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketobjectlockconfiguration", httpTraceAll(api.GetBucketObjectLockConfigHandler)))).Queries("object-lock", "")
|
||||||
// GetBucketVersioning
|
// GetBucketVersioning
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketversioning", httpTraceAll(api.GetBucketVersioningHandler))).Queries("versioning", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketversioning", httpTraceAll(api.GetBucketVersioningHandler)))).Queries("versioning", "")
|
||||||
// GetBucketNotification
|
// GetBucketNotification
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketnotification", httpTraceAll(api.GetBucketNotificationHandler))).Queries("notification", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("getbucketnotification", httpTraceAll(api.GetBucketNotificationHandler)))).Queries("notification", "")
|
||||||
// ListenBucketNotification
|
// ListenBucketNotification
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listenbucketnotification", httpTraceAll(api.ListenBucketNotificationHandler))).Queries("events", "{events:.*}")
|
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listenbucketnotification", httpTraceAll(api.ListenBucketNotificationHandler))).Queries("events", "{events:.*}")
|
||||||
// ListMultipartUploads
|
// ListMultipartUploads
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listmultipartuploads", httpTraceAll(api.ListMultipartUploadsHandler))).Queries("uploads", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("listmultipartuploads", httpTraceAll(api.ListMultipartUploadsHandler)))).Queries("uploads", "")
|
||||||
// ListObjectsV2M
|
// ListObjectsV2M
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listobjectsv2M", httpTraceAll(api.ListObjectsV2MHandler))).Queries("list-type", "2", "metadata", "true")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("listobjectsv2M", httpTraceAll(api.ListObjectsV2MHandler)))).Queries("list-type", "2", "metadata", "true")
|
||||||
// ListObjectsV2
|
// ListObjectsV2
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listobjectsv2", httpTraceAll(api.ListObjectsV2Handler))).Queries("list-type", "2")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("listobjectsv2", httpTraceAll(api.ListObjectsV2Handler)))).Queries("list-type", "2")
|
||||||
// ListBucketVersions
|
// ListBucketVersions
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listbucketversions", httpTraceAll(api.ListBucketObjectVersionsHandler))).Queries("versions", "")
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("listbucketversions", httpTraceAll(api.ListBucketObjectVersionsHandler)))).Queries("versions", "")
|
||||||
// ListObjectsV1 (Legacy)
|
// ListObjectsV1 (Legacy)
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listobjectsv1", httpTraceAll(api.ListObjectsV1Handler)))
|
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("listobjectsv1", httpTraceAll(api.ListObjectsV1Handler))))
|
||||||
// PutBucketLifecycle
|
// PutBucketLifecycle
|
||||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketlifecycle", httpTraceAll(api.PutBucketLifecycleHandler))).Queries("lifecycle", "")
|
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putbucketlifecycle", httpTraceAll(api.PutBucketLifecycleHandler)))).Queries("lifecycle", "")
|
||||||
// PutBucketEncryption
|
// PutBucketEncryption
|
||||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketencryption", httpTraceAll(api.PutBucketEncryptionHandler))).Queries("encryption", "")
|
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putbucketencryption", httpTraceAll(api.PutBucketEncryptionHandler)))).Queries("encryption", "")
|
||||||
|
|
||||||
// PutBucketPolicy
|
// PutBucketPolicy
|
||||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketpolicy", httpTraceAll(api.PutBucketPolicyHandler))).Queries("policy", "")
|
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putbucketpolicy", httpTraceAll(api.PutBucketPolicyHandler)))).Queries("policy", "")
|
||||||
|
|
||||||
// PutBucketObjectLockConfig
|
// PutBucketObjectLockConfig
|
||||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketobjectlockconfig", httpTraceAll(api.PutBucketObjectLockConfigHandler))).Queries("object-lock", "")
|
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putbucketobjectlockconfig", httpTraceAll(api.PutBucketObjectLockConfigHandler)))).Queries("object-lock", "")
|
||||||
|
// PutBucketTaggingHandler
|
||||||
|
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putbuckettagging", httpTraceAll(api.PutBucketTaggingHandler)))).Queries("tagging", "")
|
||||||
// PutBucketVersioning
|
// PutBucketVersioning
|
||||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketversioning", httpTraceAll(api.PutBucketVersioningHandler))).Queries("versioning", "")
|
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putbucketversioning", httpTraceAll(api.PutBucketVersioningHandler)))).Queries("versioning", "")
|
||||||
// PutBucketNotification
|
// PutBucketNotification
|
||||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketnotification", httpTraceAll(api.PutBucketNotificationHandler))).Queries("notification", "")
|
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putbucketnotification", httpTraceAll(api.PutBucketNotificationHandler)))).Queries("notification", "")
|
||||||
// PutBucket
|
// PutBucket
|
||||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucket", httpTraceAll(api.PutBucketHandler)))
|
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("putbucket", httpTraceAll(api.PutBucketHandler))))
|
||||||
// HeadBucket
|
// HeadBucket
|
||||||
bucket.Methods(http.MethodHead).HandlerFunc(collectAPIStats("headbucket", httpTraceAll(api.HeadBucketHandler)))
|
bucket.Methods(http.MethodHead).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("headbucket", httpTraceAll(api.HeadBucketHandler))))
|
||||||
// PostPolicy
|
// PostPolicy
|
||||||
bucket.Methods(http.MethodPost).HeadersRegexp(xhttp.ContentType, "multipart/form-data*").HandlerFunc(collectAPIStats("postpolicybucket", httpTraceHdrs(api.PostPolicyBucketHandler)))
|
bucket.Methods(http.MethodPost).HeadersRegexp(xhttp.ContentType, "multipart/form-data*").HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("postpolicybucket", httpTraceHdrs(api.PostPolicyBucketHandler))))
|
||||||
// DeleteMultipleObjects
|
// DeleteMultipleObjects
|
||||||
bucket.Methods(http.MethodPost).HandlerFunc(collectAPIStats("deletemultipleobjects", httpTraceAll(api.DeleteMultipleObjectsHandler))).Queries("delete", "")
|
bucket.Methods(http.MethodPost).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("deletemultipleobjects", httpTraceAll(api.DeleteMultipleObjectsHandler)))).Queries("delete", "")
|
||||||
// DeleteBucketPolicy
|
// DeleteBucketPolicy
|
||||||
bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucketpolicy", httpTraceAll(api.DeleteBucketPolicyHandler))).Queries("policy", "")
|
bucket.Methods(http.MethodDelete).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("deletebucketpolicy", httpTraceAll(api.DeleteBucketPolicyHandler)))).Queries("policy", "")
|
||||||
// DeleteBucketLifecycle
|
// DeleteBucketLifecycle
|
||||||
bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucketlifecycle", httpTraceAll(api.DeleteBucketLifecycleHandler))).Queries("lifecycle", "")
|
bucket.Methods(http.MethodDelete).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("deletebucketlifecycle", httpTraceAll(api.DeleteBucketLifecycleHandler)))).Queries("lifecycle", "")
|
||||||
// DeleteBucketEncryption
|
// DeleteBucketEncryption
|
||||||
bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucketencryption", httpTraceAll(api.DeleteBucketEncryptionHandler))).Queries("encryption", "")
|
bucket.Methods(http.MethodDelete).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("deletebucketencryption", httpTraceAll(api.DeleteBucketEncryptionHandler)))).Queries("encryption", "")
|
||||||
// DeleteBucket
|
// DeleteBucket
|
||||||
bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucket", httpTraceAll(api.DeleteBucketHandler)))
|
bucket.Methods(http.MethodDelete).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("deletebucket", httpTraceAll(api.DeleteBucketHandler))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Root operation
|
/// Root operation
|
||||||
|
|
||||||
// ListBuckets
|
// ListBuckets
|
||||||
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(collectAPIStats("listbuckets", httpTraceAll(api.ListBucketsHandler)))
|
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("listbuckets", httpTraceAll(api.ListBucketsHandler))))
|
||||||
|
|
||||||
|
// S3 browser with signature v4 adds '//' for ListBuckets request, so rather
|
||||||
|
// than failing with UnknownAPIRequest we simply handle it for now.
|
||||||
|
apiRouter.Methods(http.MethodGet).Path(SlashSeparator + SlashSeparator).HandlerFunc(
|
||||||
|
maxClients(collectAPIStats("listbuckets", httpTraceAll(api.ListBucketsHandler))))
|
||||||
|
|
||||||
// If none of the routes match add default error handler routes
|
// If none of the routes match add default error handler routes
|
||||||
apiRouter.NotFoundHandler = http.HandlerFunc(collectAPIStats("notfound", httpTraceAll(errorResponseHandler)))
|
apiRouter.NotFoundHandler = http.HandlerFunc(collectAPIStats("notfound", httpTraceAll(errorResponseHandler)))
|
||||||
|
|||||||
+145
-23
@@ -26,12 +26,15 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
xjwt "github.com/minio/minio/cmd/jwt"
|
xjwt "github.com/minio/minio/cmd/jwt"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
|
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
||||||
"github.com/minio/minio/pkg/bucket/policy"
|
"github.com/minio/minio/pkg/bucket/policy"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||||
@@ -118,9 +121,7 @@ func getRequestAuthType(r *http.Request) authType {
|
|||||||
return authTypeUnknown
|
return authTypeUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkAdminRequestAuthType checks whether the request is a valid signature V2 or V4 request.
|
func validateAdminSignature(ctx context.Context, r *http.Request, region string) (auth.Credentials, map[string]interface{}, bool, APIErrorCode) {
|
||||||
// It does not accept presigned or JWT or anonymous requests.
|
|
||||||
func checkAdminRequestAuthType(ctx context.Context, r *http.Request, action iampolicy.AdminAction, region string) (auth.Credentials, APIErrorCode) {
|
|
||||||
var cred auth.Credentials
|
var cred auth.Credentials
|
||||||
var owner bool
|
var owner bool
|
||||||
s3Err := ErrAccessDenied
|
s3Err := ErrAccessDenied
|
||||||
@@ -129,7 +130,7 @@ func checkAdminRequestAuthType(ctx context.Context, r *http.Request, action iamp
|
|||||||
// We only support admin credentials to access admin APIs.
|
// We only support admin credentials to access admin APIs.
|
||||||
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return cred, s3Err
|
return cred, nil, owner, s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
// we only support V4 (no presign) with auth body
|
// we only support V4 (no presign) with auth body
|
||||||
@@ -139,14 +140,24 @@ func checkAdminRequestAuthType(ctx context.Context, r *http.Request, action iamp
|
|||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("requestHeaders", dumpRequest(r))
|
reqInfo := (&logger.ReqInfo{}).AppendTags("requestHeaders", dumpRequest(r))
|
||||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||||
logger.LogIf(ctx, errors.New(getAPIError(s3Err).Description), logger.Application)
|
logger.LogIf(ctx, errors.New(getAPIError(s3Err).Description), logger.Application)
|
||||||
|
return cred, nil, owner, s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims map[string]interface{}
|
claims, s3Err := checkClaimsFromToken(r, cred)
|
||||||
claims, s3Err = checkClaimsFromToken(r, cred)
|
if s3Err != ErrNone {
|
||||||
|
return cred, nil, owner, s3Err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cred, claims, owner, ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAdminRequestAuthType checks whether the request is a valid signature V2 or V4 request.
|
||||||
|
// It does not accept presigned or JWT or anonymous requests.
|
||||||
|
func checkAdminRequestAuthType(ctx context.Context, r *http.Request, action iampolicy.AdminAction, region string) (auth.Credentials, APIErrorCode) {
|
||||||
|
cred, claims, owner, s3Err := validateAdminSignature(ctx, r, region)
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return cred, s3Err
|
return cred, s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
Action: iampolicy.Action(action),
|
Action: iampolicy.Action(action),
|
||||||
@@ -173,15 +184,13 @@ func getSessionToken(r *http.Request) (token string) {
|
|||||||
// Fetch claims in the security token returned by the client, doesn't return
|
// Fetch claims in the security token returned by the client, doesn't return
|
||||||
// errors - upon errors the returned claims map will be empty.
|
// errors - upon errors the returned claims map will be empty.
|
||||||
func mustGetClaimsFromToken(r *http.Request) map[string]interface{} {
|
func mustGetClaimsFromToken(r *http.Request) map[string]interface{} {
|
||||||
claims, _ := getClaimsFromToken(r)
|
claims, _ := getClaimsFromToken(r, getSessionToken(r))
|
||||||
return claims
|
return claims
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch claims in the security token returned by the client.
|
// Fetch claims in the security token returned by the client.
|
||||||
func getClaimsFromToken(r *http.Request) (map[string]interface{}, error) {
|
func getClaimsFromToken(r *http.Request, token string) (map[string]interface{}, error) {
|
||||||
claims := xjwt.NewMapClaims()
|
claims := xjwt.NewMapClaims()
|
||||||
|
|
||||||
token := getSessionToken(r)
|
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return claims.Map(), nil
|
return claims.Map(), nil
|
||||||
}
|
}
|
||||||
@@ -211,8 +220,9 @@ func getClaimsFromToken(r *http.Request) (map[string]interface{}, error) {
|
|||||||
// If OPA is not set, session token should
|
// If OPA is not set, session token should
|
||||||
// have a policy and its mandatory, reject
|
// have a policy and its mandatory, reject
|
||||||
// requests without policy claim.
|
// requests without policy claim.
|
||||||
_, pok := claims.Lookup(iamPolicyClaimName())
|
_, pokOpenID := claims.Lookup(iamPolicyClaimNameOpenID())
|
||||||
if !pok {
|
_, pokSA := claims.Lookup(iamPolicyClaimNameSA())
|
||||||
|
if !pokOpenID && !pokSA {
|
||||||
return nil, errAuthentication
|
return nil, errAuthentication
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +236,7 @@ func getClaimsFromToken(r *http.Request) (map[string]interface{}, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// Base64 decoding fails, we should log to indicate
|
// Base64 decoding fails, we should log to indicate
|
||||||
// something is malforming the request sent by client.
|
// something is malforming the request sent by client.
|
||||||
logger.LogIf(context.Background(), err, logger.Application)
|
logger.LogIf(r.Context(), err, logger.Application)
|
||||||
return nil, errAuthentication
|
return nil, errAuthentication
|
||||||
}
|
}
|
||||||
claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes)
|
claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes)
|
||||||
@@ -241,12 +251,15 @@ func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]in
|
|||||||
if token != "" && cred.AccessKey == "" {
|
if token != "" && cred.AccessKey == "" {
|
||||||
return nil, ErrNoAccessKey
|
return nil, ErrNoAccessKey
|
||||||
}
|
}
|
||||||
|
if cred.IsServiceAccount() && token == "" {
|
||||||
|
token = cred.SessionToken
|
||||||
|
}
|
||||||
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
|
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
|
||||||
return nil, ErrInvalidToken
|
return nil, ErrInvalidToken
|
||||||
}
|
}
|
||||||
claims, err := getClaimsFromToken(r)
|
claims, err := getClaimsFromToken(r, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, toAPIErrorCode(context.Background(), err)
|
return nil, toAPIErrorCode(r.Context(), err)
|
||||||
}
|
}
|
||||||
return claims, ErrNone
|
return claims, ErrNone
|
||||||
}
|
}
|
||||||
@@ -271,7 +284,7 @@ func checkRequestAuthTypeToAccessKey(ctx context.Context, r *http.Request, actio
|
|||||||
var cred auth.Credentials
|
var cred auth.Credentials
|
||||||
switch getRequestAuthType(r) {
|
switch getRequestAuthType(r) {
|
||||||
case authTypeUnknown, authTypeStreamingSigned:
|
case authTypeUnknown, authTypeStreamingSigned:
|
||||||
return accessKey, owner, ErrAccessDenied
|
return accessKey, owner, ErrSignatureVersionNotSupported
|
||||||
case authTypePresignedV2, authTypeSignedV2:
|
case authTypePresignedV2, authTypeSignedV2:
|
||||||
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
|
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
|
||||||
return accessKey, owner, s3Err
|
return accessKey, owner, s3Err
|
||||||
@@ -333,7 +346,7 @@ func checkRequestAuthTypeToAccessKey(ctx context.Context, r *http.Request, actio
|
|||||||
// Request is allowed return the appropriate access key.
|
// Request is allowed return the appropriate access key.
|
||||||
return cred.AccessKey, owner, ErrNone
|
return cred.AccessKey, owner, ErrNone
|
||||||
}
|
}
|
||||||
return accessKey, owner, ErrAccessDenied
|
return cred.AccessKey, owner, ErrAccessDenied
|
||||||
}
|
}
|
||||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
@@ -347,7 +360,7 @@ func checkRequestAuthTypeToAccessKey(ctx context.Context, r *http.Request, actio
|
|||||||
// Request is allowed return the appropriate access key.
|
// Request is allowed return the appropriate access key.
|
||||||
return cred.AccessKey, owner, ErrNone
|
return cred.AccessKey, owner, ErrNone
|
||||||
}
|
}
|
||||||
return accessKey, owner, ErrAccessDenied
|
return cred.AccessKey, owner, ErrAccessDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify if request has valid AWS Signature Version '2'.
|
// Verify if request has valid AWS Signature Version '2'.
|
||||||
@@ -410,7 +423,7 @@ func isReqAuthenticated(ctx context.Context, r *http.Request, region string, sty
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return toAPIErrorCode(ctx, err)
|
return toAPIErrorCode(ctx, err)
|
||||||
}
|
}
|
||||||
r.Body = ioutil.NopCloser(reader)
|
r.Body = reader
|
||||||
return ErrNone
|
return ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,7 +473,107 @@ func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.handler.ServeHTTP(w, r)
|
a.handler.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeErrorResponse(context.Background(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL, guessIsBrowserReq(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSignature(atype authType, r *http.Request) (auth.Credentials, bool, map[string]interface{}, APIErrorCode) {
|
||||||
|
var cred auth.Credentials
|
||||||
|
var owner bool
|
||||||
|
var s3Err APIErrorCode
|
||||||
|
switch atype {
|
||||||
|
case authTypeUnknown, authTypeStreamingSigned:
|
||||||
|
return cred, owner, nil, ErrSignatureVersionNotSupported
|
||||||
|
case authTypeSignedV2, authTypePresignedV2:
|
||||||
|
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
|
||||||
|
return cred, owner, nil, s3Err
|
||||||
|
}
|
||||||
|
cred, owner, s3Err = getReqAccessKeyV2(r)
|
||||||
|
case authTypePresigned, authTypeSigned:
|
||||||
|
region := globalServerRegion
|
||||||
|
if s3Err = isReqAuthenticated(GlobalContext, r, region, serviceS3); s3Err != ErrNone {
|
||||||
|
return cred, owner, nil, s3Err
|
||||||
|
}
|
||||||
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
|
}
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
return cred, owner, nil, s3Err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, s3Err := checkClaimsFromToken(r, cred)
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
return cred, owner, nil, s3Err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cred, owner, claims, ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate time.Time, retMode objectlock.RetMode, byPassSet bool, r *http.Request, cred auth.Credentials, owner bool, claims map[string]interface{}) (s3Err APIErrorCode) {
|
||||||
|
var retSet bool
|
||||||
|
if cred.AccessKey == "" {
|
||||||
|
conditions := getConditionValues(r, "", "", nil)
|
||||||
|
conditions["object-lock-mode"] = []string{string(retMode)}
|
||||||
|
conditions["object-lock-retain-until-date"] = []string{retDate.Format(time.RFC3339)}
|
||||||
|
if retDays > 0 {
|
||||||
|
conditions["object-lock-remaining-retention-days"] = []string{strconv.Itoa(retDays)}
|
||||||
|
}
|
||||||
|
if retMode == objectlock.RetGovernance && byPassSet {
|
||||||
|
byPassSet = globalPolicySys.IsAllowed(policy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Action: policy.Action(policy.BypassGovernanceRetentionAction),
|
||||||
|
BucketName: bucketName,
|
||||||
|
ConditionValues: conditions,
|
||||||
|
IsOwner: false,
|
||||||
|
ObjectName: objectName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if globalPolicySys.IsAllowed(policy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Action: policy.Action(policy.PutObjectRetentionAction),
|
||||||
|
BucketName: bucketName,
|
||||||
|
ConditionValues: conditions,
|
||||||
|
IsOwner: false,
|
||||||
|
ObjectName: objectName,
|
||||||
|
}) {
|
||||||
|
retSet = true
|
||||||
|
}
|
||||||
|
if byPassSet || retSet {
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
return ErrAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions := getConditionValues(r, "", cred.AccessKey, claims)
|
||||||
|
conditions["object-lock-mode"] = []string{string(retMode)}
|
||||||
|
conditions["object-lock-retain-until-date"] = []string{retDate.Format(time.RFC3339)}
|
||||||
|
if retDays > 0 {
|
||||||
|
conditions["object-lock-remaining-retention-days"] = []string{strconv.Itoa(retDays)}
|
||||||
|
}
|
||||||
|
if retMode == objectlock.RetGovernance && byPassSet {
|
||||||
|
byPassSet = globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Action: policy.BypassGovernanceRetentionAction,
|
||||||
|
BucketName: bucketName,
|
||||||
|
ObjectName: objectName,
|
||||||
|
ConditionValues: conditions,
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: claims,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Action: policy.PutObjectRetentionAction,
|
||||||
|
BucketName: bucketName,
|
||||||
|
ConditionValues: conditions,
|
||||||
|
ObjectName: objectName,
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: claims,
|
||||||
|
}) {
|
||||||
|
retSet = true
|
||||||
|
}
|
||||||
|
if byPassSet || retSet {
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
return ErrAccessDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
// isPutActionAllowed - check if PUT operation is allowed on the resource, this
|
// isPutActionAllowed - check if PUT operation is allowed on the resource, this
|
||||||
@@ -471,7 +584,7 @@ func isPutActionAllowed(atype authType, bucketName, objectName string, r *http.R
|
|||||||
var owner bool
|
var owner bool
|
||||||
switch atype {
|
switch atype {
|
||||||
case authTypeUnknown:
|
case authTypeUnknown:
|
||||||
return ErrAccessDenied
|
return ErrSignatureVersionNotSupported
|
||||||
case authTypeSignedV2, authTypePresignedV2:
|
case authTypeSignedV2, authTypePresignedV2:
|
||||||
cred, owner, s3Err = getReqAccessKeyV2(r)
|
cred, owner, s3Err = getReqAccessKeyV2(r)
|
||||||
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
|
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
|
||||||
@@ -487,10 +600,19 @@ func isPutActionAllowed(atype authType, bucketName, objectName string, r *http.R
|
|||||||
return s3Err
|
return s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do not check for PutObjectRetentionAction permission,
|
||||||
|
// if mode and retain until date are not set.
|
||||||
|
// Can happen when bucket has default lock config set
|
||||||
|
if action == iampolicy.PutObjectRetentionAction &&
|
||||||
|
r.Header.Get(xhttp.AmzObjectLockMode) == "" &&
|
||||||
|
r.Header.Get(xhttp.AmzObjectLockRetainUntilDate) == "" {
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
if cred.AccessKey == "" {
|
if cred.AccessKey == "" {
|
||||||
if globalPolicySys.IsAllowed(policy.Args{
|
if globalPolicySys.IsAllowed(policy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
Action: policy.PutObjectAction,
|
Action: policy.Action(action),
|
||||||
BucketName: bucketName,
|
BucketName: bucketName,
|
||||||
ConditionValues: getConditionValues(r, "", "", nil),
|
ConditionValues: getConditionValues(r, "", "", nil),
|
||||||
IsOwner: false,
|
IsOwner: false,
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ func TestIsReqAuthenticated(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckAdminRequestAuthType(t *testing.T) {
|
func TestCheckAdminRequestAuthType(t *testing.T) {
|
||||||
objLayer, fsDir, err := prepareFS()
|
objLayer, fsDir, err := prepareFS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -425,3 +426,48 @@ func TestCheckAdminRequestAuthType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateAdminSignature(t *testing.T) {
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
objLayer, fsDir, err := prepareFS()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(fsDir)
|
||||||
|
|
||||||
|
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||||
|
t.Fatalf("unable initialize config file, %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
creds, err := auth.CreateCredentials("admin", "mypassword")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable create credential, %s", err)
|
||||||
|
}
|
||||||
|
globalActiveCred = creds
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
AccessKey string
|
||||||
|
SecretKey string
|
||||||
|
ErrCode APIErrorCode
|
||||||
|
}{
|
||||||
|
{"", "", ErrInvalidAccessKeyID},
|
||||||
|
{"admin", "", ErrSignatureDoesNotMatch},
|
||||||
|
{"admin", "wrongpassword", ErrSignatureDoesNotMatch},
|
||||||
|
{"wronguser", "mypassword", ErrInvalidAccessKeyID},
|
||||||
|
{"", "mypassword", ErrInvalidAccessKeyID},
|
||||||
|
{"admin", "mypassword", ErrNone},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
req := mustNewRequest("GET", "http://localhost:9000/", 0, nil, t)
|
||||||
|
if err := signRequestV4(req, testCase.AccessKey, testCase.SecretKey); err != nil {
|
||||||
|
t.Fatalf("Unable to inititalized new signed http request %s", err)
|
||||||
|
}
|
||||||
|
_, _, _, s3Error := validateAdminSignature(ctx, req, globalMinioDefaultRegion)
|
||||||
|
if s3Error != testCase.ErrCode {
|
||||||
|
t.Errorf("Test %d: Unexpected s3error returned wanted %d, got %d", i+1, testCase.ErrCode, s3Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+19
-57
@@ -66,8 +66,7 @@ func waitForLowHTTPReq(tolerance int32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for heal requests and process them
|
// Wait for heal requests and process them
|
||||||
func (h *healRoutine) run() {
|
func (h *healRoutine) run(ctx context.Context, objAPI ObjectLayer) {
|
||||||
ctx := context.Background()
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case task, ok := <-h.tasks:
|
case task, ok := <-h.tasks:
|
||||||
@@ -76,25 +75,26 @@ func (h *healRoutine) run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait and proceed if there are active requests
|
// Wait and proceed if there are active requests
|
||||||
waitForLowHTTPReq(int32(globalEndpoints.Nodes()))
|
waitForLowHTTPReq(int32(globalEndpoints.NEndpoints()))
|
||||||
|
|
||||||
var res madmin.HealResultItem
|
var res madmin.HealResultItem
|
||||||
var err error
|
var err error
|
||||||
bucket, object := path2BucketObject(task.path)
|
bucket, object := path2BucketObject(task.path)
|
||||||
switch {
|
switch {
|
||||||
case bucket == "" && object == "":
|
case bucket == "" && object == "":
|
||||||
res, err = bgHealDiskFormat(ctx, task.opts)
|
res, err = healDiskFormat(ctx, objAPI, task.opts)
|
||||||
case bucket != "" && object == "":
|
case bucket != "" && object == "":
|
||||||
res, err = bgHealBucket(ctx, bucket, task.opts)
|
res, err = objAPI.HealBucket(ctx, bucket, task.opts.DryRun, task.opts.Remove)
|
||||||
case bucket != "" && object != "":
|
case bucket != "" && object != "":
|
||||||
res, err = bgHealObject(ctx, bucket, object, task.opts)
|
res, err = objAPI.HealObject(ctx, bucket, object, task.opts)
|
||||||
|
}
|
||||||
|
if task.path != slashSeparator && task.path != nopHeal {
|
||||||
|
ObjectPathUpdated(task.path)
|
||||||
}
|
}
|
||||||
if task.responseCh != nil {
|
|
||||||
task.responseCh <- healResult{result: res, err: err}
|
task.responseCh <- healResult{result: res, err: err}
|
||||||
}
|
|
||||||
case <-h.doneCh:
|
case <-h.doneCh:
|
||||||
return
|
return
|
||||||
case <-GlobalServiceDoneCh:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,45 +108,28 @@ func initHealRoutine() *healRoutine {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startBackgroundHealing() {
|
func startBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
var objAPI ObjectLayer
|
|
||||||
for {
|
|
||||||
objAPI = newObjectLayerWithoutSafeModeFn()
|
|
||||||
if objAPI == nil {
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the background healer
|
// Run the background healer
|
||||||
globalBackgroundHealRoutine = initHealRoutine()
|
globalBackgroundHealRoutine = initHealRoutine()
|
||||||
go globalBackgroundHealRoutine.run()
|
go globalBackgroundHealRoutine.run(ctx, objAPI)
|
||||||
|
|
||||||
// Launch the background healer sequence to track
|
// Launch the background healer sequence to track
|
||||||
// background healing operations
|
// background healing operations, ignore errors
|
||||||
info := objAPI.StorageInfo(ctx, false)
|
// errors are handled into offline disks already.
|
||||||
|
info, _ := objAPI.StorageInfo(ctx, false)
|
||||||
numDisks := info.Backend.OnlineDisks.Sum() + info.Backend.OfflineDisks.Sum()
|
numDisks := info.Backend.OnlineDisks.Sum() + info.Backend.OfflineDisks.Sum()
|
||||||
nh := newBgHealSequence(numDisks)
|
nh := newBgHealSequence(numDisks)
|
||||||
globalBackgroundHealState.LaunchNewHealSequence(nh)
|
globalBackgroundHealState.LaunchNewHealSequence(nh)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initBackgroundHealing() {
|
func initBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
|
||||||
go startBackgroundHealing()
|
go startBackgroundHealing(ctx, objAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// bgHealDiskFormat - heals format.json, return value indicates if a
|
// healDiskFormat - heals format.json, return value indicates if a
|
||||||
// failure error occurred.
|
// failure error occurred.
|
||||||
func bgHealDiskFormat(ctx context.Context, opts madmin.HealOpts) (madmin.HealResultItem, error) {
|
func healDiskFormat(ctx context.Context, objAPI ObjectLayer, opts madmin.HealOpts) (madmin.HealResultItem, error) {
|
||||||
// Get current object layer instance.
|
res, err := objAPI.HealFormat(ctx, opts.DryRun)
|
||||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
|
||||||
if objectAPI == nil {
|
|
||||||
return madmin.HealResultItem{}, errServerNotInitialized
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := objectAPI.HealFormat(ctx, opts.DryRun)
|
|
||||||
|
|
||||||
// return any error, ignore error returned when disks have
|
// return any error, ignore error returned when disks have
|
||||||
// already healed.
|
// already healed.
|
||||||
@@ -167,24 +150,3 @@ func bgHealDiskFormat(ctx context.Context, opts madmin.HealOpts) (madmin.HealRes
|
|||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// bghealBucket - traverses and heals given bucket
|
|
||||||
func bgHealBucket(ctx context.Context, bucket string, opts madmin.HealOpts) (madmin.HealResultItem, error) {
|
|
||||||
// Get current object layer instance.
|
|
||||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
|
||||||
if objectAPI == nil {
|
|
||||||
return madmin.HealResultItem{}, errServerNotInitialized
|
|
||||||
}
|
|
||||||
|
|
||||||
return objectAPI.HealBucket(ctx, bucket, opts.DryRun, opts.Remove)
|
|
||||||
}
|
|
||||||
|
|
||||||
// bgHealObject - heal the given object and record result
|
|
||||||
func bgHealObject(ctx context.Context, bucket, object string, opts madmin.HealOpts) (madmin.HealResultItem, error) {
|
|
||||||
// Get current object layer instance.
|
|
||||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
|
||||||
if objectAPI == nil {
|
|
||||||
return madmin.HealResultItem{}, errServerNotInitialized
|
|
||||||
}
|
|
||||||
return objectAPI.HealObject(ctx, bucket, object, opts.DryRun, opts.Remove, opts.ScanMode)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,32 +25,19 @@ import (
|
|||||||
|
|
||||||
const defaultMonitorNewDiskInterval = time.Minute * 10
|
const defaultMonitorNewDiskInterval = time.Minute * 10
|
||||||
|
|
||||||
func initLocalDisksAutoHeal() {
|
func initLocalDisksAutoHeal(ctx context.Context, objAPI ObjectLayer) {
|
||||||
go monitorLocalDisksAndHeal()
|
go monitorLocalDisksAndHeal(ctx, objAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// monitorLocalDisksAndHeal - ensures that detected new disks are healed
|
// monitorLocalDisksAndHeal - ensures that detected new disks are healed
|
||||||
// 1. Only the concerned erasure set will be listed and healed
|
// 1. Only the concerned erasure set will be listed and healed
|
||||||
// 2. Only the node hosting the disk is responsible to perform the heal
|
// 2. Only the node hosting the disk is responsible to perform the heal
|
||||||
func monitorLocalDisksAndHeal() {
|
func monitorLocalDisksAndHeal(ctx context.Context, objAPI ObjectLayer) {
|
||||||
// Wait until the object layer is ready
|
|
||||||
var objAPI ObjectLayer
|
|
||||||
for {
|
|
||||||
objAPI = newObjectLayerWithoutSafeModeFn()
|
|
||||||
if objAPI == nil {
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
z, ok := objAPI.(*xlZones)
|
z, ok := objAPI.(*xlZones)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
var bgSeq *healSequence
|
var bgSeq *healSequence
|
||||||
var found bool
|
var found bool
|
||||||
|
|
||||||
@@ -64,10 +51,13 @@ func monitorLocalDisksAndHeal() {
|
|||||||
|
|
||||||
// Perform automatic disk healing when a disk is replaced locally.
|
// Perform automatic disk healing when a disk is replaced locally.
|
||||||
for {
|
for {
|
||||||
time.Sleep(defaultMonitorNewDiskInterval)
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(defaultMonitorNewDiskInterval):
|
||||||
// Attempt a heal as the server starts-up first.
|
// Attempt a heal as the server starts-up first.
|
||||||
localDisksInZoneHeal := make([]Endpoints, len(z.zones))
|
localDisksInZoneHeal := make([]Endpoints, len(z.zones))
|
||||||
|
var healNewDisks bool
|
||||||
for i, ep := range globalEndpoints {
|
for i, ep := range globalEndpoints {
|
||||||
localDisksToHeal := Endpoints{}
|
localDisksToHeal := Endpoints{}
|
||||||
for _, endpoint := range ep.Endpoints {
|
for _, endpoint := range ep.Endpoints {
|
||||||
@@ -85,13 +75,19 @@ func monitorLocalDisksAndHeal() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
localDisksInZoneHeal[i] = localDisksToHeal
|
localDisksInZoneHeal[i] = localDisksToHeal
|
||||||
|
healNewDisks = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reformat disks only if needed.
|
||||||
|
if !healNewDisks {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reformat disks
|
// Reformat disks
|
||||||
bgSeq.sourceCh <- SlashSeparator
|
bgSeq.sourceCh <- healSource{path: SlashSeparator}
|
||||||
|
|
||||||
// Ensure that reformatting disks is finished
|
// Ensure that reformatting disks is finished
|
||||||
bgSeq.sourceCh <- nopHeal
|
bgSeq.sourceCh <- healSource{path: nopHeal}
|
||||||
|
|
||||||
var erasureSetInZoneToHeal = make([][]int, len(localDisksInZoneHeal))
|
var erasureSetInZoneToHeal = make([][]int, len(localDisksInZoneHeal))
|
||||||
// Compute the list of erasure set to heal
|
// Compute the list of erasure set to heal
|
||||||
@@ -126,4 +122,5 @@ func monitorLocalDisksAndHeal() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-15
@@ -28,11 +28,6 @@ import (
|
|||||||
humanize "github.com/dustin/go-humanize"
|
humanize "github.com/dustin/go-humanize"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Prepare XL/FS backend for benchmark.
|
|
||||||
func prepareBenchmarkBackend(instanceType string) (ObjectLayer, []string, error) {
|
|
||||||
return prepareTestBackend(instanceType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark utility functions for ObjectLayer.PutObject().
|
// Benchmark utility functions for ObjectLayer.PutObject().
|
||||||
// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
|
// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
|
||||||
func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||||
@@ -40,7 +35,7 @@ func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
|||||||
// obtains random bucket name.
|
// obtains random bucket name.
|
||||||
bucket := getRandomBucketName()
|
bucket := getRandomBucketName()
|
||||||
// create bucket.
|
// create bucket.
|
||||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
err = obj.MakeBucketWithLocation(context.Background(), bucket, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -81,7 +76,7 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
|||||||
object := getRandomObjectName()
|
object := getRandomObjectName()
|
||||||
|
|
||||||
// create bucket.
|
// create bucket.
|
||||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
err = obj.MakeBucketWithLocation(context.Background(), bucket, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -135,7 +130,9 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
|||||||
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectPartBenchmark function.
|
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectPartBenchmark function.
|
||||||
func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
|
func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
|
||||||
// create a temp XL/FS backend.
|
// create a temp XL/FS backend.
|
||||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
objLayer, disks, err := prepareTestBackend(ctx, instanceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
||||||
}
|
}
|
||||||
@@ -149,7 +146,9 @@ func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
|
|||||||
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectBenchmark function.
|
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectBenchmark function.
|
||||||
func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
|
func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
|
||||||
// create a temp XL/FS backend.
|
// create a temp XL/FS backend.
|
||||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
objLayer, disks, err := prepareTestBackend(ctx, instanceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
||||||
}
|
}
|
||||||
@@ -163,7 +162,9 @@ func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
|
|||||||
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for put object.
|
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for put object.
|
||||||
func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int) {
|
func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int) {
|
||||||
// create a temp XL/FS backend.
|
// create a temp XL/FS backend.
|
||||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
objLayer, disks, err := prepareTestBackend(ctx, instanceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
||||||
}
|
}
|
||||||
@@ -180,7 +181,7 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
|||||||
// obtains random bucket name.
|
// obtains random bucket name.
|
||||||
bucket := getRandomBucketName()
|
bucket := getRandomBucketName()
|
||||||
// create bucket.
|
// create bucket.
|
||||||
err := obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
err := obj.MakeBucketWithLocation(context.Background(), bucket, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -242,7 +243,9 @@ func generateBytesData(size int) []byte {
|
|||||||
// creates XL/FS backend setup, obtains the object layer and calls the runGetObjectBenchmark function.
|
// creates XL/FS backend setup, obtains the object layer and calls the runGetObjectBenchmark function.
|
||||||
func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
|
func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
|
||||||
// create a temp XL/FS backend.
|
// create a temp XL/FS backend.
|
||||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
objLayer, disks, err := prepareTestBackend(ctx, instanceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
||||||
}
|
}
|
||||||
@@ -256,7 +259,9 @@ func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
|
|||||||
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for ObjectLayer.GetObject() .
|
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for ObjectLayer.GetObject() .
|
||||||
func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int) {
|
func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int) {
|
||||||
// create a temp XL/FS backend.
|
// create a temp XL/FS backend.
|
||||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
objLayer, disks, err := prepareTestBackend(ctx, instanceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
||||||
}
|
}
|
||||||
@@ -273,7 +278,7 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
|||||||
// obtains random bucket name.
|
// obtains random bucket name.
|
||||||
bucket := getRandomBucketName()
|
bucket := getRandomBucketName()
|
||||||
// create bucket.
|
// create bucket.
|
||||||
err := obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
err := obj.MakeBucketWithLocation(context.Background(), bucket, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -317,7 +322,7 @@ func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
|||||||
// obtains random bucket name.
|
// obtains random bucket name.
|
||||||
bucket := getRandomBucketName()
|
bucket := getRandomBucketName()
|
||||||
// create bucket.
|
// create bucket.
|
||||||
err := obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
err := obj.MakeBucketWithLocation(context.Background(), bucket, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-4
@@ -18,7 +18,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
@@ -27,6 +26,14 @@ import (
|
|||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type errHashMismatch struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *errHashMismatch) Error() string {
|
||||||
|
return err.message
|
||||||
|
}
|
||||||
|
|
||||||
// Calculates bitrot in chunks and writes the hash into the stream.
|
// Calculates bitrot in chunks and writes the hash into the stream.
|
||||||
type streamingBitrotWriter struct {
|
type streamingBitrotWriter struct {
|
||||||
iow *io.PipeWriter
|
iow *io.PipeWriter
|
||||||
@@ -132,9 +139,9 @@ func (b *streamingBitrotReader) ReadAt(buf []byte, offset int64) (int, error) {
|
|||||||
b.h.Write(buf)
|
b.h.Write(buf)
|
||||||
|
|
||||||
if !bytes.Equal(b.h.Sum(nil), b.hashBytes) {
|
if !bytes.Equal(b.h.Sum(nil), b.hashBytes) {
|
||||||
err = fmt.Errorf("hashes do not match expected %s, got %s",
|
err := &errHashMismatch{fmt.Sprintf("hashes do not match expected %s, got %s",
|
||||||
hex.EncodeToString(b.hashBytes), hex.EncodeToString(b.h.Sum(nil)))
|
hex.EncodeToString(b.hashBytes), hex.EncodeToString(b.h.Sum(nil)))}
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(GlobalContext, err)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
b.currOffset += int64(len(buf))
|
b.currOffset += int64(len(buf))
|
||||||
|
|||||||
+4
-5
@@ -17,7 +17,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"hash"
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
@@ -36,12 +35,12 @@ type wholeBitrotWriter struct {
|
|||||||
func (b *wholeBitrotWriter) Write(p []byte) (int, error) {
|
func (b *wholeBitrotWriter) Write(p []byte) (int, error) {
|
||||||
err := b.disk.AppendFile(b.volume, b.filePath, p)
|
err := b.disk.AppendFile(b.volume, b.filePath, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(GlobalContext, err)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
_, err = b.Hash.Write(p)
|
_, err = b.Hash.Write(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(GlobalContext, err)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return len(p), nil
|
return len(p), nil
|
||||||
@@ -70,14 +69,14 @@ func (b *wholeBitrotReader) ReadAt(buf []byte, offset int64) (n int, err error)
|
|||||||
if b.buf == nil {
|
if b.buf == nil {
|
||||||
b.buf = make([]byte, b.tillOffset-offset)
|
b.buf = make([]byte, b.tillOffset-offset)
|
||||||
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
|
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
|
||||||
ctx := context.Background()
|
ctx := GlobalContext
|
||||||
logger.GetReqInfo(ctx).AppendTags("disk", b.disk.String())
|
logger.GetReqInfo(ctx).AppendTags("disk", b.disk.String())
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(b.buf) < len(buf) {
|
if len(b.buf) < len(buf) {
|
||||||
logger.LogIf(context.Background(), errLessData)
|
logger.LogIf(GlobalContext, errLessData)
|
||||||
return 0, errLessData
|
return 0, errLessData
|
||||||
}
|
}
|
||||||
n = copy(buf, b.buf)
|
n = copy(buf, b.buf)
|
||||||
|
|||||||
+2
-3
@@ -17,7 +17,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"hash"
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
@@ -72,7 +71,7 @@ func (a BitrotAlgorithm) New() hash.Hash {
|
|||||||
hh, _ := highwayhash.New(magicHighwayHash256Key) // New will never return error since key is 256 bit
|
hh, _ := highwayhash.New(magicHighwayHash256Key) // New will never return error since key is 256 bit
|
||||||
return hh
|
return hh
|
||||||
default:
|
default:
|
||||||
logger.CriticalIf(context.Background(), errors.New("Unsupported bitrot algorithm"))
|
logger.CriticalIf(GlobalContext, errors.New("Unsupported bitrot algorithm"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +87,7 @@ func (a BitrotAlgorithm) Available() bool {
|
|||||||
func (a BitrotAlgorithm) String() string {
|
func (a BitrotAlgorithm) String() string {
|
||||||
name, ok := bitrotAlgorithms[a]
|
name, ok := bitrotAlgorithms[a]
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.CriticalIf(context.Background(), errors.New("Unsupported bitrot algorithm"))
|
logger.CriticalIf(GlobalContext, errors.New("Unsupported bitrot algorithm"))
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ func testBitrotReaderWriterAlgo(t *testing.T, bitrotAlgo BitrotAlgorithm) {
|
|||||||
volume := "testvol"
|
volume := "testvol"
|
||||||
filePath := "testfile"
|
filePath := "testfile"
|
||||||
|
|
||||||
disk, err := newPosix(tmpDir)
|
disk, err := newPosix(tmpDir, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ func (s1 ServerSystemConfig) Diff(s2 ServerSystemConfig) error {
|
|||||||
return fmt.Errorf("Expected platform '%s', found to be running '%s'",
|
return fmt.Errorf("Expected platform '%s', found to be running '%s'",
|
||||||
s1.MinioPlatform, s2.MinioPlatform)
|
s1.MinioPlatform, s2.MinioPlatform)
|
||||||
}
|
}
|
||||||
if s1.MinioEndpoints.Nodes() != s2.MinioEndpoints.Nodes() {
|
if s1.MinioEndpoints.NEndpoints() != s2.MinioEndpoints.NEndpoints() {
|
||||||
return fmt.Errorf("Expected number of endpoints %d, seen %d", s1.MinioEndpoints.Nodes(),
|
return fmt.Errorf("Expected number of endpoints %d, seen %d", s1.MinioEndpoints.NEndpoints(),
|
||||||
s2.MinioEndpoints.Nodes())
|
s2.MinioEndpoints.NEndpoints())
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, ep := range s1.MinioEndpoints {
|
for i, ep := range s1.MinioEndpoints {
|
||||||
@@ -110,7 +110,7 @@ func registerBootstrapRESTHandlers(router *mux.Router) {
|
|||||||
httpTraceHdrs(server.VerifyHandler))
|
httpTraceHdrs(server.VerifyHandler))
|
||||||
}
|
}
|
||||||
|
|
||||||
// client to talk to bootstrap Nodes.
|
// client to talk to bootstrap NEndpoints.
|
||||||
type bootstrapRESTClient struct {
|
type bootstrapRESTClient struct {
|
||||||
endpoint Endpoint
|
endpoint Endpoint
|
||||||
restClient *rest.Client
|
restClient *rest.Client
|
||||||
@@ -126,7 +126,7 @@ func (client *bootstrapRESTClient) reConnect() {
|
|||||||
// permanently. The only way to restore the connection is at the xl-sets layer by xlsets.monitorAndConnectEndpoints()
|
// permanently. The only way to restore the connection is at the xl-sets layer by xlsets.monitorAndConnectEndpoints()
|
||||||
// after verifying format.json
|
// after verifying format.json
|
||||||
func (client *bootstrapRESTClient) call(method string, values url.Values, body io.Reader, length int64) (respBody io.ReadCloser, err error) {
|
func (client *bootstrapRESTClient) call(method string, values url.Values, body io.Reader, length int64) (respBody io.ReadCloser, err error) {
|
||||||
return client.callWithContext(context.Background(), method, values, body, length)
|
return client.callWithContext(GlobalContext, method, values, body, length)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper to restClient.Call to handle network errors, in case of network error the connection is marked disconnected
|
// Wrapper to restClient.Call to handle network errors, in case of network error the connection is marked disconnected
|
||||||
@@ -247,7 +247,7 @@ func newBootstrapRESTClient(endpoint Endpoint) (*bootstrapRESTClient, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trFn := newCustomHTTPTransport(tlsConfig, rest.DefaultRESTTimeout, rest.DefaultRESTTimeout)
|
trFn := newCustomHTTPTransport(tlsConfig, rest.DefaultRESTTimeout)
|
||||||
restClient, err := rest.NewClient(serverURL, trFn, newAuthToken)
|
restClient, err := rest.NewClient(serverURL, trFn, newAuthToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
bucketsse "github.com/minio/minio/pkg/bucket/encryption"
|
|
||||||
"github.com/minio/minio/pkg/bucket/policy"
|
"github.com/minio/minio/pkg/bucket/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,7 +69,12 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
|||||||
// Parse bucket encryption xml
|
// Parse bucket encryption xml
|
||||||
encConfig, err := validateBucketSSEConfig(io.LimitReader(r.Body, maxBucketSSEConfigSize))
|
encConfig, err := validateBucketSSEConfig(io.LimitReader(r.Body, maxBucketSSEConfigSize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL, guessIsBrowserReq(r))
|
apiErr := APIError{
|
||||||
|
Code: "MalformedXML",
|
||||||
|
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrMalformedXML].Description, err),
|
||||||
|
HTTPStatusCode: errorCodes[ErrMalformedXML].HTTPStatusCode,
|
||||||
|
}
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,17 +84,17 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the bucket encryption configuration in the object layer
|
configData, err := xml.Marshal(encConfig)
|
||||||
if err = objAPI.SetBucketSSEConfig(ctx, bucket, encConfig); err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the in-memory bucket encryption cache
|
// Store the bucket encryption configuration in the object layer
|
||||||
globalBucketSSEConfigSys.Set(bucket, *encConfig)
|
if err = globalBucketMetadataSys.Update(bucket, bucketSSEConfig, configData); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
// Update peer MinIO servers of the updated bucket encryption config
|
return
|
||||||
globalNotificationSys.SetBucketSSEConfig(ctx, bucket, encConfig)
|
}
|
||||||
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
@@ -122,21 +127,20 @@ func (api objectAPIHandlers) GetBucketEncryptionHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch bucket encryption configuration from object layer
|
config, err := globalBucketMetadataSys.GetSSEConfig(bucket)
|
||||||
var encConfig *bucketsse.BucketSSEConfig
|
if err != nil {
|
||||||
if encConfig, err = objAPI.GetBucketSSEConfig(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var encConfigData []byte
|
configData, err := xml.Marshal(config)
|
||||||
if encConfigData, err = xml.Marshal(encConfig); err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write bucket encryption configuration to client
|
// Write bucket encryption configuration to client
|
||||||
writeSuccessResponseXML(w, encConfigData)
|
writeSuccessResponseXML(w, configData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteBucketEncryptionHandler - Removes bucket encryption configuration
|
// DeleteBucketEncryptionHandler - Removes bucket encryption configuration
|
||||||
@@ -167,15 +171,10 @@ func (api objectAPIHandlers) DeleteBucketEncryptionHandler(w http.ResponseWriter
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete bucket encryption config from object layer
|
// Delete bucket encryption config from object layer
|
||||||
if err = objAPI.DeleteBucketSSEConfig(ctx, bucket); err != nil {
|
if err = globalBucketMetadataSys.Update(bucket, bucketSSEConfig, nil); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove entry from the in-memory bucket encryption cache
|
|
||||||
globalBucketSSEConfigSys.Remove(bucket)
|
|
||||||
// Update peer MinIO servers of the updated bucket encryption config
|
|
||||||
globalNotificationSys.RemoveBucketSSEConfig(ctx, bucket)
|
|
||||||
|
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-116
@@ -17,142 +17,32 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"path"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
bucketsse "github.com/minio/minio/pkg/bucket/encryption"
|
bucketsse "github.com/minio/minio/pkg/bucket/encryption"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BucketSSEConfigSys - in-memory cache of bucket encryption config
|
// BucketSSEConfigSys - in-memory cache of bucket encryption config
|
||||||
type BucketSSEConfigSys struct {
|
type BucketSSEConfigSys struct{}
|
||||||
sync.RWMutex
|
|
||||||
bucketSSEConfigMap map[string]bucketsse.BucketSSEConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBucketSSEConfigSys - Creates an empty in-memory bucket encryption configuration cache
|
// NewBucketSSEConfigSys - Creates an empty in-memory bucket encryption configuration cache
|
||||||
func NewBucketSSEConfigSys() *BucketSSEConfigSys {
|
func NewBucketSSEConfigSys() *BucketSSEConfigSys {
|
||||||
return &BucketSSEConfigSys{
|
return &BucketSSEConfigSys{}
|
||||||
bucketSSEConfigMap: make(map[string]bucketsse.BucketSSEConfig),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// load - Loads the bucket encryption configuration for the given list of buckets
|
|
||||||
func (sys *BucketSSEConfigSys) load(buckets []BucketInfo, objAPI ObjectLayer) error {
|
|
||||||
for _, bucket := range buckets {
|
|
||||||
config, err := objAPI.GetBucketSSEConfig(context.Background(), bucket.Name)
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(BucketSSEConfigNotFound); ok {
|
|
||||||
sys.Remove(bucket.Name)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sys.Set(bucket.Name, *config)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init - Initializes in-memory bucket encryption config cache for the given list of buckets
|
|
||||||
func (sys *BucketSSEConfigSys) Init(buckets []BucketInfo, objAPI ObjectLayer) error {
|
|
||||||
if objAPI == nil {
|
|
||||||
return errServerNotInitialized
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't cache bucket encryption config in gateway mode, nothing to do.
|
|
||||||
if globalIsGateway {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load bucket encryption config cache once during boot.
|
|
||||||
return sys.load(buckets, objAPI)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get - gets bucket encryption config for the given bucket.
|
// Get - gets bucket encryption config for the given bucket.
|
||||||
func (sys *BucketSSEConfigSys) Get(bucket string) (config bucketsse.BucketSSEConfig, ok bool) {
|
func (sys *BucketSSEConfigSys) Get(bucket string) (*bucketsse.BucketSSEConfig, error) {
|
||||||
// We don't cache bucket encryption config in gateway mode.
|
|
||||||
if globalIsGateway {
|
if globalIsGateway {
|
||||||
objAPI := newObjectLayerWithoutSafeModeFn()
|
objAPI := newObjectLayerWithoutSafeModeFn()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return
|
return nil, errServerNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := objAPI.GetBucketSSEConfig(context.Background(), bucket)
|
return nil, BucketSSEConfigNotFound{Bucket: bucket}
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return *cfg, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sys.Lock()
|
return globalBucketMetadataSys.GetSSEConfig(bucket)
|
||||||
defer sys.Unlock()
|
|
||||||
config, ok = sys.bucketSSEConfigMap[bucket]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set - sets bucket encryption config to given bucket name.
|
|
||||||
func (sys *BucketSSEConfigSys) Set(bucket string, config bucketsse.BucketSSEConfig) {
|
|
||||||
// We don't cache bucket encryption config in gateway mode.
|
|
||||||
if globalIsGateway {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sys.Lock()
|
|
||||||
defer sys.Unlock()
|
|
||||||
sys.bucketSSEConfigMap[bucket] = config
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove - removes bucket encryption config for given bucket.
|
|
||||||
func (sys *BucketSSEConfigSys) Remove(bucket string) {
|
|
||||||
sys.Lock()
|
|
||||||
defer sys.Unlock()
|
|
||||||
|
|
||||||
delete(sys.bucketSSEConfigMap, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveBucketSSEConfig - save bucket encryption config for given bucket.
|
|
||||||
func saveBucketSSEConfig(ctx context.Context, objAPI ObjectLayer, bucket string, config *bucketsse.BucketSSEConfig) error {
|
|
||||||
data, err := xml.Marshal(config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path to store bucket encryption config for the given bucket.
|
|
||||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketSSEConfig)
|
|
||||||
return saveConfig(ctx, objAPI, configFile, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getBucketSSEConfig - get bucket encryption config for given bucket.
|
|
||||||
func getBucketSSEConfig(objAPI ObjectLayer, bucket string) (*bucketsse.BucketSSEConfig, error) {
|
|
||||||
// Path to bucket-encryption.xml for the given bucket.
|
|
||||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketSSEConfig)
|
|
||||||
configData, err := readConfig(context.Background(), objAPI, configFile)
|
|
||||||
if err != nil {
|
|
||||||
if err == errConfigNotFound {
|
|
||||||
err = BucketSSEConfigNotFound{Bucket: bucket}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bucketsse.ParseBucketSSEConfig(bytes.NewReader(configData))
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeBucketSSEConfig - removes bucket encryption config for given bucket.
|
|
||||||
func removeBucketSSEConfig(ctx context.Context, objAPI ObjectLayer, bucket string) error {
|
|
||||||
// Path to bucket-encryption.xml for the given bucket.
|
|
||||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketSSEConfig)
|
|
||||||
|
|
||||||
if err := objAPI.DeleteObject(ctx, minioMetaBucket, configFile); err != nil {
|
|
||||||
if _, ok := err.(ObjectNotFound); ok {
|
|
||||||
return BucketSSEConfigNotFound{Bucket: bucket}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
|
|||||||
+196
-93
@@ -17,7 +17,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -31,6 +30,7 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v6/pkg/set"
|
"github.com/minio/minio-go/v6/pkg/set"
|
||||||
|
"github.com/minio/minio-go/v6/pkg/tags"
|
||||||
"github.com/minio/minio/cmd/config/etcd/dns"
|
"github.com/minio/minio/cmd/config/etcd/dns"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
@@ -47,8 +47,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
getBucketVersioningResponse = `<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"/>`
|
getBucketVersioningResponse = `<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"/>`
|
||||||
objectLockConfig = "object-lock.xml"
|
objectLockConfig = "object-lock.xml"
|
||||||
bucketObjectLockEnabledConfigFile = "object-lock-enabled.json"
|
bucketTaggingConfigFile = "tagging.xml"
|
||||||
bucketObjectLockEnabledConfig = `{"x-amz-bucket-object-lock-enabled":true}`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if there are buckets on server without corresponding entry in etcd backend and
|
// Check if there are buckets on server without corresponding entry in etcd backend and
|
||||||
@@ -70,7 +69,7 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
|
|||||||
// Get buckets in the DNS
|
// Get buckets in the DNS
|
||||||
dnsBuckets, err := globalDNSConfig.List()
|
dnsBuckets, err := globalDNSConfig.List()
|
||||||
if err != nil && err != dns.ErrNoEntriesFound {
|
if err != nil && err != dns.ErrNoEntriesFound {
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(GlobalContext, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,12 +117,12 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
|
|||||||
|
|
||||||
for _, err := range g.Wait() {
|
for _, err := range g.Wait() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(GlobalContext, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, bucket := range bucketsInConflict.ToSlice() {
|
for _, bucket := range bucketsInConflict.ToSlice() {
|
||||||
logger.LogIf(context.Background(), fmt.Errorf("Unable to add bucket DNS entry for bucket %s, an entry exists for the same bucket. Use one of these IP addresses %v to access the bucket", bucket, globalDomainIPs.ToSlice()))
|
logger.LogIf(GlobalContext, fmt.Errorf("Unable to add bucket DNS entry for bucket %s, an entry exists for the same bucket. Use one of these IP addresses %v to access the bucket", bucket, globalDomainIPs.ToSlice()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove buckets that are in DNS for this server, but aren't local
|
// Remove buckets that are in DNS for this server, but aren't local
|
||||||
@@ -140,7 +139,7 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
|
|||||||
// We go to here, so we know the bucket no longer exists,
|
// We go to here, so we know the bucket no longer exists,
|
||||||
// but is registered in DNS to this server
|
// but is registered in DNS to this server
|
||||||
if err = globalDNSConfig.Delete(bucket); err != nil {
|
if err = globalDNSConfig.Delete(bucket); err != nil {
|
||||||
logger.LogIf(context.Background(), fmt.Errorf("Failed to remove DNS entry for %s due to %w",
|
logger.LogIf(GlobalContext, fmt.Errorf("Failed to remove DNS entry for %s due to %w",
|
||||||
bucket, err))
|
bucket, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,7 +265,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
listBuckets := objectAPI.ListBuckets
|
listBuckets := objectAPI.ListBuckets
|
||||||
|
|
||||||
accessKey, owner, s3Error := checkRequestAuthTypeToAccessKey(ctx, r, policy.ListAllMyBucketsAction, "", "")
|
accessKey, owner, s3Error := checkRequestAuthTypeToAccessKey(ctx, r, policy.ListAllMyBucketsAction, "", "")
|
||||||
if s3Error != ErrNone {
|
if s3Error != ErrNone && s3Error != ErrAccessDenied {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -295,6 +294,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s3Error == ErrAccessDenied {
|
||||||
// Set prefix value for "s3:prefix" policy conditionals.
|
// Set prefix value for "s3:prefix" policy conditionals.
|
||||||
r.Header.Set("prefix", "")
|
r.Header.Set("prefix", "")
|
||||||
|
|
||||||
@@ -303,8 +303,10 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
|
|
||||||
// err will be nil here as we already called this function
|
// err will be nil here as we already called this function
|
||||||
// earlier in this request.
|
// earlier in this request.
|
||||||
claims, _ := getClaimsFromToken(r)
|
claims, _ := getClaimsFromToken(r, getSessionToken(r))
|
||||||
var newBucketsInfo []BucketInfo
|
n := 0
|
||||||
|
// Use the following trick to filter in place
|
||||||
|
// https://github.com/golang/go/wiki/SliceTricks#filter-in-place
|
||||||
for _, bucketInfo := range bucketsInfo {
|
for _, bucketInfo := range bucketsInfo {
|
||||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
AccountName: accessKey,
|
AccountName: accessKey,
|
||||||
@@ -315,12 +317,20 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
ObjectName: "",
|
ObjectName: "",
|
||||||
Claims: claims,
|
Claims: claims,
|
||||||
}) {
|
}) {
|
||||||
newBucketsInfo = append(newBucketsInfo, bucketInfo)
|
bucketsInfo[n] = bucketInfo
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bucketsInfo = bucketsInfo[:n]
|
||||||
|
// No buckets can be filtered return access denied error.
|
||||||
|
if len(bucketsInfo) == 0 {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate response.
|
// Generate response.
|
||||||
response := generateListBucketsResponse(newBucketsInfo)
|
response := generateListBucketsResponse(bucketsInfo)
|
||||||
encodedSuccessResponse := encodeResponse(response)
|
encodedSuccessResponse := encodeResponse(response)
|
||||||
|
|
||||||
// Write response.
|
// Write response.
|
||||||
@@ -388,11 +398,14 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
govBypassPerms := checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object.ObjectName)
|
|
||||||
if _, err := enforceRetentionBypassForDelete(ctx, r, bucket, object.ObjectName, getObjectInfoFn, govBypassPerms); err != ErrNone {
|
if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled {
|
||||||
dErrs[index] = err
|
if apiErr := enforceRetentionBypassForDelete(ctx, r, bucket, object.ObjectName, getObjectInfoFn); apiErr != ErrNone {
|
||||||
|
dErrs[index] = apiErr
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Avoid duplicate objects, we use map to filter them out.
|
// Avoid duplicate objects, we use map to filter them out.
|
||||||
if _, ok := objectsToDelete[object.ObjectName]; !ok {
|
if _, ok := objectsToDelete[object.ObjectName]; !ok {
|
||||||
objectsToDelete[object.ObjectName] = index
|
objectsToDelete[object.ObjectName] = index
|
||||||
@@ -514,25 +527,20 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if err == dns.ErrNoEntriesFound {
|
if err == dns.ErrNoEntriesFound {
|
||||||
// Proceed to creating a bucket.
|
// Proceed to creating a bucket.
|
||||||
if err = objectAPI.MakeBucketWithLocation(ctx, bucket, location); err != nil {
|
if err = objectAPI.MakeBucketWithLocation(ctx, bucket, location, objectLockEnabled); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if objectLockEnabled {
|
|
||||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
|
|
||||||
if err = saveConfig(ctx, objectAPI, configFile, []byte(bucketObjectLockEnabledConfig)); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = globalDNSConfig.Put(bucket); err != nil {
|
if err = globalDNSConfig.Put(bucket); err != nil {
|
||||||
objectAPI.DeleteBucket(ctx, bucket)
|
objectAPI.DeleteBucket(ctx, bucket, false)
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load updated bucket metadata into memory.
|
||||||
|
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
||||||
|
|
||||||
// Make sure to add Location information here only for bucket
|
// Make sure to add Location information here only for bucket
|
||||||
w.Header().Set(xhttp.Location,
|
w.Header().Set(xhttp.Location,
|
||||||
getObjectLocation(r, globalDomainNames, bucket, ""))
|
getObjectLocation(r, globalDomainNames, bucket, ""))
|
||||||
@@ -557,21 +565,14 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Proceed to creating a bucket.
|
// Proceed to creating a bucket.
|
||||||
err := objectAPI.MakeBucketWithLocation(ctx, bucket, location)
|
err := objectAPI.MakeBucketWithLocation(ctx, bucket, location, objectLockEnabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if objectLockEnabled && !globalIsGateway {
|
// Load updated bucket metadata into memory.
|
||||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
|
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
||||||
if err = saveConfig(ctx, objectAPI, configFile, []byte(bucketObjectLockEnabledConfig)); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
globalBucketObjectLockConfig.Set(bucket, objectlock.Retention{})
|
|
||||||
globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, objectlock.Retention{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure to add Location information here only for bucket
|
// Make sure to add Location information here only for bucket
|
||||||
w.Header().Set(xhttp.Location, path.Clean(r.URL.Path)) // Clean any trailing slashes.
|
w.Header().Set(xhttp.Location, path.Clean(r.URL.Path)) // Clean any trailing slashes.
|
||||||
@@ -606,7 +607,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
bucket := mux.Vars(r)["bucket"]
|
bucket := mux.Vars(r)["bucket"]
|
||||||
|
|
||||||
// To detect if the client has disconnected.
|
// To detect if the client has disconnected.
|
||||||
r.Body = &detectDisconnect{r.Body, r.Context().Done()}
|
r.Body = &contextReader{r.Body, r.Context()}
|
||||||
|
|
||||||
// Require Content-Length to be set in the request
|
// Require Content-Length to be set in the request
|
||||||
size := r.ContentLength
|
size := r.ContentLength
|
||||||
@@ -742,14 +743,16 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
}
|
}
|
||||||
rawReader := hashReader
|
rawReader := hashReader
|
||||||
pReader := NewPutObjReader(rawReader, nil, nil)
|
pReader := NewPutObjReader(rawReader, nil, nil)
|
||||||
var objectEncryptionKey []byte
|
var objectEncryptionKey crypto.ObjectKey
|
||||||
|
|
||||||
// Check if bucket encryption is enabled
|
// Check if bucket encryption is enabled
|
||||||
_, encEnabled := globalBucketSSEConfigSys.Get(bucket)
|
if _, err = globalBucketSSEConfigSys.Get(bucket); err == nil || globalAutoEncryption {
|
||||||
// This request header needs to be set prior to setting ObjectOptions
|
// This request header needs to be set prior to setting ObjectOptions
|
||||||
if (globalAutoEncryption || encEnabled) && !crypto.SSEC.IsRequested(r.Header) {
|
if !crypto.SSEC.IsRequested(r.Header) {
|
||||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// get gateway encryption options
|
// get gateway encryption options
|
||||||
var opts ObjectOptions
|
var opts ObjectOptions
|
||||||
opts, err = putOpts(ctx, r, bucket, object, metadata)
|
opts, err = putOpts(ctx, r, bucket, object, metadata)
|
||||||
@@ -784,7 +787,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pReader = NewPutObjReader(rawReader, hashReader, objectEncryptionKey)
|
pReader = NewPutObjReader(rawReader, hashReader, &objectEncryptionKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -883,15 +886,41 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forceDelete := false
|
||||||
|
if value := r.Header.Get(xhttp.MinIOForceDelete); value != "" {
|
||||||
|
switch value {
|
||||||
|
case "true":
|
||||||
|
forceDelete = true
|
||||||
|
case "false":
|
||||||
|
default:
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if forceDelete {
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.ForceDeleteBucketAction, bucket, ""); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if forceDelete {
|
||||||
|
if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
deleteBucket := objectAPI.DeleteBucket
|
deleteBucket := objectAPI.DeleteBucket
|
||||||
|
|
||||||
// Attempt to delete bucket.
|
// Attempt to delete bucket.
|
||||||
if err := deleteBucket(ctx, bucket); err != nil {
|
if err := deleteBucket(ctx, bucket, forceDelete); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -899,13 +928,13 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
if globalDNSConfig != nil {
|
if globalDNSConfig != nil {
|
||||||
if err := globalDNSConfig.Delete(bucket); err != nil {
|
if err := globalDNSConfig.Delete(bucket); err != nil {
|
||||||
// Deleting DNS entry failed, attempt to create the bucket again.
|
// Deleting DNS entry failed, attempt to create the bucket again.
|
||||||
objectAPI.MakeBucketWithLocation(ctx, bucket, "")
|
objectAPI.MakeBucketWithLocation(ctx, bucket, "", false)
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
globalNotificationSys.DeleteBucket(ctx, bucket)
|
globalNotificationSys.DeleteBucketMetadata(ctx, bucket)
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
@@ -984,15 +1013,11 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deny if WORM is enabled
|
|
||||||
if globalWORMEnabled {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketObjectLockConfigurationAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketObjectLockConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := objectlock.ParseObjectLockConfig(r.Body)
|
config, err := objectlock.ParseObjectLockConfig(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||||
@@ -1000,39 +1025,23 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
|||||||
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
|
|
||||||
configData, err := readConfig(ctx, objectAPI, configFile)
|
configData, err := xml.Marshal(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aerr := toAPIError(ctx, err)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
if err == errConfigNotFound {
|
|
||||||
aerr = errorCodes.ToAPIErr(ErrObjectLockConfigurationNotAllowed)
|
|
||||||
}
|
|
||||||
writeErrorResponse(ctx, w, aerr, r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if string(configData) != bucketObjectLockEnabledConfig {
|
// Deny object locking configuration settings on existing buckets without object lock enabled.
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL, guessIsBrowserReq(r))
|
if _, err = globalBucketMetadataSys.GetObjectLockConfig(bucket); err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := xml.Marshal(config)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
configFile = path.Join(bucketConfigPrefix, bucket, objectLockConfig)
|
|
||||||
if err = saveConfig(ctx, objectAPI, configFile, data); err != nil {
|
if err = globalBucketMetadataSys.Update(bucket, objectLockConfig, configData); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if config.Rule != nil {
|
|
||||||
retention := config.ToRetention()
|
|
||||||
globalBucketObjectLockConfig.Set(bucket, retention)
|
|
||||||
globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, retention)
|
|
||||||
} else {
|
|
||||||
globalBucketObjectLockConfig.Remove(bucket)
|
|
||||||
globalNotificationSys.RemoveBucketObjectLockConfig(ctx, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
@@ -1056,43 +1065,137 @@ func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWri
|
|||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if user has permissions to perform this operation
|
// check if user has permissions to perform this operation
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketObjectLockConfigurationAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketObjectLockConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
|
|
||||||
configData, err := readConfig(ctx, objectAPI, configFile)
|
|
||||||
if err != nil {
|
|
||||||
var aerr APIError
|
|
||||||
if err == errConfigNotFound {
|
|
||||||
aerr = errorCodes.ToAPIErr(ErrMethodNotAllowed)
|
|
||||||
} else {
|
|
||||||
aerr = toAPIError(ctx, err)
|
|
||||||
}
|
|
||||||
writeErrorResponse(ctx, w, aerr, r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(configData) != bucketObjectLockEnabledConfig {
|
config, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
configFile = path.Join(bucketConfigPrefix, bucket, objectLockConfig)
|
|
||||||
configData, err = readConfig(ctx, objectAPI, configFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != errConfigNotFound {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if configData, err = xml.Marshal(objectlock.NewObjectLockConfig()); err != nil {
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessResponseXML(w, configData)
|
writeSuccessResponseXML(w, configData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PutBucketTaggingHandler - PUT Bucket tagging.
|
||||||
|
// ----------
|
||||||
|
func (api objectAPIHandlers) PutBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "PutBucketTagging")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "PutBucketTagging", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
|
objectAPI := api.ObjectAPI()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketTaggingAction, bucket, ""); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tags, err := tags.ParseBucketXML(io.LimitReader(r.Body, r.ContentLength))
|
||||||
|
if err != nil {
|
||||||
|
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||||
|
apiErr.Description = err.Error()
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := xml.Marshal(tags)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = globalBucketMetadataSys.Update(bucket, bucketTaggingConfigFile, configData); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write success response.
|
||||||
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBucketTaggingHandler - GET Bucket tagging.
|
||||||
|
// ----------
|
||||||
|
func (api objectAPIHandlers) GetBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "GetBucketTagging")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "GetBucketTagging", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
|
objectAPI := api.ObjectAPI()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user has permissions to perform this operation
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketTaggingAction, bucket, ""); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := globalBucketMetadataSys.GetTaggingConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write success response.
|
||||||
|
writeSuccessResponseXML(w, configData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBucketTaggingHandler - DELETE Bucket tagging.
|
||||||
|
// ----------
|
||||||
|
func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "DeleteBucketTagging")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "DeleteBucketTagging", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
|
objectAPI := api.ObjectAPI()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketTaggingAction, bucket, ""); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := globalBucketMetadataSys.Update(bucket, bucketTaggingConfigFile, nil); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write success response.
|
||||||
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -29,6 +28,51 @@ import (
|
|||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Wrapper for calling RemoveBucket HTTP handler tests for both XL multiple disks and single node setup.
|
||||||
|
func TestRemoveBucketHandler(t *testing.T) {
|
||||||
|
ExecObjectLayerAPITest(t, testRemoveBucketHandler, []string{"RemoveBucket"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRemoveBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
|
credentials auth.Credentials, t *testing.T) {
|
||||||
|
_, err := obj.PutObject(GlobalContext, bucketName, "test-object", mustGetPutObjReader(t, bytes.NewBuffer([]byte{}), int64(0), "", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), ObjectOptions{})
|
||||||
|
// if object upload fails stop the test.
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error uploading object: <ERROR> %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize httptest Recorder, this records any mutations to response writer inside the handler.
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
// construct HTTP request for DELETE bucket.
|
||||||
|
req, err := newTestSignedRequestV4("DELETE", getBucketLocationURL("", bucketName), 0, nil, credentials.AccessKey, credentials.SecretKey, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %s: Failed to create HTTP request for RemoveBucketHandler: <ERROR> %v", instanceType, err)
|
||||||
|
}
|
||||||
|
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
||||||
|
// Call the ServeHTTP to execute the handler.
|
||||||
|
apiRouter.ServeHTTP(rec, req)
|
||||||
|
switch rec.Code {
|
||||||
|
case http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent:
|
||||||
|
t.Fatalf("Test %v: expected failure, but succeeded with %v", instanceType, rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify response of the V2 signed HTTP request.
|
||||||
|
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||||
|
recV2 := httptest.NewRecorder()
|
||||||
|
// construct HTTP request for DELETE bucket.
|
||||||
|
reqV2, err := newTestSignedRequestV2("DELETE", getBucketLocationURL("", bucketName), 0, nil, credentials.AccessKey, credentials.SecretKey, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %s: Failed to create HTTP request for RemoveBucketHandler: <ERROR> %v", instanceType, err)
|
||||||
|
}
|
||||||
|
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
||||||
|
// Call the ServeHTTP to execute the handler.
|
||||||
|
apiRouter.ServeHTTP(recV2, reqV2)
|
||||||
|
switch recV2.Code {
|
||||||
|
case http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent:
|
||||||
|
t.Fatalf("Test %v: expected failure, but succeeded with %v", instanceType, recV2.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Wrapper for calling GetBucketPolicy HTTP handler tests for both XL multiple disks and single node setup.
|
// Wrapper for calling GetBucketPolicy HTTP handler tests for both XL multiple disks and single node setup.
|
||||||
func TestGetBucketLocationHandler(t *testing.T) {
|
func TestGetBucketLocationHandler(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testGetBucketLocationHandler, []string{"GetBucketLocation"})
|
ExecObjectLayerAPITest(t, testGetBucketLocationHandler, []string{"GetBucketLocation"})
|
||||||
@@ -71,7 +115,7 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
|||||||
errorResponse: APIErrorResponse{
|
errorResponse: APIErrorResponse{
|
||||||
Resource: SlashSeparator + bucketName + SlashSeparator,
|
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||||
Code: "InvalidAccessKeyId",
|
Code: "InvalidAccessKeyId",
|
||||||
Message: "The access key ID you provided does not exist in our records.",
|
Message: "The Access Key Id you provided does not exist in our records.",
|
||||||
},
|
},
|
||||||
shouldPass: false,
|
shouldPass: false,
|
||||||
},
|
},
|
||||||
@@ -625,7 +669,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
objectName := "test-object-" + strconv.Itoa(i)
|
objectName := "test-object-" + strconv.Itoa(i)
|
||||||
// uploading the object.
|
// uploading the object.
|
||||||
_, err = obj.PutObject(context.Background(), bucketName, objectName, mustGetPutObjReader(t, bytes.NewBuffer(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{})
|
_, err = obj.PutObject(GlobalContext, bucketName, objectName, mustGetPutObjReader(t, bytes.NewBuffer(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{})
|
||||||
// if object upload fails stop the test.
|
// if object upload fails stop the test.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Put Object %d: Error uploading object: <ERROR> %v", i, err)
|
t.Fatalf("Put Object %d: Error uploading object: <ERROR> %v", i, err)
|
||||||
|
|||||||
@@ -72,13 +72,22 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = objAPI.SetBucketLifecycle(ctx, bucket, bucketLifecycle); err != nil {
|
// Validate the received bucket policy document
|
||||||
|
if err = bucketLifecycle.Validate(); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
globalLifecycleSys.Set(bucket, *bucketLifecycle)
|
configData, err := xml.Marshal(bucketLifecycle)
|
||||||
globalNotificationSys.SetBucketLifecycle(ctx, bucket, bucketLifecycle)
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = globalBucketMetadataSys.Update(bucket, bucketLifecycleConfig, configData); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
@@ -110,21 +119,20 @@ func (api objectAPIHandlers) GetBucketLifecycleHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read bucket access lifecycle.
|
config, err := globalBucketMetadataSys.GetLifecycleConfig(bucket)
|
||||||
bucketLifecycle, err := objAPI.GetBucketLifecycle(ctx, bucket)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleData, err := xml.Marshal(bucketLifecycle)
|
configData, err := xml.Marshal(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write lifecycle configuration to client.
|
// Write lifecycle configuration to client.
|
||||||
writeSuccessResponseXML(w, lifecycleData)
|
writeSuccessResponseXML(w, configData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteBucketLifecycleHandler - This HTTP handler removes bucket lifecycle configuration.
|
// DeleteBucketLifecycleHandler - This HTTP handler removes bucket lifecycle configuration.
|
||||||
@@ -153,14 +161,11 @@ func (api objectAPIHandlers) DeleteBucketLifecycleHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := objAPI.DeleteBucketLifecycle(ctx, bucket); err != nil {
|
if err := globalBucketMetadataSys.Update(bucket, bucketLifecycleConfig, nil); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
globalLifecycleSys.Remove(bucket)
|
|
||||||
globalNotificationSys.RemoveBucketLifecycle(ctx, bucket)
|
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test S3 Bucket lifecycle APIs with wrong credentials
|
||||||
|
func TestBucketLifecycleWrongCredentials(t *testing.T) {
|
||||||
|
ExecObjectLayerAPITest(t, testBucketLifecycleHandlersWrongCredentials, []string{"GetBucketLifecycle", "PutBucketLifecycle", "DeleteBucketLifecycle"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for authentication
|
||||||
|
func testBucketLifecycleHandlersWrongCredentials(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
|
credentials auth.Credentials, t *testing.T) {
|
||||||
|
// test cases with sample input and expected output.
|
||||||
|
testCases := []struct {
|
||||||
|
method string
|
||||||
|
bucketName string
|
||||||
|
accessKey string
|
||||||
|
secretKey string
|
||||||
|
// Sent body
|
||||||
|
body []byte
|
||||||
|
// Expected response
|
||||||
|
expectedRespStatus int
|
||||||
|
lifecycleResponse []byte
|
||||||
|
errorResponse APIErrorResponse
|
||||||
|
shouldPass bool
|
||||||
|
}{
|
||||||
|
// GET empty credentials
|
||||||
|
{
|
||||||
|
method: "GET", bucketName: bucketName,
|
||||||
|
accessKey: "",
|
||||||
|
secretKey: "",
|
||||||
|
expectedRespStatus: http.StatusForbidden,
|
||||||
|
lifecycleResponse: []byte(""),
|
||||||
|
errorResponse: APIErrorResponse{
|
||||||
|
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||||
|
Code: "AccessDenied",
|
||||||
|
Message: "Access Denied.",
|
||||||
|
},
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
// GET wrong credentials
|
||||||
|
{
|
||||||
|
method: "GET", bucketName: bucketName,
|
||||||
|
accessKey: "abcd",
|
||||||
|
secretKey: "abcd",
|
||||||
|
expectedRespStatus: http.StatusForbidden,
|
||||||
|
lifecycleResponse: []byte(""),
|
||||||
|
errorResponse: APIErrorResponse{
|
||||||
|
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||||
|
Code: "InvalidAccessKeyId",
|
||||||
|
Message: "The Access Key Id you provided does not exist in our records.",
|
||||||
|
},
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
// PUT empty credentials
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
bucketName: bucketName,
|
||||||
|
accessKey: "",
|
||||||
|
secretKey: "",
|
||||||
|
expectedRespStatus: http.StatusForbidden,
|
||||||
|
lifecycleResponse: []byte(""),
|
||||||
|
errorResponse: APIErrorResponse{
|
||||||
|
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||||
|
Code: "AccessDenied",
|
||||||
|
Message: "Access Denied.",
|
||||||
|
},
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
// PUT wrong credentials
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
bucketName: bucketName,
|
||||||
|
accessKey: "abcd",
|
||||||
|
secretKey: "abcd",
|
||||||
|
expectedRespStatus: http.StatusForbidden,
|
||||||
|
lifecycleResponse: []byte(""),
|
||||||
|
errorResponse: APIErrorResponse{
|
||||||
|
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||||
|
Code: "InvalidAccessKeyId",
|
||||||
|
Message: "The Access Key Id you provided does not exist in our records.",
|
||||||
|
},
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
// DELETE empty credentials
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
bucketName: bucketName,
|
||||||
|
accessKey: "",
|
||||||
|
secretKey: "",
|
||||||
|
expectedRespStatus: http.StatusForbidden,
|
||||||
|
lifecycleResponse: []byte(""),
|
||||||
|
errorResponse: APIErrorResponse{
|
||||||
|
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||||
|
Code: "AccessDenied",
|
||||||
|
Message: "Access Denied.",
|
||||||
|
},
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
// DELETE wrong credentials
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
bucketName: bucketName,
|
||||||
|
accessKey: "abcd",
|
||||||
|
secretKey: "abcd",
|
||||||
|
expectedRespStatus: http.StatusForbidden,
|
||||||
|
lifecycleResponse: []byte(""),
|
||||||
|
errorResponse: APIErrorResponse{
|
||||||
|
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||||
|
Code: "InvalidAccessKeyId",
|
||||||
|
Message: "The Access Key Id you provided does not exist in our records.",
|
||||||
|
},
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testBucketLifecycle(obj, instanceType, bucketName, apiRouter, t, testCases)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test S3 Bucket lifecycle APIs
|
||||||
|
func TestBucketLifecycle(t *testing.T) {
|
||||||
|
ExecObjectLayerAPITest(t, testBucketLifecycleHandlers, []string{"GetBucketLifecycle", "PutBucketLifecycle", "DeleteBucketLifecycle"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple tests of bucket lifecycle: PUT, GET, DELETE.
|
||||||
|
// Tests are related and the order is important.
|
||||||
|
func testBucketLifecycleHandlers(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
|
creds auth.Credentials, t *testing.T) {
|
||||||
|
|
||||||
|
// test cases with sample input and expected output.
|
||||||
|
testCases := []struct {
|
||||||
|
method string
|
||||||
|
bucketName string
|
||||||
|
accessKey string
|
||||||
|
secretKey string
|
||||||
|
// Sent body
|
||||||
|
body []byte
|
||||||
|
// Expected response
|
||||||
|
expectedRespStatus int
|
||||||
|
lifecycleResponse []byte
|
||||||
|
errorResponse APIErrorResponse
|
||||||
|
shouldPass bool
|
||||||
|
}{
|
||||||
|
// Test case - 1.
|
||||||
|
// Filter contains more than (Prefix,Tag,And) rule
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
bucketName: bucketName,
|
||||||
|
accessKey: creds.AccessKey,
|
||||||
|
secretKey: creds.SecretKey,
|
||||||
|
body: []byte(`<LifecycleConfiguration><Rule><ID>id</ID><Filter><Prefix>logs/</Prefix><Tag><Key>Key1</Key><Value>Value1</Value></Tag></Filter><Status>Enabled</Status><Expiration><Days>365</Days></Expiration></Rule></LifecycleConfiguration>`),
|
||||||
|
expectedRespStatus: http.StatusBadRequest,
|
||||||
|
lifecycleResponse: []byte(``),
|
||||||
|
errorResponse: APIErrorResponse{
|
||||||
|
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||||
|
Code: "InvalidRequest",
|
||||||
|
Message: "Filter must have exactly one of Prefix, Tag, or And specified",
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
// Date contains wrong format
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
bucketName: bucketName,
|
||||||
|
accessKey: creds.AccessKey,
|
||||||
|
secretKey: creds.SecretKey,
|
||||||
|
body: []byte(`<LifecycleConfiguration><Rule><ID>id</ID><Filter><Prefix>logs/</Prefix><Tag><Key>Key1</Key><Value>Value1</Value></Tag></Filter><Status>Enabled</Status><Expiration><Date>365</Date></Expiration></Rule></LifecycleConfiguration>`),
|
||||||
|
expectedRespStatus: http.StatusBadRequest,
|
||||||
|
lifecycleResponse: []byte(``),
|
||||||
|
errorResponse: APIErrorResponse{
|
||||||
|
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||||
|
Code: "InvalidRequest",
|
||||||
|
Message: "Date must be provided in ISO 8601 format",
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
bucketName: bucketName,
|
||||||
|
accessKey: creds.AccessKey,
|
||||||
|
secretKey: creds.SecretKey,
|
||||||
|
body: []byte(`<?xml version="1.0" encoding="UTF-8"?><LifecycleConfiguration><Rule><ID>id</ID><Filter><Prefix>logs/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>365</Days></Expiration></Rule></LifecycleConfiguration>`),
|
||||||
|
expectedRespStatus: http.StatusOK,
|
||||||
|
lifecycleResponse: []byte(``),
|
||||||
|
errorResponse: APIErrorResponse{},
|
||||||
|
shouldPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
accessKey: creds.AccessKey,
|
||||||
|
secretKey: creds.SecretKey,
|
||||||
|
bucketName: bucketName,
|
||||||
|
body: []byte(``),
|
||||||
|
expectedRespStatus: http.StatusOK,
|
||||||
|
lifecycleResponse: []byte(`<LifecycleConfiguration><Rule><ID>id</ID><Status>Enabled</Status><Filter><Prefix>logs/</Prefix></Filter><Expiration><Days>365</Days></Expiration></Rule></LifecycleConfiguration>`),
|
||||||
|
errorResponse: APIErrorResponse{},
|
||||||
|
shouldPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
accessKey: creds.AccessKey,
|
||||||
|
secretKey: creds.SecretKey,
|
||||||
|
bucketName: bucketName,
|
||||||
|
body: []byte(``),
|
||||||
|
expectedRespStatus: http.StatusNoContent,
|
||||||
|
lifecycleResponse: []byte(``),
|
||||||
|
errorResponse: APIErrorResponse{},
|
||||||
|
shouldPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
accessKey: creds.AccessKey,
|
||||||
|
secretKey: creds.SecretKey,
|
||||||
|
bucketName: bucketName,
|
||||||
|
body: []byte(``),
|
||||||
|
expectedRespStatus: http.StatusNotFound,
|
||||||
|
lifecycleResponse: []byte(``),
|
||||||
|
errorResponse: APIErrorResponse{
|
||||||
|
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||||
|
Code: "NoSuchLifecycleConfiguration",
|
||||||
|
Message: "The lifecycle configuration does not exist",
|
||||||
|
},
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testBucketLifecycle(obj, instanceType, bucketName, apiRouter, t, testCases)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testBucketLifecycle is a generic testing of lifecycle requests
|
||||||
|
func testBucketLifecycle(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
|
t *testing.T, testCases []struct {
|
||||||
|
method string
|
||||||
|
bucketName string
|
||||||
|
accessKey string
|
||||||
|
secretKey string
|
||||||
|
body []byte
|
||||||
|
expectedRespStatus int
|
||||||
|
lifecycleResponse []byte
|
||||||
|
errorResponse APIErrorResponse
|
||||||
|
shouldPass bool
|
||||||
|
}) {
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
// initialize httptest Recorder, this records any mutations to response writer inside the handler.
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
// construct HTTP request
|
||||||
|
req, err := newTestSignedRequestV4(testCase.method, getBucketLifecycleURL("", testCase.bucketName),
|
||||||
|
int64(len(testCase.body)), bytes.NewReader(testCase.body), testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for GetBucketLocationHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
|
}
|
||||||
|
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
||||||
|
// Call the ServeHTTP to execute the handler.
|
||||||
|
apiRouter.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != testCase.expectedRespStatus {
|
||||||
|
t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
|
||||||
|
}
|
||||||
|
if testCase.shouldPass && !bytes.Equal(testCase.lifecycleResponse, rec.Body.Bytes()) {
|
||||||
|
t.Errorf("Test %d: %s: Expected the response to be `%s`, but instead found `%s`", i+1, instanceType, string(testCase.lifecycleResponse), rec.Body.String())
|
||||||
|
}
|
||||||
|
errorResponse := APIErrorResponse{}
|
||||||
|
err = xml.Unmarshal(rec.Body.Bytes(), &errorResponse)
|
||||||
|
if err != nil && !testCase.shouldPass {
|
||||||
|
t.Fatalf("Test %d: %s: Unable to marshal response body %s", i+1, instanceType, rec.Body.String())
|
||||||
|
}
|
||||||
|
if errorResponse.Resource != testCase.errorResponse.Resource {
|
||||||
|
t.Errorf("Test %d: %s: Expected the error resource to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Resource, errorResponse.Resource)
|
||||||
|
}
|
||||||
|
if errorResponse.Message != testCase.errorResponse.Message {
|
||||||
|
t.Errorf("Test %d: %s: Expected the error message to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Message, errorResponse.Message)
|
||||||
|
}
|
||||||
|
if errorResponse.Code != testCase.errorResponse.Code {
|
||||||
|
t.Errorf("Test %d: %s: Expected the error code to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Code, errorResponse.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2019 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
|
||||||
|
// Disabled means the lifecycle rule is inactive
|
||||||
|
Disabled = "Disabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LifecycleSys - Bucket lifecycle subsystem.
|
||||||
|
type LifecycleSys struct{}
|
||||||
|
|
||||||
|
// Get - gets lifecycle config associated to a given bucket name.
|
||||||
|
func (sys *LifecycleSys) Get(bucketName string) (lc *lifecycle.Lifecycle, err error) {
|
||||||
|
if globalIsGateway {
|
||||||
|
objAPI := newObjectLayerWithoutSafeModeFn()
|
||||||
|
if objAPI == nil {
|
||||||
|
return nil, errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, BucketLifecycleNotFound{Bucket: bucketName}
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalBucketMetadataSys.GetLifecycleConfig(bucketName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLifecycleSys - creates new lifecycle system.
|
||||||
|
func NewLifecycleSys() *LifecycleSys {
|
||||||
|
return &LifecycleSys{}
|
||||||
|
}
|
||||||
@@ -99,26 +99,15 @@ func (api objectAPIHandlers) ListBucketObjectVersionsHandler(w http.ResponseWrit
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsInfo.Objects {
|
for i := range listObjectsInfo.Objects {
|
||||||
var actualSize int64
|
if crypto.IsEncrypted(listObjectsInfo.Objects[i].UserDefined) {
|
||||||
if listObjectsInfo.Objects[i].IsCompressed() {
|
|
||||||
// Read the decompressed size from the meta.json.
|
|
||||||
actualSize = listObjectsInfo.Objects[i].GetActualSize()
|
|
||||||
if actualSize < 0 {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidDecompressedSize),
|
|
||||||
r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Set the info.Size to the actualSize.
|
|
||||||
listObjectsInfo.Objects[i].Size = actualSize
|
|
||||||
} else if crypto.IsEncrypted(listObjectsInfo.Objects[i].UserDefined) {
|
|
||||||
listObjectsInfo.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsInfo.Objects[i], false)
|
listObjectsInfo.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsInfo.Objects[i], false)
|
||||||
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].DecryptedSize()
|
}
|
||||||
|
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].GetActualSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
response := generateListVersionsResponse(bucket, prefix, marker, delimiter, encodingType, maxkeys, listObjectsInfo)
|
response := generateListVersionsResponse(bucket, prefix, marker, delimiter, encodingType, maxkeys, listObjectsInfo)
|
||||||
|
|
||||||
@@ -181,25 +170,15 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsV2Info.Objects {
|
for i := range listObjectsV2Info.Objects {
|
||||||
var actualSize int64
|
if crypto.IsEncrypted(listObjectsV2Info.Objects[i].UserDefined) {
|
||||||
if listObjectsV2Info.Objects[i].IsCompressed() {
|
|
||||||
// Read the decompressed size from the meta.json.
|
|
||||||
actualSize = listObjectsV2Info.Objects[i].GetActualSize()
|
|
||||||
if actualSize < 0 {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidDecompressedSize), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Set the info.Size to the actualSize.
|
|
||||||
listObjectsV2Info.Objects[i].Size = actualSize
|
|
||||||
} else if crypto.IsEncrypted(listObjectsV2Info.Objects[i].UserDefined) {
|
|
||||||
listObjectsV2Info.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsV2Info.Objects[i], false)
|
listObjectsV2Info.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsV2Info.Objects[i], false)
|
||||||
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].DecryptedSize()
|
}
|
||||||
|
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].GetActualSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
response := generateListObjectsV2Response(bucket, prefix, token,
|
response := generateListObjectsV2Response(bucket, prefix, token,
|
||||||
listObjectsV2Info.NextContinuationToken, startAfter,
|
listObjectsV2Info.NextContinuationToken, startAfter,
|
||||||
@@ -265,25 +244,15 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsV2Info.Objects {
|
for i := range listObjectsV2Info.Objects {
|
||||||
var actualSize int64
|
if crypto.IsEncrypted(listObjectsV2Info.Objects[i].UserDefined) {
|
||||||
if listObjectsV2Info.Objects[i].IsCompressed() {
|
|
||||||
// Read the decompressed size from the meta.json.
|
|
||||||
actualSize = listObjectsV2Info.Objects[i].GetActualSize()
|
|
||||||
if actualSize < 0 {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidDecompressedSize), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Set the info.Size to the actualSize.
|
|
||||||
listObjectsV2Info.Objects[i].Size = actualSize
|
|
||||||
} else if crypto.IsEncrypted(listObjectsV2Info.Objects[i].UserDefined) {
|
|
||||||
listObjectsV2Info.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsV2Info.Objects[i], false)
|
listObjectsV2Info.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsV2Info.Objects[i], false)
|
||||||
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].DecryptedSize()
|
}
|
||||||
|
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].GetActualSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
response := generateListObjectsV2Response(bucket, prefix, token,
|
response := generateListObjectsV2Response(bucket, prefix, token,
|
||||||
listObjectsV2Info.NextContinuationToken, startAfter,
|
listObjectsV2Info.NextContinuationToken, startAfter,
|
||||||
@@ -344,25 +313,16 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsInfo.Objects {
|
for i := range listObjectsInfo.Objects {
|
||||||
var actualSize int64
|
if crypto.IsEncrypted(listObjectsInfo.Objects[i].UserDefined) {
|
||||||
if listObjectsInfo.Objects[i].IsCompressed() {
|
|
||||||
// Read the decompressed size from the meta.json.
|
|
||||||
actualSize = listObjectsInfo.Objects[i].GetActualSize()
|
|
||||||
if actualSize < 0 {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidDecompressedSize), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Set the info.Size to the actualSize.
|
|
||||||
listObjectsInfo.Objects[i].Size = actualSize
|
|
||||||
} else if crypto.IsEncrypted(listObjectsInfo.Objects[i].UserDefined) {
|
|
||||||
listObjectsInfo.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsInfo.Objects[i], false)
|
listObjectsInfo.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsInfo.Objects[i], false)
|
||||||
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].DecryptedSize()
|
}
|
||||||
|
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].GetActualSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
|
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v6/pkg/tags"
|
||||||
|
bucketsse "github.com/minio/minio/pkg/bucket/encryption"
|
||||||
|
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||||
|
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
||||||
|
"github.com/minio/minio/pkg/bucket/policy"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
"github.com/minio/minio/pkg/madmin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BucketMetadataSys captures all bucket metadata for a given cluster.
|
||||||
|
type BucketMetadataSys struct {
|
||||||
|
sync.RWMutex
|
||||||
|
metadataMap map[string]BucketMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove bucket metadata from memory.
|
||||||
|
func (sys *BucketMetadataSys) Remove(bucket string) {
|
||||||
|
if globalIsGateway && globalGatewayName != "nas" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sys.Lock()
|
||||||
|
delete(sys.metadataMap, bucket)
|
||||||
|
sys.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set - sets a new metadata in-memory.
|
||||||
|
// Only a shallow copy is saved and fields with references
|
||||||
|
// cannot be modified without causing a race condition,
|
||||||
|
// so they should be replaced atomically and not appended to, etc.
|
||||||
|
// Data is not persisted to disk.
|
||||||
|
func (sys *BucketMetadataSys) Set(bucket string, meta BucketMetadata) {
|
||||||
|
if globalIsGateway {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if bucket != minioMetaBucket {
|
||||||
|
sys.Lock()
|
||||||
|
sys.metadataMap[bucket] = meta
|
||||||
|
sys.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update update bucket metadata for the specified config file.
|
||||||
|
// The configData data should not be modified after being sent here.
|
||||||
|
func (sys *BucketMetadataSys) Update(bucket string, configFile string, configData []byte) error {
|
||||||
|
objAPI := newObjectLayerWithoutSafeModeFn()
|
||||||
|
if objAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIsGateway && globalGatewayName != "nas" {
|
||||||
|
// This code is needed only for gateway implementations.
|
||||||
|
if configFile == bucketPolicyConfig {
|
||||||
|
config, err := policy.ParseConfig(bytes.NewReader(configData), bucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return objAPI.SetBucketPolicy(GlobalContext, bucket, config)
|
||||||
|
}
|
||||||
|
return NotImplemented{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bucket == minioMetaBucket {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, err := loadBucketMetadata(GlobalContext, objAPI, bucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch configFile {
|
||||||
|
case bucketPolicyConfig:
|
||||||
|
meta.PolicyConfigJSON = configData
|
||||||
|
case bucketNotificationConfig:
|
||||||
|
meta.NotificationConfigXML = configData
|
||||||
|
case bucketLifecycleConfig:
|
||||||
|
meta.LifecycleConfigXML = configData
|
||||||
|
case bucketSSEConfig:
|
||||||
|
meta.EncryptionConfigXML = configData
|
||||||
|
case bucketTaggingConfigFile:
|
||||||
|
meta.TaggingConfigXML = configData
|
||||||
|
case objectLockConfig:
|
||||||
|
meta.ObjectLockConfigXML = configData
|
||||||
|
case bucketQuotaConfigFile:
|
||||||
|
meta.QuotaConfigJSON = configData
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown bucket %s metadata update requested %s", bucket, configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := meta.Save(GlobalContext, objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Set(bucket, meta)
|
||||||
|
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get metadata for a bucket.
|
||||||
|
// If no metadata exists errConfigNotFound is returned and a new metadata is returned.
|
||||||
|
// Only a shallow copy is returned, so referenced data should not be modified,
|
||||||
|
// but can be replaced atomically.
|
||||||
|
func (sys *BucketMetadataSys) Get(bucket string) (BucketMetadata, error) {
|
||||||
|
if globalIsGateway || bucket == minioMetaBucket {
|
||||||
|
return newBucketMetadata(bucket), errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.RLock()
|
||||||
|
defer sys.RUnlock()
|
||||||
|
|
||||||
|
meta, ok := sys.metadataMap[bucket]
|
||||||
|
if !ok {
|
||||||
|
return newBucketMetadata(bucket), errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTaggingConfig returns configured tagging config
|
||||||
|
// The returned object may not be modified.
|
||||||
|
func (sys *BucketMetadataSys) GetTaggingConfig(bucket string) (*tags.Tags, error) {
|
||||||
|
meta, err := sys.GetConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errConfigNotFound) {
|
||||||
|
return nil, BucketTaggingNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if meta.taggingConfig == nil {
|
||||||
|
return nil, BucketTaggingNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
|
return meta.taggingConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectLockConfig returns configured object lock config
|
||||||
|
// The returned object may not be modified.
|
||||||
|
func (sys *BucketMetadataSys) GetObjectLockConfig(bucket string) (*objectlock.Config, error) {
|
||||||
|
meta, err := sys.GetConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errConfigNotFound) {
|
||||||
|
return nil, BucketObjectLockConfigNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if meta.objectLockConfig == nil {
|
||||||
|
return nil, BucketObjectLockConfigNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
|
return meta.objectLockConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLifecycleConfig returns configured lifecycle config
|
||||||
|
// The returned object may not be modified.
|
||||||
|
func (sys *BucketMetadataSys) GetLifecycleConfig(bucket string) (*lifecycle.Lifecycle, error) {
|
||||||
|
meta, err := sys.GetConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errConfigNotFound) {
|
||||||
|
return nil, BucketLifecycleNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if meta.lifecycleConfig == nil {
|
||||||
|
return nil, BucketLifecycleNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
|
return meta.lifecycleConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationConfig returns configured notification config
|
||||||
|
// The returned object may not be modified.
|
||||||
|
func (sys *BucketMetadataSys) GetNotificationConfig(bucket string) (*event.Config, error) {
|
||||||
|
meta, err := sys.GetConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return meta.notificationConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSSEConfig returns configured SSE config
|
||||||
|
// The returned object may not be modified.
|
||||||
|
func (sys *BucketMetadataSys) GetSSEConfig(bucket string) (*bucketsse.BucketSSEConfig, error) {
|
||||||
|
meta, err := sys.GetConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errConfigNotFound) {
|
||||||
|
return nil, BucketSSEConfigNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if meta.sseConfig == nil {
|
||||||
|
return nil, BucketSSEConfigNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
|
return meta.sseConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPolicyConfig returns configured bucket policy
|
||||||
|
// The returned object may not be modified.
|
||||||
|
func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, error) {
|
||||||
|
meta, err := sys.GetConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errConfigNotFound) {
|
||||||
|
return nil, BucketPolicyNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if meta.policyConfig == nil {
|
||||||
|
return nil, BucketPolicyNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
|
return meta.policyConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuotaConfig returns configured bucket quota
|
||||||
|
// The returned object may not be modified.
|
||||||
|
func (sys *BucketMetadataSys) GetQuotaConfig(bucket string) (*madmin.BucketQuota, error) {
|
||||||
|
meta, err := sys.GetConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return meta.quotaConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig returns a specific configuration from the bucket metadata.
|
||||||
|
// The returned object may not be modified.
|
||||||
|
func (sys *BucketMetadataSys) GetConfig(bucket string) (BucketMetadata, error) {
|
||||||
|
objAPI := newObjectLayerWithoutSafeModeFn()
|
||||||
|
if objAPI == nil {
|
||||||
|
return newBucketMetadata(bucket), errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIsGateway && globalGatewayName != "nas" {
|
||||||
|
return newBucketMetadata(bucket), NotImplemented{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bucket == minioMetaBucket {
|
||||||
|
return newBucketMetadata(bucket), errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.RLock()
|
||||||
|
meta, ok := sys.metadataMap[bucket]
|
||||||
|
sys.RUnlock()
|
||||||
|
if ok {
|
||||||
|
return meta, nil
|
||||||
|
}
|
||||||
|
meta, err := loadBucketMetadata(GlobalContext, objAPI, bucket)
|
||||||
|
if err != nil {
|
||||||
|
return meta, err
|
||||||
|
}
|
||||||
|
sys.Lock()
|
||||||
|
sys.metadataMap[bucket] = meta
|
||||||
|
sys.Unlock()
|
||||||
|
return meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init - initializes bucket metadata system for all buckets.
|
||||||
|
func (sys *BucketMetadataSys) Init(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||||
|
if objAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
// In gateway mode, we don't need to load the policies
|
||||||
|
// from the backend.
|
||||||
|
if globalIsGateway {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load PolicySys once during boot.
|
||||||
|
return sys.load(ctx, buckets, objAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads bucket metadata for all buckets into BucketMetadataSys.
|
||||||
|
func (sys *BucketMetadataSys) load(ctx context.Context, buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
for _, bucket := range buckets {
|
||||||
|
meta, err := loadBucketMetadata(ctx, objAPI, bucket.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sys.metadataMap[bucket.Name] = meta
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBucketMetadataSys - creates new policy system.
|
||||||
|
func NewBucketMetadataSys() *BucketMetadataSys {
|
||||||
|
return &BucketMetadataSys{
|
||||||
|
metadataMap: make(map[string]BucketMetadata),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v6/pkg/tags"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
bucketsse "github.com/minio/minio/pkg/bucket/encryption"
|
||||||
|
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||||
|
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
||||||
|
"github.com/minio/minio/pkg/bucket/policy"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
"github.com/minio/minio/pkg/madmin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
legacyBucketObjectLockEnabledConfigFile = "object-lock-enabled.json"
|
||||||
|
legacyBucketObjectLockEnabledConfig = `{"x-amz-bucket-object-lock-enabled":true}`
|
||||||
|
|
||||||
|
bucketMetadataFile = ".metadata.bin"
|
||||||
|
bucketMetadataFormat = 1
|
||||||
|
bucketMetadataVersion = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
enabledBucketObjectLockConfig = []byte(`<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><ObjectLockEnabled>Enabled</ObjectLockEnabled></ObjectLockConfiguration>`)
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate msgp -file $GOFILE
|
||||||
|
|
||||||
|
// BucketMetadata contains bucket metadata.
|
||||||
|
// When adding/removing fields, regenerate the marshal code using the go generate above.
|
||||||
|
// Only changing meaning of fields requires a version bump.
|
||||||
|
// bucketMetadataFormat refers to the format.
|
||||||
|
// bucketMetadataVersion can be used to track a rolling upgrade of a field.
|
||||||
|
type BucketMetadata struct {
|
||||||
|
Name string
|
||||||
|
Created time.Time
|
||||||
|
LockEnabled bool // legacy not used anymore.
|
||||||
|
PolicyConfigJSON []byte
|
||||||
|
NotificationConfigXML []byte
|
||||||
|
LifecycleConfigXML []byte
|
||||||
|
ObjectLockConfigXML []byte
|
||||||
|
EncryptionConfigXML []byte
|
||||||
|
TaggingConfigXML []byte
|
||||||
|
QuotaConfigJSON []byte
|
||||||
|
|
||||||
|
// Unexported fields. Must be updated atomically.
|
||||||
|
policyConfig *policy.Policy
|
||||||
|
notificationConfig *event.Config
|
||||||
|
lifecycleConfig *lifecycle.Lifecycle
|
||||||
|
objectLockConfig *objectlock.Config
|
||||||
|
sseConfig *bucketsse.BucketSSEConfig
|
||||||
|
taggingConfig *tags.Tags
|
||||||
|
quotaConfig *madmin.BucketQuota
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBucketMetadata creates BucketMetadata with the supplied name and Created to Now.
|
||||||
|
func newBucketMetadata(name string) BucketMetadata {
|
||||||
|
return BucketMetadata{
|
||||||
|
Name: name,
|
||||||
|
Created: UTCNow(),
|
||||||
|
notificationConfig: &event.Config{
|
||||||
|
XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/",
|
||||||
|
},
|
||||||
|
quotaConfig: &madmin.BucketQuota{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load - loads the metadata of bucket by name from ObjectLayer api.
|
||||||
|
// If an error is returned the returned metadata will be default initialized.
|
||||||
|
func (b *BucketMetadata) Load(ctx context.Context, api ObjectLayer, name string) error {
|
||||||
|
configFile := path.Join(bucketConfigPrefix, name, bucketMetadataFile)
|
||||||
|
data, err := readConfig(ctx, api, configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(data) <= 4 {
|
||||||
|
return fmt.Errorf("loadBucketMetadata: no data")
|
||||||
|
}
|
||||||
|
// Read header
|
||||||
|
switch binary.LittleEndian.Uint16(data[0:2]) {
|
||||||
|
case bucketMetadataFormat:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("loadBucketMetadata: unknown format: %d", binary.LittleEndian.Uint16(data[0:2]))
|
||||||
|
}
|
||||||
|
switch binary.LittleEndian.Uint16(data[2:4]) {
|
||||||
|
case bucketMetadataVersion:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("loadBucketMetadata: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK, parse data.
|
||||||
|
_, err = b.UnmarshalMsg(data[4:])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadBucketMetadata loads and migrates to bucket metadata.
|
||||||
|
func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string) (BucketMetadata, error) {
|
||||||
|
b := newBucketMetadata(bucket)
|
||||||
|
err := b.Load(ctx, objectAPI, bucket)
|
||||||
|
if err == nil {
|
||||||
|
return b, b.convertLegacyConfigs(ctx, objectAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != errConfigNotFound {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old bucket without bucket metadata. Hence we migrate existing settings.
|
||||||
|
return b, b.convertLegacyConfigs(ctx, objectAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAllConfigs will parse all configs and populate the private fields.
|
||||||
|
// The first error encountered is returned.
|
||||||
|
func (b *BucketMetadata) parseAllConfigs(ctx context.Context, objectAPI ObjectLayer) (err error) {
|
||||||
|
if len(b.PolicyConfigJSON) != 0 {
|
||||||
|
b.policyConfig, err = policy.ParseConfig(bytes.NewReader(b.PolicyConfigJSON), b.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.policyConfig = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.NotificationConfigXML) != 0 {
|
||||||
|
if err = xml.Unmarshal(b.NotificationConfigXML, b.notificationConfig); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.LifecycleConfigXML) != 0 {
|
||||||
|
b.lifecycleConfig, err = lifecycle.ParseLifecycleConfig(bytes.NewReader(b.LifecycleConfigXML))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.lifecycleConfig = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.EncryptionConfigXML) != 0 {
|
||||||
|
b.sseConfig, err = bucketsse.ParseBucketSSEConfig(bytes.NewReader(b.EncryptionConfigXML))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.sseConfig = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.TaggingConfigXML) != 0 {
|
||||||
|
b.taggingConfig, err = tags.ParseBucketXML(bytes.NewReader(b.TaggingConfigXML))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.taggingConfig = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.ObjectLockConfigXML) != 0 {
|
||||||
|
b.objectLockConfig, err = objectlock.ParseObjectLockConfig(bytes.NewReader(b.ObjectLockConfigXML))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.objectLockConfig = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.QuotaConfigJSON) != 0 {
|
||||||
|
b.quotaConfig, err = parseBucketQuota(b.Name, b.QuotaConfigJSON)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI ObjectLayer) error {
|
||||||
|
legacyConfigs := []string{
|
||||||
|
legacyBucketObjectLockEnabledConfigFile,
|
||||||
|
bucketPolicyConfig,
|
||||||
|
bucketNotificationConfig,
|
||||||
|
bucketLifecycleConfig,
|
||||||
|
bucketQuotaConfigFile,
|
||||||
|
bucketSSEConfig,
|
||||||
|
bucketTaggingConfigFile,
|
||||||
|
objectLockConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
configs := make(map[string][]byte)
|
||||||
|
|
||||||
|
// Handle migration from lockEnabled to newer format.
|
||||||
|
if b.LockEnabled {
|
||||||
|
configs[objectLockConfig] = enabledBucketObjectLockConfig
|
||||||
|
b.LockEnabled = false // legacy value unset it
|
||||||
|
// we are only interested in b.ObjectLockConfigXML or objectLockConfig value
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, legacyFile := range legacyConfigs {
|
||||||
|
configFile := path.Join(bucketConfigPrefix, b.Name, legacyFile)
|
||||||
|
|
||||||
|
configData, err := readConfig(ctx, objectAPI, configFile)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errConfigNotFound) {
|
||||||
|
// legacy file config not found, proceed to look for new metadata.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configs[legacyFile] = configData
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(configs) == 0 {
|
||||||
|
// nothing to update, return right away.
|
||||||
|
return b.parseAllConfigs(ctx, objectAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
for legacyFile, configData := range configs {
|
||||||
|
switch legacyFile {
|
||||||
|
case legacyBucketObjectLockEnabledConfigFile:
|
||||||
|
if string(configData) == legacyBucketObjectLockEnabledConfig {
|
||||||
|
b.ObjectLockConfigXML = enabledBucketObjectLockConfig
|
||||||
|
b.LockEnabled = false // legacy value unset it
|
||||||
|
// we are only interested in b.ObjectLockConfigXML
|
||||||
|
}
|
||||||
|
case bucketPolicyConfig:
|
||||||
|
b.PolicyConfigJSON = configData
|
||||||
|
case bucketNotificationConfig:
|
||||||
|
b.NotificationConfigXML = configData
|
||||||
|
case bucketLifecycleConfig:
|
||||||
|
b.LifecycleConfigXML = configData
|
||||||
|
case bucketSSEConfig:
|
||||||
|
b.EncryptionConfigXML = configData
|
||||||
|
case bucketTaggingConfigFile:
|
||||||
|
b.TaggingConfigXML = configData
|
||||||
|
case objectLockConfig:
|
||||||
|
b.ObjectLockConfigXML = configData
|
||||||
|
case bucketQuotaConfigFile:
|
||||||
|
b.QuotaConfigJSON = configData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Save(ctx, objectAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for legacyFile := range configs {
|
||||||
|
configFile := path.Join(bucketConfigPrefix, b.Name, legacyFile)
|
||||||
|
if err := deleteConfig(ctx, objectAPI, configFile); err != nil && !errors.Is(err, errConfigNotFound) {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save config to supplied ObjectLayer api.
|
||||||
|
func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error {
|
||||||
|
if err := b.parseAllConfigs(ctx, api); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, 4, b.Msgsize()+4)
|
||||||
|
|
||||||
|
// Initialize the header.
|
||||||
|
binary.LittleEndian.PutUint16(data[0:2], bucketMetadataFormat)
|
||||||
|
binary.LittleEndian.PutUint16(data[2:4], bucketMetadataVersion)
|
||||||
|
|
||||||
|
// Marshal the bucket metadata
|
||||||
|
data, err := b.MarshalMsg(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile := path.Join(bucketConfigPrefix, b.Name, bucketMetadataFile)
|
||||||
|
return saveConfig(ctx, api, configFile, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteBucketMetadata deletes bucket metadata
|
||||||
|
// If config does not exist no error is returned.
|
||||||
|
func deleteBucketMetadata(ctx context.Context, obj ObjectLayer, bucket string) error {
|
||||||
|
configFile := path.Join(bucketConfigPrefix, bucket, bucketMetadataFile)
|
||||||
|
if err := deleteConfig(ctx, obj, configFile); err != nil && err != errConfigNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tinylib/msgp/msgp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecodeMsg implements msgp.Decodable
|
||||||
|
func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||||
|
var field []byte
|
||||||
|
_ = field
|
||||||
|
var zb0001 uint32
|
||||||
|
zb0001, err = dc.ReadMapHeader()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for zb0001 > 0 {
|
||||||
|
zb0001--
|
||||||
|
field, err = dc.ReadMapKeyPtr()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch msgp.UnsafeString(field) {
|
||||||
|
case "Name":
|
||||||
|
z.Name, err = dc.ReadString()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Name")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "Created":
|
||||||
|
z.Created, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Created")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "LockEnabled":
|
||||||
|
z.LockEnabled, err = dc.ReadBool()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "LockEnabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "PolicyConfigJSON":
|
||||||
|
z.PolicyConfigJSON, err = dc.ReadBytes(z.PolicyConfigJSON)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "PolicyConfigJSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "NotificationConfigXML":
|
||||||
|
z.NotificationConfigXML, err = dc.ReadBytes(z.NotificationConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "NotificationConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "LifecycleConfigXML":
|
||||||
|
z.LifecycleConfigXML, err = dc.ReadBytes(z.LifecycleConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "LifecycleConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "ObjectLockConfigXML":
|
||||||
|
z.ObjectLockConfigXML, err = dc.ReadBytes(z.ObjectLockConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjectLockConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "EncryptionConfigXML":
|
||||||
|
z.EncryptionConfigXML, err = dc.ReadBytes(z.EncryptionConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "EncryptionConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "TaggingConfigXML":
|
||||||
|
z.TaggingConfigXML, err = dc.ReadBytes(z.TaggingConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "TaggingConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "QuotaConfigJSON":
|
||||||
|
z.QuotaConfigJSON, err = dc.ReadBytes(z.QuotaConfigJSON)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "QuotaConfigJSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = dc.Skip()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeMsg implements msgp.Encodable
|
||||||
|
func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
|
// map header, size 10
|
||||||
|
// write "Name"
|
||||||
|
err = en.Append(0x8a, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteString(z.Name)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Name")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "Created"
|
||||||
|
err = en.Append(0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.Created)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Created")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "LockEnabled"
|
||||||
|
err = en.Append(0xab, 0x4c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteBool(z.LockEnabled)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "LockEnabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "PolicyConfigJSON"
|
||||||
|
err = en.Append(0xb0, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteBytes(z.PolicyConfigJSON)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "PolicyConfigJSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "NotificationConfigXML"
|
||||||
|
err = en.Append(0xb5, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteBytes(z.NotificationConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "NotificationConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "LifecycleConfigXML"
|
||||||
|
err = en.Append(0xb2, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteBytes(z.LifecycleConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "LifecycleConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "ObjectLockConfigXML"
|
||||||
|
err = en.Append(0xb3, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteBytes(z.ObjectLockConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjectLockConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "EncryptionConfigXML"
|
||||||
|
err = en.Append(0xb3, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteBytes(z.EncryptionConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "EncryptionConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "TaggingConfigXML"
|
||||||
|
err = en.Append(0xb0, 0x54, 0x61, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteBytes(z.TaggingConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "TaggingConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "QuotaConfigJSON"
|
||||||
|
err = en.Append(0xaf, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteBytes(z.QuotaConfigJSON)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "QuotaConfigJSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalMsg implements msgp.Marshaler
|
||||||
|
func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
|
o = msgp.Require(b, z.Msgsize())
|
||||||
|
// map header, size 10
|
||||||
|
// string "Name"
|
||||||
|
o = append(o, 0x8a, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||||
|
o = msgp.AppendString(o, z.Name)
|
||||||
|
// string "Created"
|
||||||
|
o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
|
||||||
|
o = msgp.AppendTime(o, z.Created)
|
||||||
|
// string "LockEnabled"
|
||||||
|
o = append(o, 0xab, 0x4c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64)
|
||||||
|
o = msgp.AppendBool(o, z.LockEnabled)
|
||||||
|
// string "PolicyConfigJSON"
|
||||||
|
o = append(o, 0xb0, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e)
|
||||||
|
o = msgp.AppendBytes(o, z.PolicyConfigJSON)
|
||||||
|
// string "NotificationConfigXML"
|
||||||
|
o = append(o, 0xb5, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
|
||||||
|
o = msgp.AppendBytes(o, z.NotificationConfigXML)
|
||||||
|
// string "LifecycleConfigXML"
|
||||||
|
o = append(o, 0xb2, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
|
||||||
|
o = msgp.AppendBytes(o, z.LifecycleConfigXML)
|
||||||
|
// string "ObjectLockConfigXML"
|
||||||
|
o = append(o, 0xb3, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
|
||||||
|
o = msgp.AppendBytes(o, z.ObjectLockConfigXML)
|
||||||
|
// string "EncryptionConfigXML"
|
||||||
|
o = append(o, 0xb3, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
|
||||||
|
o = msgp.AppendBytes(o, z.EncryptionConfigXML)
|
||||||
|
// string "TaggingConfigXML"
|
||||||
|
o = append(o, 0xb0, 0x54, 0x61, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x58, 0x4d, 0x4c)
|
||||||
|
o = msgp.AppendBytes(o, z.TaggingConfigXML)
|
||||||
|
// string "QuotaConfigJSON"
|
||||||
|
o = append(o, 0xaf, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4a, 0x53, 0x4f, 0x4e)
|
||||||
|
o = msgp.AppendBytes(o, z.QuotaConfigJSON)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalMsg implements msgp.Unmarshaler
|
||||||
|
func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||||
|
var field []byte
|
||||||
|
_ = field
|
||||||
|
var zb0001 uint32
|
||||||
|
zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for zb0001 > 0 {
|
||||||
|
zb0001--
|
||||||
|
field, bts, err = msgp.ReadMapKeyZC(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch msgp.UnsafeString(field) {
|
||||||
|
case "Name":
|
||||||
|
z.Name, bts, err = msgp.ReadStringBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Name")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "Created":
|
||||||
|
z.Created, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Created")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "LockEnabled":
|
||||||
|
z.LockEnabled, bts, err = msgp.ReadBoolBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "LockEnabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "PolicyConfigJSON":
|
||||||
|
z.PolicyConfigJSON, bts, err = msgp.ReadBytesBytes(bts, z.PolicyConfigJSON)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "PolicyConfigJSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "NotificationConfigXML":
|
||||||
|
z.NotificationConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.NotificationConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "NotificationConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "LifecycleConfigXML":
|
||||||
|
z.LifecycleConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.LifecycleConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "LifecycleConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "ObjectLockConfigXML":
|
||||||
|
z.ObjectLockConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.ObjectLockConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjectLockConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "EncryptionConfigXML":
|
||||||
|
z.EncryptionConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.EncryptionConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "EncryptionConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "TaggingConfigXML":
|
||||||
|
z.TaggingConfigXML, bts, err = msgp.ReadBytesBytes(bts, z.TaggingConfigXML)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "TaggingConfigXML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "QuotaConfigJSON":
|
||||||
|
z.QuotaConfigJSON, bts, err = msgp.ReadBytesBytes(bts, z.QuotaConfigJSON)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "QuotaConfigJSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
bts, err = msgp.Skip(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o = bts
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||||
|
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.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tinylib/msgp/msgp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarshalUnmarshalBucketMetadata(t *testing.T) {
|
||||||
|
v := BucketMetadata{}
|
||||||
|
bts, err := v.MarshalMsg(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
left, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
|
||||||
|
left, err = msgp.Skip(bts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(left) > 0 {
|
||||||
|
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMarshalMsgBucketMetadata(b *testing.B) {
|
||||||
|
v := BucketMetadata{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgBucketMetadata(b *testing.B) {
|
||||||
|
v := BucketMetadata{}
|
||||||
|
bts := make([]byte, 0, v.Msgsize())
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bts, _ = v.MarshalMsg(bts[0:0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnmarshalBucketMetadata(b *testing.B) {
|
||||||
|
v := BucketMetadata{}
|
||||||
|
bts, _ := v.MarshalMsg(nil)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(bts)))
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := v.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecodeBucketMetadata(t *testing.T) {
|
||||||
|
v := BucketMetadata{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodeBucketMetadata Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := BucketMetadata{}
|
||||||
|
err := msgp.Decode(&buf, &vn)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
err = msgp.NewReader(&buf).Skip()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEncodeBucketMetadata(b *testing.B) {
|
||||||
|
v := BucketMetadata{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
en := msgp.NewWriter(msgp.Nowhere)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.EncodeMsg(en)
|
||||||
|
}
|
||||||
|
en.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecodeBucketMetadata(b *testing.B) {
|
||||||
|
v := BucketMetadata{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||||
|
dc := msgp.NewReader(rd)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := v.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,11 +19,8 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -39,8 +36,6 @@ const (
|
|||||||
bucketNotificationConfig = "notification.xml"
|
bucketNotificationConfig = "notification.xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errNoSuchNotifications = errors.New("The specified bucket does not have bucket notifications")
|
|
||||||
|
|
||||||
// GetBucketNotificationHandler - This HTTP handler returns event notification configuration
|
// GetBucketNotificationHandler - This HTTP handler returns event notification configuration
|
||||||
// as per http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
|
// as per http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
|
||||||
// It returns empty configuration if its not set.
|
// It returns empty configuration if its not set.
|
||||||
@@ -74,41 +69,15 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct path to notification.xml for the given bucket.
|
config, err := globalBucketMetadataSys.GetNotificationConfig(bucketName)
|
||||||
configFile := path.Join(bucketConfigPrefix, bucketName, bucketNotificationConfig)
|
|
||||||
|
|
||||||
var config = event.Config{}
|
|
||||||
configData, err := readConfig(ctx, objAPI, configFile)
|
|
||||||
if err != nil {
|
|
||||||
if err != errConfigNotFound {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
config.SetRegion(globalServerRegion)
|
|
||||||
config.XMLNS = "http://s3.amazonaws.com/doc/2006-03-01/"
|
|
||||||
notificationBytes, err := xml.Marshal(config)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSuccessResponseXML(w, notificationBytes)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config.SetRegion(globalServerRegion)
|
config.SetRegion(globalServerRegion)
|
||||||
|
|
||||||
if err = xml.Unmarshal(configData, &config); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = config.Validate(globalServerRegion, globalNotificationSys.targetList); err != nil {
|
if err = config.Validate(globalServerRegion, globalNotificationSys.targetList); err != nil {
|
||||||
arnErr, ok := err.(*event.ErrARNNotFound)
|
arnErr, ok := err.(*event.ErrARNNotFound)
|
||||||
if !ok {
|
if ok {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for i, queue := range config.QueueList {
|
for i, queue := range config.QueueList {
|
||||||
// Remove ARN not found queues, because we previously allowed
|
// Remove ARN not found queues, because we previously allowed
|
||||||
// adding unexpected entries into the config.
|
// adding unexpected entries into the config.
|
||||||
@@ -116,7 +85,7 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
// With newer config disallowing changing / turning off
|
// With newer config disallowing changing / turning off
|
||||||
// notification targets without removing ARN in notification
|
// notification targets without removing ARN in notification
|
||||||
// configuration we won't see this problem anymore.
|
// configuration we won't see this problem anymore.
|
||||||
if reflect.DeepEqual(queue.ARN, arnErr.ARN) {
|
if reflect.DeepEqual(queue.ARN, arnErr.ARN) && i < len(config.QueueList) {
|
||||||
config.QueueList = append(config.QueueList[:i],
|
config.QueueList = append(config.QueueList[:i],
|
||||||
config.QueueList[i+1:]...)
|
config.QueueList[i+1:]...)
|
||||||
}
|
}
|
||||||
@@ -125,20 +94,19 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
// never reach a stage where we will have stale
|
// never reach a stage where we will have stale
|
||||||
// notification configs.
|
// notification configs.
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If xml namespace is empty, set a default value before returning.
|
configData, err := xml.Marshal(config)
|
||||||
if config.XMLNS == "" {
|
|
||||||
config.XMLNS = "http://s3.amazonaws.com/doc/2006-03-01/"
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationBytes, err := xml.Marshal(config)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSuccessResponseXML(w, notificationBytes)
|
writeSuccessResponseXML(w, configData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutBucketNotificationHandler - This HTTP handler stores given notification configuration as per
|
// PutBucketNotificationHandler - This HTTP handler stores given notification configuration as per
|
||||||
@@ -179,8 +147,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lreader := io.LimitReader(r.Body, r.ContentLength)
|
config, err := event.ParseConfig(io.LimitReader(r.Body, r.ContentLength), globalServerRegion, globalNotificationSys.targetList)
|
||||||
config, err := event.ParseConfig(lreader, globalServerRegion, globalNotificationSys.targetList)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||||
if event.IsEventError(err) {
|
if event.IsEventError(err) {
|
||||||
@@ -190,14 +157,19 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = saveNotificationConfig(ctx, objectAPI, bucketName, config); err != nil {
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = globalBucketMetadataSys.Update(bucketName, bucketNotificationConfig, configData); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rulesMap := config.ToRulesMap()
|
rulesMap := config.ToRulesMap()
|
||||||
globalNotificationSys.AddRulesMap(bucketName, rulesMap)
|
globalNotificationSys.AddRulesMap(bucketName, rulesMap)
|
||||||
globalNotificationSys.PutBucketNotification(ctx, bucketName, rulesMap)
|
|
||||||
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
@@ -282,16 +254,13 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
|
|
||||||
w.Header().Set(xhttp.ContentType, "text/event-stream")
|
w.Header().Set(xhttp.ContentType, "text/event-stream")
|
||||||
|
|
||||||
doneCh := make(chan struct{})
|
|
||||||
defer close(doneCh)
|
|
||||||
|
|
||||||
// Listen Publisher and peer-listen-client uses nonblocking send and hence does not wait for slow receivers.
|
// Listen Publisher and peer-listen-client uses nonblocking send and hence does not wait for slow receivers.
|
||||||
// Use buffered channel to take care of burst sends or slow w.Write()
|
// Use buffered channel to take care of burst sends or slow w.Write()
|
||||||
listenCh := make(chan interface{}, 4000)
|
listenCh := make(chan interface{}, 4000)
|
||||||
|
|
||||||
peers := getRestClients(globalEndpoints)
|
peers := newPeerRestClients(globalEndpoints)
|
||||||
|
|
||||||
globalHTTPListen.Subscribe(listenCh, doneCh, func(evI interface{}) bool {
|
globalHTTPListen.Subscribe(listenCh, ctx.Done(), func(evI interface{}) bool {
|
||||||
ev, ok := evI.(event.Event)
|
ev, ok := evI.(event.Event)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
@@ -299,18 +268,14 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
if ev.S3.Bucket.Name != values.Get(peerRESTListenBucket) {
|
if ev.S3.Bucket.Name != values.Get(peerRESTListenBucket) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
objectName, uerr := url.QueryUnescape(ev.S3.Object.Key)
|
return rulesMap.MatchSimple(ev.EventName, ev.S3.Object.Key)
|
||||||
if uerr != nil {
|
|
||||||
objectName = ev.S3.Object.Key
|
|
||||||
}
|
|
||||||
return len(rulesMap.Match(ev.EventName, objectName).ToSlice()) != 0
|
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
if peer == nil {
|
if peer == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
peer.Listen(listenCh, doneCh, values)
|
peer.Listen(listenCh, ctx.Done(), values)
|
||||||
}
|
}
|
||||||
|
|
||||||
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
|
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
|
||||||
@@ -336,7 +301,7 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
case <-GlobalServiceDoneCh:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2019-2020 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/auth"
|
||||||
|
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
||||||
|
"github.com/minio/minio/pkg/bucket/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BucketObjectLockSys - map of bucket and retention configuration.
|
||||||
|
type BucketObjectLockSys struct{}
|
||||||
|
|
||||||
|
// Get - Get retention configuration.
|
||||||
|
func (sys *BucketObjectLockSys) Get(bucketName string) (r objectlock.Retention, err error) {
|
||||||
|
if globalIsGateway {
|
||||||
|
objAPI := newObjectLayerWithoutSafeModeFn()
|
||||||
|
if objAPI == nil {
|
||||||
|
return r, errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := globalBucketMetadataSys.GetObjectLockConfig(bucketName)
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(BucketObjectLockConfigNotFound); ok {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
|
||||||
|
}
|
||||||
|
return config.ToRetention(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar to enforceRetentionBypassForDelete but for WebUI
|
||||||
|
func enforceRetentionBypassForDeleteWeb(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, govBypassPerms bool) APIErrorCode {
|
||||||
|
opts, err := getOpts(ctx, r, bucket, object)
|
||||||
|
if err != nil {
|
||||||
|
return toAPIErrorCode(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oi, err := getObjectInfoFn(ctx, bucket, object, opts)
|
||||||
|
if err != nil {
|
||||||
|
return toAPIErrorCode(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lhold := objectlock.GetObjectLegalHoldMeta(oi.UserDefined)
|
||||||
|
if lhold.Status.Valid() && lhold.Status == objectlock.LegalHoldOn {
|
||||||
|
return ErrObjectLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
|
||||||
|
if ret.Mode.Valid() {
|
||||||
|
switch ret.Mode {
|
||||||
|
case objectlock.RetCompliance:
|
||||||
|
// In compliance mode, a protected object version can't be overwritten
|
||||||
|
// or deleted by any user, including the root user in your AWS account.
|
||||||
|
// When an object is locked in compliance mode, its retention mode can't
|
||||||
|
// be changed, and its retention period can't be shortened. Compliance mode
|
||||||
|
// ensures that an object version can't be overwritten or deleted for the
|
||||||
|
// duration of the retention period.
|
||||||
|
t, err := objectlock.UTCNowNTP()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return ErrObjectLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ret.RetainUntilDate.Before(t) {
|
||||||
|
return ErrObjectLocked
|
||||||
|
}
|
||||||
|
return ErrNone
|
||||||
|
case objectlock.RetGovernance:
|
||||||
|
// In governance mode, users can't overwrite or delete an object
|
||||||
|
// version or alter its lock settings unless they have special
|
||||||
|
// permissions. With governance mode, you protect objects against
|
||||||
|
// being deleted by most users, but you can still grant some users
|
||||||
|
// permission to alter the retention settings or delete the object
|
||||||
|
// if necessary. You can also use governance mode to test retention-period
|
||||||
|
// settings before creating a compliance-mode retention period.
|
||||||
|
// To override or remove governance-mode retention settings, a
|
||||||
|
// user must have the s3:BypassGovernanceRetention permission
|
||||||
|
// and must explicitly include x-amz-bypass-governance-retention:true
|
||||||
|
// as a request header with any request that requires overriding
|
||||||
|
// governance mode.
|
||||||
|
byPassSet := govBypassPerms && objectlock.IsObjectLockGovernanceBypassSet(r.Header)
|
||||||
|
if !byPassSet {
|
||||||
|
t, err := objectlock.UTCNowNTP()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return ErrObjectLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ret.RetainUntilDate.Before(t) {
|
||||||
|
return ErrObjectLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
if !govBypassPerms {
|
||||||
|
return ErrObjectLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// enforceRetentionForDeletion checks if it is appropriate to remove an
|
||||||
|
// object according to locking configuration when this is lifecycle/ bucket quota asking.
|
||||||
|
func enforceRetentionForDeletion(ctx context.Context, objInfo ObjectInfo) (locked bool) {
|
||||||
|
lhold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
|
||||||
|
if lhold.Status.Valid() && lhold.Status == objectlock.LegalHoldOn {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
|
||||||
|
if ret.Mode.Valid() && (ret.Mode == objectlock.RetCompliance || ret.Mode == objectlock.RetGovernance) {
|
||||||
|
t, err := objectlock.UTCNowNTP()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ret.RetainUntilDate.After(t) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// enforceRetentionBypassForDelete enforces whether an existing object under governance can be deleted
|
||||||
|
// with governance bypass headers set in the request.
|
||||||
|
// Objects under site wide WORM can never be overwritten.
|
||||||
|
// For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR
|
||||||
|
// governance bypass headers are set and user has governance bypass permissions.
|
||||||
|
// Objects in "Compliance" mode can be overwritten only if retention date is past.
|
||||||
|
func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn) APIErrorCode {
|
||||||
|
opts, err := getOpts(ctx, r, bucket, object)
|
||||||
|
if err != nil {
|
||||||
|
return toAPIErrorCode(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oi, err := getObjectInfoFn(ctx, bucket, object, opts)
|
||||||
|
if err != nil {
|
||||||
|
return toAPIErrorCode(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lhold := objectlock.GetObjectLegalHoldMeta(oi.UserDefined)
|
||||||
|
if lhold.Status.Valid() && lhold.Status == objectlock.LegalHoldOn {
|
||||||
|
return ErrObjectLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
|
||||||
|
if ret.Mode.Valid() {
|
||||||
|
switch ret.Mode {
|
||||||
|
case objectlock.RetCompliance:
|
||||||
|
// In compliance mode, a protected object version can't be overwritten
|
||||||
|
// or deleted by any user, including the root user in your AWS account.
|
||||||
|
// When an object is locked in compliance mode, its retention mode can't
|
||||||
|
// be changed, and its retention period can't be shortened. Compliance mode
|
||||||
|
// ensures that an object version can't be overwritten or deleted for the
|
||||||
|
// duration of the retention period.
|
||||||
|
t, err := objectlock.UTCNowNTP()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return ErrObjectLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ret.RetainUntilDate.Before(t) {
|
||||||
|
return ErrObjectLocked
|
||||||
|
}
|
||||||
|
return ErrNone
|
||||||
|
case objectlock.RetGovernance:
|
||||||
|
// In governance mode, users can't overwrite or delete an object
|
||||||
|
// version or alter its lock settings unless they have special
|
||||||
|
// permissions. With governance mode, you protect objects against
|
||||||
|
// being deleted by most users, but you can still grant some users
|
||||||
|
// permission to alter the retention settings or delete the object
|
||||||
|
// if necessary. You can also use governance mode to test retention-period
|
||||||
|
// settings before creating a compliance-mode retention period.
|
||||||
|
// To override or remove governance-mode retention settings, a
|
||||||
|
// user must have the s3:BypassGovernanceRetention permission
|
||||||
|
// and must explicitly include x-amz-bypass-governance-retention:true
|
||||||
|
// as a request header with any request that requires overriding
|
||||||
|
// governance mode.
|
||||||
|
//
|
||||||
|
byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
|
||||||
|
if !byPassSet {
|
||||||
|
t, err := objectlock.UTCNowNTP()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return ErrObjectLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ret.RetainUntilDate.Before(t) {
|
||||||
|
return ErrObjectLocked
|
||||||
|
}
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
|
||||||
|
// If you try to delete objects protected by governance mode and have s3:BypassGovernanceRetention
|
||||||
|
// or s3:GetBucketObjectLockConfiguration permissions, the operation will succeed.
|
||||||
|
govBypassPerms1 := checkRequestAuthType(ctx, r, policy.BypassGovernanceRetentionAction, bucket, object)
|
||||||
|
govBypassPerms2 := checkRequestAuthType(ctx, r, policy.GetBucketObjectLockConfigurationAction, bucket, object)
|
||||||
|
if govBypassPerms1 != ErrNone && govBypassPerms2 != ErrNone {
|
||||||
|
return ErrAccessDenied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// enforceRetentionBypassForPut enforces whether an existing object under governance can be overwritten
|
||||||
|
// with governance bypass headers set in the request.
|
||||||
|
// Objects under site wide WORM cannot be overwritten.
|
||||||
|
// For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR
|
||||||
|
// governance bypass headers are set and user has governance bypass permissions.
|
||||||
|
// Objects in compliance mode can be overwritten only if retention date is being extended. No mode change is permitted.
|
||||||
|
func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool, claims map[string]interface{}) (ObjectInfo, APIErrorCode) {
|
||||||
|
byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
|
||||||
|
opts, err := getOpts(ctx, r, bucket, object)
|
||||||
|
if err != nil {
|
||||||
|
return ObjectInfo{}, toAPIErrorCode(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oi, err := getObjectInfoFn(ctx, bucket, object, opts)
|
||||||
|
if err != nil {
|
||||||
|
return oi, toAPIErrorCode(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := objectlock.UTCNowNTP()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return oi, ErrObjectLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass in relative days from current time, to additionally to verify "object-lock-remaining-retention-days" policy if any.
|
||||||
|
days := int(math.Ceil(math.Abs(objRetention.RetainUntilDate.Sub(t).Hours()) / 24))
|
||||||
|
|
||||||
|
ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
|
||||||
|
if ret.Mode.Valid() {
|
||||||
|
// Retention has expired you may change whatever you like.
|
||||||
|
if ret.RetainUntilDate.Before(t) {
|
||||||
|
perm := isPutRetentionAllowed(bucket, object,
|
||||||
|
days, objRetention.RetainUntilDate.Time,
|
||||||
|
objRetention.Mode, byPassSet, r, cred,
|
||||||
|
owner, claims)
|
||||||
|
return oi, perm
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ret.Mode {
|
||||||
|
case objectlock.RetGovernance:
|
||||||
|
govPerm := isPutRetentionAllowed(bucket, object, days,
|
||||||
|
objRetention.RetainUntilDate.Time, objRetention.Mode,
|
||||||
|
byPassSet, r, cred, owner, claims)
|
||||||
|
// Governance mode retention period cannot be shortened, if x-amz-bypass-governance is not set.
|
||||||
|
if !byPassSet {
|
||||||
|
if objRetention.Mode != objectlock.RetGovernance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
|
||||||
|
return oi, ErrObjectLocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return oi, govPerm
|
||||||
|
case objectlock.RetCompliance:
|
||||||
|
// Compliance retention mode cannot be changed or shortened.
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
|
||||||
|
if objRetention.Mode != objectlock.RetCompliance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
|
||||||
|
return oi, ErrObjectLocked
|
||||||
|
}
|
||||||
|
compliancePerm := isPutRetentionAllowed(bucket, object,
|
||||||
|
days, objRetention.RetainUntilDate.Time, objRetention.Mode,
|
||||||
|
false, r, cred, owner, claims)
|
||||||
|
return oi, compliancePerm
|
||||||
|
}
|
||||||
|
return oi, ErrNone
|
||||||
|
} // No pre-existing retention metadata present.
|
||||||
|
|
||||||
|
perm := isPutRetentionAllowed(bucket, object,
|
||||||
|
days, objRetention.RetainUntilDate.Time,
|
||||||
|
objRetention.Mode, byPassSet, r, cred, owner, claims)
|
||||||
|
return oi, perm
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPutObjectLockAllowed enforces object retention policy and legal hold policy
|
||||||
|
// for requests with WORM headers
|
||||||
|
// See https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-managing.html for the spec.
|
||||||
|
// For non-existing objects with object retention headers set, this method returns ErrNone if bucket has
|
||||||
|
// locking enabled and user has requisite permissions (s3:PutObjectRetention)
|
||||||
|
// If object exists on object store and site wide WORM enabled - this method
|
||||||
|
// returns an error. For objects in "Governance" mode, overwrite is allowed if the retention date has expired.
|
||||||
|
// For objects in "Compliance" mode, retention date cannot be shortened, and mode cannot be altered.
|
||||||
|
// For objects with legal hold header set, the s3:PutObjectLegalHold permission is expected to be set
|
||||||
|
// Both legal hold and retention can be applied independently on an object
|
||||||
|
func checkPutObjectLockAllowed(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, retentionPermErr, legalHoldPermErr APIErrorCode) (objectlock.RetMode, objectlock.RetentionDate, objectlock.ObjectLegalHold, APIErrorCode) {
|
||||||
|
var mode objectlock.RetMode
|
||||||
|
var retainDate objectlock.RetentionDate
|
||||||
|
var legalHold objectlock.ObjectLegalHold
|
||||||
|
|
||||||
|
retentionRequested := objectlock.IsObjectLockRetentionRequested(r.Header)
|
||||||
|
legalHoldRequested := objectlock.IsObjectLockLegalHoldRequested(r.Header)
|
||||||
|
|
||||||
|
retentionCfg, err := globalBucketObjectLockSys.Get(bucket)
|
||||||
|
if err != nil {
|
||||||
|
return mode, retainDate, legalHold, ErrInvalidBucketObjectLockConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
if !retentionCfg.LockEnabled {
|
||||||
|
if legalHoldRequested || retentionRequested {
|
||||||
|
return mode, retainDate, legalHold, ErrInvalidBucketObjectLockConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this not a WORM enabled bucket, we should return right here.
|
||||||
|
return mode, retainDate, legalHold, ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
var objExists bool
|
||||||
|
opts, err := getOpts(ctx, r, bucket, object)
|
||||||
|
if err != nil {
|
||||||
|
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := objectlock.UTCNowNTP()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return mode, retainDate, legalHold, ErrObjectLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
if objInfo, err := getObjectInfoFn(ctx, bucket, object, opts); err == nil {
|
||||||
|
objExists = true
|
||||||
|
r := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
|
||||||
|
if r.Mode == objectlock.RetCompliance && r.RetainUntilDate.After(t) {
|
||||||
|
return mode, retainDate, legalHold, ErrObjectLocked
|
||||||
|
}
|
||||||
|
mode = r.Mode
|
||||||
|
retainDate = r.RetainUntilDate
|
||||||
|
legalHold = objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
|
||||||
|
// Disallow overwriting an object on legal hold
|
||||||
|
if legalHold.Status == objectlock.LegalHoldOn {
|
||||||
|
return mode, retainDate, legalHold, ErrObjectLocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if legalHoldRequested {
|
||||||
|
var lerr error
|
||||||
|
if legalHold, lerr = objectlock.ParseObjectLockLegalHoldHeaders(r.Header); lerr != nil {
|
||||||
|
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if retentionRequested {
|
||||||
|
legalHold, err := objectlock.ParseObjectLockLegalHoldHeaders(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
||||||
|
}
|
||||||
|
rMode, rDate, err := objectlock.ParseObjectLockRetentionHeaders(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
||||||
|
}
|
||||||
|
if objExists && retainDate.After(t) {
|
||||||
|
return mode, retainDate, legalHold, ErrObjectLocked
|
||||||
|
}
|
||||||
|
if retentionPermErr != ErrNone {
|
||||||
|
return mode, retainDate, legalHold, retentionPermErr
|
||||||
|
}
|
||||||
|
return rMode, rDate, legalHold, ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
if !retentionRequested && retentionCfg.Validity > 0 {
|
||||||
|
if retentionPermErr != ErrNone {
|
||||||
|
return mode, retainDate, legalHold, retentionPermErr
|
||||||
|
}
|
||||||
|
t, err := objectlock.UTCNowNTP()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return mode, retainDate, legalHold, ErrObjectLocked
|
||||||
|
}
|
||||||
|
// AWS S3 just creates a new version of object when an object is being overwritten.
|
||||||
|
if objExists && retainDate.After(t) {
|
||||||
|
return mode, retainDate, legalHold, ErrObjectLocked
|
||||||
|
}
|
||||||
|
if !legalHoldRequested {
|
||||||
|
// inherit retention from bucket configuration
|
||||||
|
return retentionCfg.Mode, objectlock.RetentionDate{Time: t.Add(retentionCfg.Validity)}, legalHold, ErrNone
|
||||||
|
}
|
||||||
|
return "", objectlock.RetentionDate{}, legalHold, ErrNone
|
||||||
|
}
|
||||||
|
return mode, retainDate, legalHold, ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBucketObjectLockSys returns initialized BucketObjectLockSys
|
||||||
|
func NewBucketObjectLockSys() *BucketObjectLockSys {
|
||||||
|
return &BucketObjectLockSys{}
|
||||||
|
}
|
||||||
@@ -87,13 +87,16 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = objAPI.SetBucketPolicy(ctx, bucket, bucketPolicy); err != nil {
|
configData, err := json.Marshal(bucketPolicy)
|
||||||
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
globalPolicySys.Set(bucket, *bucketPolicy)
|
if err = globalBucketMetadataSys.Update(bucket, bucketPolicyConfig, configData); err != nil {
|
||||||
globalNotificationSys.SetBucketPolicy(ctx, bucket, bucketPolicy)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
@@ -125,14 +128,11 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := objAPI.DeleteBucketPolicy(ctx, bucket); err != nil {
|
if err := globalBucketMetadataSys.Update(bucket, bucketPolicyConfig, nil); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
globalPolicySys.Remove(bucket)
|
|
||||||
globalNotificationSys.RemoveBucketPolicy(ctx, bucket)
|
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
}
|
}
|
||||||
@@ -164,18 +164,18 @@ func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *ht
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read bucket access policy.
|
// Read bucket access policy.
|
||||||
bucketPolicy, err := objAPI.GetBucketPolicy(ctx, bucket)
|
config, err := globalBucketMetadataSys.GetPolicyConfig(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
policyData, err := json.Marshal(bucketPolicy)
|
configData, err := json.Marshal(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to client.
|
// Write to client.
|
||||||
w.Write(policyData)
|
writeSuccessResponseJSON(w, configData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -103,7 +102,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T) {
|
||||||
|
|
||||||
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
||||||
if err := obj.MakeBucketWithLocation(context.Background(), bucketName1, ""); err != nil {
|
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName1, "", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2018 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
miniogopolicy "github.com/minio/minio-go/v6/pkg/policy"
|
||||||
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/bucket/policy"
|
||||||
|
"github.com/minio/minio/pkg/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PolicySys - policy subsystem.
|
||||||
|
type PolicySys struct{}
|
||||||
|
|
||||||
|
// Get returns stored bucket policy
|
||||||
|
func (sys *PolicySys) Get(bucket string) (*policy.Policy, error) {
|
||||||
|
if globalIsGateway {
|
||||||
|
objAPI := newObjectLayerFn()
|
||||||
|
if objAPI == nil {
|
||||||
|
return nil, errServerNotInitialized
|
||||||
|
}
|
||||||
|
return objAPI.GetBucketPolicy(GlobalContext, bucket)
|
||||||
|
}
|
||||||
|
return globalBucketMetadataSys.GetPolicyConfig(bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
||||||
|
func (sys *PolicySys) IsAllowed(args policy.Args) bool {
|
||||||
|
p, err := sys.Get(args.BucketName)
|
||||||
|
if err == nil {
|
||||||
|
return p.IsAllowed(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log unhandled errors.
|
||||||
|
if _, ok := err.(BucketPolicyNotFound); !ok {
|
||||||
|
logger.LogIf(GlobalContext, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// As policy is not available for given bucket name, returns IsOwner i.e.
|
||||||
|
// operation is allowed only for owner.
|
||||||
|
return args.IsOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPolicySys - creates new policy system.
|
||||||
|
func NewPolicySys() *PolicySys {
|
||||||
|
return &PolicySys{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConditionValues(r *http.Request, lc string, username string, claims map[string]interface{}) map[string][]string {
|
||||||
|
currTime := UTCNow()
|
||||||
|
|
||||||
|
principalType := "Anonymous"
|
||||||
|
if username != "" {
|
||||||
|
principalType = "User"
|
||||||
|
}
|
||||||
|
|
||||||
|
args := map[string][]string{
|
||||||
|
"CurrentTime": {currTime.Format(time.RFC3339)},
|
||||||
|
"EpochTime": {strconv.FormatInt(currTime.Unix(), 10)},
|
||||||
|
"SecureTransport": {strconv.FormatBool(r.TLS != nil)},
|
||||||
|
"SourceIp": {handlers.GetSourceIP(r)},
|
||||||
|
"UserAgent": {r.UserAgent()},
|
||||||
|
"Referer": {r.Referer()},
|
||||||
|
"principaltype": {principalType},
|
||||||
|
"userid": {username},
|
||||||
|
"username": {username},
|
||||||
|
}
|
||||||
|
|
||||||
|
if lc != "" {
|
||||||
|
args["LocationConstraint"] = []string{lc}
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneHeader := r.Header.Clone()
|
||||||
|
|
||||||
|
for _, objLock := range []string{
|
||||||
|
xhttp.AmzObjectLockMode,
|
||||||
|
xhttp.AmzObjectLockLegalHold,
|
||||||
|
xhttp.AmzObjectLockRetainUntilDate,
|
||||||
|
} {
|
||||||
|
if values, ok := cloneHeader[objLock]; ok {
|
||||||
|
args[strings.TrimPrefix(objLock, "X-Amz-")] = values
|
||||||
|
}
|
||||||
|
cloneHeader.Del(objLock)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, values := range cloneHeader {
|
||||||
|
if existingValues, found := args[key]; found {
|
||||||
|
args[key] = append(existingValues, values...)
|
||||||
|
} else {
|
||||||
|
args[key] = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cloneURLValues = url.Values{}
|
||||||
|
for k, v := range r.URL.Query() {
|
||||||
|
cloneURLValues[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, objLock := range []string{
|
||||||
|
xhttp.AmzObjectLockMode,
|
||||||
|
xhttp.AmzObjectLockLegalHold,
|
||||||
|
xhttp.AmzObjectLockRetainUntilDate,
|
||||||
|
} {
|
||||||
|
if values, ok := cloneURLValues[objLock]; ok {
|
||||||
|
args[strings.TrimPrefix(objLock, "X-Amz-")] = values
|
||||||
|
}
|
||||||
|
cloneURLValues.Del(objLock)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, values := range cloneURLValues {
|
||||||
|
if existingValues, found := args[key]; found {
|
||||||
|
args[key] = append(existingValues, values...)
|
||||||
|
} else {
|
||||||
|
args[key] = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT specific values
|
||||||
|
for k, v := range claims {
|
||||||
|
vStr, ok := v.(string)
|
||||||
|
if ok {
|
||||||
|
args[k] = []string{vStr}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolicyToBucketAccessPolicy - converts policy.Policy to minio-go/policy.BucketAccessPolicy.
|
||||||
|
func PolicyToBucketAccessPolicy(bucketPolicy *policy.Policy) (*miniogopolicy.BucketAccessPolicy, error) {
|
||||||
|
// Return empty BucketAccessPolicy for empty bucket policy.
|
||||||
|
if bucketPolicy == nil {
|
||||||
|
return &miniogopolicy.BucketAccessPolicy{Version: policy.DefaultVersion}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(bucketPolicy)
|
||||||
|
if err != nil {
|
||||||
|
// This should not happen because bucketPolicy is valid to convert to JSON data.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var policyInfo miniogopolicy.BucketAccessPolicy
|
||||||
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
if err = json.Unmarshal(data, &policyInfo); err != nil {
|
||||||
|
// This should not happen because data is valid to JSON data.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &policyInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BucketAccessPolicyToPolicy - converts minio-go/policy.BucketAccessPolicy to policy.Policy.
|
||||||
|
func BucketAccessPolicyToPolicy(policyInfo *miniogopolicy.BucketAccessPolicy) (*policy.Policy, error) {
|
||||||
|
data, err := json.Marshal(policyInfo)
|
||||||
|
if err != nil {
|
||||||
|
// This should not happen because policyInfo is valid to convert to JSON data.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var bucketPolicy policy.Policy
|
||||||
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
if err = json.Unmarshal(data, &bucketPolicy); err != nil {
|
||||||
|
// This should not happen because data is valid to JSON data.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &bucketPolicy, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/config"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/env"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
"github.com/minio/minio/pkg/madmin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BucketQuotaSys - map of bucket and quota configuration.
|
||||||
|
type BucketQuotaSys struct {
|
||||||
|
bucketStorageCache timedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get - Get quota configuration.
|
||||||
|
func (sys *BucketQuotaSys) Get(bucketName string) (*madmin.BucketQuota, error) {
|
||||||
|
if globalIsGateway {
|
||||||
|
objAPI := newObjectLayerFn()
|
||||||
|
if objAPI == nil {
|
||||||
|
return nil, errServerNotInitialized
|
||||||
|
}
|
||||||
|
return &madmin.BucketQuota{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalBucketMetadataSys.GetQuotaConfig(bucketName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBucketQuotaSys returns initialized BucketQuotaSys
|
||||||
|
func NewBucketQuotaSys() *BucketQuotaSys {
|
||||||
|
return &BucketQuotaSys{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBucketQuota parses BucketQuota from json
|
||||||
|
func parseBucketQuota(bucket string, data []byte) (quotaCfg *madmin.BucketQuota, err error) {
|
||||||
|
quotaCfg = &madmin.BucketQuota{}
|
||||||
|
if err = json.Unmarshal(data, quotaCfg); err != nil {
|
||||||
|
return quotaCfg, err
|
||||||
|
}
|
||||||
|
if !quotaCfg.IsValid() {
|
||||||
|
return quotaCfg, fmt.Errorf("Invalid quota config %#v", quotaCfg)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sys *BucketQuotaSys) check(ctx context.Context, bucket string, size int64) error {
|
||||||
|
objAPI := newObjectLayerWithoutSafeModeFn()
|
||||||
|
if objAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
q, err := sys.Get(bucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if q.Type == madmin.FIFOQuota {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if q.Quota == 0 {
|
||||||
|
// No quota set return quickly.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.bucketStorageCache.Once.Do(func() {
|
||||||
|
sys.bucketStorageCache.TTL = 10 * time.Second
|
||||||
|
sys.bucketStorageCache.Update = func() (interface{}, error) {
|
||||||
|
return loadDataUsageFromBackend(ctx, objAPI)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
v, err := sys.bucketStorageCache.Get()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dui := v.(DataUsageInfo)
|
||||||
|
|
||||||
|
bui, ok := dui.BucketsUsage[bucket]
|
||||||
|
if !ok {
|
||||||
|
// bucket not found, cannot enforce quota
|
||||||
|
// call will fail anyways later.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bui.Size + uint64(size)) > q.Quota {
|
||||||
|
return BucketQuotaExceeded{Bucket: bucket}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func enforceBucketQuota(ctx context.Context, bucket string, size int64) error {
|
||||||
|
if size < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalBucketQuotaSys.check(ctx, bucket, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
bgQuotaInterval = 1 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
// initQuotaEnforcement starts the routine that deletes objects in bucket
|
||||||
|
// that exceeds the FIFO quota
|
||||||
|
func initQuotaEnforcement(ctx context.Context, objAPI ObjectLayer) {
|
||||||
|
if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOn {
|
||||||
|
go startBucketQuotaEnforcement(ctx, objAPI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startBucketQuotaEnforcement(ctx context.Context, objAPI ObjectLayer) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.NewTimer(bgQuotaInterval).C:
|
||||||
|
logger.LogIf(ctx, enforceFIFOQuota(ctx, objAPI))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enforceFIFOQuota deletes objects in FIFO order until sufficient objects
|
||||||
|
// have been deleted so as to bring bucket usage within quota
|
||||||
|
func enforceFIFOQuota(ctx context.Context, objectAPI ObjectLayer) error {
|
||||||
|
// Turn off quota enforcement if data usage info is unavailable.
|
||||||
|
if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOff {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buckets, err := objectAPI.ListBuckets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, binfo := range buckets {
|
||||||
|
bucket := binfo.Name
|
||||||
|
|
||||||
|
bui, ok := dataUsageInfo.BucketsUsage[bucket]
|
||||||
|
if !ok {
|
||||||
|
// bucket doesn't exist anymore, or we
|
||||||
|
// do not have any information to proceed.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the current bucket has quota restrictions, if not skip it
|
||||||
|
cfg, err := globalBucketQuotaSys.Get(bucket)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Type != madmin.FIFOQuota {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var toFree uint64
|
||||||
|
if bui.Size > cfg.Quota && cfg.Quota > 0 {
|
||||||
|
toFree = bui.Size - cfg.Quota
|
||||||
|
}
|
||||||
|
|
||||||
|
if toFree == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate new results channel to receive ObjectInfo.
|
||||||
|
objInfoCh := make(chan ObjectInfo)
|
||||||
|
|
||||||
|
// Walk through all objects
|
||||||
|
if err := objectAPI.Walk(ctx, bucket, "", objInfoCh); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// reuse the fileScorer used by disk cache to score entries by
|
||||||
|
// ModTime to find the oldest objects in bucket to delete. In
|
||||||
|
// the context of bucket quota enforcement - number of hits are
|
||||||
|
// irrelevant.
|
||||||
|
scorer, err := newFileScorer(toFree, time.Now().Unix(), 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rcfg, _ := globalBucketObjectLockSys.Get(bucket)
|
||||||
|
|
||||||
|
for obj := range objInfoCh {
|
||||||
|
// skip objects currently under retention
|
||||||
|
if rcfg.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
scorer.addFile(obj.Name, obj.ModTime, obj.Size, 1)
|
||||||
|
}
|
||||||
|
var objects []string
|
||||||
|
numKeys := len(scorer.fileNames())
|
||||||
|
for i, key := range scorer.fileNames() {
|
||||||
|
objects = append(objects, key)
|
||||||
|
if len(objects) < maxDeleteList && (i < numKeys-1) {
|
||||||
|
// skip deletion until maxObjectList or end of slice
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(objects) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Deletes a list of objects.
|
||||||
|
deleteErrs, err := objectAPI.DeleteObjects(ctx, bucket, objects)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
} else {
|
||||||
|
for i := range deleteErrs {
|
||||||
|
if deleteErrs[i] != nil {
|
||||||
|
logger.LogIf(ctx, deleteErrs[i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Notify object deleted event.
|
||||||
|
sendEvent(eventArgs{
|
||||||
|
EventName: event.ObjectRemovedDelete,
|
||||||
|
BucketName: bucket,
|
||||||
|
Object: ObjectInfo{
|
||||||
|
Name: objects[i],
|
||||||
|
},
|
||||||
|
Host: "Internal: [FIFO-QUOTA-EXPIRY]",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
objects = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+30
-9
@@ -17,11 +17,11 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -41,7 +41,7 @@ func init() {
|
|||||||
logger.RegisterError(config.FmtError)
|
logger.RegisterError(config.FmtError)
|
||||||
|
|
||||||
// Initialize globalConsoleSys system
|
// Initialize globalConsoleSys system
|
||||||
globalConsoleSys = NewConsoleLogger(context.Background())
|
globalConsoleSys = NewConsoleLogger(GlobalContext)
|
||||||
logger.AddTarget(globalConsoleSys)
|
logger.AddTarget(globalConsoleSys)
|
||||||
|
|
||||||
gob.Register(StorageErr(""))
|
gob.Register(StorageErr(""))
|
||||||
@@ -145,10 +145,16 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
|||||||
|
|
||||||
// Fetch address option
|
// Fetch address option
|
||||||
globalCLIContext.Addr = ctx.GlobalString("address")
|
globalCLIContext.Addr = ctx.GlobalString("address")
|
||||||
if globalCLIContext.Addr == "" || globalCLIContext.Addr == ":"+globalMinioDefaultPort {
|
if globalCLIContext.Addr == "" || globalCLIContext.Addr == ":"+GlobalMinioDefaultPort {
|
||||||
globalCLIContext.Addr = ctx.String("address")
|
globalCLIContext.Addr = ctx.String("address")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check "no-compat" flag from command line argument.
|
||||||
|
globalCLIContext.StrictS3Compat = true
|
||||||
|
if ctx.IsSet("no-compat") || ctx.GlobalIsSet("no-compat") {
|
||||||
|
globalCLIContext.StrictS3Compat = false
|
||||||
|
}
|
||||||
|
|
||||||
// Set all config, certs and CAs directories.
|
// Set all config, certs and CAs directories.
|
||||||
var configSet, certsSet bool
|
var configSet, certsSet bool
|
||||||
globalConfigDir, configSet = newConfigDirFromCtx(ctx, "config-dir", defaultConfigDir.Get)
|
globalConfigDir, configSet = newConfigDirFromCtx(ctx, "config-dir", defaultConfigDir.Get)
|
||||||
@@ -164,18 +170,27 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
|||||||
globalCertsCADir = &ConfigDir{path: filepath.Join(globalCertsDir.Get(), certsCADir)}
|
globalCertsCADir = &ConfigDir{path: filepath.Join(globalCertsDir.Get(), certsCADir)}
|
||||||
|
|
||||||
logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get())
|
logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get())
|
||||||
|
|
||||||
// Check "compat" flag from command line argument.
|
|
||||||
globalCLIContext.StrictS3Compat = ctx.IsSet("compat") || ctx.GlobalIsSet("compat")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCommonEnvVars() {
|
func handleCommonEnvVars() {
|
||||||
var err error
|
wormEnabled, err := config.LookupWorm()
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(config.ErrInvalidWormValue(err), "Invalid worm configuration")
|
||||||
|
}
|
||||||
|
if wormEnabled {
|
||||||
|
logger.Fatal(errors.New("WORM is deprecated"), "global MINIO_WORM support is removed, please downgrade your server or migrate to https://github.com/minio/minio/tree/master/docs/retention")
|
||||||
|
}
|
||||||
|
|
||||||
globalBrowserEnabled, err = config.ParseBool(env.Get(config.EnvBrowser, config.EnableOn))
|
globalBrowserEnabled, err = config.ParseBool(env.Get(config.EnvBrowser, config.EnableOn))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable")
|
logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalFSOSync, err = config.ParseBool(env.Get(config.EnvFSOSync, config.EnableOff))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(config.ErrInvalidFSOSyncValue(err), "Invalid MINIO_FS_OSYNC value in environment variable")
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -226,9 +241,15 @@ func handleCommonEnvVars() {
|
|||||||
globalConfigEncrypted = true
|
globalConfigEncrypted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
globalWORMEnabled, err = config.LookupWorm()
|
if env.IsSet(config.EnvAccessKeyOld) && env.IsSet(config.EnvSecretKeyOld) {
|
||||||
|
oldCred, err := auth.CreateCredentials(env.Get(config.EnvAccessKeyOld, ""), env.Get(config.EnvSecretKeyOld, ""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(config.ErrInvalidWormValue(err), "Invalid worm configuration")
|
logger.Fatal(config.ErrInvalidCredentials(err),
|
||||||
|
"Unable to validate the old credentials inherited from the shell environment")
|
||||||
|
}
|
||||||
|
globalOldCred = oldCred
|
||||||
|
os.Unsetenv(config.EnvAccessKeyOld)
|
||||||
|
os.Unsetenv(config.EnvSecretKeyOld)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) ([]b
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deleteConfig(ctx context.Context, objAPI ObjectLayer, configFile string) error {
|
func deleteConfig(ctx context.Context, objAPI ObjectLayer, configFile string) error {
|
||||||
return objAPI.DeleteObject(ctx, minioMetaBucket, configFile)
|
err := objAPI.DeleteObject(ctx, minioMetaBucket, configFile)
|
||||||
|
if err != nil && isErrObjectNotFound(err) {
|
||||||
|
return errConfigNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data []byte) error {
|
func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data []byte) error {
|
||||||
|
|||||||
+70
-19
@@ -17,16 +17,15 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/config"
|
"github.com/minio/minio/cmd/config"
|
||||||
|
"github.com/minio/minio/cmd/config/api"
|
||||||
"github.com/minio/minio/cmd/config/cache"
|
"github.com/minio/minio/cmd/config/cache"
|
||||||
"github.com/minio/minio/cmd/config/compress"
|
"github.com/minio/minio/cmd/config/compress"
|
||||||
"github.com/minio/minio/cmd/config/etcd"
|
"github.com/minio/minio/cmd/config/etcd"
|
||||||
xetcd "github.com/minio/minio/cmd/config/etcd"
|
|
||||||
"github.com/minio/minio/cmd/config/etcd/dns"
|
"github.com/minio/minio/cmd/config/etcd/dns"
|
||||||
xldap "github.com/minio/minio/cmd/config/identity/ldap"
|
xldap "github.com/minio/minio/cmd/config/identity/ldap"
|
||||||
"github.com/minio/minio/cmd/config/identity/openid"
|
"github.com/minio/minio/cmd/config/identity/openid"
|
||||||
@@ -50,6 +49,7 @@ func initHelp() {
|
|||||||
config.IdentityOpenIDSubSys: openid.DefaultKVS,
|
config.IdentityOpenIDSubSys: openid.DefaultKVS,
|
||||||
config.PolicyOPASubSys: opa.DefaultKVS,
|
config.PolicyOPASubSys: opa.DefaultKVS,
|
||||||
config.RegionSubSys: config.DefaultRegionKVS,
|
config.RegionSubSys: config.DefaultRegionKVS,
|
||||||
|
config.APISubSys: api.DefaultKVS,
|
||||||
config.CredentialsSubSys: config.DefaultCredentialKVS,
|
config.CredentialsSubSys: config.DefaultCredentialKVS,
|
||||||
config.KmsVaultSubSys: crypto.DefaultVaultKVS,
|
config.KmsVaultSubSys: crypto.DefaultVaultKVS,
|
||||||
config.KmsKesSubSys: crypto.DefaultKesKVS,
|
config.KmsKesSubSys: crypto.DefaultKesKVS,
|
||||||
@@ -102,6 +102,10 @@ func initHelp() {
|
|||||||
Key: config.KmsKesSubSys,
|
Key: config.KmsKesSubSys,
|
||||||
Description: "enable external MinIO key encryption service",
|
Description: "enable external MinIO key encryption service",
|
||||||
},
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: config.APISubSys,
|
||||||
|
Description: "manage global HTTP API call specific features, such as throttling, authentication types, etc.",
|
||||||
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: config.LoggerWebhookSubSys,
|
Key: config.LoggerWebhookSubSys,
|
||||||
Description: "send server logs to webhook endpoints",
|
Description: "send server logs to webhook endpoints",
|
||||||
@@ -176,6 +180,7 @@ func initHelp() {
|
|||||||
var helpMap = map[string]config.HelpKVS{
|
var helpMap = map[string]config.HelpKVS{
|
||||||
"": helpSubSys, // Help for all sub-systems.
|
"": helpSubSys, // Help for all sub-systems.
|
||||||
config.RegionSubSys: config.RegionHelp,
|
config.RegionSubSys: config.RegionHelp,
|
||||||
|
config.APISubSys: api.Help,
|
||||||
config.StorageClassSubSys: storageclass.Help,
|
config.StorageClassSubSys: storageclass.Help,
|
||||||
config.EtcdSubSys: etcd.Help,
|
config.EtcdSubSys: etcd.Help,
|
||||||
config.CacheSubSys: cache.Help,
|
config.CacheSubSys: cache.Help,
|
||||||
@@ -223,6 +228,10 @@ func validateConfig(s config.Config) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := api.LookupConfig(s[config.APISubSys][config.Default]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if globalIsXL {
|
if globalIsXL {
|
||||||
if _, err := storageclass.LookupConfig(s[config.StorageClassSubSys][config.Default],
|
if _, err := storageclass.LookupConfig(s[config.StorageClassSubSys][config.Default],
|
||||||
globalXLSetDriveCount); err != nil {
|
globalXLSetDriveCount); err != nil {
|
||||||
@@ -252,7 +261,7 @@ func validateConfig(s config.Config) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), NewCustomHTTPTransport())
|
kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), NewGatewayHTTPTransport())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -270,17 +279,27 @@ func validateConfig(s config.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
|
if _, err := openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
|
||||||
NewCustomHTTPTransport(), xhttp.DrainBody); err != nil {
|
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
{
|
||||||
globalRootCAs); err != nil {
|
cfg, err := xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
||||||
|
globalRootCAs)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if cfg.Enabled {
|
||||||
|
conn, cerr := cfg.Connect()
|
||||||
|
if cerr != nil {
|
||||||
|
return cerr
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
||||||
NewCustomHTTPTransport(), xhttp.DrainBody); err != nil {
|
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,12 +307,12 @@ func validateConfig(s config.Config) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return notify.TestNotificationTargets(s, GlobalServiceDoneCh, NewCustomHTTPTransport(),
|
return notify.TestNotificationTargets(s, GlobalContext.Done(), NewGatewayHTTPTransport(),
|
||||||
globalNotificationSys.ConfiguredTargetIDs())
|
globalNotificationSys.ConfiguredTargetIDs())
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookupConfigs(s config.Config) {
|
func lookupConfigs(s config.Config) {
|
||||||
ctx := context.Background()
|
ctx := GlobalContext
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if !globalActiveCred.IsValid() {
|
if !globalActiveCred.IsValid() {
|
||||||
@@ -304,13 +323,13 @@ func lookupConfigs(s config.Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
etcdCfg, err := xetcd.LookupConfig(s[config.EtcdSubSys][config.Default], globalRootCAs)
|
etcdCfg, err := etcd.LookupConfig(s[config.EtcdSubSys][config.Default], globalRootCAs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize etcd config: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize etcd config: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if etcdCfg.Enabled {
|
if etcdCfg.Enabled {
|
||||||
globalEtcdClient, err = xetcd.New(etcdCfg)
|
globalEtcdClient, err = etcd.New(etcdCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize etcd config: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize etcd config: %w", err))
|
||||||
}
|
}
|
||||||
@@ -341,6 +360,19 @@ func lookupConfigs(s config.Config) {
|
|||||||
logger.LogIf(ctx, fmt.Errorf("Invalid region configuration: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Invalid region configuration: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apiConfig, err := api.LookupConfig(s[config.APISubSys][config.Default])
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Invalid api configuration: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
apiRequestsMax := apiConfig.APIRequestsMax
|
||||||
|
if len(globalEndpoints.Hosts()) > 0 {
|
||||||
|
apiRequestsMax /= len(globalEndpoints.Hosts())
|
||||||
|
}
|
||||||
|
|
||||||
|
globalAPIThrottling.init(apiRequestsMax, apiConfig.APIRequestsDeadline)
|
||||||
|
globalReadyDeadline = apiConfig.APIReadyDeadline
|
||||||
|
|
||||||
if globalIsXL {
|
if globalIsXL {
|
||||||
globalStorageClass, err = storageclass.LookupConfig(s[config.StorageClassSubSys][config.Default],
|
globalStorageClass, err = storageclass.LookupConfig(s[config.StorageClassSubSys][config.Default],
|
||||||
globalXLSetDriveCount)
|
globalXLSetDriveCount)
|
||||||
@@ -363,7 +395,7 @@ func lookupConfigs(s config.Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), NewCustomHTTPTransport())
|
kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), NewGatewayHTTPTransport())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to setup KMS config: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to setup KMS config: %w", err))
|
||||||
}
|
}
|
||||||
@@ -382,13 +414,13 @@ func lookupConfigs(s config.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
|
globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
|
||||||
NewCustomHTTPTransport(), xhttp.DrainBody)
|
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
||||||
NewCustomHTTPTransport(), xhttp.DrainBody)
|
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||||
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))
|
||||||
}
|
}
|
||||||
@@ -413,18 +445,37 @@ func lookupConfigs(s config.Config) {
|
|||||||
for _, l := range loggerCfg.HTTP {
|
for _, l := range loggerCfg.HTTP {
|
||||||
if l.Enabled {
|
if l.Enabled {
|
||||||
// Enable http logging
|
// Enable http logging
|
||||||
logger.AddTarget(http.New(l.Endpoint, loggerUserAgent, string(logger.All), NewCustomHTTPTransport()))
|
logger.AddTarget(
|
||||||
|
http.New(http.WithEndpoint(l.Endpoint),
|
||||||
|
http.WithAuthToken(l.AuthToken),
|
||||||
|
http.WithUserAgent(loggerUserAgent),
|
||||||
|
http.WithLogKind(string(logger.All)),
|
||||||
|
http.WithTransport(NewGatewayHTTPTransport()),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, l := range loggerCfg.Audit {
|
for _, l := range loggerCfg.Audit {
|
||||||
if l.Enabled {
|
if l.Enabled {
|
||||||
// Enable http audit logging
|
// Enable http audit logging
|
||||||
logger.AddAuditTarget(http.New(l.Endpoint, loggerUserAgent, string(logger.All), NewCustomHTTPTransport()))
|
logger.AddAuditTarget(
|
||||||
|
http.New(http.WithEndpoint(l.Endpoint),
|
||||||
|
http.WithAuthToken(l.AuthToken),
|
||||||
|
http.WithUserAgent(loggerUserAgent),
|
||||||
|
http.WithLogKind(string(logger.All)),
|
||||||
|
http.WithTransport(NewGatewayHTTPTransport()),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
globalConfigTargetList, err = notify.GetNotificationTargets(s, GlobalServiceDoneCh, NewCustomHTTPTransport())
|
globalConfigTargetList, err = notify.GetNotificationTargets(s, GlobalContext.Done(), NewGatewayHTTPTransport())
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
globalEnvTargetList, err = notify.GetNotificationTargets(newServerConfig(), GlobalContext.Done(), NewGatewayHTTPTransport())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
|
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
|
||||||
}
|
}
|
||||||
@@ -525,11 +576,11 @@ func newSrvConfig(objAPI ObjectLayer) error {
|
|||||||
globalServerConfigMu.Unlock()
|
globalServerConfigMu.Unlock()
|
||||||
|
|
||||||
// Save config into file.
|
// Save config into file.
|
||||||
return saveServerConfig(context.Background(), objAPI, globalServerConfig)
|
return saveServerConfig(GlobalContext, objAPI, globalServerConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getValidConfig(objAPI ObjectLayer) (config.Config, error) {
|
func getValidConfig(objAPI ObjectLayer) (config.Config, error) {
|
||||||
return readServerConfig(context.Background(), objAPI)
|
return readServerConfig(GlobalContext, objAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadConfig - loads a new config from disk, overrides params
|
// loadConfig - loads a new config from disk, overrides params
|
||||||
|
|||||||
+23
-98
@@ -21,15 +21,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"time"
|
||||||
"strings"
|
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
etcd "github.com/coreos/etcd/clientv3"
|
etcd "github.com/coreos/etcd/clientv3"
|
||||||
"github.com/minio/minio/cmd/config"
|
"github.com/minio/minio/cmd/config"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/env"
|
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,37 +36,10 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If its server mode or nas gateway, migrate the backend.
|
encrypted, err := checkBackendEncrypted(objAPI)
|
||||||
doneCh := make(chan struct{})
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to encrypt config %w", err)
|
||||||
var encrypted bool
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Migrating Config backend needs a retry mechanism for
|
|
||||||
// the following reasons:
|
|
||||||
// - Read quorum is lost just after the initialization
|
|
||||||
// of the object layer.
|
|
||||||
retryTimerCh := newRetryTimerSimple(doneCh)
|
|
||||||
var stop bool
|
|
||||||
for !stop {
|
|
||||||
select {
|
|
||||||
case <-retryTimerCh:
|
|
||||||
if encrypted, err = checkBackendEncrypted(objAPI); err != nil {
|
|
||||||
if err == errDiskNotFound ||
|
|
||||||
strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) {
|
|
||||||
logger.Info("Waiting for config backend to be encrypted..")
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
close(doneCh)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stop = true
|
|
||||||
case <-globalOSSignalCh:
|
|
||||||
close(doneCh)
|
|
||||||
return fmt.Errorf("Config encryption process stopped gracefully")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(doneCh)
|
|
||||||
|
|
||||||
if encrypted {
|
if encrypted {
|
||||||
// backend is encrypted, but credentials are not specified
|
// backend is encrypted, but credentials are not specified
|
||||||
@@ -88,38 +59,12 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activeCredOld, err := getOldCreds()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
doneCh = make(chan struct{})
|
|
||||||
defer close(doneCh)
|
|
||||||
|
|
||||||
retryTimerCh = newRetryTimerSimple(doneCh)
|
|
||||||
|
|
||||||
// Migrating Config backend needs a retry mechanism for
|
|
||||||
// the following reasons:
|
|
||||||
// - Read quorum is lost just after the initialization
|
|
||||||
// of the object layer.
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-retryTimerCh:
|
|
||||||
// Migrate IAM configuration
|
// Migrate IAM configuration
|
||||||
if err = migrateConfigPrefixToEncrypted(objAPI, activeCredOld, encrypted); err != nil {
|
if err = migrateConfigPrefixToEncrypted(objAPI, globalOldCred, encrypted); err != nil {
|
||||||
if err == errDiskNotFound ||
|
return fmt.Errorf("Unable to migrate all config at .minio.sys/config/: %w", err)
|
||||||
strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) ||
|
|
||||||
strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) {
|
|
||||||
logger.Info("Waiting for config backend to be encrypted..")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
case <-globalOSSignalCh:
|
|
||||||
return fmt.Errorf("Config encryption process stopped gracefully")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -140,7 +85,7 @@ func checkBackendEtcdEncrypted(ctx context.Context, client *etcd.Client) (bool,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkBackendEncrypted(objAPI ObjectLayer) (bool, error) {
|
func checkBackendEncrypted(objAPI ObjectLayer) (bool, error) {
|
||||||
data, err := readConfig(context.Background(), objAPI, backendEncryptedFile)
|
data, err := readConfig(GlobalContext, objAPI, backendEncryptedFile)
|
||||||
if err != nil && err != errConfigNotFound {
|
if err != nil && err != errConfigNotFound {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -164,25 +109,7 @@ func decryptData(edata []byte, creds ...auth.Credentials) ([]byte, error) {
|
|||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOldCreds() (activeCredOld auth.Credentials, err error) {
|
func migrateIAMConfigsEtcdToEncrypted(ctx context.Context, client *etcd.Client) error {
|
||||||
accessKeyOld := env.Get(config.EnvAccessKeyOld, "")
|
|
||||||
secretKeyOld := env.Get(config.EnvSecretKeyOld, "")
|
|
||||||
if accessKeyOld != "" && secretKeyOld != "" {
|
|
||||||
activeCredOld, err = auth.CreateCredentials(accessKeyOld, secretKeyOld)
|
|
||||||
if err != nil {
|
|
||||||
return activeCredOld, err
|
|
||||||
}
|
|
||||||
// Once we have obtained the rotating creds
|
|
||||||
os.Unsetenv(config.EnvAccessKeyOld)
|
|
||||||
os.Unsetenv(config.EnvSecretKeyOld)
|
|
||||||
}
|
|
||||||
return activeCredOld, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
encrypted, err := checkBackendEtcdEncrypted(ctx, client)
|
encrypted, err := checkBackendEtcdEncrypted(ctx, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -206,20 +133,15 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activeCredOld, err := getOldCreds()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if encrypted {
|
if encrypted {
|
||||||
// No key rotation requested, and backend is
|
// No key rotation requested, and backend is
|
||||||
// already encrypted. We proceed without migration.
|
// already encrypted. We proceed without migration.
|
||||||
if !activeCredOld.IsValid() {
|
if !globalOldCred.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// No real reason to rotate if old and new creds are same.
|
// No real reason to rotate if old and new creds are same.
|
||||||
if activeCredOld.Equal(globalActiveCred) {
|
if globalOldCred.Equal(globalActiveCred) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +150,10 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error {
|
|||||||
logger.Info("Attempting encryption of all IAM users and policies on etcd")
|
logger.Info("Attempting encryption of all IAM users and policies on etcd")
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := client.Get(ctx, minioConfigPrefix, etcd.WithPrefix(), etcd.WithKeysOnly())
|
listCtx, cancel := context.WithTimeout(ctx, 1*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
r, err := client.Get(listCtx, minioConfigPrefix, etcd.WithPrefix(), etcd.WithKeysOnly())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -254,8 +179,8 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error {
|
|||||||
|
|
||||||
var data []byte
|
var data []byte
|
||||||
// Is rotating of creds requested?
|
// Is rotating of creds requested?
|
||||||
if activeCredOld.IsValid() {
|
if globalOldCred.IsValid() {
|
||||||
data, err = decryptData(cdata, activeCredOld, globalActiveCred)
|
data, err = decryptData(cdata, globalOldCred, globalActiveCred)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == madmin.ErrMaliciousData {
|
if err == madmin.ErrMaliciousData {
|
||||||
return config.ErrInvalidRotatingCredentialsBackendEncrypted(nil)
|
return config.ErrInvalidRotatingCredentialsBackendEncrypted(nil)
|
||||||
@@ -285,7 +210,7 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if encrypted && globalActiveCred.IsValid() && activeCredOld.IsValid() {
|
if encrypted && globalActiveCred.IsValid() && globalOldCred.IsValid() {
|
||||||
logger.Info("Rotation complete, please make sure to unset MINIO_ACCESS_KEY_OLD and MINIO_SECRET_KEY_OLD envs")
|
logger.Info("Rotation complete, please make sure to unset MINIO_ACCESS_KEY_OLD and MINIO_SECRET_KEY_OLD envs")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,14 +234,14 @@ func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, activeCredOld auth.Crede
|
|||||||
logger.Info("Attempting encryption of all config, IAM users and policies on MinIO backend")
|
logger.Info("Attempting encryption of all config, IAM users and policies on MinIO backend")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := saveConfig(context.Background(), objAPI, backendEncryptedFile, backendEncryptedMigrationIncomplete)
|
err := saveConfig(GlobalContext, objAPI, backendEncryptedFile, backendEncryptedMigrationIncomplete)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var marker string
|
var marker string
|
||||||
for {
|
for {
|
||||||
res, err := objAPI.ListObjects(context.Background(), minioMetaBucket,
|
res, err := objAPI.ListObjects(GlobalContext, minioMetaBucket,
|
||||||
minioConfigPrefix, marker, "", maxObjectList)
|
minioConfigPrefix, marker, "", maxObjectList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -327,7 +252,7 @@ func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, activeCredOld auth.Crede
|
|||||||
cencdata []byte
|
cencdata []byte
|
||||||
)
|
)
|
||||||
|
|
||||||
cdata, err = readConfig(context.Background(), objAPI, obj.Name)
|
cdata, err = readConfig(GlobalContext, objAPI, obj.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -360,7 +285,7 @@ func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, activeCredOld auth.Crede
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = saveConfig(context.Background(), objAPI, obj.Name, cencdata); err != nil {
|
if err = saveConfig(GlobalContext, objAPI, obj.Name, cencdata); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,5 +301,5 @@ func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, activeCredOld auth.Crede
|
|||||||
logger.Info("Rotation complete, please make sure to unset MINIO_ACCESS_KEY_OLD and MINIO_SECRET_KEY_OLD envs")
|
logger.Info("Rotation complete, please make sure to unset MINIO_ACCESS_KEY_OLD and MINIO_SECRET_KEY_OLD envs")
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveConfig(context.Background(), objAPI, backendEncryptedFile, backendEncryptedMigrationComplete)
|
return saveConfig(GlobalContext, objAPI, backendEncryptedFile, backendEncryptedMigrationComplete)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-12
@@ -18,7 +18,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -2430,7 +2429,7 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if globalEtcdClient != nil {
|
if globalEtcdClient != nil {
|
||||||
deleteKeyEtcd(context.Background(), globalEtcdClient, configFile)
|
deleteKeyEtcd(GlobalContext, globalEtcdClient, configFile)
|
||||||
} else {
|
} else {
|
||||||
// Rename config.json to config.json.deprecated only upon
|
// Rename config.json to config.json.deprecated only upon
|
||||||
// success of this function.
|
// success of this function.
|
||||||
@@ -2440,7 +2439,7 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Verify if backend already has the file (after holding lock)
|
// Verify if backend already has the file (after holding lock)
|
||||||
if err = checkConfig(context.Background(), objAPI, configFile); err != errConfigNotFound {
|
if err = checkConfig(GlobalContext, objAPI, configFile); err != errConfigNotFound {
|
||||||
return err
|
return err
|
||||||
} // if errConfigNotFound proceed to migrate..
|
} // if errConfigNotFound proceed to migrate..
|
||||||
|
|
||||||
@@ -2466,7 +2465,7 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
|||||||
// Initialize the server config, if no config exists.
|
// Initialize the server config, if no config exists.
|
||||||
return newSrvConfig(objAPI)
|
return newSrvConfig(objAPI)
|
||||||
}
|
}
|
||||||
return saveServerConfig(context.Background(), objAPI, config)
|
return saveServerConfig(GlobalContext, objAPI, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrates '.minio.sys/config.json' to v33.
|
// Migrates '.minio.sys/config.json' to v33.
|
||||||
@@ -2502,7 +2501,7 @@ func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkConfigVersion(objAPI ObjectLayer, configFile string, version string) (bool, []byte, error) {
|
func checkConfigVersion(objAPI ObjectLayer, configFile string, version string) (bool, []byte, error) {
|
||||||
data, err := readConfig(context.Background(), objAPI, configFile)
|
data, err := readConfig(GlobalContext, objAPI, configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
@@ -2548,7 +2547,7 @@ func migrateV27ToV28MinioSys(objAPI ObjectLayer) error {
|
|||||||
cfg.Version = "28"
|
cfg.Version = "28"
|
||||||
cfg.KMS = crypto.KMSConfig{}
|
cfg.KMS = crypto.KMSConfig{}
|
||||||
|
|
||||||
if err = saveServerConfig(context.Background(), objAPI, cfg); err != nil {
|
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
|
||||||
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %w", err)
|
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2575,7 +2574,7 @@ func migrateV28ToV29MinioSys(objAPI ObjectLayer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg.Version = "29"
|
cfg.Version = "29"
|
||||||
if err = saveServerConfig(context.Background(), objAPI, cfg); err != nil {
|
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
|
||||||
return fmt.Errorf("Failed to migrate config from ‘28’ to ‘29’. %w", err)
|
return fmt.Errorf("Failed to migrate config from ‘28’ to ‘29’. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2607,7 +2606,7 @@ func migrateV29ToV30MinioSys(objAPI ObjectLayer) error {
|
|||||||
cfg.Compression.Extensions = strings.Split(compress.DefaultExtensions, config.ValueSeparator)
|
cfg.Compression.Extensions = strings.Split(compress.DefaultExtensions, config.ValueSeparator)
|
||||||
cfg.Compression.MimeTypes = strings.Split(compress.DefaultMimeTypes, config.ValueSeparator)
|
cfg.Compression.MimeTypes = strings.Split(compress.DefaultMimeTypes, config.ValueSeparator)
|
||||||
|
|
||||||
if err = saveServerConfig(context.Background(), objAPI, cfg); err != nil {
|
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
|
||||||
return fmt.Errorf("Failed to migrate config from ‘29’ to ‘30’. %w", err)
|
return fmt.Errorf("Failed to migrate config from ‘29’ to ‘30’. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2642,7 +2641,7 @@ func migrateV30ToV31MinioSys(objAPI ObjectLayer) error {
|
|||||||
AuthToken: "",
|
AuthToken: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = saveServerConfig(context.Background(), objAPI, cfg); err != nil {
|
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
|
||||||
return fmt.Errorf("Failed to migrate config from ‘30’ to ‘31’. %w", err)
|
return fmt.Errorf("Failed to migrate config from ‘30’ to ‘31’. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2672,7 +2671,7 @@ func migrateV31ToV32MinioSys(objAPI ObjectLayer) error {
|
|||||||
cfg.Notify.NSQ = make(map[string]target.NSQArgs)
|
cfg.Notify.NSQ = make(map[string]target.NSQArgs)
|
||||||
cfg.Notify.NSQ["1"] = target.NSQArgs{}
|
cfg.Notify.NSQ["1"] = target.NSQArgs{}
|
||||||
|
|
||||||
if err = saveServerConfig(context.Background(), objAPI, cfg); err != nil {
|
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
|
||||||
return fmt.Errorf("Failed to migrate config from ‘31’ to ‘32’. %w", err)
|
return fmt.Errorf("Failed to migrate config from ‘31’ to ‘32’. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2700,7 +2699,7 @@ func migrateV32ToV33MinioSys(objAPI ObjectLayer) error {
|
|||||||
|
|
||||||
cfg.Version = "33"
|
cfg.Version = "33"
|
||||||
|
|
||||||
if err = saveServerConfig(context.Background(), objAPI, cfg); err != nil {
|
if err = saveServerConfig(GlobalContext, objAPI, cfg); err != nil {
|
||||||
return fmt.Errorf("Failed to migrate config from '32' to '33' . %w", err)
|
return fmt.Errorf("Failed to migrate config from '32' to '33' . %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2777,7 +2776,7 @@ func migrateMinioSysConfigToKV(objAPI ObjectLayer) error {
|
|||||||
notify.SetNotifyWebhook(newCfg, k, args)
|
notify.SetNotifyWebhook(newCfg, k, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = saveServerConfig(context.Background(), objAPI, newCfg); err != nil {
|
if err = saveServerConfig(GlobalContext, objAPI, newCfg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-11
@@ -40,9 +40,6 @@ const (
|
|||||||
|
|
||||||
// MinIO configuration file.
|
// MinIO configuration file.
|
||||||
minioConfigFile = "config.json"
|
minioConfigFile = "config.json"
|
||||||
|
|
||||||
// MinIO configuration backup file
|
|
||||||
minioConfigBackupFile = minioConfigFile + ".backup"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func listServerConfigHistory(ctx context.Context, objAPI ObjectLayer, withData bool, count int) (
|
func listServerConfigHistory(ctx context.Context, objAPI ObjectLayer, withData bool, count int) (
|
||||||
@@ -168,13 +165,14 @@ func readServerConfig(ctx context.Context, objAPI ObjectLayer) (config.Config, e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = config.New()
|
var srvCfg = config.New()
|
||||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
if err = json.Unmarshal(configData, &config); err != nil {
|
if err = json.Unmarshal(configData, &srvCfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
// Add any missing entries
|
||||||
|
return srvCfg.Merge(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigSys - config system.
|
// ConfigSys - config system.
|
||||||
@@ -186,16 +184,14 @@ func (sys *ConfigSys) Load(objAPI ObjectLayer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WatchConfigNASDisk - watches nas disk on periodic basis.
|
// WatchConfigNASDisk - watches nas disk on periodic basis.
|
||||||
func (sys *ConfigSys) WatchConfigNASDisk(objAPI ObjectLayer) {
|
func (sys *ConfigSys) WatchConfigNASDisk(ctx context.Context, objAPI ObjectLayer) {
|
||||||
configInterval := globalRefreshIAMInterval
|
configInterval := globalRefreshIAMInterval
|
||||||
watchDisk := func() {
|
watchDisk := func() {
|
||||||
ticker := time.NewTicker(configInterval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-GlobalServiceDoneCh:
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-time.After(configInterval):
|
||||||
loadConfig(objAPI)
|
loadConfig(objAPI)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 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 api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/config"
|
||||||
|
"github.com/minio/minio/pkg/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
apiRequestsMax = "requests_max"
|
||||||
|
apiRequestsDeadline = "requests_deadline"
|
||||||
|
apiReadyDeadline = "ready_deadline"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultKVS - default storage class config
|
||||||
|
var (
|
||||||
|
DefaultKVS = config.KVS{
|
||||||
|
config.KV{
|
||||||
|
Key: apiRequestsMax,
|
||||||
|
Value: "0",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: apiRequestsDeadline,
|
||||||
|
Value: "10s",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: apiReadyDeadline,
|
||||||
|
Value: "10s",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config storage class configuration
|
||||||
|
type Config struct {
|
||||||
|
APIRequestsMax int `json:"requests_max"`
|
||||||
|
APIRequestsDeadline time.Duration `json:"requests_deadline"`
|
||||||
|
APIReadyDeadline time.Duration `json:"ready_deadline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON.
|
||||||
|
func (sCfg *Config) UnmarshalJSON(data []byte) error {
|
||||||
|
type Alias Config
|
||||||
|
aux := &struct {
|
||||||
|
*Alias
|
||||||
|
}{
|
||||||
|
Alias: (*Alias)(sCfg),
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, &aux)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupConfig - lookup api config and override with valid environment settings if any.
|
||||||
|
func LookupConfig(kvs config.KVS) (cfg Config, err error) {
|
||||||
|
if err = config.CheckValidKeys(config.APISubSys, kvs, DefaultKVS); err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check environment variables parameters
|
||||||
|
requestsMax, err := strconv.Atoi(env.Get(config.EnvAPIRequestsMax, kvs.Get(apiRequestsMax)))
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestsMax < 0 {
|
||||||
|
return cfg, errors.New("invalid API max requests value")
|
||||||
|
}
|
||||||
|
|
||||||
|
requestsDeadline, err := time.ParseDuration(env.Get(config.EnvAPIRequestsDeadline, kvs.Get(apiRequestsDeadline)))
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
readyDeadline, err := time.ParseDuration(env.Get(config.EnvAPIReadyDeadline, kvs.Get(apiReadyDeadline)))
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg = Config{
|
||||||
|
APIRequestsMax: requestsMax,
|
||||||
|
APIRequestsDeadline: requestsDeadline,
|
||||||
|
APIReadyDeadline: readyDeadline,
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 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 api
|
||||||
|
|
||||||
|
import "github.com/minio/minio/cmd/config"
|
||||||
|
|
||||||
|
// Help template for storageclass feature.
|
||||||
|
var (
|
||||||
|
Help = config.HelpKVS{
|
||||||
|
config.HelpKV{
|
||||||
|
Key: apiRequestsMax,
|
||||||
|
Description: `set the maximum number of concurrent requests, e.g. "1600"`,
|
||||||
|
Optional: true,
|
||||||
|
Type: "number",
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: apiRequestsDeadline,
|
||||||
|
Description: `set the deadline for API requests waiting to be processed e.g. "1m"`,
|
||||||
|
Optional: true,
|
||||||
|
Type: "duration",
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: apiReadyDeadline,
|
||||||
|
Description: `set the deadline for health check API /minio/health/ready e.g. "1m"`,
|
||||||
|
Optional: true,
|
||||||
|
Type: "duration",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 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 config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
color "github.com/minio/minio/pkg/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extra ASN1 OIDs that we may need to handle
|
||||||
|
var (
|
||||||
|
oidEmailAddress = []int{1, 2, 840, 113549, 1, 9, 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
// printName prints the fields of a distinguished name, which include such
|
||||||
|
// things as its common name and locality.
|
||||||
|
func printName(names []pkix.AttributeTypeAndValue, buf *strings.Builder) []string {
|
||||||
|
values := []string{}
|
||||||
|
for _, name := range names {
|
||||||
|
oid := name.Type
|
||||||
|
if len(oid) == 4 && oid[0] == 2 && oid[1] == 5 && oid[2] == 4 {
|
||||||
|
switch oid[3] {
|
||||||
|
case 3:
|
||||||
|
values = append(values, fmt.Sprintf("CN=%s", name.Value))
|
||||||
|
case 6:
|
||||||
|
values = append(values, fmt.Sprintf("C=%s", name.Value))
|
||||||
|
case 8:
|
||||||
|
values = append(values, fmt.Sprintf("ST=%s", name.Value))
|
||||||
|
case 10:
|
||||||
|
values = append(values, fmt.Sprintf("O=%s", name.Value))
|
||||||
|
case 11:
|
||||||
|
values = append(values, fmt.Sprintf("OU=%s", name.Value))
|
||||||
|
default:
|
||||||
|
values = append(values, fmt.Sprintf("UnknownOID=%s", name.Type.String()))
|
||||||
|
}
|
||||||
|
} else if oid.Equal(oidEmailAddress) {
|
||||||
|
values = append(values, fmt.Sprintf("emailAddress=%s", name.Value))
|
||||||
|
} else {
|
||||||
|
values = append(values, fmt.Sprintf("UnknownOID=%s", name.Type.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(values) > 0 {
|
||||||
|
buf.WriteString(values[0])
|
||||||
|
for i := 1; i < len(values); i++ {
|
||||||
|
buf.WriteString(", " + values[i])
|
||||||
|
}
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertificateText returns a human-readable string representation
|
||||||
|
// of the certificate cert. The format is similar to the OpenSSL
|
||||||
|
// way of printing certificates (not identical).
|
||||||
|
func CertificateText(cert *x509.Certificate) string {
|
||||||
|
var buf strings.Builder
|
||||||
|
|
||||||
|
buf.WriteString(color.Blue("\nCertificate:\n"))
|
||||||
|
if cert.SignatureAlgorithm != x509.UnknownSignatureAlgorithm {
|
||||||
|
buf.WriteString(color.Blue("%4sSignature Algorithm: ", "") + color.Bold(fmt.Sprintf("%s\n", cert.SignatureAlgorithm)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issuer information
|
||||||
|
buf.WriteString(color.Blue("%4sIssuer: ", ""))
|
||||||
|
printName(cert.Issuer.Names, &buf)
|
||||||
|
|
||||||
|
// Validity information
|
||||||
|
buf.WriteString(color.Blue("%4sValidity\n", ""))
|
||||||
|
buf.WriteString(color.Bold(fmt.Sprintf("%8sNot Before: %s\n", "", cert.NotBefore.Format(http.TimeFormat))))
|
||||||
|
buf.WriteString(color.Bold(fmt.Sprintf("%8sNot After : %s\n", "", cert.NotAfter.Format(http.TimeFormat))))
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
+52
-7
@@ -70,6 +70,7 @@ const (
|
|||||||
RegionSubSys = "region"
|
RegionSubSys = "region"
|
||||||
EtcdSubSys = "etcd"
|
EtcdSubSys = "etcd"
|
||||||
StorageClassSubSys = "storage_class"
|
StorageClassSubSys = "storage_class"
|
||||||
|
APISubSys = "api"
|
||||||
CompressionSubSys = "compression"
|
CompressionSubSys = "compression"
|
||||||
KmsVaultSubSys = "kms_vault"
|
KmsVaultSubSys = "kms_vault"
|
||||||
KmsKesSubSys = "kms_kes"
|
KmsKesSubSys = "kms_kes"
|
||||||
@@ -101,6 +102,7 @@ var SubSystems = set.CreateStringSet([]string{
|
|||||||
RegionSubSys,
|
RegionSubSys,
|
||||||
EtcdSubSys,
|
EtcdSubSys,
|
||||||
CacheSubSys,
|
CacheSubSys,
|
||||||
|
APISubSys,
|
||||||
StorageClassSubSys,
|
StorageClassSubSys,
|
||||||
CompressionSubSys,
|
CompressionSubSys,
|
||||||
KmsVaultSubSys,
|
KmsVaultSubSys,
|
||||||
@@ -128,6 +130,7 @@ var SubSystemsSingleTargets = set.CreateStringSet([]string{
|
|||||||
RegionSubSys,
|
RegionSubSys,
|
||||||
EtcdSubSys,
|
EtcdSubSys,
|
||||||
CacheSubSys,
|
CacheSubSys,
|
||||||
|
APISubSys,
|
||||||
StorageClassSubSys,
|
StorageClassSubSys,
|
||||||
CompressionSubSys,
|
CompressionSubSys,
|
||||||
KmsVaultSubSys,
|
KmsVaultSubSys,
|
||||||
@@ -196,6 +199,23 @@ func (kvs KVS) Empty() bool {
|
|||||||
return len(kvs) == 0
|
return len(kvs) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keys returns the list of keys for the current KVS
|
||||||
|
func (kvs KVS) Keys() []string {
|
||||||
|
var keys = make([]string, len(kvs))
|
||||||
|
var foundComment bool
|
||||||
|
for i := range kvs {
|
||||||
|
if kvs[i].Key == madmin.CommentKey {
|
||||||
|
foundComment = true
|
||||||
|
}
|
||||||
|
keys[i] = kvs[i].Key
|
||||||
|
}
|
||||||
|
// Comment KV not found, add it explicitly.
|
||||||
|
if !foundComment {
|
||||||
|
keys = append(keys, madmin.CommentKey)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
func (kvs KVS) String() string {
|
func (kvs KVS) String() string {
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
for _, kv := range kvs {
|
for _, kv := range kvs {
|
||||||
@@ -414,6 +434,26 @@ func LookupWorm() (bool, error) {
|
|||||||
return ParseBool(env.Get(EnvWorm, EnableOff))
|
return ParseBool(env.Get(EnvWorm, EnableOff))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge - merges a new config with all the
|
||||||
|
// missing values for default configs,
|
||||||
|
// returns a config.
|
||||||
|
func (c Config) Merge() Config {
|
||||||
|
cp := New()
|
||||||
|
for subSys, tgtKV := range c {
|
||||||
|
for tgt := range tgtKV {
|
||||||
|
ckvs := c[subSys][tgt]
|
||||||
|
for _, kv := range cp[subSys][Default] {
|
||||||
|
_, ok := c[subSys][tgt].Lookup(kv.Key)
|
||||||
|
if !ok {
|
||||||
|
ckvs.Set(kv.Key, kv.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cp[subSys][tgt] = ckvs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cp
|
||||||
|
}
|
||||||
|
|
||||||
// New - initialize a new server config.
|
// New - initialize a new server config.
|
||||||
func New() Config {
|
func New() Config {
|
||||||
srvCfg := make(Config)
|
srvCfg := make(Config)
|
||||||
@@ -581,9 +621,20 @@ func (c Config) SetKVS(s string, defaultKVS map[string]KVS) error {
|
|||||||
return Errorf("sub-system '%s' only supports single target", subSystemValue[0])
|
return Errorf("sub-system '%s' only supports single target", subSystemValue[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tgt := Default
|
||||||
|
subSys := subSystemValue[0]
|
||||||
|
if len(subSystemValue) == 2 {
|
||||||
|
tgt = subSystemValue[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := madmin.KvFields(inputs[1], defaultKVS[subSys].Keys())
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return Errorf("sub-system '%s' cannot have empty keys", subSys)
|
||||||
|
}
|
||||||
|
|
||||||
var kvs = KVS{}
|
var kvs = KVS{}
|
||||||
var prevK string
|
var prevK string
|
||||||
for _, v := range strings.Fields(inputs[1]) {
|
for _, v := range fields {
|
||||||
kv := strings.SplitN(v, KvSeparator, 2)
|
kv := strings.SplitN(v, KvSeparator, 2)
|
||||||
if len(kv) == 0 {
|
if len(kv) == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -604,12 +655,6 @@ func (c Config) SetKVS(s string, defaultKVS map[string]KVS) error {
|
|||||||
return Errorf("key '%s', cannot have empty value", kv[0])
|
return Errorf("key '%s', cannot have empty value", kv[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
tgt := Default
|
|
||||||
subSys := subSystemValue[0]
|
|
||||||
if len(subSystemValue) == 2 {
|
|
||||||
tgt = subSystemValue[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok := kvs.Lookup(Enable)
|
_, ok := kvs.Lookup(Enable)
|
||||||
// Check if state is required
|
// Check if state is required
|
||||||
_, enableRequired := defaultKVS[subSys].Lookup(Enable)
|
_, enableRequired := defaultKVS[subSys].Lookup(Enable)
|
||||||
|
|||||||
@@ -17,7 +17,96 @@
|
|||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/madmin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVFields(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
keys []string
|
||||||
|
expectedFields map[string]struct{}
|
||||||
|
}{
|
||||||
|
// No keys present
|
||||||
|
{
|
||||||
|
input: "",
|
||||||
|
keys: []string{"comment"},
|
||||||
|
expectedFields: map[string]struct{}{},
|
||||||
|
},
|
||||||
|
// No keys requested for tokenizing
|
||||||
|
{
|
||||||
|
input: `comment="Hi this is my comment ="`,
|
||||||
|
keys: []string{},
|
||||||
|
expectedFields: map[string]struct{}{},
|
||||||
|
},
|
||||||
|
// Single key requested and present
|
||||||
|
{
|
||||||
|
input: `comment="Hi this is my comment ="`,
|
||||||
|
keys: []string{"comment"},
|
||||||
|
expectedFields: map[string]struct{}{`comment="Hi this is my comment ="`: {}},
|
||||||
|
},
|
||||||
|
// Keys and input order of k=v is same.
|
||||||
|
{
|
||||||
|
input: `connection_string="host=localhost port=2832" comment="really long comment"`,
|
||||||
|
keys: []string{"connection_string", "comment"},
|
||||||
|
expectedFields: map[string]struct{}{
|
||||||
|
`connection_string="host=localhost port=2832"`: {},
|
||||||
|
`comment="really long comment"`: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Keys with spaces in between
|
||||||
|
{
|
||||||
|
input: `enable=on format=namespace connection_string=" host=localhost port=5432 dbname = cesnietor sslmode=disable" table=holicrayoli`,
|
||||||
|
keys: []string{"enable", "connection_string", "comment", "format", "table"},
|
||||||
|
expectedFields: map[string]struct{}{
|
||||||
|
`enable=on`: {},
|
||||||
|
`format=namespace`: {},
|
||||||
|
`connection_string=" host=localhost port=5432 dbname = cesnietor sslmode=disable"`: {},
|
||||||
|
`table=holicrayoli`: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// One of the keys is not present and order of input has changed.
|
||||||
|
{
|
||||||
|
input: `comment="really long comment" connection_string="host=localhost port=2832"`,
|
||||||
|
keys: []string{"connection_string", "comment", "format"},
|
||||||
|
expectedFields: map[string]struct{}{
|
||||||
|
`connection_string="host=localhost port=2832"`: {},
|
||||||
|
`comment="really long comment"`: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Incorrect delimiter, expected fields should be empty.
|
||||||
|
{
|
||||||
|
input: `comment:"really long comment" connection_string:"host=localhost port=2832"`,
|
||||||
|
keys: []string{"connection_string", "comment"},
|
||||||
|
expectedFields: map[string]struct{}{},
|
||||||
|
},
|
||||||
|
// Incorrect type of input v/s required keys.
|
||||||
|
{
|
||||||
|
input: `comme="really long comment" connection_str="host=localhost port=2832"`,
|
||||||
|
keys: []string{"connection_string", "comment"},
|
||||||
|
expectedFields: map[string]struct{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
gotFields := madmin.KvFields(test.input, test.keys)
|
||||||
|
if len(gotFields) != len(test.expectedFields) {
|
||||||
|
t.Errorf("Expected keys %d, found %d", len(test.expectedFields), len(gotFields))
|
||||||
|
}
|
||||||
|
found := true
|
||||||
|
for _, field := range gotFields {
|
||||||
|
_, ok := test.expectedFields[field]
|
||||||
|
found = found && ok
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Expected %s, got %s", test.expectedFields, gotFields)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidRegion(t *testing.T) {
|
func TestValidRegion(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ const (
|
|||||||
EnvRegionName = "MINIO_REGION_NAME"
|
EnvRegionName = "MINIO_REGION_NAME"
|
||||||
EnvPublicIPs = "MINIO_PUBLIC_IPS"
|
EnvPublicIPs = "MINIO_PUBLIC_IPS"
|
||||||
EnvEndpoints = "MINIO_ENDPOINTS"
|
EnvEndpoints = "MINIO_ENDPOINTS"
|
||||||
|
EnvFSOSync = "MINIO_FS_OSYNC"
|
||||||
|
|
||||||
|
// API sub-system
|
||||||
|
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
|
||||||
|
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
|
||||||
|
EnvAPIReadyDeadline = "MINIO_API_READY_DEADLINE"
|
||||||
|
|
||||||
EnvUpdate = "MINIO_UPDATE"
|
EnvUpdate = "MINIO_UPDATE"
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user