mirror of
https://github.com/pgsty/minio.git
synced 2026-08-14 10:43:15 +03:00
Compare commits
229 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 07a7f329e7 | |||
| c7ca791c58 | |||
| d9be8bc693 | |||
| 9fc7537f2a | |||
| f1b2462193 | |||
| 8fbf2b0b2a | |||
| c93157019f | |||
| e3b44c3829 | |||
| 978bd4e2c4 | |||
| bb942c7311 | |||
| 5d25b10f72 | |||
| eac02c04f7 | |||
| 1330e59307 | |||
| 2dd14c0b89 | |||
| 5b8975bf4b | |||
| 6f66f1a910 | |||
| deb3911f5e | |||
| 23a8411732 | |||
| ece0d4ac53 | |||
| 4c92bec619 | |||
| dcd63b4146 | |||
| 224b4f13b8 | |||
| 51a9d1bdb7 | |||
| b2db1e96e2 | |||
| ab7d3cd508 | |||
| 852fb320f7 | |||
| 8fb37a8417 |
@@ -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
|
||||||
+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 \
|
||||||
|
|||||||
+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 \
|
||||||
|
|||||||
+2
-2
@@ -57,7 +57,7 @@ RUN yarn test
|
|||||||
#-------------------------------------------------------------
|
#-------------------------------------------------------------
|
||||||
# Stage 3: Run Gateway Tests
|
# Stage 3: Run Gateway Tests
|
||||||
#-------------------------------------------------------------
|
#-------------------------------------------------------------
|
||||||
FROM ubuntu:16.04
|
FROM ubuntu:18.04
|
||||||
|
|
||||||
COPY --from=0 /go/src/github.com/minio/minio/minio /usr/bin/minio
|
COPY --from=0 /go/src/github.com/minio/minio/minio /usr/bin/minio
|
||||||
COPY buildscripts/gateway-tests.sh /usr/bin/gateway-tests.sh
|
COPY buildscripts/gateway-tests.sh /usr/bin/gateway-tests.sh
|
||||||
@@ -66,7 +66,7 @@ COPY mint /mint
|
|||||||
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
|
ENV GOPATH /usr/local/gopath
|
||||||
ENV PATH $GOPATH/bin:$GOROOT/bin:$PATH
|
ENV PATH $GOPATH/bin:$GOROOT/bin:$PATH
|
||||||
ENV SIMPLE_CI 1
|
ENV SIMPLE_CI 1
|
||||||
ENV MINT_ROOT_DIR /mint
|
ENV MINT_ROOT_DIR /mint
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ getdeps:
|
|||||||
ifeq ($(GOARCH),s390x)
|
ifeq ($(GOARCH),s390x)
|
||||||
@which staticcheck 1>/dev/null || (echo "Installing staticcheck" && GO111MODULE=off go get honnef.co/go/tools/cmd/staticcheck)
|
@which staticcheck 1>/dev/null || (echo "Installing staticcheck" && GO111MODULE=off go get honnef.co/go/tools/cmd/staticcheck)
|
||||||
else
|
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)
|
@which staticcheck 1>/dev/null || (echo "Installing staticcheck" && wget --quiet https://github.com/dominikh/go-tools/releases/download/2020.1.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
|
endif
|
||||||
@which misspell 1>/dev/null || (echo "Installing misspell" && GO111MODULE=off go get -u github.com/client9/misspell/cmd/misspell)
|
@which misspell 1>/dev/null || (echo "Installing misspell" && GO111MODULE=off go get -u github.com/client9/misspell/cmd/misspell)
|
||||||
|
|
||||||
@@ -71,19 +71,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 +101,4 @@ clean:
|
|||||||
@rm -rvf minio
|
@rm -rvf minio
|
||||||
@rm -rvf build
|
@rm -rvf build
|
||||||
@rm -rvf release
|
@rm -rvf release
|
||||||
|
@rm -rvf .verify*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 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)
|
||||||
|
|
||||||
@@ -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不等。
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ export class Login extends React.Component {
|
|||||||
<OpenIDLoginButton
|
<OpenIDLoginButton
|
||||||
className="btn openid-btn"
|
className="btn openid-btn"
|
||||||
clientId={this.state.clientId}
|
clientId={this.state.clientId}
|
||||||
authorizationEndpoint={this.state.discoveryDoc.authorization_endpoint}
|
authEp={this.state.discoveryDoc.authorization_endpoint}
|
||||||
|
authScopes={this.state.discoveryDoc.scopes_supported}
|
||||||
>
|
>
|
||||||
Log in with OpenID
|
Log in with OpenID
|
||||||
</OpenIDLoginButton>
|
</OpenIDLoginButton>
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export class OpenIDLogin extends React.Component {
|
|||||||
|
|
||||||
const authURL = buildOpenIDAuthURL(
|
const authURL = buildOpenIDAuthURL(
|
||||||
this.state.discoveryDoc.authorization_endpoint,
|
this.state.discoveryDoc.authorization_endpoint,
|
||||||
|
this.state.discoveryDoc.scopes_supported,
|
||||||
redirectURI,
|
redirectURI,
|
||||||
this.state.clientID,
|
this.state.clientID,
|
||||||
nonce
|
nonce
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export class OpenIDLoginButton extends React.Component {
|
|||||||
|
|
||||||
handleClick(event) {
|
handleClick(event) {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
const { authorizationEndpoint, clientId } = this.props
|
const { authEp, authScopes, clientId } = this.props
|
||||||
|
|
||||||
let redirectURI = window.location.href.split("#")[0]
|
let redirectURI = window.location.href.split("#")[0]
|
||||||
if (redirectURI.endsWith('/')) {
|
if (redirectURI.endsWith('/')) {
|
||||||
@@ -40,7 +40,7 @@ export class OpenIDLoginButton extends React.Component {
|
|||||||
const nonce = getRandomString(16)
|
const nonce = getRandomString(16)
|
||||||
storage.setItem(OPEN_ID_NONCE_KEY, nonce)
|
storage.setItem(OPEN_ID_NONCE_KEY, nonce)
|
||||||
|
|
||||||
const authURL = buildOpenIDAuthURL(authorizationEndpoint, redirectURI, clientId, nonce)
|
const authURL = buildOpenIDAuthURL(authEp, authScopes, redirectURI, clientId, nonce)
|
||||||
window.location = authURL
|
window.location = authURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -16,14 +16,13 @@
|
|||||||
|
|
||||||
export const OPEN_ID_NONCE_KEY = 'openIDKey'
|
export const OPEN_ID_NONCE_KEY = 'openIDKey'
|
||||||
|
|
||||||
export const buildOpenIDAuthURL = (authorizationEndpoint, redirectURI, clientID, nonce) => {
|
export const buildOpenIDAuthURL = (authEp, authScopes, redirectURI, clientID, nonce) => {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
params.set("response_type", "id_token")
|
params.set("response_type", "id_token")
|
||||||
params.set("scope", "openid")
|
params.set("scope", authScopes.join(" "))
|
||||||
params.set("client_id", clientID)
|
params.set("client_id", clientID)
|
||||||
params.set("redirect_uri", redirectURI)
|
params.set("redirect_uri", redirectURI)
|
||||||
params.set("nonce", nonce)
|
params.set("nonce", nonce)
|
||||||
|
|
||||||
return `${authorizationEndpoint}?${params.toString()}`
|
return `${authEp}?${params.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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) {
|
||||||
@@ -53,7 +57,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() {
|
||||||
@@ -69,6 +86,15 @@ export class ObjectActions extends React.Component {
|
|||||||
>
|
>
|
||||||
<i className="fas fa-share-alt" />
|
<i className="fas fa-share-alt" />
|
||||||
</a>
|
</a>
|
||||||
|
{getDataType(object.name, object.contentType) == "image" && (
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
className="fiad-action"
|
||||||
|
onClick={this.showPreviewModal.bind(this)}
|
||||||
|
>
|
||||||
|
<i className="far fa-file-image" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
<a
|
<a
|
||||||
href=""
|
href=""
|
||||||
className="fiad-action"
|
className="fiad-action"
|
||||||
@@ -77,14 +103,22 @@ export class ObjectActions extends React.Component {
|
|||||||
<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 +128,17 @@ 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 {
|
||||||
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,65 @@
|
|||||||
|
/*
|
||||||
|
* 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: "",
|
||||||
|
}
|
||||||
|
props.getObjectURL(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
|
||||||
@@ -66,6 +66,49 @@ describe("ObjectActions", () => {
|
|||||||
expect(deleteObject).toHaveBeenCalledWith("obj1")
|
expect(deleteObject).toHaveBeenCalledWith("obj1")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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(2) // 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(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ 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"
|
||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
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"
|
||||||
|
|
||||||
export const SET_LIST = "objects/SET_LIST"
|
export const SET_LIST = "objects/SET_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,7 +221,7 @@ 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 currentBucket = getCurrentBucket(getState())
|
const currentBucket = getCurrentBucket(getState())
|
||||||
const currentPrefix = getCurrentPrefix(getState())
|
const currentPrefix = getCurrentPrefix(getState())
|
||||||
const objectName = `${currentPrefix}${object}`
|
const objectName = `${currentPrefix}${object}`
|
||||||
@@ -232,22 +232,22 @@ export const shareObject = (object, days, hours, minutes) => {
|
|||||||
host: location.host,
|
host: location.host,
|
||||||
bucket: currentBucket,
|
bucket: currentBucket,
|
||||||
object: objectName,
|
object: objectName,
|
||||||
expiry: expiry
|
expiry: expiry,
|
||||||
})
|
})
|
||||||
.then(obj => {
|
.then((obj) => {
|
||||||
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,7 +265,7 @@ export const shareObject = (object, days, hours, minutes) => {
|
|||||||
dispatch(
|
dispatch(
|
||||||
alertActions.set({
|
alertActions.set({
|
||||||
type: "success",
|
type: "success",
|
||||||
message: `Object shared.`
|
message: `Object shared.`,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -276,18 +276,17 @@ export const showShareObject = (object, url) => ({
|
|||||||
type: SET_SHARE_OBJECT,
|
type: SET_SHARE_OBJECT,
|
||||||
show: true,
|
show: true,
|
||||||
object,
|
object,
|
||||||
url
|
url,
|
||||||
})
|
})
|
||||||
|
|
||||||
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,52 +294,73 @@ 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 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()
|
const state = getState()
|
||||||
const req = {
|
const req = {
|
||||||
bucketName: getCurrentBucket(state),
|
bucketName: getCurrentBucket(state),
|
||||||
prefix: getCurrentPrefix(state),
|
prefix: getCurrentPrefix(state),
|
||||||
objects: getCheckedList(state)
|
objects: getCheckedList(state),
|
||||||
}
|
}
|
||||||
if (!web.LoggedIn()) {
|
if (!web.LoggedIn()) {
|
||||||
const requestUrl = location.origin + "/minio/zip?token="
|
const requestUrl = location.origin + "/minio/zip?token="
|
||||||
@@ -348,17 +368,15 @@ export const downloadCheckedObjects = () => {
|
|||||||
} else {
|
} else {
|
||||||
return web
|
return web
|
||||||
.CreateURLToken()
|
.CreateURLToken()
|
||||||
.then(res => {
|
.then((res) => {
|
||||||
const requestUrl = `${
|
const requestUrl = `${location.origin}${minioBrowserPrefix}/zip?token=${res.token}`
|
||||||
location.origin
|
|
||||||
}${minioBrowserPrefix}/zip?token=${res.token}`
|
|
||||||
downloadZip(requestUrl, req, dispatch)
|
downloadZip(requestUrl, req, dispatch)
|
||||||
})
|
})
|
||||||
.catch(err =>
|
.catch((err) =>
|
||||||
dispatch(
|
dispatch(
|
||||||
alertActions.set({
|
alertActions.set({
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: err.message
|
message: err.message,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -374,11 +392,11 @@ 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 ? "-" : ""
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
checks = ["all", "-ST1005", "-ST1000", "-SA4000", "-SA9004", "-SA1019", "-SA1008", "-U1000", "-ST1003", "-ST1018"]
|
||||||
+31
-31
File diff suppressed because one or more lines are too long
@@ -9,7 +9,7 @@ function _init() {
|
|||||||
export CGO_ENABLED=0
|
export CGO_ENABLED=0
|
||||||
|
|
||||||
## List of architectures and OS to test coss compilation.
|
## List of architectures and OS to test coss compilation.
|
||||||
SUPPORTED_OSARCH="linux/ppc64le linux/arm64 linux/s390x darwin/amd64 freebsd/amd64"
|
SUPPORTED_OSARCH="linux/ppc64le linux/arm64 linux/s390x darwin/amd64 freebsd/amd64 windows/amd64 linux/arm linux/386"
|
||||||
}
|
}
|
||||||
|
|
||||||
function _build() {
|
function _build() {
|
||||||
@@ -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
|
||||||
|
|||||||
+37
-139
@@ -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()
|
||||||
@@ -74,140 +90,22 @@ function __init__()
|
|||||||
echo '{"version": "3", "credential": {"accessKey": "minio", "secretKey": "minio123"}, "region": "us-east-1"}' > "$MINIO_CONFIG_DIR/config.json"
|
echo '{"version": "3", "credential": {"accessKey": "minio", "secretKey": "minio123"}, "region": "us-east-1"}' > "$MINIO_CONFIG_DIR/config.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
function perform_test_1() {
|
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
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Testing in Distributed Erasure setup healing test case 1"
|
echo "Testing Distributed Erasure setup healing of drives"
|
||||||
echo "Remove the contents of the disks belonging to '2' erasure set"
|
echo "Remove the contents of the disks belonging to '${1}' erasure set"
|
||||||
|
|
||||||
rm -rf ${WORK_DIR}/2/*/
|
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
|
|
||||||
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
|
|
||||||
echo "FAILED"
|
|
||||||
purge "$WORK_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function perform_test_2() {
|
|
||||||
minio_pids=( $(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
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Testing in Distributed Erasure setup healing test case 2"
|
|
||||||
echo "Remove the contents of the disks belonging to '1' erasure set"
|
|
||||||
|
|
||||||
rm -rf ${WORK_DIR}/1/*/
|
|
||||||
|
|
||||||
minio_pids=( $(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
|
|
||||||
done
|
|
||||||
|
|
||||||
rv=$(check_online)
|
|
||||||
if [ "$rv" == "1" ]; then
|
|
||||||
for pid in "${minio_pids[@]}"; do
|
|
||||||
kill -9 "$pid"
|
|
||||||
done
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
function perform_test_3() {
|
|
||||||
minio_pids=( $(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
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Testing in Distributed Erasure setup healing test case 3"
|
|
||||||
echo "Remove the contents of the disks belonging to '3' erasure set"
|
|
||||||
|
|
||||||
rm -rf ${WORK_DIR}/3/*/
|
|
||||||
|
|
||||||
minio_pids=( $(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
|
|
||||||
done
|
|
||||||
|
|
||||||
rv=$(check_online)
|
|
||||||
if [ "$rv" == "1" ]; then
|
|
||||||
for pid in "${minio_pids[@]}"; do
|
|
||||||
kill -9 "$pid"
|
|
||||||
done
|
|
||||||
for i in $(seq 1 3); do
|
|
||||||
echo "server$i log:"
|
|
||||||
cat "${WORK_DIR}/dist-minio-$[8000+$i].log"
|
|
||||||
done
|
done
|
||||||
echo "FAILED"
|
echo "FAILED"
|
||||||
purge "$WORK_DIR"
|
purge "$WORK_DIR"
|
||||||
@@ -217,9 +115,9 @@ function perform_test_3() {
|
|||||||
|
|
||||||
function main()
|
function main()
|
||||||
{
|
{
|
||||||
perform_test_1
|
perform_test "2"
|
||||||
perform_test_2
|
perform_test "1"
|
||||||
perform_test_3
|
perform_test "3"
|
||||||
}
|
}
|
||||||
|
|
||||||
( __init__ "$@" && main "$@" )
|
( __init__ "$@" && main "$@" )
|
||||||
|
|||||||
@@ -35,50 +35,45 @@ 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, "DelConfigKVHandler")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, 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 +98,22 @@ 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, "SetConfigKVHandler")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, 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 +151,17 @@ 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, "GetConfigKVHandler")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -195,7 +184,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)
|
||||||
@@ -208,7 +197,7 @@ 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, "ClearConfigHistoryKVHandler")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
_, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -243,7 +232,7 @@ func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *
|
|||||||
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, "RestoreConfigHistoryKVHandler")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
_, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -289,7 +278,7 @@ func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r
|
|||||||
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, "ListConfigHistoryKVHandler")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -313,7 +302,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 +312,11 @@ 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, "HelpConfigKVHandler")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
_, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -349,28 +338,22 @@ 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, "SetConfigHandler")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, 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 +386,18 @@ 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, "GetConfigHandler")
|
||||||
|
|
||||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -471,7 +454,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)
|
||||||
|
|||||||
+264
-58
@@ -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,7 +51,7 @@ 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")
|
||||||
|
|
||||||
@@ -60,12 +60,6 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
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 +87,17 @@ 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)
|
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 +110,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,7 +119,7 @@ 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")
|
||||||
|
|
||||||
@@ -151,7 +146,7 @@ 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")
|
||||||
|
|
||||||
@@ -194,7 +189,7 @@ 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")
|
||||||
|
|
||||||
@@ -221,7 +216,7 @@ 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")
|
||||||
|
|
||||||
@@ -245,7 +240,7 @@ 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")
|
||||||
|
|
||||||
@@ -280,7 +275,7 @@ 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")
|
||||||
|
|
||||||
@@ -289,12 +284,6 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
|||||||
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,7 +308,7 @@ 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")
|
||||||
|
|
||||||
@@ -328,12 +317,6 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
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 +361,197 @@ 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")
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, cred.AccessKey)
|
||||||
|
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")
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if cred.AccessKey != user || cred.ParentUser != user {
|
||||||
|
// The service account belongs to another user but return not found error to mitigate brute force attacks.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
|
||||||
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,7 +561,58 @@ 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) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "InfoCannedPolicy")
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCannedPolicies - GET /minio/admin/v3/list-canned-policies
|
||||||
func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListCannedPolicies")
|
ctx := newContext(r, w, "ListCannedPolicies")
|
||||||
|
|
||||||
@@ -412,7 +627,16 @@ func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = json.NewEncoder(w).Encode(policies); err != nil {
|
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)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -420,7 +644,7 @@ 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>
|
// 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")
|
||||||
|
|
||||||
@@ -432,12 +656,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,7 +670,7 @@ 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")
|
||||||
|
|
||||||
@@ -464,12 +682,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,7 +720,7 @@ 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")
|
||||||
|
|
||||||
@@ -522,15 +734,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
|
||||||
}
|
}
|
||||||
|
|||||||
+218
-258
@@ -21,7 +21,6 @@ import (
|
|||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -42,13 +41,11 @@ 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"
|
||||||
)
|
)
|
||||||
@@ -98,7 +95,7 @@ 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) {
|
||||||
@@ -141,9 +138,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,7 +174,7 @@ func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceActionHandler - POST /minio/admin/v2/service?action={action}
|
// ServiceActionHandler - 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) ServiceActionHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -266,7 +263,7 @@ 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) {
|
||||||
@@ -291,7 +288,7 @@ 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) {
|
||||||
@@ -377,155 +374,6 @@ func (a adminAPIHandlers) AccountingUsageInfoHandler(w http.ResponseWriter, r *h
|
|||||||
writeSuccessResponseJSON(w, usageInfoJSON)
|
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,
|
||||||
@@ -621,7 +469,7 @@ 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) {
|
||||||
@@ -717,7 +565,7 @@ 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) {
|
||||||
@@ -789,7 +637,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 +647,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.
|
||||||
//
|
//
|
||||||
@@ -982,13 +830,24 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Aggregate healing result
|
// Aggregate healing result
|
||||||
var aggregatedHealStateResult = madmin.BgHealState{}
|
var aggregatedHealStateResult = madmin.BgHealState{
|
||||||
|
ScannedItemsCount: bgHealStates[0].ScannedItemsCount,
|
||||||
|
LastHealActivity: bgHealStates[0].LastHealActivity,
|
||||||
|
NextHealRound: bgHealStates[0].NextHealRound,
|
||||||
|
}
|
||||||
|
|
||||||
|
bgHealStates = bgHealStates[1:]
|
||||||
|
|
||||||
for _, state := range bgHealStates {
|
for _, state := range bgHealStates {
|
||||||
aggregatedHealStateResult.ScannedItemsCount += state.ScannedItemsCount
|
aggregatedHealStateResult.ScannedItemsCount += state.ScannedItemsCount
|
||||||
if aggregatedHealStateResult.LastHealActivity.Before(state.LastHealActivity) {
|
if !state.LastHealActivity.IsZero() && aggregatedHealStateResult.LastHealActivity.Before(state.LastHealActivity) {
|
||||||
aggregatedHealStateResult.LastHealActivity = 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 {
|
||||||
@@ -1081,7 +940,7 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
|||||||
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,7 +963,7 @@ 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) {
|
||||||
@@ -1121,16 +980,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 +994,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 +1013,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1191,20 +1047,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 +1070,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,13 +1082,13 @@ 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, "KMSKeyStatusHandler")
|
||||||
|
|
||||||
@@ -1303,64 +1157,169 @@ 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.OBDInfoAdminAction)
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerHardwareInfoAdminAction)
|
|
||||||
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
|
|
||||||
jsonBytes, err := json.Marshal(cpus)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reply with cpu hardware information (across nodes in a
|
|
||||||
// distributed setup) as json.
|
|
||||||
writeSuccessResponseJSON(w, jsonBytes)
|
|
||||||
|
|
||||||
case madmin.NETWORK:
|
|
||||||
// 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
|
|
||||||
jsonBytes, err := json.Marshal(networks)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reply with cpu network information (across nodes in a
|
|
||||||
// distributed setup) as json.
|
|
||||||
writeSuccessResponseJSON(w, jsonBytes)
|
|
||||||
|
|
||||||
default:
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCommonHeaders(w)
|
||||||
|
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 {
|
||||||
|
errResp(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deadlinedCtx, cancel := context.WithTimeout(ctx, deadline)
|
||||||
|
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
nsLock := objectAPI.NewNSLock(deadlinedCtx, minioMetaBucket, "obd-in-progress")
|
||||||
|
if err := nsLock.GetLock(newDynamicTimeout(deadline, deadline)); err != nil { // returns a locked lock
|
||||||
|
errResp(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer nsLock.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(obdInfoCh)
|
||||||
|
|
||||||
|
if cpu, ok := vars["syscpu"]; ok && cpu == "true" {
|
||||||
|
cpuInfo := getLocalCPUOBDInfo(deadlinedCtx)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Notify all other MinIO peers to report drive obd numbers
|
||||||
|
driveOBDs := globalNotificationSys.DriveOBDInfo(deadlinedCtx)
|
||||||
|
obdInfo.Perf.DriveInfo = append(obdInfo.Perf.DriveInfo, driveOBDs...)
|
||||||
|
|
||||||
|
partialWrite(obdInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if net, ok := vars["perfnet"]; ok && net == "true" && globalIsDistXL {
|
||||||
|
obdInfo.Perf.Net = append(obdInfo.Perf.Net, globalNotificationSys.NetOBDInfo(deadlinedCtx))
|
||||||
|
obdInfo.Perf.Net = append(obdInfo.Perf.Net, globalNotificationSys.DispatchNetOBDInfo(deadlinedCtx)...)
|
||||||
|
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) {
|
||||||
@@ -1398,10 +1357,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)
|
||||||
@@ -1425,7 +1384,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 +1394,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1496,7 +1455,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
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 +1479,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 +1532,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 +1599,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
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-11
@@ -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
|
||||||
}
|
}
|
||||||
@@ -70,9 +71,9 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
|||||||
globalConfigSys = NewConfigSys()
|
globalConfigSys = NewConfigSys()
|
||||||
|
|
||||||
globalIAMSys = NewIAMSys()
|
globalIAMSys = NewIAMSys()
|
||||||
globalIAMSys.Init(objLayer)
|
globalIAMSys.Init(ctx, objLayer)
|
||||||
|
|
||||||
buckets, err := objLayer.ListBuckets(context.Background())
|
buckets, err := objLayer.ListBuckets(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -103,20 +104,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 +192,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,10 +263,14 @@ 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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
defer adminTestBed.TearDown()
|
defer adminTestBed.TearDown()
|
||||||
|
|
||||||
// Initialize admin peers to make admin RPC calls.
|
// Initialize admin peers to make admin RPC calls.
|
||||||
@@ -313,12 +321,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)
|
||||||
|
|||||||
+137
-33
@@ -106,12 +106,12 @@ 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)
|
ticker := time.NewTicker(time.Minute * 5)
|
||||||
@@ -127,7 +127,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 +183,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
|
||||||
@@ -306,6 +306,12 @@ func (ahs *allHealState) PopHealStatusJSON(path string,
|
|||||||
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 {
|
||||||
@@ -316,11 +322,9 @@ type healSequence struct {
|
|||||||
path string
|
path string
|
||||||
|
|
||||||
// List of entities (format, buckets, objects) to heal
|
// List of entities (format, buckets, objects) to heal
|
||||||
sourceCh chan string
|
sourceCh chan healSource
|
||||||
|
|
||||||
// Report healing progress, false if this is a background
|
// Report healing progress
|
||||||
// healing since currently there is no entity which will
|
|
||||||
// receive realtime healing status
|
|
||||||
reportProgress bool
|
reportProgress bool
|
||||||
|
|
||||||
// time at which heal sequence was started
|
// time at which heal sequence was started
|
||||||
@@ -352,14 +356,23 @@ type healSequence 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
|
||||||
@@ -369,7 +382,7 @@ func newHealSequence(bucket, objPrefix, clientAddr string,
|
|||||||
|
|
||||||
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 := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||||
|
|
||||||
return &healSequence{
|
return &healSequence{
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
@@ -390,9 +403,83 @@ func newHealSequence(bucket, objPrefix, clientAddr string,
|
|||||||
traverseAndHealDoneCh: make(chan error),
|
traverseAndHealDoneCh: make(chan error),
|
||||||
stopSignalCh: make(chan struct{}),
|
stopSignalCh: make(chan struct{}),
|
||||||
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 {
|
||||||
@@ -548,14 +635,38 @@ 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)
|
var respCh = make(chan healResult)
|
||||||
defer close(respCh)
|
defer close(respCh)
|
||||||
// Send heal request
|
// Send heal request
|
||||||
globalBackgroundHealRoutine.queueHealTask(healTask{path: path, responseCh: respCh, opts: h.settings})
|
task := healTask{
|
||||||
|
path: source.path,
|
||||||
|
responseCh: respCh,
|
||||||
|
opts: h.settings,
|
||||||
|
}
|
||||||
|
if source.opts != nil {
|
||||||
|
task.opts = *source.opts
|
||||||
|
}
|
||||||
|
globalBackgroundHealRoutine.queueHealTask(task)
|
||||||
// Wait for answer and push result to the client
|
// Wait for answer and push result to the client
|
||||||
res := <-respCh
|
res := <-respCh
|
||||||
if !h.reportProgress {
|
if !h.reportProgress {
|
||||||
|
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 && !isErrObjectNotFound(res.err) {
|
||||||
|
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]++
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
res.result.Type = healType
|
res.result.Type = healType
|
||||||
@@ -575,8 +686,6 @@ func (h *healSequence) queueHealTask(path string, healType madmin.HealItemType)
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 +693,28 @@ func (h *healSequence) healItemsFromSourceCh() error {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case path := <-h.sourceCh:
|
case source := <-h.sourceCh:
|
||||||
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.traverseAndHealDoneCh:
|
||||||
return nil
|
return nil
|
||||||
case <-GlobalServiceDoneCh:
|
case <-h.ctx.Done():
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -632,11 +741,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)
|
||||||
}
|
}
|
||||||
@@ -673,12 +777,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 +806,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 +848,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 +871,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 +889,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)
|
||||||
}
|
}
|
||||||
|
|||||||
+160
-120
@@ -24,9 +24,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
adminPathPrefix = minioReservedBucketPath + "/admin"
|
adminPathPrefix = minioReservedBucketPath + "/admin"
|
||||||
adminAPIVersion = madmin.AdminAPIVersion
|
adminAPIVersionV2 = madmin.AdminAPIVersionV2
|
||||||
adminAPIVersionPrefix = SlashSeparator + madmin.AdminAPIVersion
|
adminAPIVersion = 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,127 +43,165 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
|
|||||||
|
|
||||||
/// Service operations
|
/// Service operations
|
||||||
|
|
||||||
// Restart and stop MinIO service.
|
adminVersions := []string{
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix+"/service").HandlerFunc(httpTraceAll(adminAPI.ServiceActionHandler)).Queries("action", "{action:.*}")
|
adminAPIVersionPrefix,
|
||||||
// Update MinIO servers.
|
adminAPIVersionV2Prefix,
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix+"/update").HandlerFunc(httpTraceAll(adminAPI.ServerUpdateHandler)).Queries("updateURL", "{updateURL:.*}")
|
|
||||||
|
|
||||||
// Info operations
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
|
|
||||||
// Harware Info operations
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/hardware").HandlerFunc(httpTraceAll(adminAPI.ServerHardwareInfoHandler)).Queries("hwType", "{hwType:.*}")
|
|
||||||
|
|
||||||
// StorageInfo operations
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/storageinfo").HandlerFunc(httpTraceAll(adminAPI.StorageInfoHandler))
|
|
||||||
// DataUsageInfo operations
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/datausageinfo").HandlerFunc(httpTraceAll(adminAPI.DataUsageInfoHandler))
|
|
||||||
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/accountingusageinfo").HandlerFunc(httpTraceAll(adminAPI.AccountingUsageInfoHandler))
|
|
||||||
|
|
||||||
if globalIsDistXL || globalIsXL {
|
|
||||||
/// Heal operations
|
|
||||||
|
|
||||||
// Heal processing endpoint.
|
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix + "/heal/").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix + "/heal/{bucket}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix + "/heal/{bucket}/{prefix:.*}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
|
||||||
|
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix + "/background-heal/status").HandlerFunc(httpTraceAll(adminAPI.BackgroundHealStatusHandler))
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminAPIVersionPrefix+"/profiling/start").HandlerFunc(httpTraceAll(adminAPI.StartProfilingHandler)).
|
|
||||||
Queries("profilerType", "{profilerType:.*}")
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/profiling/download").HandlerFunc(httpTraceAll(adminAPI.DownloadProfilingHandler))
|
|
||||||
|
|
||||||
// Config KV operations.
|
|
||||||
if enableConfigOps {
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/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.MethodDelete).Path(adminAPIVersionPrefix + "/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
|
for _, adminVersion := range adminVersions {
|
||||||
if enableConfigOps {
|
// Restart and stop MinIO service.
|
||||||
// Get config
|
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/service").HandlerFunc(httpTraceAll(adminAPI.ServiceActionHandler)).Queries("action", "{action:.*}")
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
// Update MinIO servers.
|
||||||
// Set config
|
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update").HandlerFunc(httpTraceAll(adminAPI.ServerUpdateHandler)).Queries("updateURL", "{updateURL:.*}")
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix + "/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
|
|
||||||
|
// Info operations
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
|
||||||
|
|
||||||
|
// StorageInfo operations
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/storageinfo").HandlerFunc(httpTraceAll(adminAPI.StorageInfoHandler))
|
||||||
|
// DataUsageInfo operations
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/datausageinfo").HandlerFunc(httpTraceAll(adminAPI.DataUsageInfoHandler))
|
||||||
|
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountingusageinfo").HandlerFunc(httpTraceAll(adminAPI.AccountingUsageInfoHandler))
|
||||||
|
|
||||||
|
if globalIsDistXL || globalIsXL {
|
||||||
|
/// Heal operations
|
||||||
|
|
||||||
|
// Heal processing endpoint.
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}/{prefix:.*}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||||
|
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/background-heal/status").HandlerFunc(httpTraceAll(adminAPI.BackgroundHealStatusHandler))
|
||||||
|
|
||||||
|
/// Health operations
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profiling operations
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/profiling/start").HandlerFunc(httpTraceAll(adminAPI.StartProfilingHandler)).
|
||||||
|
Queries("profilerType", "{profilerType:.*}")
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/profiling/download").HandlerFunc(httpTraceAll(adminAPI.DownloadProfilingHandler))
|
||||||
|
|
||||||
|
// Config KV operations.
|
||||||
|
if enableConfigOps {
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-config-kv").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKVHandler)).Queries("key", "{key:.*}")
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/set-config-kv").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKVHandler))
|
||||||
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/del-config-kv").HandlerFunc(httpTraceHdrs(adminAPI.DelConfigKVHandler))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// Get config
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
||||||
|
// Set config
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
|
||||||
|
}
|
||||||
|
|
||||||
|
if enableIAMOps {
|
||||||
|
// -- IAM APIs --
|
||||||
|
|
||||||
|
// Add policy IAM
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}")
|
||||||
|
|
||||||
|
// Add user IAM
|
||||||
|
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
|
||||||
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
|
||||||
|
|
||||||
|
// Set user or group policy
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-user-or-group-policy").
|
||||||
|
HandlerFunc(httpTraceHdrs(adminAPI.SetPolicyForUserOrGroup)).
|
||||||
|
Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}")
|
||||||
|
|
||||||
|
// Remove user IAM
|
||||||
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
|
||||||
|
|
||||||
|
// List users
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))
|
||||||
|
|
||||||
|
// User info
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/user-info").HandlerFunc(httpTraceHdrs(adminAPI.GetUserInfo)).Queries("accessKey", "{accessKey:.*}")
|
||||||
|
|
||||||
|
// Add/Remove members from group
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/update-group-members").HandlerFunc(httpTraceHdrs(adminAPI.UpdateGroupMembers))
|
||||||
|
|
||||||
|
// Get Group
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/group").HandlerFunc(httpTraceHdrs(adminAPI.GetGroup)).Queries("group", "{group:.*}")
|
||||||
|
|
||||||
|
// List Groups
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/groups").HandlerFunc(httpTraceHdrs(adminAPI.ListGroups))
|
||||||
|
|
||||||
|
// Set Group Status
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(httpTraceHdrs(adminAPI.SetGroupStatus)).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Top APIs --
|
||||||
|
// Top locks
|
||||||
|
if globalIsDistXL {
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/top/locks").HandlerFunc(httpTraceHdrs(adminAPI.TopLocksHandler))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP Trace
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/trace").HandlerFunc(adminAPI.TraceHandler)
|
||||||
|
|
||||||
|
// Console Logs
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/log").HandlerFunc(httpTraceAll(adminAPI.ConsoleLogHandler))
|
||||||
|
|
||||||
|
// -- KMS APIs --
|
||||||
|
//
|
||||||
|
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 enableIAMOps {
|
|
||||||
// -- IAM APIs --
|
|
||||||
|
|
||||||
// Add policy IAM
|
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix+"/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name",
|
|
||||||
"{name:.*}")
|
|
||||||
|
|
||||||
// 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(adminAPIVersionPrefix+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}")
|
|
||||||
|
|
||||||
// Remove policy IAM
|
|
||||||
adminRouter.Methods(http.MethodDelete).Path(adminAPIVersionPrefix+"/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
|
|
||||||
|
|
||||||
// Set user or group policy
|
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix+"/set-user-or-group-policy").
|
|
||||||
HandlerFunc(httpTraceHdrs(adminAPI.SetPolicyForUserOrGroup)).
|
|
||||||
Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}")
|
|
||||||
|
|
||||||
// Remove user IAM
|
|
||||||
adminRouter.Methods(http.MethodDelete).Path(adminAPIVersionPrefix+"/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
|
|
||||||
|
|
||||||
// List users
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))
|
|
||||||
|
|
||||||
// User info
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/user-info").HandlerFunc(httpTraceHdrs(adminAPI.GetUserInfo)).Queries("accessKey", "{accessKey:.*}")
|
|
||||||
|
|
||||||
// Add/Remove members from group
|
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix + "/update-group-members").HandlerFunc(httpTraceHdrs(adminAPI.UpdateGroupMembers))
|
|
||||||
|
|
||||||
// Get Group
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/group").HandlerFunc(httpTraceHdrs(adminAPI.GetGroup)).Queries("group", "{group:.*}")
|
|
||||||
|
|
||||||
// List Groups
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/groups").HandlerFunc(httpTraceHdrs(adminAPI.ListGroups))
|
|
||||||
|
|
||||||
// Set Group Status
|
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix+"/set-group-status").HandlerFunc(httpTraceHdrs(adminAPI.SetGroupStatus)).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
|
|
||||||
|
|
||||||
// List policies
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Top APIs --
|
|
||||||
// Top locks
|
|
||||||
if globalIsDistXL {
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/top/locks").HandlerFunc(httpTraceHdrs(adminAPI.TopLocksHandler))
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP Trace
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/trace").HandlerFunc(adminAPI.TraceHandler)
|
|
||||||
|
|
||||||
// Console Logs
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/log").HandlerFunc(httpTraceAll(adminAPI.ConsoleLogHandler))
|
|
||||||
|
|
||||||
// -- KMS APIs --
|
|
||||||
//
|
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/kms/key/status").HandlerFunc(httpTraceAll(adminAPI.KMSKeyStatusHandler))
|
|
||||||
|
|
||||||
// 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))
|
||||||
adminRouter.MethodNotAllowedHandler = http.HandlerFunc(httpTraceAll(errorResponseHandler))
|
adminRouter.MethodNotAllowedHandler = 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
|
||||||
|
|||||||
+47
-15
@@ -25,7 +25,6 @@ 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"
|
||||||
@@ -122,7 +121,8 @@ const (
|
|||||||
ErrMissingCredTag
|
ErrMissingCredTag
|
||||||
ErrCredMalformed
|
ErrCredMalformed
|
||||||
ErrInvalidRegion
|
ErrInvalidRegion
|
||||||
ErrInvalidService
|
ErrInvalidServiceS3
|
||||||
|
ErrInvalidServiceSTS
|
||||||
ErrInvalidRequestVersion
|
ErrInvalidRequestVersion
|
||||||
ErrMissingSignTag
|
ErrMissingSignTag
|
||||||
ErrMissingSignHeadersTag
|
ErrMissingSignHeadersTag
|
||||||
@@ -150,6 +150,7 @@ const (
|
|||||||
ErrBadRequest
|
ErrBadRequest
|
||||||
ErrKeyTooLongError
|
ErrKeyTooLongError
|
||||||
ErrInvalidBucketObjectLockConfiguration
|
ErrInvalidBucketObjectLockConfiguration
|
||||||
|
ErrObjectLockConfigurationNotFound
|
||||||
ErrObjectLockConfigurationNotAllowed
|
ErrObjectLockConfigurationNotAllowed
|
||||||
ErrNoSuchObjectLockConfiguration
|
ErrNoSuchObjectLockConfiguration
|
||||||
ErrObjectLocked
|
ErrObjectLocked
|
||||||
@@ -210,6 +211,7 @@ const (
|
|||||||
ErrInvalidResourceName
|
ErrInvalidResourceName
|
||||||
ErrServerNotInitialized
|
ErrServerNotInitialized
|
||||||
ErrOperationTimedOut
|
ErrOperationTimedOut
|
||||||
|
ErrOperationMaxedOut
|
||||||
ErrInvalidRequest
|
ErrInvalidRequest
|
||||||
// MinIO storage class error codes
|
// MinIO storage class error codes
|
||||||
ErrInvalidStorageClass
|
ErrInvalidStorageClass
|
||||||
@@ -332,19 +334,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{
|
||||||
@@ -652,9 +663,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,9 +773,14 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Bucket is missing ObjectLockConfiguration",
|
Description: "Bucket is missing ObjectLockConfiguration",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
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,
|
||||||
},
|
},
|
||||||
ErrNoSuchObjectLockConfiguration: {
|
ErrNoSuchObjectLockConfiguration: {
|
||||||
@@ -1078,6 +1099,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 +1599,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",
|
||||||
@@ -1641,7 +1677,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
|
||||||
@@ -1769,6 +1805,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:
|
||||||
@@ -1877,12 +1915,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.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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().Set(xhttp.AmzMpPartsCount, 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())
|
||||||
|
|||||||
+16
-3
@@ -429,7 +429,12 @@ func generateListVersionsResponse(bucket, prefix, marker, delimiter, encodingTyp
|
|||||||
content.ETag = "\"" + object.ETag + "\""
|
content.ETag = "\"" + object.ETag + "\""
|
||||||
}
|
}
|
||||||
content.Size = object.Size
|
content.Size = object.Size
|
||||||
content.StorageClass = object.StorageClass
|
if 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
|
||||||
@@ -475,7 +480,11 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingTy
|
|||||||
content.ETag = "\"" + object.ETag + "\""
|
content.ETag = "\"" + object.ETag + "\""
|
||||||
}
|
}
|
||||||
content.Size = object.Size
|
content.Size = object.Size
|
||||||
content.StorageClass = object.StorageClass
|
if object.StorageClass != "" {
|
||||||
|
content.StorageClass = object.StorageClass
|
||||||
|
} else {
|
||||||
|
content.StorageClass = globalMinioDefaultStorageClass
|
||||||
|
}
|
||||||
content.Owner = owner
|
content.Owner = owner
|
||||||
contents = append(contents, content)
|
contents = append(contents, content)
|
||||||
}
|
}
|
||||||
@@ -521,7 +530,11 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
|||||||
content.ETag = "\"" + object.ETag + "\""
|
content.ETag = "\"" + object.ETag + "\""
|
||||||
}
|
}
|
||||||
content.Size = object.Size
|
content.Size = object.Size
|
||||||
content.StorageClass = object.StorageClass
|
if 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)
|
||||||
|
|||||||
+119
-61
@@ -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,197 @@ 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(
|
||||||
|
maxClients(collectAPIStats("getbucketreplication", httpTraceAll(api.GetBucketReplicationHandler)))).Queries("replication", "")
|
||||||
// GetBucketTaggingHandler - this is a dummy call.
|
// GetBucketTaggingHandler - this is a dummy call.
|
||||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbuckettagging", httpTraceAll(api.GetBucketTaggingHandler))).Queries("tagging", "")
|
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", "")
|
||||||
// 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))))
|
||||||
|
|
||||||
// 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)))
|
||||||
|
|||||||
+144
-22
@@ -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'.
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+15
-53
@@ -18,6 +18,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
@@ -66,8 +67,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 +76,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)
|
||||||
}
|
}
|
||||||
|
ObjectPathUpdated(path.Join(bucket, object))
|
||||||
if task.responseCh != nil {
|
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,22 +109,10 @@ 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
|
||||||
@@ -133,20 +122,14 @@ func startBackgroundHealing() {
|
|||||||
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,64 +51,67 @@ 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():
|
||||||
// Attempt a heal as the server starts-up first.
|
return
|
||||||
localDisksInZoneHeal := make([]Endpoints, len(z.zones))
|
case <-time.After(defaultMonitorNewDiskInterval):
|
||||||
for i, ep := range globalEndpoints {
|
// Attempt a heal as the server starts-up first.
|
||||||
localDisksToHeal := Endpoints{}
|
localDisksInZoneHeal := make([]Endpoints, len(z.zones))
|
||||||
for _, endpoint := range ep.Endpoints {
|
for i, ep := range globalEndpoints {
|
||||||
if !endpoint.IsLocal {
|
localDisksToHeal := Endpoints{}
|
||||||
|
for _, endpoint := range ep.Endpoints {
|
||||||
|
if !endpoint.IsLocal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Try to connect to the current endpoint
|
||||||
|
// and reformat if the current disk is not formatted
|
||||||
|
_, _, err := connectEndpoint(endpoint)
|
||||||
|
if err == errUnformattedDisk {
|
||||||
|
localDisksToHeal = append(localDisksToHeal, endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(localDisksToHeal) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Try to connect to the current endpoint
|
localDisksInZoneHeal[i] = localDisksToHeal
|
||||||
// and reformat if the current disk is not formatted
|
|
||||||
_, _, err := connectEndpoint(endpoint)
|
|
||||||
if err == errUnformattedDisk {
|
|
||||||
localDisksToHeal = append(localDisksToHeal, endpoint)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(localDisksToHeal) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
localDisksInZoneHeal[i] = localDisksToHeal
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
for i, localDisksToHeal := range localDisksInZoneHeal {
|
for i, localDisksToHeal := range localDisksInZoneHeal {
|
||||||
var erasureSetToHeal []int
|
var erasureSetToHeal []int
|
||||||
for _, endpoint := range localDisksToHeal {
|
for _, endpoint := range localDisksToHeal {
|
||||||
// Load the new format of this passed endpoint
|
// Load the new format of this passed endpoint
|
||||||
_, format, err := connectEndpoint(endpoint)
|
_, format, err := connectEndpoint(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
|
// Calculate the set index where the current endpoint belongs
|
||||||
|
setIndex, _, err := findDiskIndex(z.zones[i].format, format)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
erasureSetToHeal = append(erasureSetToHeal, setIndex)
|
||||||
}
|
}
|
||||||
// Calculate the set index where the current endpoint belongs
|
erasureSetInZoneToHeal[i] = erasureSetToHeal
|
||||||
setIndex, _, err := findDiskIndex(z.zones[i].format, format)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
erasureSetToHeal = append(erasureSetToHeal, setIndex)
|
|
||||||
}
|
}
|
||||||
erasureSetInZoneToHeal[i] = erasureSetToHeal
|
|
||||||
}
|
|
||||||
|
|
||||||
// Heal all erasure sets that need
|
// Heal all erasure sets that need
|
||||||
for i, erasureSetToHeal := range erasureSetInZoneToHeal {
|
for i, erasureSetToHeal := range erasureSetInZoneToHeal {
|
||||||
for _, setIndex := range erasureSetToHeal {
|
for _, setIndex := range erasureSetToHeal {
|
||||||
err := healErasureSet(ctx, setIndex, z.zones[i].sets[setIndex])
|
err := healErasureSet(ctx, setIndex, z.zones[i].sets[setIndex])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-10
@@ -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) {
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
+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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func NewBucketSSEConfigSys() *BucketSSEConfigSys {
|
|||||||
// load - Loads the bucket encryption configuration for the given list of buckets
|
// load - Loads the bucket encryption configuration for the given list of buckets
|
||||||
func (sys *BucketSSEConfigSys) load(buckets []BucketInfo, objAPI ObjectLayer) error {
|
func (sys *BucketSSEConfigSys) load(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||||
for _, bucket := range buckets {
|
for _, bucket := range buckets {
|
||||||
config, err := objAPI.GetBucketSSEConfig(context.Background(), bucket.Name)
|
config, err := objAPI.GetBucketSSEConfig(GlobalContext, bucket.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(BucketSSEConfigNotFound); ok {
|
if _, ok := err.(BucketSSEConfigNotFound); ok {
|
||||||
sys.Remove(bucket.Name)
|
sys.Remove(bucket.Name)
|
||||||
@@ -81,7 +81,7 @@ func (sys *BucketSSEConfigSys) Get(bucket string) (config bucketsse.BucketSSECon
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := objAPI.GetBucketSSEConfig(context.Background(), bucket)
|
cfg, err := objAPI.GetBucketSSEConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ func saveBucketSSEConfig(ctx context.Context, objAPI ObjectLayer, bucket string,
|
|||||||
func getBucketSSEConfig(objAPI ObjectLayer, bucket string) (*bucketsse.BucketSSEConfig, error) {
|
func getBucketSSEConfig(objAPI ObjectLayer, bucket string) (*bucketsse.BucketSSEConfig, error) {
|
||||||
// Path to bucket-encryption.xml for the given bucket.
|
// Path to bucket-encryption.xml for the given bucket.
|
||||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketSSEConfig)
|
configFile := path.Join(bucketConfigPrefix, bucket, bucketSSEConfig)
|
||||||
configData, err := readConfig(context.Background(), objAPI, configFile)
|
configData, err := readConfig(GlobalContext, objAPI, configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == errConfigNotFound {
|
if err == errConfigNotFound {
|
||||||
err = BucketSSEConfigNotFound{Bucket: bucket}
|
err = BucketSSEConfigNotFound{Bucket: bucket}
|
||||||
|
|||||||
+83
-64
@@ -17,7 +17,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -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,32 +294,43 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set prefix value for "s3:prefix" policy conditionals.
|
if s3Error == ErrAccessDenied {
|
||||||
r.Header.Set("prefix", "")
|
// Set prefix value for "s3:prefix" policy conditionals.
|
||||||
|
r.Header.Set("prefix", "")
|
||||||
|
|
||||||
// Set delimiter value for "s3:delimiter" policy conditionals.
|
// Set delimiter value for "s3:delimiter" policy conditionals.
|
||||||
r.Header.Set("delimiter", SlashSeparator)
|
r.Header.Set("delimiter", SlashSeparator)
|
||||||
|
|
||||||
// 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
|
||||||
for _, bucketInfo := range bucketsInfo {
|
// Use the following trick to filter in place
|
||||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
// https://github.com/golang/go/wiki/SliceTricks#filter-in-place
|
||||||
AccountName: accessKey,
|
for _, bucketInfo := range bucketsInfo {
|
||||||
Action: iampolicy.ListBucketAction,
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
BucketName: bucketInfo.Name,
|
AccountName: accessKey,
|
||||||
ConditionValues: getConditionValues(r, "", accessKey, claims),
|
Action: iampolicy.ListBucketAction,
|
||||||
IsOwner: owner,
|
BucketName: bucketInfo.Name,
|
||||||
ObjectName: "",
|
ConditionValues: getConditionValues(r, "", accessKey, claims),
|
||||||
Claims: claims,
|
IsOwner: owner,
|
||||||
}) {
|
ObjectName: "",
|
||||||
newBucketsInfo = append(newBucketsInfo, bucketInfo)
|
Claims: claims,
|
||||||
|
}) {
|
||||||
|
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.
|
||||||
@@ -342,13 +352,6 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content-Length is required and should be non-zero
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
|
||||||
if r.ContentLength <= 0 {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content-Md5 is requied should be set
|
// Content-Md5 is requied should be set
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||||
if _, ok := r.Header[xhttp.ContentMD5]; !ok {
|
if _, ok := r.Header[xhttp.ContentMD5]; !ok {
|
||||||
@@ -356,27 +359,21 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate incoming content length bytes.
|
// Content-Length is required and should be non-zero
|
||||||
var deleteXMLBytes []byte
|
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||||
const maxBodySize = 2 * 1000 * 1024 // The max. XML contains 1000 object names (each at most 1024 bytes long) + XML overhead
|
if r.ContentLength <= 0 {
|
||||||
if r.ContentLength > maxBodySize { // Only allocated memory for at most 1000 objects
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL, guessIsBrowserReq(r))
|
||||||
deleteXMLBytes = make([]byte, maxBodySize)
|
|
||||||
} else {
|
|
||||||
deleteXMLBytes = make([]byte, r.ContentLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read incoming body XML bytes.
|
|
||||||
if _, err := io.ReadFull(r.Body, deleteXMLBytes); err != nil {
|
|
||||||
logger.LogIf(ctx, err, logger.Application)
|
|
||||||
writeErrorResponse(ctx, w, toAdminAPIErr(ctx, err), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The max. XML contains 100000 object names (each at most 1024 bytes long) + XML overhead
|
||||||
|
const maxBodySize = 2 * 100000 * 1024
|
||||||
|
|
||||||
// Unmarshal list of keys to be deleted.
|
// Unmarshal list of keys to be deleted.
|
||||||
deleteObjects := &DeleteObjectsRequest{}
|
deleteObjects := &DeleteObjectsRequest{}
|
||||||
if err := xml.Unmarshal(deleteXMLBytes, deleteObjects); err != nil {
|
if err := xmlDecoder(r.Body, deleteObjects, maxBodySize); err != nil {
|
||||||
logger.LogIf(ctx, err, logger.Application)
|
logger.LogIf(ctx, err, logger.Application)
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,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 _, ok := globalBucketObjectLockConfig.Get(bucket); ok {
|
||||||
dErrs[index] = err
|
if err := enforceRetentionBypassForDelete(ctx, r, bucket, object.ObjectName, getObjectInfoFn); err != ErrNone {
|
||||||
continue
|
dErrs[index] = err
|
||||||
|
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
|
||||||
@@ -541,7 +541,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -755,7 +755,7 @@ 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)
|
_, encEnabled := globalBucketSSEConfigSys.Get(bucket)
|
||||||
@@ -797,7 +797,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,15 +896,39 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketAction, bucket, ""); s3Error != ErrNone {
|
forceDelete := false
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
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 {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := globalBucketObjectLockConfig.Get(bucket); ok && forceDelete {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
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
|
||||||
}
|
}
|
||||||
@@ -997,11 +1021,6 @@ 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
|
||||||
@@ -1079,7 +1098,7 @@ func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
var aerr APIError
|
var aerr APIError
|
||||||
if err == errConfigNotFound {
|
if err == errConfigNotFound {
|
||||||
aerr = errorCodes.ToAPIErr(ErrMethodNotAllowed)
|
aerr = errorCodes.ToAPIErr(ErrObjectLockConfigurationNotFound)
|
||||||
} else {
|
} else {
|
||||||
aerr = toAPIError(ctx, err)
|
aerr = toAPIError(ctx, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"})
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
@@ -116,7 +115,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:]...)
|
||||||
}
|
}
|
||||||
@@ -282,16 +281,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 +295,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 +328,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, ""); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-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(""))
|
||||||
@@ -149,6 +149,12 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
|||||||
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,13 +170,17 @@ 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")
|
||||||
@@ -226,9 +236,15 @@ func handleCommonEnvVars() {
|
|||||||
globalConfigEncrypted = true
|
globalConfigEncrypted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
globalWORMEnabled, err = config.LookupWorm()
|
if env.IsSet(config.EnvAccessKeyOld) && env.IsSet(config.EnvSecretKeyOld) {
|
||||||
if err != nil {
|
oldCred, err := auth.CreateCredentials(env.Get(config.EnvAccessKeyOld, ""), env.Get(config.EnvSecretKeyOld, ""))
|
||||||
logger.Fatal(config.ErrInvalidWormValue(err), "Invalid worm configuration")
|
if err != nil {
|
||||||
|
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
-20
@@ -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],
|
||||||
return err
|
globalRootCAs)
|
||||||
|
if err != nil {
|
||||||
|
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,18 @@ 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)
|
||||||
|
|
||||||
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 +394,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 +413,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 +444,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 +575,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
|
||||||
|
|||||||
+29
-50
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,12 +48,17 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error {
|
|||||||
// of the object layer.
|
// of the object layer.
|
||||||
retryTimerCh := newRetryTimerSimple(doneCh)
|
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||||
var stop bool
|
var stop bool
|
||||||
|
|
||||||
|
rquorum := InsufficientReadQuorum{}
|
||||||
|
wquorum := InsufficientWriteQuorum{}
|
||||||
|
|
||||||
for !stop {
|
for !stop {
|
||||||
select {
|
select {
|
||||||
case <-retryTimerCh:
|
case <-retryTimerCh:
|
||||||
if encrypted, err = checkBackendEncrypted(objAPI); err != nil {
|
if encrypted, err = checkBackendEncrypted(objAPI); err != nil {
|
||||||
if err == errDiskNotFound ||
|
if errors.Is(err, errDiskNotFound) ||
|
||||||
strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) {
|
errors.As(err, &rquorum) ||
|
||||||
|
isErrBucketNotFound(err) {
|
||||||
logger.Info("Waiting for config backend to be encrypted..")
|
logger.Info("Waiting for config backend to be encrypted..")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -88,11 +91,6 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activeCredOld, err := getOldCreds()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
doneCh = make(chan struct{})
|
doneCh = make(chan struct{})
|
||||||
defer close(doneCh)
|
defer close(doneCh)
|
||||||
|
|
||||||
@@ -106,10 +104,11 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error {
|
|||||||
select {
|
select {
|
||||||
case <-retryTimerCh:
|
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 ||
|
if errors.Is(err, errDiskNotFound) ||
|
||||||
strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) ||
|
errors.As(err, &rquorum) ||
|
||||||
strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) {
|
errors.As(err, &wquorum) ||
|
||||||
|
isErrBucketNotFound(err) {
|
||||||
logger.Info("Waiting for config backend to be encrypted..")
|
logger.Info("Waiting for config backend to be encrypted..")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -140,7 +139,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 +163,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 +187,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 +204,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 +233,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 +264,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 +288,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 +306,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 +339,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 +355,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-5
@@ -186,16 +186,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,91 @@
|
|||||||
|
/*
|
||||||
|
* 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultKVS - default storage class config
|
||||||
|
var (
|
||||||
|
DefaultKVS = config.KVS{
|
||||||
|
config.KV{
|
||||||
|
Key: apiRequestsMax,
|
||||||
|
Value: "0",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: apiRequestsDeadline,
|
||||||
|
Value: "10s",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config storage class configuration
|
||||||
|
type Config struct {
|
||||||
|
APIRequestsMax int `json:"requests_max"`
|
||||||
|
APIRequestsDeadline time.Duration `json:"requests_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
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg = Config{
|
||||||
|
APIRequestsMax: requestsMax,
|
||||||
|
APIRequestsDeadline: requestsDeadline,
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
Vendored
+18
-7
@@ -28,13 +28,15 @@ import (
|
|||||||
|
|
||||||
// Config represents cache config settings
|
// Config represents cache config settings
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Enabled bool `json:"-"`
|
Enabled bool `json:"-"`
|
||||||
Drives []string `json:"drives"`
|
Drives []string `json:"drives"`
|
||||||
Expiry int `json:"expiry"`
|
Expiry int `json:"expiry"`
|
||||||
MaxUse int `json:"maxuse"`
|
MaxUse int `json:"maxuse"`
|
||||||
Quota int `json:"quota"`
|
Quota int `json:"quota"`
|
||||||
Exclude []string `json:"exclude"`
|
Exclude []string `json:"exclude"`
|
||||||
After int `json:"after"`
|
After int `json:"after"`
|
||||||
|
WatermarkLow int `json:"watermark_low"`
|
||||||
|
WatermarkHigh int `json:"watermark_high"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON - implements JSON unmarshal interface for unmarshalling
|
// UnmarshalJSON - implements JSON unmarshal interface for unmarshalling
|
||||||
@@ -64,6 +66,15 @@ func (cfg *Config) UnmarshalJSON(data []byte) (err error) {
|
|||||||
if _cfg.After < 0 {
|
if _cfg.After < 0 {
|
||||||
return errors.New("cache after value should not be less than 0")
|
return errors.New("cache after value should not be less than 0")
|
||||||
}
|
}
|
||||||
|
if _cfg.WatermarkLow < 0 || _cfg.WatermarkLow > 100 {
|
||||||
|
return errors.New("config low watermark value should be between 0 and 100")
|
||||||
|
}
|
||||||
|
if _cfg.WatermarkHigh < 0 || _cfg.WatermarkHigh > 100 {
|
||||||
|
return errors.New("config high watermark value should be between 0 and 100")
|
||||||
|
}
|
||||||
|
if _cfg.WatermarkLow > 0 && (_cfg.WatermarkLow >= _cfg.WatermarkHigh) {
|
||||||
|
return errors.New("config low watermark value should be less than high watermark")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+12
@@ -56,5 +56,17 @@ var (
|
|||||||
Optional: true,
|
Optional: true,
|
||||||
Type: "number",
|
Type: "number",
|
||||||
},
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: WatermarkLow,
|
||||||
|
Description: `% of cache use at which to stop cache eviction`,
|
||||||
|
Optional: true,
|
||||||
|
Type: "number",
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: WatermarkHigh,
|
||||||
|
Description: `% of cache use at which to start cache eviction`,
|
||||||
|
Optional: true,
|
||||||
|
Type: "number",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Vendored
+57
-16
@@ -26,25 +26,31 @@ import (
|
|||||||
|
|
||||||
// Cache ENVs
|
// Cache ENVs
|
||||||
const (
|
const (
|
||||||
Drives = "drives"
|
Drives = "drives"
|
||||||
Exclude = "exclude"
|
Exclude = "exclude"
|
||||||
Expiry = "expiry"
|
Expiry = "expiry"
|
||||||
MaxUse = "maxuse"
|
MaxUse = "maxuse"
|
||||||
Quota = "quota"
|
Quota = "quota"
|
||||||
After = "after"
|
After = "after"
|
||||||
|
WatermarkLow = "watermark_low"
|
||||||
|
WatermarkHigh = "watermark_high"
|
||||||
|
|
||||||
EnvCacheDrives = "MINIO_CACHE_DRIVES"
|
EnvCacheDrives = "MINIO_CACHE_DRIVES"
|
||||||
EnvCacheExclude = "MINIO_CACHE_EXCLUDE"
|
EnvCacheExclude = "MINIO_CACHE_EXCLUDE"
|
||||||
EnvCacheExpiry = "MINIO_CACHE_EXPIRY"
|
EnvCacheExpiry = "MINIO_CACHE_EXPIRY"
|
||||||
EnvCacheMaxUse = "MINIO_CACHE_MAXUSE"
|
EnvCacheMaxUse = "MINIO_CACHE_MAXUSE"
|
||||||
EnvCacheQuota = "MINIO_CACHE_QUOTA"
|
EnvCacheQuota = "MINIO_CACHE_QUOTA"
|
||||||
EnvCacheAfter = "MINIO_CACHE_AFTER"
|
EnvCacheAfter = "MINIO_CACHE_AFTER"
|
||||||
|
EnvCacheWatermarkLow = "MINIO_CACHE_WATERMARK_LOW"
|
||||||
|
EnvCacheWatermarkHigh = "MINIO_CACHE_WATERMARK_HIGH"
|
||||||
|
|
||||||
EnvCacheEncryptionMasterKey = "MINIO_CACHE_ENCRYPTION_MASTER_KEY"
|
EnvCacheEncryptionMasterKey = "MINIO_CACHE_ENCRYPTION_MASTER_KEY"
|
||||||
|
|
||||||
DefaultExpiry = "90"
|
DefaultExpiry = "90"
|
||||||
DefaultQuota = "80"
|
DefaultQuota = "80"
|
||||||
DefaultAfter = "0"
|
DefaultAfter = "0"
|
||||||
|
DefaultWaterMarkLow = "70"
|
||||||
|
DefaultWaterMarkHigh = "80"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultKVS - default KV settings for caching.
|
// DefaultKVS - default KV settings for caching.
|
||||||
@@ -70,6 +76,14 @@ var (
|
|||||||
Key: After,
|
Key: After,
|
||||||
Value: DefaultAfter,
|
Value: DefaultAfter,
|
||||||
},
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: WatermarkLow,
|
||||||
|
Value: DefaultWaterMarkLow,
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: WatermarkHigh,
|
||||||
|
Value: DefaultWaterMarkHigh,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -87,7 +101,6 @@ func Enabled(kvs config.KVS) bool {
|
|||||||
// variables and merge them with provided CacheConfiguration.
|
// variables and merge them with provided CacheConfiguration.
|
||||||
func LookupConfig(kvs config.KVS) (Config, error) {
|
func LookupConfig(kvs config.KVS) (Config, error) {
|
||||||
cfg := Config{}
|
cfg := Config{}
|
||||||
|
|
||||||
if err := config.CheckValidKeys(config.CacheSubSys, kvs, DefaultKVS); err != nil {
|
if err := config.CheckValidKeys(config.CacheSubSys, kvs, DefaultKVS); err != nil {
|
||||||
return cfg, err
|
return cfg, err
|
||||||
}
|
}
|
||||||
@@ -154,5 +167,33 @@ func LookupConfig(kvs config.KVS) (Config, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lowWMStr := env.Get(EnvCacheWatermarkLow, kvs.Get(WatermarkLow)); lowWMStr != "" {
|
||||||
|
cfg.WatermarkLow, err = strconv.Atoi(lowWMStr)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, config.ErrInvalidCacheWatermarkLow(err)
|
||||||
|
}
|
||||||
|
// WatermarkLow should be a valid percentage.
|
||||||
|
if cfg.WatermarkLow < 0 || cfg.WatermarkLow > 100 {
|
||||||
|
err := errors.New("config min watermark value should be between 0 and 100")
|
||||||
|
return cfg, config.ErrInvalidCacheWatermarkLow(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if highWMStr := env.Get(EnvCacheWatermarkHigh, kvs.Get(WatermarkHigh)); highWMStr != "" {
|
||||||
|
cfg.WatermarkHigh, err = strconv.Atoi(highWMStr)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, config.ErrInvalidCacheWatermarkHigh(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxWatermark should be a valid percentage.
|
||||||
|
if cfg.WatermarkHigh < 0 || cfg.WatermarkHigh > 100 {
|
||||||
|
err := errors.New("config high watermark value should be between 0 and 100")
|
||||||
|
return cfg, config.ErrInvalidCacheWatermarkHigh(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg.WatermarkLow > cfg.WatermarkHigh {
|
||||||
|
err := errors.New("config high watermark value should be greater than low watermark value")
|
||||||
|
return cfg, config.ErrInvalidCacheWatermarkHigh(err)
|
||||||
|
}
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-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 {
|
||||||
@@ -581,9 +601,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 +635,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 {
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ const (
|
|||||||
EnvPublicIPs = "MINIO_PUBLIC_IPS"
|
EnvPublicIPs = "MINIO_PUBLIC_IPS"
|
||||||
EnvEndpoints = "MINIO_ENDPOINTS"
|
EnvEndpoints = "MINIO_ENDPOINTS"
|
||||||
|
|
||||||
|
// API sub-system
|
||||||
|
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
|
||||||
|
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
|
||||||
|
|
||||||
EnvUpdate = "MINIO_UPDATE"
|
EnvUpdate = "MINIO_UPDATE"
|
||||||
|
|
||||||
EnvWorm = "MINIO_WORM" // legacy
|
EnvWorm = "MINIO_WORM" // legacy
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func (u Err) Clone() Err {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the error message
|
// Error returns the error message
|
||||||
func (u Err) Error() string {
|
func (u Err) Error() string {
|
||||||
if u.detail == "" {
|
if u.detail == "" {
|
||||||
if u.msg != "" {
|
if u.msg != "" {
|
||||||
|
|||||||
+13
-1
@@ -33,7 +33,7 @@ var (
|
|||||||
ErrInvalidErasureSetSize = newErrFn(
|
ErrInvalidErasureSetSize = newErrFn(
|
||||||
"Invalid erasure set size",
|
"Invalid erasure set size",
|
||||||
"Please check the passed value",
|
"Please check the passed value",
|
||||||
"Erasure set can only accept any of [4, 6, 8, 10, 12, 14, 16] values",
|
"Erasure set can only accept any of [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] values",
|
||||||
)
|
)
|
||||||
|
|
||||||
ErrInvalidWormValue = newErrFn(
|
ErrInvalidWormValue = newErrFn(
|
||||||
@@ -72,6 +72,18 @@ var (
|
|||||||
"MINIO_CACHE_AFTER: Valid cache after value must be 0 or greater",
|
"MINIO_CACHE_AFTER: Valid cache after value must be 0 or greater",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ErrInvalidCacheWatermarkLow = newErrFn(
|
||||||
|
"Invalid cache low watermark value",
|
||||||
|
"Please check the passed value",
|
||||||
|
"MINIO_CACHE_WATERMARK_LOW: Valid cache low watermark value must be between 0-100",
|
||||||
|
)
|
||||||
|
|
||||||
|
ErrInvalidCacheWatermarkHigh = newErrFn(
|
||||||
|
"Invalid cache high watermark value",
|
||||||
|
"Please check the passed value",
|
||||||
|
"MINIO_CACHE_WATERMARK_HIGH: Valid cache high watermark value must be between 0-100",
|
||||||
|
)
|
||||||
|
|
||||||
ErrInvalidCacheEncryptionKey = newErrFn(
|
ErrInvalidCacheEncryptionKey = newErrFn(
|
||||||
"Invalid cache encryption master key value",
|
"Invalid cache encryption master key value",
|
||||||
"Please check the passed value",
|
"Please check the passed value",
|
||||||
|
|||||||
@@ -60,11 +60,7 @@ func (c *CoreDNS) List() (map[string][]SrvRecord, error) {
|
|||||||
if record.Key == "" {
|
if record.Key == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := srvRecords[record.Key]; ok {
|
srvRecords[record.Key] = append(srvRecords[record.Key], record)
|
||||||
srvRecords[record.Key] = append(srvRecords[record.Key], record)
|
|
||||||
} else {
|
|
||||||
srvRecords[record.Key] = []SrvRecord{record}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return srvRecords, nil
|
return srvRecords, nil
|
||||||
|
|||||||
+194
-103
@@ -21,7 +21,8 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"net"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/config"
|
"github.com/minio/minio/cmd/config"
|
||||||
@@ -44,34 +45,45 @@ type Config struct {
|
|||||||
STSExpiryDuration string `json:"stsExpiryDuration"`
|
STSExpiryDuration string `json:"stsExpiryDuration"`
|
||||||
|
|
||||||
// Format string for usernames
|
// Format string for usernames
|
||||||
UsernameFormat string `json:"usernameFormat"`
|
UsernameFormat string `json:"usernameFormat"`
|
||||||
|
UsernameFormats []string `json:"-"`
|
||||||
|
UsernameSearchFilter string `json:"-"`
|
||||||
|
UsernameSearchBaseDNS []string `json:"-"`
|
||||||
|
|
||||||
GroupSearchBaseDN string `json:"groupSearchBaseDN"`
|
GroupSearchBaseDN string `json:"groupSearchBaseDN"`
|
||||||
GroupSearchFilter string `json:"groupSearchFilter"`
|
GroupSearchBaseDNS []string `json:"-"`
|
||||||
GroupNameAttribute string `json:"groupNameAttribute"`
|
GroupSearchFilter string `json:"groupSearchFilter"`
|
||||||
|
GroupNameAttribute string `json:"groupNameAttribute"`
|
||||||
|
|
||||||
stsExpiryDuration time.Duration // contains converted value
|
stsExpiryDuration time.Duration // contains converted value
|
||||||
tlsSkipVerify bool // allows skipping TLS verification
|
tlsSkipVerify bool // allows skipping TLS verification
|
||||||
|
serverInsecure bool // allows plain text connection to LDAP Server
|
||||||
rootCAs *x509.CertPool
|
rootCAs *x509.CertPool
|
||||||
}
|
}
|
||||||
|
|
||||||
// LDAP keys and envs.
|
// LDAP keys and envs.
|
||||||
const (
|
const (
|
||||||
ServerAddr = "server_addr"
|
ServerAddr = "server_addr"
|
||||||
STSExpiry = "sts_expiry"
|
STSExpiry = "sts_expiry"
|
||||||
UsernameFormat = "username_format"
|
UsernameFormat = "username_format"
|
||||||
GroupSearchFilter = "group_search_filter"
|
UsernameSearchFilter = "username_search_filter"
|
||||||
GroupNameAttribute = "group_name_attribute"
|
UsernameSearchBaseDN = "username_search_base_dn"
|
||||||
GroupSearchBaseDN = "group_search_base_dn"
|
GroupSearchFilter = "group_search_filter"
|
||||||
TLSSkipVerify = "tls_skip_verify"
|
GroupNameAttribute = "group_name_attribute"
|
||||||
|
GroupSearchBaseDN = "group_search_base_dn"
|
||||||
|
TLSSkipVerify = "tls_skip_verify"
|
||||||
|
ServerInsecure = "server_insecure"
|
||||||
|
|
||||||
EnvServerAddr = "MINIO_IDENTITY_LDAP_SERVER_ADDR"
|
EnvServerAddr = "MINIO_IDENTITY_LDAP_SERVER_ADDR"
|
||||||
EnvSTSExpiry = "MINIO_IDENTITY_LDAP_STS_EXPIRY"
|
EnvSTSExpiry = "MINIO_IDENTITY_LDAP_STS_EXPIRY"
|
||||||
EnvTLSSkipVerify = "MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY"
|
EnvTLSSkipVerify = "MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY"
|
||||||
EnvUsernameFormat = "MINIO_IDENTITY_LDAP_USERNAME_FORMAT"
|
EnvServerInsecure = "MINIO_IDENTITY_LDAP_SERVER_INSECURE"
|
||||||
EnvGroupSearchFilter = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER"
|
EnvUsernameFormat = "MINIO_IDENTITY_LDAP_USERNAME_FORMAT"
|
||||||
EnvGroupNameAttribute = "MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE"
|
EnvUsernameSearchFilter = "MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER"
|
||||||
EnvGroupSearchBaseDN = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN"
|
EnvUsernameSearchBaseDN = "MINIO_IDENTITY_LDAP_USERNAME_SEARCH_BASE_DN"
|
||||||
|
EnvGroupSearchFilter = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER"
|
||||||
|
EnvGroupNameAttribute = "MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE"
|
||||||
|
EnvGroupSearchBaseDN = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultKVS - default config for LDAP config
|
// DefaultKVS - default config for LDAP config
|
||||||
@@ -82,11 +94,15 @@ var (
|
|||||||
Value: "",
|
Value: "",
|
||||||
},
|
},
|
||||||
config.KV{
|
config.KV{
|
||||||
Key: STSExpiry,
|
Key: UsernameFormat,
|
||||||
Value: "1h",
|
Value: "",
|
||||||
},
|
},
|
||||||
config.KV{
|
config.KV{
|
||||||
Key: UsernameFormat,
|
Key: UsernameSearchFilter,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: UsernameSearchBaseDN,
|
||||||
Value: "",
|
Value: "",
|
||||||
},
|
},
|
||||||
config.KV{
|
config.KV{
|
||||||
@@ -101,19 +117,146 @@ var (
|
|||||||
Key: GroupSearchBaseDN,
|
Key: GroupSearchBaseDN,
|
||||||
Value: "",
|
Value: "",
|
||||||
},
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: STSExpiry,
|
||||||
|
Value: "1h",
|
||||||
|
},
|
||||||
config.KV{
|
config.KV{
|
||||||
Key: TLSSkipVerify,
|
Key: TLSSkipVerify,
|
||||||
Value: config.EnableOff,
|
Value: config.EnableOff,
|
||||||
},
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: ServerInsecure,
|
||||||
|
Value: config.EnableOff,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dnDelimiter = ";"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getGroups(conn *ldap.Conn, sreq *ldap.SearchRequest) ([]string, error) {
|
||||||
|
var groups []string
|
||||||
|
sres, err := conn.Search(sreq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, entry := range sres.Entries {
|
||||||
|
// We only queried one attribute,
|
||||||
|
// so we only look up the first one.
|
||||||
|
groups = append(groups, entry.Attributes[0].Values...)
|
||||||
|
}
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Config) bind(conn *ldap.Conn, username, password string) ([]string, error) {
|
||||||
|
var bindDNS = make([]string, len(l.UsernameFormats))
|
||||||
|
for i, usernameFormat := range l.UsernameFormats {
|
||||||
|
bindDN := fmt.Sprintf(usernameFormat, username)
|
||||||
|
// Bind with user credentials to validate the password
|
||||||
|
if err := conn.Bind(bindDN, password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bindDNS[i] = bindDN
|
||||||
|
}
|
||||||
|
return bindDNS, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var standardAttributes = []string{
|
||||||
|
"givenName",
|
||||||
|
"sn",
|
||||||
|
"cn",
|
||||||
|
"memberOf",
|
||||||
|
"email",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind - binds to ldap, searches LDAP and returns list of groups.
|
||||||
|
func (l *Config) Bind(username, password string) ([]string, error) {
|
||||||
|
conn, err := l.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
bindDNS, err := l.bind(conn, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var groups []string
|
||||||
|
if l.UsernameSearchFilter != "" {
|
||||||
|
for _, userSearchBase := range l.UsernameSearchBaseDNS {
|
||||||
|
filter := strings.Replace(l.UsernameSearchFilter, "%s",
|
||||||
|
ldap.EscapeFilter(username), -1)
|
||||||
|
|
||||||
|
searchRequest := ldap.NewSearchRequest(
|
||||||
|
userSearchBase,
|
||||||
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||||
|
filter,
|
||||||
|
standardAttributes,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
groups, err = getGroups(conn, searchRequest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.GroupSearchFilter != "" {
|
||||||
|
for _, groupSearchBase := range l.GroupSearchBaseDNS {
|
||||||
|
var filters []string
|
||||||
|
if l.GroupNameAttribute == "" {
|
||||||
|
filters = []string{strings.Replace(l.GroupSearchFilter, "%s",
|
||||||
|
ldap.EscapeFilter(username), -1)}
|
||||||
|
} else {
|
||||||
|
// With group name attribute specified, make sure to
|
||||||
|
// include search queries for CN distinguished name
|
||||||
|
for _, bindDN := range bindDNS {
|
||||||
|
filters = append(filters, strings.Replace(l.GroupSearchFilter, "%s",
|
||||||
|
ldap.EscapeFilter(bindDN), -1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, filter := range filters {
|
||||||
|
searchRequest := ldap.NewSearchRequest(
|
||||||
|
groupSearchBase,
|
||||||
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||||
|
filter,
|
||||||
|
standardAttributes,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
var newGroups []string
|
||||||
|
newGroups, err = getGroups(conn, searchRequest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
groups = append(groups, newGroups...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Connect connect to ldap server.
|
// Connect connect to ldap server.
|
||||||
func (l *Config) Connect() (ldapConn *ldap.Conn, err error) {
|
func (l *Config) Connect() (ldapConn *ldap.Conn, err error) {
|
||||||
if l == nil {
|
if l == nil {
|
||||||
// Happens when LDAP is not configured.
|
return nil, errors.New("LDAP is not configured")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, _, err = net.SplitHostPort(l.ServerAddr); err != nil {
|
||||||
|
// User default LDAP port if none specified "636"
|
||||||
|
l.ServerAddr = net.JoinHostPort(l.ServerAddr, "636")
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.serverInsecure {
|
||||||
|
return ldap.Dial("tcp", l.ServerAddr)
|
||||||
|
}
|
||||||
|
|
||||||
return ldap.DialTLS("tcp", l.ServerAddr, &tls.Config{
|
return ldap.DialTLS("tcp", l.ServerAddr, &tls.Config{
|
||||||
InsecureSkipVerify: l.tlsSkipVerify,
|
InsecureSkipVerify: l.tlsSkipVerify,
|
||||||
RootCAs: l.rootCAs,
|
RootCAs: l.rootCAs,
|
||||||
@@ -154,6 +297,12 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
|
|||||||
l.STSExpiryDuration = v
|
l.STSExpiryDuration = v
|
||||||
l.stsExpiryDuration = expDur
|
l.stsExpiryDuration = expDur
|
||||||
}
|
}
|
||||||
|
if v := env.Get(EnvServerInsecure, kvs.Get(ServerInsecure)); v != "" {
|
||||||
|
l.serverInsecure, err = config.ParseBool(v)
|
||||||
|
if err != nil {
|
||||||
|
return l, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if v := env.Get(EnvTLSSkipVerify, kvs.Get(TLSSkipVerify)); v != "" {
|
if v := env.Get(EnvTLSSkipVerify, kvs.Get(TLSSkipVerify)); v != "" {
|
||||||
l.tlsSkipVerify, err = config.ParseBool(v)
|
l.tlsSkipVerify, err = config.ParseBool(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -161,102 +310,44 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v := env.Get(EnvUsernameFormat, kvs.Get(UsernameFormat)); v != "" {
|
if v := env.Get(EnvUsernameFormat, kvs.Get(UsernameFormat)); v != "" {
|
||||||
subs, err := NewSubstituter("username", "test")
|
if !strings.Contains(v, "%s") {
|
||||||
if err != nil {
|
return l, errors.New("LDAP username format doesn't have '%s' substitution")
|
||||||
return l, err
|
|
||||||
}
|
}
|
||||||
if _, err := subs.Substitute(v); err != nil {
|
l.UsernameFormats = strings.Split(v, dnDelimiter)
|
||||||
return l, err
|
|
||||||
}
|
|
||||||
l.UsernameFormat = v
|
|
||||||
} else {
|
} else {
|
||||||
return l, fmt.Errorf("'%s' cannot be empty and must have a value", UsernameFormat)
|
return l, fmt.Errorf("'%s' cannot be empty and must have a value", UsernameFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v := env.Get(EnvUsernameSearchFilter, kvs.Get(UsernameSearchFilter)); v != "" {
|
||||||
|
if !strings.Contains(v, "%s") {
|
||||||
|
return l, errors.New("LDAP username search filter doesn't have '%s' substitution")
|
||||||
|
}
|
||||||
|
l.UsernameSearchFilter = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := env.Get(EnvUsernameSearchBaseDN, kvs.Get(UsernameSearchBaseDN)); v != "" {
|
||||||
|
l.UsernameSearchBaseDNS = strings.Split(v, dnDelimiter)
|
||||||
|
}
|
||||||
|
|
||||||
grpSearchFilter := env.Get(EnvGroupSearchFilter, kvs.Get(GroupSearchFilter))
|
grpSearchFilter := env.Get(EnvGroupSearchFilter, kvs.Get(GroupSearchFilter))
|
||||||
grpSearchNameAttr := env.Get(EnvGroupNameAttribute, kvs.Get(GroupNameAttribute))
|
grpSearchNameAttr := env.Get(EnvGroupNameAttribute, kvs.Get(GroupNameAttribute))
|
||||||
grpSearchBaseDN := env.Get(EnvGroupSearchBaseDN, kvs.Get(GroupSearchBaseDN))
|
grpSearchBaseDN := env.Get(EnvGroupSearchBaseDN, kvs.Get(GroupSearchBaseDN))
|
||||||
|
|
||||||
// Either all group params must be set or none must be set.
|
// Either all group params must be set or none must be set.
|
||||||
allNotSet := grpSearchFilter == "" && grpSearchNameAttr == "" && grpSearchBaseDN == ""
|
var allSet bool
|
||||||
allSet := grpSearchFilter != "" && grpSearchNameAttr != "" && grpSearchBaseDN != ""
|
if grpSearchFilter != "" {
|
||||||
if !allNotSet && !allSet {
|
if grpSearchNameAttr == "" || grpSearchBaseDN == "" {
|
||||||
return l, errors.New("All group related parameters must be set")
|
return l, errors.New("All group related parameters must be set")
|
||||||
|
}
|
||||||
|
allSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if allSet {
|
if allSet {
|
||||||
subs, err := NewSubstituter("username", "test", "usernamedn", "test2")
|
|
||||||
if err != nil {
|
|
||||||
return l, err
|
|
||||||
}
|
|
||||||
if _, err := subs.Substitute(grpSearchFilter); err != nil {
|
|
||||||
return l, fmt.Errorf("Only username and usernamedn may be substituted in the group search filter string: %s", err)
|
|
||||||
}
|
|
||||||
l.GroupSearchFilter = grpSearchFilter
|
l.GroupSearchFilter = grpSearchFilter
|
||||||
l.GroupNameAttribute = grpSearchNameAttr
|
l.GroupNameAttribute = grpSearchNameAttr
|
||||||
subs, err = NewSubstituter("username", "test", "usernamedn", "test2")
|
l.GroupSearchBaseDNS = strings.Split(grpSearchBaseDN, dnDelimiter)
|
||||||
if err != nil {
|
|
||||||
return l, err
|
|
||||||
}
|
|
||||||
if _, err := subs.Substitute(grpSearchBaseDN); err != nil {
|
|
||||||
return l, fmt.Errorf("Only username and usernamedn may be substituted in the base DN string: %s", err)
|
|
||||||
}
|
|
||||||
l.GroupSearchBaseDN = grpSearchBaseDN
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l.rootCAs = rootCAs
|
l.rootCAs = rootCAs
|
||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Substituter - This type is to allow restricted runtime
|
|
||||||
// substitutions of variables in LDAP configuration items during
|
|
||||||
// runtime.
|
|
||||||
type Substituter struct {
|
|
||||||
vals map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSubstituter - sets up the substituter for usage, for e.g.:
|
|
||||||
//
|
|
||||||
// subber := NewSubstituter("username", "john")
|
|
||||||
func NewSubstituter(v ...string) (Substituter, error) {
|
|
||||||
if len(v)%2 != 0 {
|
|
||||||
return Substituter{}, errors.New("Need an even number of arguments")
|
|
||||||
}
|
|
||||||
vals := make(map[string]string)
|
|
||||||
for i := 0; i < len(v); i += 2 {
|
|
||||||
vals[v[i]] = v[i+1]
|
|
||||||
}
|
|
||||||
return Substituter{vals: vals}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Substitute - performs substitution on the given string `t`. Returns
|
|
||||||
// an error if there are any variables in the input that do not have
|
|
||||||
// values in the substituter. E.g.:
|
|
||||||
//
|
|
||||||
// subber.Substitute("uid=${username},cn=users,dc=example,dc=com")
|
|
||||||
//
|
|
||||||
// or
|
|
||||||
//
|
|
||||||
// subber.Substitute("uid={username},cn=users,dc=example,dc=com")
|
|
||||||
//
|
|
||||||
// returns "uid=john,cn=users,dc=example,dc=com"
|
|
||||||
//
|
|
||||||
// whereas:
|
|
||||||
//
|
|
||||||
// subber.Substitute("uid=${usernamedn}")
|
|
||||||
//
|
|
||||||
// returns an error.
|
|
||||||
func (s *Substituter) Substitute(t string) (string, error) {
|
|
||||||
for k, v := range s.vals {
|
|
||||||
reDollar := regexp.MustCompile(fmt.Sprintf(`\$\{%s\}`, k))
|
|
||||||
t = reDollar.ReplaceAllLiteralString(t, v)
|
|
||||||
reFlower := regexp.MustCompile(fmt.Sprintf(`\{%s\}`, k))
|
|
||||||
t = reFlower.ReplaceAllLiteralString(t, v)
|
|
||||||
}
|
|
||||||
// Check if all requested substitutions have been made.
|
|
||||||
re := regexp.MustCompile(`\{.*\}`)
|
|
||||||
if re.MatchString(t) {
|
|
||||||
return "", errors.New("unsupported substitution requested")
|
|
||||||
}
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 ldap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSubstituter(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
KV []string
|
|
||||||
SubstitutableStr string
|
|
||||||
SubstitutedStr string
|
|
||||||
ErrExpected bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
KV: []string{"username", "john"},
|
|
||||||
SubstitutableStr: "uid=${username},cn=users,dc=example,dc=com",
|
|
||||||
SubstitutedStr: "uid=john,cn=users,dc=example,dc=com",
|
|
||||||
ErrExpected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
KV: []string{"username", "john"},
|
|
||||||
SubstitutableStr: "uid={username},cn=users,dc=example,dc=com",
|
|
||||||
SubstitutedStr: "uid=john,cn=users,dc=example,dc=com",
|
|
||||||
ErrExpected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
KV: []string{"username", "john"},
|
|
||||||
SubstitutableStr: "(&(objectclass=group)(member=${username}))",
|
|
||||||
SubstitutedStr: "(&(objectclass=group)(member=john))",
|
|
||||||
ErrExpected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
KV: []string{"username", "john"},
|
|
||||||
SubstitutableStr: "(&(objectclass=group)(member={username}))",
|
|
||||||
SubstitutedStr: "(&(objectclass=group)(member=john))",
|
|
||||||
ErrExpected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
KV: []string{"username", "john"},
|
|
||||||
SubstitutableStr: "uid=${{username}},cn=users,dc=example,dc=com",
|
|
||||||
ErrExpected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
KV: []string{"username", "john"},
|
|
||||||
SubstitutableStr: "uid=${usernamedn},cn=users,dc=example,dc=com",
|
|
||||||
ErrExpected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
KV: []string{"username"},
|
|
||||||
SubstitutableStr: "uid=${usernamedn},cn=users,dc=example,dc=com",
|
|
||||||
ErrExpected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
KV: []string{"username", "john"},
|
|
||||||
SubstitutableStr: "(&(objectclass=user)(sAMAccountName={username})(memberOf=CN=myorg,OU=Rialto,OU=Application Managed,OU=Groups,DC=amr,DC=corp,DC=myorg,DC=com))",
|
|
||||||
SubstitutedStr: "(&(objectclass=user)(sAMAccountName=john)(memberOf=CN=myorg,OU=Rialto,OU=Application Managed,OU=Groups,DC=amr,DC=corp,DC=myorg,DC=com))",
|
|
||||||
ErrExpected: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
test := test
|
|
||||||
t.Run(test.SubstitutableStr, func(t *testing.T) {
|
|
||||||
subber, err := NewSubstituter(test.KV...)
|
|
||||||
if err != nil && !test.ErrExpected {
|
|
||||||
t.Errorf("Unexpected failure %s", err)
|
|
||||||
}
|
|
||||||
gotStr, err := subber.Substitute(test.SubstitutableStr)
|
|
||||||
if err != nil && !test.ErrExpected {
|
|
||||||
t.Errorf("Unexpected failure %s", err)
|
|
||||||
}
|
|
||||||
if gotStr != test.SubstitutedStr {
|
|
||||||
t.Errorf("Expected %s, got %s", test.SubstitutedStr, gotStr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,24 +28,33 @@ var (
|
|||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: UsernameFormat,
|
Key: UsernameFormat,
|
||||||
Description: `username bind DNs e.g. "uid=%s,cn=accounts,dc=myldapserver,dc=com"`,
|
Description: `";" separated list of username bind DNs e.g. "uid=%s,cn=accounts,dc=myldapserver,dc=com"`,
|
||||||
|
Type: "list",
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: UsernameSearchFilter,
|
||||||
|
Description: `user search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"`,
|
||||||
Type: "string",
|
Type: "string",
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: GroupSearchFilter,
|
Key: GroupSearchFilter,
|
||||||
Description: `search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))"`,
|
Description: `search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))"`,
|
||||||
Optional: true,
|
|
||||||
Type: "string",
|
|
||||||
},
|
|
||||||
config.HelpKV{
|
|
||||||
Key: GroupNameAttribute,
|
|
||||||
Description: `search attribute for group name e.g. "cn"`,
|
|
||||||
Optional: true,
|
|
||||||
Type: "string",
|
Type: "string",
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: GroupSearchBaseDN,
|
Key: GroupSearchBaseDN,
|
||||||
Description: `group search base DNs e.g. "dc=myldapserver,dc=com"`,
|
Description: `";" separated list of group search base DNs e.g. "dc=myldapserver,dc=com"`,
|
||||||
|
Type: "list",
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: UsernameSearchBaseDN,
|
||||||
|
Description: `";" separated list of username search DNs`,
|
||||||
|
Type: "list",
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: GroupNameAttribute,
|
||||||
|
Description: `search attribute for group name e.g. "cn"`,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Type: "string",
|
Type: "string",
|
||||||
},
|
},
|
||||||
@@ -57,7 +66,13 @@ var (
|
|||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: TLSSkipVerify,
|
Key: TLSSkipVerify,
|
||||||
Description: `trust server TLS without verification, defaults to "on" (verify)`,
|
Description: `trust server TLS without verification, defaults to "off" (verify)`,
|
||||||
|
Optional: true,
|
||||||
|
Type: "on|off",
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: ServerInsecure,
|
||||||
|
Description: `allow plain text connection to AD/LDAP server, defaults to "off"`,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Type: "on|off",
|
Type: "on|off",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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 openid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
// Needed for SHA3 to work - See: https://golang.org/src/crypto/crypto.go?s=1034:1288
|
||||||
|
_ "golang.org/x/crypto/sha3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Specific instances for EC256 and company
|
||||||
|
var (
|
||||||
|
SigningMethodES3256 *jwt.SigningMethodECDSA
|
||||||
|
SigningMethodES3384 *jwt.SigningMethodECDSA
|
||||||
|
SigningMethodES3512 *jwt.SigningMethodECDSA
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// ES256
|
||||||
|
SigningMethodES3256 = &jwt.SigningMethodECDSA{Name: "ES3256", Hash: crypto.SHA3_256, KeySize: 32, CurveBits: 256}
|
||||||
|
jwt.RegisterSigningMethod(SigningMethodES3256.Alg(), func() jwt.SigningMethod {
|
||||||
|
return SigningMethodES3256
|
||||||
|
})
|
||||||
|
|
||||||
|
// ES384
|
||||||
|
SigningMethodES3384 = &jwt.SigningMethodECDSA{Name: "ES3384", Hash: crypto.SHA3_384, KeySize: 48, CurveBits: 384}
|
||||||
|
jwt.RegisterSigningMethod(SigningMethodES3384.Alg(), func() jwt.SigningMethod {
|
||||||
|
return SigningMethodES3384
|
||||||
|
})
|
||||||
|
|
||||||
|
// ES512
|
||||||
|
SigningMethodES3512 = &jwt.SigningMethodECDSA{Name: "ES3512", Hash: crypto.SHA3_512, KeySize: 66, CurveBits: 521}
|
||||||
|
jwt.RegisterSigningMethod(SigningMethodES3512.Alg(), func() jwt.SigningMethod {
|
||||||
|
return SigningMethodES3512
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ import (
|
|||||||
"github.com/minio/minio/cmd/config"
|
"github.com/minio/minio/cmd/config"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/env"
|
"github.com/minio/minio/pkg/env"
|
||||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
"github.com/minio/minio/pkg/iam/policy"
|
||||||
xnet "github.com/minio/minio/pkg/net"
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -168,7 +168,10 @@ func updateClaimsExpiry(dsecs string, claims map[string]interface{}) error {
|
|||||||
// Validate - validates the access token.
|
// Validate - validates the access token.
|
||||||
func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) {
|
func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) {
|
||||||
jp := new(jwtgo.Parser)
|
jp := new(jwtgo.Parser)
|
||||||
jp.ValidMethods = []string{"RS256", "RS384", "RS512", "ES256", "ES384", "ES512"}
|
jp.ValidMethods = []string{
|
||||||
|
"RS256", "RS384", "RS512", "ES256", "ES384", "ES512",
|
||||||
|
"RS3256", "RS3384", "RS3512", "ES3256", "ES3384", "ES3512",
|
||||||
|
}
|
||||||
|
|
||||||
keyFuncCallback := func(jwtToken *jwtgo.Token) (interface{}, error) {
|
keyFuncCallback := func(jwtToken *jwtgo.Token) (interface{}, error) {
|
||||||
kid, ok := jwtToken.Header["kid"].(string)
|
kid, ok := jwtToken.Header["kid"].(string)
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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 openid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
// Needed for SHA3 to work - See: https://golang.org/src/crypto/crypto.go?s=1034:1288
|
||||||
|
_ "golang.org/x/crypto/sha3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Specific instances for RS256 and company
|
||||||
|
var (
|
||||||
|
SigningMethodRS3256 *jwt.SigningMethodRSA
|
||||||
|
SigningMethodRS3384 *jwt.SigningMethodRSA
|
||||||
|
SigningMethodRS3512 *jwt.SigningMethodRSA
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// RS3256
|
||||||
|
SigningMethodRS3256 = &jwt.SigningMethodRSA{Name: "RS3256", Hash: crypto.SHA3_256}
|
||||||
|
jwt.RegisterSigningMethod(SigningMethodRS3256.Alg(), func() jwt.SigningMethod {
|
||||||
|
return SigningMethodRS3256
|
||||||
|
})
|
||||||
|
|
||||||
|
// RS3384
|
||||||
|
SigningMethodRS3384 = &jwt.SigningMethodRSA{Name: "RS3384", Hash: crypto.SHA3_384}
|
||||||
|
jwt.RegisterSigningMethod(SigningMethodRS3384.Alg(), func() jwt.SigningMethod {
|
||||||
|
return SigningMethodRS3384
|
||||||
|
})
|
||||||
|
|
||||||
|
// RS3512
|
||||||
|
SigningMethodRS3512 = &jwt.SigningMethodRSA{Name: "RS3512", Hash: crypto.SHA3_512}
|
||||||
|
jwt.RegisterSigningMethod(SigningMethodRS3512.Alg(), func() jwt.SigningMethod {
|
||||||
|
return SigningMethodRS3512
|
||||||
|
})
|
||||||
|
}
|
||||||
+10
-63
@@ -24,7 +24,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
formatComment = `'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'`
|
formatComment = `'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'`
|
||||||
queueDirComment = `staging dir for undelivered messages e.g. '/home/events'`
|
queueDirComment = `staging dir for undelivered messages e.g. '/home/events'`
|
||||||
queueLimitComment = `maximum limit for undelivered messages, defaults to '10000'`
|
queueLimitComment = `maximum limit for undelivered messages, defaults to '100000'`
|
||||||
)
|
)
|
||||||
|
|
||||||
// Help template inputs for all notification targets
|
// Help template inputs for all notification targets
|
||||||
@@ -165,6 +165,12 @@ var (
|
|||||||
Optional: true,
|
Optional: true,
|
||||||
Type: "string",
|
Type: "string",
|
||||||
},
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: target.KafkaSASLMechanism,
|
||||||
|
Description: "sasl authentication mechanism, default 'plain'",
|
||||||
|
Optional: true,
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: target.KafkaTLSClientAuth,
|
Key: target.KafkaTLSClientAuth,
|
||||||
Description: "clientAuth determines the Kafka server's policy for TLS client auth",
|
Description: "clientAuth determines the Kafka server's policy for TLS client auth",
|
||||||
@@ -291,7 +297,7 @@ var (
|
|||||||
HelpPostgres = config.HelpKVS{
|
HelpPostgres = config.HelpKVS{
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: target.PostgresConnectionString,
|
Key: target.PostgresConnectionString,
|
||||||
Description: "Postgres server connection-string",
|
Description: `Postgres server connection-string e.g. "host=localhost port=5432 dbname=minio_events user=postgres password=password sslmode=disable"`,
|
||||||
Type: "string",
|
Type: "string",
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
@@ -304,36 +310,6 @@ var (
|
|||||||
Description: formatComment,
|
Description: formatComment,
|
||||||
Type: "namespace*|access",
|
Type: "namespace*|access",
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
|
||||||
Key: target.PostgresHost,
|
|
||||||
Description: "Postgres server hostname (used only if `connection_string` is empty)",
|
|
||||||
Optional: true,
|
|
||||||
Type: "hostname",
|
|
||||||
},
|
|
||||||
config.HelpKV{
|
|
||||||
Key: target.PostgresPort,
|
|
||||||
Description: "Postgres server port, defaults to `5432` (used only if `connection_string` is empty)",
|
|
||||||
Optional: true,
|
|
||||||
Type: "port",
|
|
||||||
},
|
|
||||||
config.HelpKV{
|
|
||||||
Key: target.PostgresUsername,
|
|
||||||
Description: "database username (used only if `connection_string` is empty)",
|
|
||||||
Optional: true,
|
|
||||||
Type: "string",
|
|
||||||
},
|
|
||||||
config.HelpKV{
|
|
||||||
Key: target.PostgresPassword,
|
|
||||||
Description: "database password (used only if `connection_string` is empty)",
|
|
||||||
Optional: true,
|
|
||||||
Type: "string",
|
|
||||||
},
|
|
||||||
config.HelpKV{
|
|
||||||
Key: target.PostgresDatabase,
|
|
||||||
Description: "database name (used only if `connection_string` is empty)",
|
|
||||||
Optional: true,
|
|
||||||
Type: "string",
|
|
||||||
},
|
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: target.PostgresQueueDir,
|
Key: target.PostgresQueueDir,
|
||||||
Description: queueDirComment,
|
Description: queueDirComment,
|
||||||
@@ -357,7 +333,8 @@ var (
|
|||||||
HelpMySQL = config.HelpKVS{
|
HelpMySQL = config.HelpKVS{
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: target.MySQLDSNString,
|
Key: target.MySQLDSNString,
|
||||||
Description: "MySQL data-source-name connection string",
|
Description: `MySQL data-source-name connection string e.g. "<user>:<password>@tcp(<host>:<port>)/<database>"`,
|
||||||
|
Optional: true,
|
||||||
Type: "string",
|
Type: "string",
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
@@ -370,36 +347,6 @@ var (
|
|||||||
Description: formatComment,
|
Description: formatComment,
|
||||||
Type: "namespace*|access",
|
Type: "namespace*|access",
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
|
||||||
Key: target.MySQLHost,
|
|
||||||
Description: "MySQL server hostname (used only if `dsn_string` is empty)",
|
|
||||||
Optional: true,
|
|
||||||
Type: "hostname",
|
|
||||||
},
|
|
||||||
config.HelpKV{
|
|
||||||
Key: target.MySQLPort,
|
|
||||||
Description: "MySQL server port (used only if `dsn_string` is empty)",
|
|
||||||
Optional: true,
|
|
||||||
Type: "port",
|
|
||||||
},
|
|
||||||
config.HelpKV{
|
|
||||||
Key: target.MySQLUsername,
|
|
||||||
Description: "database username (used only if `dsn_string` is empty)",
|
|
||||||
Optional: true,
|
|
||||||
Type: "string",
|
|
||||||
},
|
|
||||||
config.HelpKV{
|
|
||||||
Key: target.MySQLPassword,
|
|
||||||
Description: "database password (used only if `dsn_string` is empty)",
|
|
||||||
Optional: true,
|
|
||||||
Type: "string",
|
|
||||||
},
|
|
||||||
config.HelpKV{
|
|
||||||
Key: target.MySQLDatabase,
|
|
||||||
Description: "database name (used only if `dsn_string` is empty)",
|
|
||||||
Optional: true,
|
|
||||||
Type: "string",
|
|
||||||
},
|
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: target.MySQLQueueDir,
|
Key: target.MySQLQueueDir,
|
||||||
Description: queueDirComment,
|
Description: queueDirComment,
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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 notify
|
package notify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
+150
-145
@@ -17,8 +17,10 @@
|
|||||||
package notify
|
package notify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -37,12 +39,16 @@ const (
|
|||||||
formatAccess = "access"
|
formatAccess = "access"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrTargetsOffline - Indicates single/multiple target failures.
|
||||||
|
var ErrTargetsOffline = errors.New("one or more targets are offline. Please use `mc admin info --json` to check the offline targets")
|
||||||
|
|
||||||
// TestNotificationTargets is similar to GetNotificationTargets()
|
// TestNotificationTargets is similar to GetNotificationTargets()
|
||||||
// avoids explicit registration.
|
// avoids explicit registration.
|
||||||
func TestNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport,
|
func TestNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport,
|
||||||
targetIDs []event.TargetID) error {
|
targetIDs []event.TargetID) error {
|
||||||
test := true
|
test := true
|
||||||
targets, err := RegisterNotificationTargets(cfg, doneCh, transport, targetIDs, test)
|
returnOnTargetError := true
|
||||||
|
targets, err := RegisterNotificationTargets(cfg, doneCh, transport, targetIDs, test, returnOnTargetError)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Close all targets since we are only testing connections.
|
// Close all targets since we are only testing connections.
|
||||||
for _, t := range targets.TargetMap() {
|
for _, t := range targets.TargetMap() {
|
||||||
@@ -57,7 +63,8 @@ func TestNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transpor
|
|||||||
// targets, returns error if any.
|
// targets, returns error if any.
|
||||||
func GetNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport) (*event.TargetList, error) {
|
func GetNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport) (*event.TargetList, error) {
|
||||||
test := false
|
test := false
|
||||||
return RegisterNotificationTargets(cfg, doneCh, transport, nil, test)
|
returnOnTargetError := false
|
||||||
|
return RegisterNotificationTargets(cfg, doneCh, transport, nil, test, returnOnTargetError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterNotificationTargets - returns TargetList which contains enabled targets in serverConfig.
|
// RegisterNotificationTargets - returns TargetList which contains enabled targets in serverConfig.
|
||||||
@@ -65,12 +72,40 @@ func GetNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport
|
|||||||
// * Add a new target in pkg/event/target package.
|
// * Add a new target in pkg/event/target package.
|
||||||
// * Add newly added target configuration to serverConfig.Notify.<TARGET_NAME>.
|
// * Add newly added target configuration to serverConfig.Notify.<TARGET_NAME>.
|
||||||
// * Handle the configuration in this function to create/add into TargetList.
|
// * Handle the configuration in this function to create/add into TargetList.
|
||||||
func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport, targetIDs []event.TargetID, test bool) (_ *event.TargetList, err error) {
|
func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport, targetIDs []event.TargetID, test bool, returnOnTargetError bool) (*event.TargetList, error) {
|
||||||
|
|
||||||
|
targetList, err := FetchRegisteredTargets(cfg, doneCh, transport, test, returnOnTargetError)
|
||||||
|
if err != nil {
|
||||||
|
return targetList, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if test {
|
||||||
|
// Verify if user is trying to disable already configured
|
||||||
|
// notification targets, based on their target IDs
|
||||||
|
for _, targetID := range targetIDs {
|
||||||
|
if !targetList.Exists(targetID) {
|
||||||
|
return nil, config.Errorf(
|
||||||
|
"Unable to disable configured targets '%v'",
|
||||||
|
targetID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchRegisteredTargets - Returns a set of configured TargetList
|
||||||
|
// If `returnOnTargetError` is set to true, The function returns when a target initialization fails
|
||||||
|
// Else, the function will return a complete TargetList irrespective of errors
|
||||||
|
func FetchRegisteredTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport, test bool, returnOnTargetError bool) (_ *event.TargetList, err error) {
|
||||||
targetList := event.NewTargetList()
|
targetList := event.NewTargetList()
|
||||||
|
var targetsOffline bool
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
// Automatically close all connections to targets when an error occur
|
// Automatically close all connections to targets when an error occur.
|
||||||
if err != nil {
|
// Close all the targets if returnOnTargetError is set
|
||||||
|
// Else, close only the failed targets
|
||||||
|
if err != nil && returnOnTargetError {
|
||||||
for _, t := range targetList.TargetMap() {
|
for _, t := range targetList.TargetMap() {
|
||||||
_ = t.Close()
|
_ = t.Close()
|
||||||
}
|
}
|
||||||
@@ -137,10 +172,18 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
|||||||
}
|
}
|
||||||
newTarget, err := target.NewAMQPTarget(id, args, doneCh, logger.LogOnceIf, test)
|
newTarget, err := target.NewAMQPTarget(id, args, doneCh, logger.LogOnceIf, test)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
targetsOffline = true
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = newTarget.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,11 +193,17 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
|||||||
}
|
}
|
||||||
newTarget, err := target.NewElasticsearchTarget(id, args, doneCh, logger.LogOnceIf, test)
|
newTarget, err := target.NewElasticsearchTarget(id, args, doneCh, logger.LogOnceIf, test)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
targetsOffline = true
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = newTarget.Close()
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,10 +214,17 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
|||||||
args.TLS.RootCAs = transport.TLSClientConfig.RootCAs
|
args.TLS.RootCAs = transport.TLSClientConfig.RootCAs
|
||||||
newTarget, err := target.NewKafkaTarget(id, args, doneCh, logger.LogOnceIf, test)
|
newTarget, err := target.NewKafkaTarget(id, args, doneCh, logger.LogOnceIf, test)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
targetsOffline = true
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = newTarget.Close()
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,10 +235,17 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
|||||||
args.RootCAs = transport.TLSClientConfig.RootCAs
|
args.RootCAs = transport.TLSClientConfig.RootCAs
|
||||||
newTarget, err := target.NewMQTTTarget(id, args, doneCh, logger.LogOnceIf, test)
|
newTarget, err := target.NewMQTTTarget(id, args, doneCh, logger.LogOnceIf, test)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
targetsOffline = true
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = newTarget.Close()
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,10 +255,17 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
|||||||
}
|
}
|
||||||
newTarget, err := target.NewMySQLTarget(id, args, doneCh, logger.LogOnceIf, test)
|
newTarget, err := target.NewMySQLTarget(id, args, doneCh, logger.LogOnceIf, test)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
targetsOffline = true
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = newTarget.Close()
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,10 +275,17 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
|||||||
}
|
}
|
||||||
newTarget, err := target.NewNATSTarget(id, args, doneCh, logger.LogOnceIf, test)
|
newTarget, err := target.NewNATSTarget(id, args, doneCh, logger.LogOnceIf, test)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
targetsOffline = true
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = newTarget.Close()
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,10 +295,17 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
|||||||
}
|
}
|
||||||
newTarget, err := target.NewNSQTarget(id, args, doneCh, logger.LogOnceIf, test)
|
newTarget, err := target.NewNSQTarget(id, args, doneCh, logger.LogOnceIf, test)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
targetsOffline = true
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = newTarget.Close()
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,10 +315,17 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
|||||||
}
|
}
|
||||||
newTarget, err := target.NewPostgreSQLTarget(id, args, doneCh, logger.LogOnceIf, test)
|
newTarget, err := target.NewPostgreSQLTarget(id, args, doneCh, logger.LogOnceIf, test)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
targetsOffline = true
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = newTarget.Close()
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,10 +335,17 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
|||||||
}
|
}
|
||||||
newTarget, err := target.NewRedisTarget(id, args, doneCh, logger.LogOnceIf, test)
|
newTarget, err := target.NewRedisTarget(id, args, doneCh, logger.LogOnceIf, test)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
targetsOffline = true
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = newTarget.Close()
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,23 +355,22 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
|||||||
}
|
}
|
||||||
newTarget, err := target.NewWebhookTarget(id, args, doneCh, logger.LogOnceIf, transport, test)
|
newTarget, err := target.NewWebhookTarget(id, args, doneCh, logger.LogOnceIf, transport, test)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
targetsOffline = true
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = newTarget.Close()
|
||||||
}
|
}
|
||||||
if err := targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
if returnOnTargetError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if test {
|
if targetsOffline {
|
||||||
// Verify if user is trying to disable already configured
|
return targetList, ErrTargetsOffline
|
||||||
// notification targets, based on their target IDs
|
|
||||||
for _, targetID := range targetIDs {
|
|
||||||
if !targetList.Exists(targetID) {
|
|
||||||
return nil, config.Errorf(
|
|
||||||
"Unable to disable configured targets '%v'",
|
|
||||||
targetID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return targetList, nil
|
return targetList, nil
|
||||||
@@ -306,8 +403,10 @@ func checkValidNotificationKeys(cfg config.Config) error {
|
|||||||
if tname != config.Default {
|
if tname != config.Default {
|
||||||
subSysTarget = subSys + config.SubSystemSeparator + tname
|
subSysTarget = subSys + config.SubSystemSeparator + tname
|
||||||
}
|
}
|
||||||
if err := config.CheckValidKeys(subSysTarget, kv, validKVS); err != nil {
|
if v, ok := kv.Lookup(config.Enable); ok && v == config.EnableOn {
|
||||||
return err
|
if err := config.CheckValidKeys(subSysTarget, kv, validKVS); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,6 +451,10 @@ var (
|
|||||||
Key: target.KafkaSASLPassword,
|
Key: target.KafkaSASLPassword,
|
||||||
Value: "",
|
Value: "",
|
||||||
},
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: target.KafkaSASLMechanism,
|
||||||
|
Value: "plain",
|
||||||
|
},
|
||||||
config.KV{
|
config.KV{
|
||||||
Key: target.KafkaClientTLSCert,
|
Key: target.KafkaClientTLSCert,
|
||||||
Value: "",
|
Value: "",
|
||||||
@@ -507,9 +610,14 @@ func GetNotifyKafka(kafkaKVS map[string]config.KVS) (map[string]target.KafkaArgs
|
|||||||
if k != config.Default {
|
if k != config.Default {
|
||||||
saslPasswordEnv = saslPasswordEnv + config.Default + k
|
saslPasswordEnv = saslPasswordEnv + config.Default + k
|
||||||
}
|
}
|
||||||
|
saslMechanismEnv := target.EnvKafkaSASLMechanism
|
||||||
|
if k != config.Default {
|
||||||
|
saslMechanismEnv = saslMechanismEnv + config.Default + k
|
||||||
|
}
|
||||||
kafkaArgs.SASL.Enable = env.Get(saslEnableEnv, kv.Get(target.KafkaSASL)) == config.EnableOn
|
kafkaArgs.SASL.Enable = env.Get(saslEnableEnv, kv.Get(target.KafkaSASL)) == config.EnableOn
|
||||||
kafkaArgs.SASL.User = env.Get(saslUsernameEnv, kv.Get(target.KafkaSASLUsername))
|
kafkaArgs.SASL.User = env.Get(saslUsernameEnv, kv.Get(target.KafkaSASLUsername))
|
||||||
kafkaArgs.SASL.Password = env.Get(saslPasswordEnv, kv.Get(target.KafkaSASLPassword))
|
kafkaArgs.SASL.Password = env.Get(saslPasswordEnv, kv.Get(target.KafkaSASLPassword))
|
||||||
|
kafkaArgs.SASL.Mechanism = env.Get(saslMechanismEnv, kv.Get(target.KafkaSASLMechanism))
|
||||||
|
|
||||||
if err = kafkaArgs.Validate(); err != nil {
|
if err = kafkaArgs.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -687,26 +795,6 @@ var (
|
|||||||
Key: target.MySQLFormat,
|
Key: target.MySQLFormat,
|
||||||
Value: formatNamespace,
|
Value: formatNamespace,
|
||||||
},
|
},
|
||||||
config.KV{
|
|
||||||
Key: target.MySQLHost,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: target.MySQLPort,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: target.MySQLUsername,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: target.MySQLPassword,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: target.MySQLDatabase,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
config.KV{
|
||||||
Key: target.MySQLDSNString,
|
Key: target.MySQLDSNString,
|
||||||
Value: "",
|
Value: "",
|
||||||
@@ -743,16 +831,6 @@ func GetNotifyMySQL(mysqlKVS map[string]config.KVS) (map[string]target.MySQLArgs
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
hostEnv := target.EnvMySQLHost
|
|
||||||
if k != config.Default {
|
|
||||||
hostEnv = hostEnv + config.Default + k
|
|
||||||
}
|
|
||||||
|
|
||||||
host, err := xnet.ParseURL(env.Get(hostEnv, kv.Get(target.MySQLHost)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
queueLimitEnv := target.EnvMySQLQueueLimit
|
queueLimitEnv := target.EnvMySQLQueueLimit
|
||||||
if k != config.Default {
|
if k != config.Default {
|
||||||
queueLimitEnv = queueLimitEnv + config.Default + k
|
queueLimitEnv = queueLimitEnv + config.Default + k
|
||||||
@@ -766,30 +844,17 @@ func GetNotifyMySQL(mysqlKVS map[string]config.KVS) (map[string]target.MySQLArgs
|
|||||||
if k != config.Default {
|
if k != config.Default {
|
||||||
formatEnv = formatEnv + config.Default + k
|
formatEnv = formatEnv + config.Default + k
|
||||||
}
|
}
|
||||||
|
|
||||||
dsnStringEnv := target.EnvMySQLDSNString
|
dsnStringEnv := target.EnvMySQLDSNString
|
||||||
if k != config.Default {
|
if k != config.Default {
|
||||||
dsnStringEnv = dsnStringEnv + config.Default + k
|
dsnStringEnv = dsnStringEnv + config.Default + k
|
||||||
}
|
}
|
||||||
|
|
||||||
tableEnv := target.EnvMySQLTable
|
tableEnv := target.EnvMySQLTable
|
||||||
if k != config.Default {
|
if k != config.Default {
|
||||||
tableEnv = tableEnv + config.Default + k
|
tableEnv = tableEnv + config.Default + k
|
||||||
}
|
}
|
||||||
portEnv := target.EnvMySQLPort
|
|
||||||
if k != config.Default {
|
|
||||||
portEnv = portEnv + config.Default + k
|
|
||||||
}
|
|
||||||
usernameEnv := target.EnvMySQLUsername
|
|
||||||
if k != config.Default {
|
|
||||||
usernameEnv = usernameEnv + config.Default + k
|
|
||||||
}
|
|
||||||
passwordEnv := target.EnvMySQLPassword
|
|
||||||
if k != config.Default {
|
|
||||||
passwordEnv = passwordEnv + config.Default + k
|
|
||||||
}
|
|
||||||
databaseEnv := target.EnvMySQLDatabase
|
|
||||||
if k != config.Default {
|
|
||||||
databaseEnv = databaseEnv + config.Default + k
|
|
||||||
}
|
|
||||||
queueDirEnv := target.EnvMySQLQueueDir
|
queueDirEnv := target.EnvMySQLQueueDir
|
||||||
if k != config.Default {
|
if k != config.Default {
|
||||||
queueDirEnv = queueDirEnv + config.Default + k
|
queueDirEnv = queueDirEnv + config.Default + k
|
||||||
@@ -799,11 +864,6 @@ func GetNotifyMySQL(mysqlKVS map[string]config.KVS) (map[string]target.MySQLArgs
|
|||||||
Format: env.Get(formatEnv, kv.Get(target.MySQLFormat)),
|
Format: env.Get(formatEnv, kv.Get(target.MySQLFormat)),
|
||||||
DSN: env.Get(dsnStringEnv, kv.Get(target.MySQLDSNString)),
|
DSN: env.Get(dsnStringEnv, kv.Get(target.MySQLDSNString)),
|
||||||
Table: env.Get(tableEnv, kv.Get(target.MySQLTable)),
|
Table: env.Get(tableEnv, kv.Get(target.MySQLTable)),
|
||||||
Host: *host,
|
|
||||||
Port: env.Get(portEnv, kv.Get(target.MySQLPort)),
|
|
||||||
User: env.Get(usernameEnv, kv.Get(target.MySQLUsername)),
|
|
||||||
Password: env.Get(passwordEnv, kv.Get(target.MySQLPassword)),
|
|
||||||
Database: env.Get(databaseEnv, kv.Get(target.MySQLDatabase)),
|
|
||||||
QueueDir: env.Get(queueDirEnv, kv.Get(target.MySQLQueueDir)),
|
QueueDir: env.Get(queueDirEnv, kv.Get(target.MySQLQueueDir)),
|
||||||
QueueLimit: queueLimit,
|
QueueLimit: queueLimit,
|
||||||
}
|
}
|
||||||
@@ -1171,26 +1231,6 @@ var (
|
|||||||
Key: target.PostgresTable,
|
Key: target.PostgresTable,
|
||||||
Value: "",
|
Value: "",
|
||||||
},
|
},
|
||||||
config.KV{
|
|
||||||
Key: target.PostgresHost,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: target.PostgresPort,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: target.PostgresUsername,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: target.PostgresPassword,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: target.PostgresDatabase,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
config.KV{
|
||||||
Key: target.PostgresQueueDir,
|
Key: target.PostgresQueueDir,
|
||||||
Value: "",
|
Value: "",
|
||||||
@@ -1219,16 +1259,6 @@ func GetNotifyPostgres(postgresKVS map[string]config.KVS) (map[string]target.Pos
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
hostEnv := target.EnvPostgresHost
|
|
||||||
if k != config.Default {
|
|
||||||
hostEnv = hostEnv + config.Default + k
|
|
||||||
}
|
|
||||||
|
|
||||||
host, err := xnet.ParseHost(env.Get(hostEnv, kv.Get(target.PostgresHost)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
queueLimitEnv := target.EnvPostgresQueueLimit
|
queueLimitEnv := target.EnvPostgresQueueLimit
|
||||||
if k != config.Default {
|
if k != config.Default {
|
||||||
queueLimitEnv = queueLimitEnv + config.Default + k
|
queueLimitEnv = queueLimitEnv + config.Default + k
|
||||||
@@ -1254,26 +1284,6 @@ func GetNotifyPostgres(postgresKVS map[string]config.KVS) (map[string]target.Pos
|
|||||||
tableEnv = tableEnv + config.Default + k
|
tableEnv = tableEnv + config.Default + k
|
||||||
}
|
}
|
||||||
|
|
||||||
portEnv := target.EnvPostgresPort
|
|
||||||
if k != config.Default {
|
|
||||||
portEnv = portEnv + config.Default + k
|
|
||||||
}
|
|
||||||
|
|
||||||
usernameEnv := target.EnvPostgresUsername
|
|
||||||
if k != config.Default {
|
|
||||||
usernameEnv = usernameEnv + config.Default + k
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordEnv := target.EnvPostgresPassword
|
|
||||||
if k != config.Default {
|
|
||||||
passwordEnv = passwordEnv + config.Default + k
|
|
||||||
}
|
|
||||||
|
|
||||||
databaseEnv := target.EnvPostgresDatabase
|
|
||||||
if k != config.Default {
|
|
||||||
databaseEnv = databaseEnv + config.Default + k
|
|
||||||
}
|
|
||||||
|
|
||||||
queueDirEnv := target.EnvPostgresQueueDir
|
queueDirEnv := target.EnvPostgresQueueDir
|
||||||
if k != config.Default {
|
if k != config.Default {
|
||||||
queueDirEnv = queueDirEnv + config.Default + k
|
queueDirEnv = queueDirEnv + config.Default + k
|
||||||
@@ -1284,11 +1294,6 @@ func GetNotifyPostgres(postgresKVS map[string]config.KVS) (map[string]target.Pos
|
|||||||
Format: env.Get(formatEnv, kv.Get(target.PostgresFormat)),
|
Format: env.Get(formatEnv, kv.Get(target.PostgresFormat)),
|
||||||
ConnectionString: env.Get(connectionStringEnv, kv.Get(target.PostgresConnectionString)),
|
ConnectionString: env.Get(connectionStringEnv, kv.Get(target.PostgresConnectionString)),
|
||||||
Table: env.Get(tableEnv, kv.Get(target.PostgresTable)),
|
Table: env.Get(tableEnv, kv.Get(target.PostgresTable)),
|
||||||
Host: *host,
|
|
||||||
Port: env.Get(portEnv, kv.Get(target.PostgresPort)),
|
|
||||||
User: env.Get(usernameEnv, kv.Get(target.PostgresUsername)),
|
|
||||||
Password: env.Get(passwordEnv, kv.Get(target.PostgresPassword)),
|
|
||||||
Database: env.Get(databaseEnv, kv.Get(target.PostgresDatabase)),
|
|
||||||
QueueDir: env.Get(queueDirEnv, kv.Get(target.PostgresQueueDir)),
|
QueueDir: env.Get(queueDirEnv, kv.Get(target.PostgresQueueDir)),
|
||||||
QueueLimit: uint64(queueLimit),
|
QueueLimit: uint64(queueLimit),
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-11
@@ -24,7 +24,6 @@ import (
|
|||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/cmd/logger/message/log"
|
"github.com/minio/minio/cmd/logger/message/log"
|
||||||
"github.com/minio/minio/cmd/logger/target/console"
|
"github.com/minio/minio/cmd/logger/target/console"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
|
||||||
xnet "github.com/minio/minio/pkg/net"
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
"github.com/minio/minio/pkg/pubsub"
|
"github.com/minio/minio/pkg/pubsub"
|
||||||
)
|
)
|
||||||
@@ -75,7 +74,7 @@ func (sys *HTTPConsoleLoggerSys) HasLogListeners() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe starts console logging for this node.
|
// Subscribe starts console logging for this node.
|
||||||
func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh chan struct{}, node string, last int, logKind string, filter func(entry interface{}) bool) {
|
func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh <-chan struct{}, node string, last int, logKind string, filter func(entry interface{}) bool) {
|
||||||
// Enable console logging for remote client.
|
// Enable console logging for remote client.
|
||||||
if !sys.HasLogListeners() {
|
if !sys.HasLogListeners() {
|
||||||
logger.AddTarget(sys)
|
logger.AddTarget(sys)
|
||||||
@@ -84,17 +83,20 @@ func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh chan s
|
|||||||
cnt := 0
|
cnt := 0
|
||||||
// by default send all console logs in the ring buffer unless node or limit query parameters
|
// by default send all console logs in the ring buffer unless node or limit query parameters
|
||||||
// are set.
|
// are set.
|
||||||
var lastN []madmin.LogInfo
|
var lastN []log.Info
|
||||||
if last > defaultLogBufferCount || last <= 0 {
|
if last > defaultLogBufferCount || last <= 0 {
|
||||||
last = defaultLogBufferCount
|
last = defaultLogBufferCount
|
||||||
}
|
}
|
||||||
|
|
||||||
lastN = make([]madmin.LogInfo, last)
|
lastN = make([]log.Info, last)
|
||||||
sys.RLock()
|
sys.RLock()
|
||||||
sys.logBuf.Do(func(p interface{}) {
|
sys.logBuf.Do(func(p interface{}) {
|
||||||
if p != nil && (p.(madmin.LogInfo)).SendLog(node, logKind) {
|
if p != nil {
|
||||||
lastN[cnt%last] = p.(madmin.LogInfo)
|
lg, ok := p.(log.Info)
|
||||||
cnt++
|
if ok && lg.SendLog(node, logKind) {
|
||||||
|
lastN[cnt%last] = lg
|
||||||
|
cnt++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
sys.RUnlock()
|
sys.RUnlock()
|
||||||
@@ -102,7 +104,7 @@ func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh chan s
|
|||||||
if cnt > 0 {
|
if cnt > 0 {
|
||||||
for i := 0; i < last; i++ {
|
for i := 0; i < last; i++ {
|
||||||
entry := lastN[(cnt+i)%last]
|
entry := lastN[(cnt+i)%last]
|
||||||
if (entry == madmin.LogInfo{}) {
|
if (entry == log.Info{}) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
@@ -118,12 +120,12 @@ func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh chan s
|
|||||||
// Send log message 'e' to console and publish to console
|
// Send log message 'e' to console and publish to console
|
||||||
// log pubsub system
|
// log pubsub system
|
||||||
func (sys *HTTPConsoleLoggerSys) Send(e interface{}, logKind string) error {
|
func (sys *HTTPConsoleLoggerSys) Send(e interface{}, logKind string) error {
|
||||||
var lg madmin.LogInfo
|
var lg log.Info
|
||||||
switch e := e.(type) {
|
switch e := e.(type) {
|
||||||
case log.Entry:
|
case log.Entry:
|
||||||
lg = madmin.LogInfo{Entry: e, NodeName: sys.nodeName}
|
lg = log.Info{Entry: e, NodeName: sys.nodeName}
|
||||||
case string:
|
case string:
|
||||||
lg = madmin.LogInfo{ConsoleMsg: e, NodeName: sys.nodeName}
|
lg = log.Info{ConsoleMsg: e, NodeName: sys.nodeName}
|
||||||
}
|
}
|
||||||
|
|
||||||
sys.pubsub.Publish(lg)
|
sys.pubsub.Publish(lg)
|
||||||
|
|||||||
+35
-90
@@ -30,90 +30,27 @@ const (
|
|||||||
bgLifecycleTick = time.Hour
|
bgLifecycleTick = time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
type lifecycleOps struct {
|
|
||||||
LastActivity time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register to the daily objects listing
|
|
||||||
var globalLifecycleOps = &lifecycleOps{}
|
|
||||||
|
|
||||||
func getLocalBgLifecycleOpsStatus() BgLifecycleOpsStatus {
|
|
||||||
return BgLifecycleOpsStatus{
|
|
||||||
LastActivity: globalLifecycleOps.LastActivity,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initDailyLifecycle starts the routine that receives the daily
|
// initDailyLifecycle starts the routine that receives the daily
|
||||||
// listing of all objects and applies any matching bucket lifecycle
|
// listing of all objects and applies any matching bucket lifecycle
|
||||||
// rules.
|
// rules.
|
||||||
func initDailyLifecycle() {
|
func initDailyLifecycle(ctx context.Context, objAPI ObjectLayer) {
|
||||||
go startDailyLifecycle()
|
go startDailyLifecycle(ctx, objAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func startDailyLifecycle() {
|
func startDailyLifecycle(ctx context.Context, objAPI ObjectLayer) {
|
||||||
var objAPI ObjectLayer
|
|
||||||
var ctx = context.Background()
|
|
||||||
|
|
||||||
// Wait until the object API is ready
|
|
||||||
for {
|
for {
|
||||||
objAPI = newObjectLayerWithoutSafeModeFn()
|
select {
|
||||||
if objAPI == nil {
|
case <-ctx.Done():
|
||||||
time.Sleep(time.Second)
|
return
|
||||||
continue
|
case <-time.NewTimer(bgLifecycleInterval).C:
|
||||||
}
|
// Perform one lifecycle operation
|
||||||
break
|
logger.LogIf(ctx, lifecycleRound(ctx, objAPI))
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the time of the last lifecycle operation in all peers node of the cluster
|
|
||||||
computeLastLifecycleActivity := func(status []BgOpsStatus) time.Time {
|
|
||||||
var lastAct time.Time
|
|
||||||
for _, st := range status {
|
|
||||||
if st.LifecycleOps.LastActivity.After(lastAct) {
|
|
||||||
lastAct = st.LifecycleOps.LastActivity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lastAct
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
// Check if we should perform lifecycle ops based on the last lifecycle activity, sleep one hour otherwise
|
|
||||||
allLifecycleStatus := []BgOpsStatus{
|
|
||||||
{LifecycleOps: getLocalBgLifecycleOpsStatus()},
|
|
||||||
}
|
|
||||||
if globalIsDistXL {
|
|
||||||
allLifecycleStatus = append(allLifecycleStatus, globalNotificationSys.BackgroundOpsStatus()...)
|
|
||||||
}
|
|
||||||
lastAct := computeLastLifecycleActivity(allLifecycleStatus)
|
|
||||||
if !lastAct.IsZero() && time.Since(lastAct) < bgLifecycleInterval {
|
|
||||||
time.Sleep(bgLifecycleTick)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform one lifecycle operation
|
|
||||||
err := lifecycleRound(ctx, objAPI)
|
|
||||||
switch err.(type) {
|
|
||||||
// Unable to hold a lock means there is another
|
|
||||||
// instance doing the lifecycle round round
|
|
||||||
case OperationTimedOut:
|
|
||||||
time.Sleep(bgLifecycleTick)
|
|
||||||
default:
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
time.Sleep(time.Minute)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var lifecycleTimeout = newDynamicTimeout(60*time.Second, time.Second)
|
|
||||||
|
|
||||||
func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
||||||
// Lock to avoid concurrent lifecycle ops from other nodes
|
|
||||||
sweepLock := objAPI.NewNSLock(ctx, "system", "daily-lifecycle-ops")
|
|
||||||
if err := sweepLock.GetLock(lifecycleTimeout); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer sweepLock.Unlock()
|
|
||||||
|
|
||||||
buckets, err := objAPI.ListBuckets(ctx)
|
buckets, err := objAPI.ListBuckets(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -126,6 +63,8 @@ func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, bucketHasLockConfig := globalBucketObjectLockConfig.Get(bucket.Name)
|
||||||
|
|
||||||
// Calculate the common prefix of all lifecycle rules
|
// Calculate the common prefix of all lifecycle rules
|
||||||
var prefixes []string
|
var prefixes []string
|
||||||
for _, rule := range l.Rules {
|
for _, rule := range l.Rules {
|
||||||
@@ -133,25 +72,37 @@ func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
|||||||
}
|
}
|
||||||
commonPrefix := lcp(prefixes)
|
commonPrefix := lcp(prefixes)
|
||||||
|
|
||||||
// List all objects and calculate lifecycle action based on object name & object modtime
|
// Allocate new results channel to receive ObjectInfo.
|
||||||
marker := ""
|
objInfoCh := make(chan ObjectInfo)
|
||||||
|
|
||||||
|
// Walk through all objects
|
||||||
|
if err := objAPI.Walk(ctx, bucket.Name, commonPrefix, objInfoCh); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
res, err := objAPI.ListObjects(ctx, bucket.Name, commonPrefix, marker, "", maxObjectList)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var objects []string
|
var objects []string
|
||||||
for _, obj := range res.Objects {
|
for obj := range objInfoCh {
|
||||||
|
if len(objects) == maxObjectList {
|
||||||
|
// Reached maximum delete requests, attempt a delete for now.
|
||||||
|
break
|
||||||
|
}
|
||||||
// Find the action that need to be executed
|
// Find the action that need to be executed
|
||||||
action := l.ComputeAction(obj.Name, obj.UserTags, obj.ModTime)
|
if l.ComputeAction(obj.Name, obj.UserTags, obj.ModTime) == lifecycle.DeleteAction {
|
||||||
switch action {
|
if bucketHasLockConfig && enforceRetentionForLifecycle(ctx, obj) {
|
||||||
case lifecycle.DeleteAction:
|
continue
|
||||||
|
}
|
||||||
objects = append(objects, obj.Name)
|
objects = append(objects, obj.Name)
|
||||||
default:
|
|
||||||
// Do nothing, for now.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nothing to do.
|
||||||
|
if len(objects) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForLowHTTPReq(int32(globalEndpoints.NEndpoints()))
|
||||||
|
|
||||||
// Deletes a list of objects.
|
// Deletes a list of objects.
|
||||||
deleteErrs, err := objAPI.DeleteObjects(ctx, bucket.Name, objects)
|
deleteErrs, err := objAPI.DeleteObjects(ctx, bucket.Name, objects)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -173,12 +124,6 @@ func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !res.IsTruncated {
|
|
||||||
// We are done here, proceed to next bucket.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
marker = res.NextMarker
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,611 @@
|
|||||||
|
/*
|
||||||
|
* 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 (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/config"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/color"
|
||||||
|
"github.com/minio/minio/pkg/env"
|
||||||
|
"github.com/willf/bloom"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Estimate bloom filter size. With this many items
|
||||||
|
dataUpdateTrackerEstItems = 1000000
|
||||||
|
// ... we want this false positive rate:
|
||||||
|
dataUpdateTrackerFP = 0.99
|
||||||
|
dataUpdateTrackerQueueSize = 10000
|
||||||
|
|
||||||
|
dataUpdateTrackerFilename = dataUsageBucket + SlashSeparator + ".tracker.bin"
|
||||||
|
dataUpdateTrackerVersion = 1
|
||||||
|
dataUpdateTrackerSaveInterval = 5 * time.Minute
|
||||||
|
|
||||||
|
// Reset bloom filters every n cycle
|
||||||
|
dataUpdateTrackerResetEvery = 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
objectUpdatedCh chan<- string
|
||||||
|
intDataUpdateTracker *dataUpdateTracker
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
intDataUpdateTracker = newDataUpdateTracker()
|
||||||
|
objectUpdatedCh = intDataUpdateTracker.input
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataUpdateTracker struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
input chan string
|
||||||
|
save chan struct{}
|
||||||
|
debug bool
|
||||||
|
saveExited chan struct{}
|
||||||
|
|
||||||
|
Current dataUpdateFilter
|
||||||
|
History dataUpdateTrackerHistory
|
||||||
|
Saved time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDataUpdateTracker returns a dataUpdateTracker with default settings.
|
||||||
|
func newDataUpdateTracker() *dataUpdateTracker {
|
||||||
|
d := &dataUpdateTracker{
|
||||||
|
Current: dataUpdateFilter{
|
||||||
|
idx: 1,
|
||||||
|
},
|
||||||
|
debug: env.Get(envDataUsageCrawlDebug, config.EnableOff) == config.EnableOn,
|
||||||
|
input: make(chan string, dataUpdateTrackerQueueSize),
|
||||||
|
save: make(chan struct{}, 1),
|
||||||
|
saveExited: make(chan struct{}),
|
||||||
|
}
|
||||||
|
d.Current.bf = d.newBloomFilter()
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataUpdateTrackerHistory []dataUpdateFilter
|
||||||
|
|
||||||
|
type dataUpdateFilter struct {
|
||||||
|
idx uint64
|
||||||
|
bf bloomFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
type bloomFilter struct {
|
||||||
|
*bloom.BloomFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
// emptyBloomFilter returns an empty bloom filter.
|
||||||
|
func emptyBloomFilter() bloomFilter {
|
||||||
|
return bloomFilter{BloomFilter: &bloom.BloomFilter{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsDir returns whether the bloom filter contains a directory.
|
||||||
|
// Note that objects in XL mode are also considered directories.
|
||||||
|
func (b bloomFilter) containsDir(in string) bool {
|
||||||
|
split := splitPathDeterministic(path.Clean(in))
|
||||||
|
|
||||||
|
if len(split) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var tmp [dataUsageHashLen]byte
|
||||||
|
hashPath(path.Join(split...)).bytes(tmp[:])
|
||||||
|
return b.Test(tmp[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// bytes returns the bloom filter serialized as a byte slice.
|
||||||
|
func (b bloomFilter) bytes() []byte {
|
||||||
|
if b.BloomFilter == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_, err := b.WriteTo(&buf)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(GlobalContext, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort the dataUpdateTrackerHistory, newest first.
|
||||||
|
// Returns whether the history is complete.
|
||||||
|
func (d dataUpdateTrackerHistory) sort() bool {
|
||||||
|
if len(d) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
sort.Slice(d, func(i, j int) bool {
|
||||||
|
return d[i].idx > d[j].idx
|
||||||
|
})
|
||||||
|
return d[0].idx-d[len(d)-1].idx == uint64(len(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeOlderThan will remove entries older than index 'n'.
|
||||||
|
func (d *dataUpdateTrackerHistory) removeOlderThan(n uint64) {
|
||||||
|
d.sort()
|
||||||
|
dd := *d
|
||||||
|
end := len(dd)
|
||||||
|
for i := end - 1; i >= 0; i-- {
|
||||||
|
if dd[i].idx < n {
|
||||||
|
end = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dd = dd[:end]
|
||||||
|
*d = dd
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBloomFilter returns a new bloom filter with default settings.
|
||||||
|
func (d *dataUpdateTracker) newBloomFilter() bloomFilter {
|
||||||
|
return bloomFilter{bloom.NewWithEstimates(dataUpdateTrackerEstItems, dataUpdateTrackerFP)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// current returns the current index.
|
||||||
|
func (d *dataUpdateTracker) current() uint64 {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
return d.Current.idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// start will load the current data from the drives start collecting information and
|
||||||
|
// start a saver goroutine.
|
||||||
|
// All of these will exit when the context is canceled.
|
||||||
|
func (d *dataUpdateTracker) start(ctx context.Context, drives ...string) {
|
||||||
|
if len(drives) <= 0 {
|
||||||
|
logger.LogIf(ctx, errors.New("dataUpdateTracker.start: No drives specified"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.load(ctx, drives...)
|
||||||
|
go d.startCollector(ctx)
|
||||||
|
go d.startSaver(ctx, dataUpdateTrackerSaveInterval, drives)
|
||||||
|
}
|
||||||
|
|
||||||
|
// load will attempt to load data tracking information from the supplied drives.
|
||||||
|
// The data will only be loaded if d.Saved is older than the one found on disk.
|
||||||
|
// The newest working cache will be kept in d.
|
||||||
|
// If no valid data usage tracker can be found d will remain unchanged.
|
||||||
|
// If object is shared the caller should lock it.
|
||||||
|
func (d *dataUpdateTracker) load(ctx context.Context, drives ...string) {
|
||||||
|
if len(drives) <= 0 {
|
||||||
|
logger.LogIf(ctx, errors.New("dataUpdateTracker.load: No drives specified"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, drive := range drives {
|
||||||
|
|
||||||
|
cacheFormatPath := pathJoin(drive, dataUpdateTrackerFilename)
|
||||||
|
f, err := os.Open(cacheFormatPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = d.deserialize(f, d.Saved)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startSaver will start a saver that will write d to all supplied drives at specific intervals.
|
||||||
|
// The saver will save and exit when supplied context is closed.
|
||||||
|
func (d *dataUpdateTracker) startSaver(ctx context.Context, interval time.Duration, drives []string) {
|
||||||
|
t := time.NewTicker(interval)
|
||||||
|
defer t.Stop()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
d.mu.Lock()
|
||||||
|
saveNow := d.save
|
||||||
|
exited := make(chan struct{})
|
||||||
|
d.saveExited = exited
|
||||||
|
d.mu.Unlock()
|
||||||
|
defer close(exited)
|
||||||
|
for {
|
||||||
|
var exit bool
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
exit = true
|
||||||
|
case <-t.C:
|
||||||
|
case <-saveNow:
|
||||||
|
}
|
||||||
|
buf.Reset()
|
||||||
|
d.mu.Lock()
|
||||||
|
d.Saved = UTCNow()
|
||||||
|
err := d.serialize(&buf)
|
||||||
|
if d.debug {
|
||||||
|
logger.Info(color.Green("dataUpdateTracker:")+" Saving: %v bytes, Current idx: %v", buf.Len(), d.Current.idx)
|
||||||
|
}
|
||||||
|
d.mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err, "Error serializing usage tracker data")
|
||||||
|
if exit {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if buf.Len() == 0 {
|
||||||
|
logger.LogIf(ctx, errors.New("zero sized output, skipping save"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, drive := range drives {
|
||||||
|
cacheFormatPath := pathJoin(drive, dataUpdateTrackerFilename)
|
||||||
|
err := ioutil.WriteFile(cacheFormatPath, buf.Bytes(), os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if exit {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serialize all data in d to dst.
|
||||||
|
// Caller should hold lock if d is expected to be shared.
|
||||||
|
// If an error is returned, there will likely be partial data written to dst.
|
||||||
|
func (d *dataUpdateTracker) serialize(dst io.Writer) (err error) {
|
||||||
|
ctx := GlobalContext
|
||||||
|
var tmp [8]byte
|
||||||
|
o := bufio.NewWriter(dst)
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
err = o.Flush()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Version
|
||||||
|
if err := o.WriteByte(dataUpdateTrackerVersion); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Timestamp.
|
||||||
|
binary.LittleEndian.PutUint64(tmp[:], uint64(d.Saved.Unix()))
|
||||||
|
if _, err := o.Write(tmp[:]); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current
|
||||||
|
binary.LittleEndian.PutUint64(tmp[:], d.Current.idx)
|
||||||
|
if _, err := o.Write(tmp[:]); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := d.Current.bf.WriteTo(o); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// History
|
||||||
|
binary.LittleEndian.PutUint64(tmp[:], uint64(len(d.History)))
|
||||||
|
if _, err := o.Write(tmp[:]); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bf := range d.History {
|
||||||
|
// Current
|
||||||
|
binary.LittleEndian.PutUint64(tmp[:], bf.idx)
|
||||||
|
if _, err := o.Write(tmp[:]); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := bf.bf.WriteTo(o); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deserialize will deserialize the supplied input if the input is newer than the supplied time.
|
||||||
|
func (d *dataUpdateTracker) deserialize(src io.Reader, newerThan time.Time) error {
|
||||||
|
ctx := GlobalContext
|
||||||
|
var dst dataUpdateTracker
|
||||||
|
var tmp [8]byte
|
||||||
|
|
||||||
|
// Version
|
||||||
|
if _, err := io.ReadFull(src, tmp[:1]); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch tmp[0] {
|
||||||
|
case dataUpdateTrackerVersion:
|
||||||
|
default:
|
||||||
|
return errors.New("dataUpdateTracker: Unknown data version")
|
||||||
|
}
|
||||||
|
// Timestamp.
|
||||||
|
if _, err := io.ReadFull(src, tmp[:8]); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t := time.Unix(int64(binary.LittleEndian.Uint64(tmp[:])), 0)
|
||||||
|
if !t.After(newerThan) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current
|
||||||
|
if _, err := io.ReadFull(src, tmp[:8]); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dst.Current.idx = binary.LittleEndian.Uint64(tmp[:])
|
||||||
|
dst.Current.bf = emptyBloomFilter()
|
||||||
|
if _, err := dst.Current.bf.ReadFrom(src); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// History
|
||||||
|
if _, err := io.ReadFull(src, tmp[:8]); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n := binary.LittleEndian.Uint64(tmp[:])
|
||||||
|
dst.History = make(dataUpdateTrackerHistory, int(n))
|
||||||
|
for i, e := range dst.History {
|
||||||
|
if _, err := io.ReadFull(src, tmp[:8]); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.idx = binary.LittleEndian.Uint64(tmp[:])
|
||||||
|
e.bf = emptyBloomFilter()
|
||||||
|
if _, err := e.bf.ReadFrom(src); err != nil {
|
||||||
|
if d.debug {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dst.History[i] = e
|
||||||
|
}
|
||||||
|
// Ignore what remains on the stream.
|
||||||
|
// Update d:
|
||||||
|
d.Current = dst.Current
|
||||||
|
d.History = dst.History
|
||||||
|
d.Saved = dst.Saved
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// start a collector that picks up entries from objectUpdatedCh
|
||||||
|
// and adds them to the current bloom filter.
|
||||||
|
func (d *dataUpdateTracker) startCollector(ctx context.Context) {
|
||||||
|
var tmp [dataUsageHashLen]byte
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case in := <-d.input:
|
||||||
|
bucket, _ := path2BucketObjectWithBasePath("", in)
|
||||||
|
if bucket == "" {
|
||||||
|
if d.debug && len(in) > 0 {
|
||||||
|
logger.Info(color.Green("data-usage:")+" no bucket (%s)", in)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isReservedOrInvalidBucket(bucket, false) {
|
||||||
|
if false && d.debug {
|
||||||
|
logger.Info(color.Green("data-usage:")+" isReservedOrInvalidBucket: %v, entry: %v", bucket, in)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
split := splitPathDeterministic(in)
|
||||||
|
|
||||||
|
// Add all paths until level 3.
|
||||||
|
d.mu.Lock()
|
||||||
|
for i := range split {
|
||||||
|
if d.debug && false {
|
||||||
|
logger.Info(color.Green("dataUpdateTracker:") + " Marking path dirty: " + color.Blue(path.Join(split[:i+1]...)))
|
||||||
|
}
|
||||||
|
hashPath(path.Join(split[:i+1]...)).bytes(tmp[:])
|
||||||
|
d.Current.bf.Add(tmp[:])
|
||||||
|
}
|
||||||
|
d.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find entry with specified index.
|
||||||
|
// Returns nil if not found.
|
||||||
|
func (d dataUpdateTrackerHistory) find(idx uint64) *dataUpdateFilter {
|
||||||
|
for _, f := range d {
|
||||||
|
if f.idx == idx {
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterFrom will return a combined bloom filter.
|
||||||
|
func (d *dataUpdateTracker) filterFrom(ctx context.Context, oldest, newest uint64) *bloomFilterResponse {
|
||||||
|
bf := d.newBloomFilter()
|
||||||
|
bfr := bloomFilterResponse{
|
||||||
|
OldestIdx: oldest,
|
||||||
|
CurrentIdx: d.Current.idx,
|
||||||
|
Complete: true,
|
||||||
|
}
|
||||||
|
// Loop through each index requested.
|
||||||
|
for idx := oldest; idx <= newest; idx++ {
|
||||||
|
v := d.History.find(idx)
|
||||||
|
if v == nil {
|
||||||
|
if d.Current.idx == idx {
|
||||||
|
// Merge current.
|
||||||
|
err := bf.Merge(d.Current.bf.BloomFilter)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
if err != nil {
|
||||||
|
bfr.Complete = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bfr.Complete = false
|
||||||
|
bfr.OldestIdx = idx + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := bf.Merge(v.bf.BloomFilter)
|
||||||
|
if err != nil {
|
||||||
|
bfr.Complete = false
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bfr.NewestIdx = idx
|
||||||
|
}
|
||||||
|
var dst bytes.Buffer
|
||||||
|
_, err := bf.WriteTo(&dst)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bfr.Filter = dst.Bytes()
|
||||||
|
|
||||||
|
return &bfr
|
||||||
|
}
|
||||||
|
|
||||||
|
// cycleFilter will cycle the bloom filter to start recording to index y if not already.
|
||||||
|
// The response will contain a bloom filter starting at index x up to, but not including index y.
|
||||||
|
// If y is 0, the response will not update y, but return the currently recorded information
|
||||||
|
// from the up until and including current y.
|
||||||
|
func (d *dataUpdateTracker) cycleFilter(ctx context.Context, oldest, current uint64) (*bloomFilterResponse, error) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
if current == 0 {
|
||||||
|
if len(d.History) == 0 {
|
||||||
|
return d.filterFrom(ctx, d.Current.idx, d.Current.idx), nil
|
||||||
|
}
|
||||||
|
d.History.sort()
|
||||||
|
return d.filterFrom(ctx, d.History[len(d.History)-1].idx, d.Current.idx), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move current to history if new one requested
|
||||||
|
if d.Current.idx != current {
|
||||||
|
if d.debug {
|
||||||
|
logger.Info(color.Green("dataUpdateTracker:")+" cycle bloom filter: %v -> %v", d.Current.idx, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.History = append(d.History, d.Current)
|
||||||
|
d.Current.idx = current
|
||||||
|
d.Current.bf = d.newBloomFilter()
|
||||||
|
select {
|
||||||
|
case d.save <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.History.removeOlderThan(oldest)
|
||||||
|
return d.filterFrom(ctx, oldest, current), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitPathDeterministic will split the provided relative path
|
||||||
|
// deterministically and return up to the first 3 elements of the path.
|
||||||
|
// Slash and dot prefixes are removed.
|
||||||
|
// Trailing slashes are removed.
|
||||||
|
// Returns 0 length if no parts are found after trimming.
|
||||||
|
func splitPathDeterministic(in string) []string {
|
||||||
|
split := strings.Split(in, SlashSeparator)
|
||||||
|
|
||||||
|
// Trim empty start/end
|
||||||
|
for len(split) > 0 {
|
||||||
|
if len(split[0]) > 0 && split[0] != "." {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
split = split[1:]
|
||||||
|
}
|
||||||
|
for len(split) > 0 {
|
||||||
|
if len(split[len(split)-1]) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
split = split[:len(split)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return up to 3 parts.
|
||||||
|
if len(split) > 3 {
|
||||||
|
split = split[:3]
|
||||||
|
}
|
||||||
|
return split
|
||||||
|
}
|
||||||
|
|
||||||
|
// bloomFilterRequest request bloom filters.
|
||||||
|
// Current index will be updated to current and entries back to Oldest is returned.
|
||||||
|
type bloomFilterRequest struct {
|
||||||
|
Oldest uint64
|
||||||
|
Current uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type bloomFilterResponse struct {
|
||||||
|
// Current index being written to.
|
||||||
|
CurrentIdx uint64
|
||||||
|
// Oldest index in the returned bloom filter.
|
||||||
|
OldestIdx uint64
|
||||||
|
// Newest Index in the returned bloom filter.
|
||||||
|
NewestIdx uint64
|
||||||
|
// Are all indexes between oldest and newest filled?
|
||||||
|
Complete bool
|
||||||
|
// Binary data of the bloom filter.
|
||||||
|
Filter []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectPathUpdated indicates a path has been updated.
|
||||||
|
// The function will never block.
|
||||||
|
func ObjectPathUpdated(s string) {
|
||||||
|
select {
|
||||||
|
case objectUpdatedCh <- s:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
/*
|
||||||
|
* 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"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/cmd/logger/message/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testLoggerI interface {
|
||||||
|
Helper()
|
||||||
|
Log(args ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type testingLogger struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
t testLoggerI
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testingLogger) Send(entry interface{}, errKind string) error {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
if t.t == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
e, ok := entry.(log.Entry)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unexpected log entry structure %#v", entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.t.Helper()
|
||||||
|
t.t.Log(e.Level, ":", errKind, e.Message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTestingLogging(t testLoggerI) func() {
|
||||||
|
tl := &testingLogger{t: t}
|
||||||
|
logger.AddTarget(tl)
|
||||||
|
return func() {
|
||||||
|
tl.mu.Lock()
|
||||||
|
defer tl.mu.Unlock()
|
||||||
|
tl.t = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDataUpdateTracker(t *testing.T) {
|
||||||
|
dut := newDataUpdateTracker()
|
||||||
|
// Change some defaults.
|
||||||
|
dut.debug = testing.Verbose()
|
||||||
|
dut.input = make(chan string)
|
||||||
|
dut.save = make(chan struct{})
|
||||||
|
|
||||||
|
defer addTestingLogging(t)()
|
||||||
|
|
||||||
|
dut.Current.bf = dut.newBloomFilter()
|
||||||
|
|
||||||
|
tmpDir, err := ioutil.TempDir("", "TestDataUpdateTracker")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = os.MkdirAll(filepath.Dir(filepath.Join(tmpDir, dataUpdateTrackerFilename)), os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
dut.start(ctx, tmpDir)
|
||||||
|
|
||||||
|
var tests = []struct {
|
||||||
|
in string
|
||||||
|
check []string // if not empty, check against these instead.
|
||||||
|
exist bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
in: "bucket/directory/file.txt",
|
||||||
|
check: []string{"bucket", "bucket/", "/bucket", "bucket/directory", "bucket/directory/", "bucket/directory/file.txt", "/bucket/directory/file.txt"},
|
||||||
|
exist: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// System bucket
|
||||||
|
in: ".minio.sys/ignoreme/pls",
|
||||||
|
exist: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Not a valid bucket
|
||||||
|
in: "./bucket/okfile.txt",
|
||||||
|
check: []string{"./bucket/okfile.txt", "/bucket/okfile.txt", "bucket/okfile.txt"},
|
||||||
|
exist: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Not a valid bucket
|
||||||
|
in: "æ/okfile.txt",
|
||||||
|
check: []string{"æ/okfile.txt", "æ/okfile.txt", "æ"},
|
||||||
|
exist: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "/bucket2/okfile2.txt",
|
||||||
|
check: []string{"./bucket2/okfile2.txt", "/bucket2/okfile2.txt", "bucket2/okfile2.txt", "bucket2"},
|
||||||
|
exist: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: "/bucket3/prefix/okfile2.txt",
|
||||||
|
check: []string{"./bucket3/prefix/okfile2.txt", "/bucket3/prefix/okfile2.txt", "bucket3/prefix/okfile2.txt", "bucket3/prefix", "bucket3"},
|
||||||
|
exist: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.in, func(t *testing.T) {
|
||||||
|
dut.input <- tt.in
|
||||||
|
dut.input <- "" // Sending empty string ensures the previous is added to filter.
|
||||||
|
dut.mu.Lock()
|
||||||
|
defer dut.mu.Unlock()
|
||||||
|
if len(tt.check) == 0 {
|
||||||
|
got := dut.Current.bf.containsDir(tt.in)
|
||||||
|
if got != tt.exist {
|
||||||
|
// For unlimited tests this could lead to false positives,
|
||||||
|
// but it should be deterministic.
|
||||||
|
t.Errorf("entry %q, got: %v, want %v", tt.in, got, tt.exist)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, check := range tt.check {
|
||||||
|
got := dut.Current.bf.containsDir(check)
|
||||||
|
if got != tt.exist {
|
||||||
|
// For unlimited tests this could lead to false positives,
|
||||||
|
// but it should be deterministic.
|
||||||
|
t.Errorf("entry %q, check: %q, got: %v, want %v", tt.in, check, got, tt.exist)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Cycle to history
|
||||||
|
_, err = dut.cycleFilter(ctx, 1, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dut.input <- "cycle2/file.txt"
|
||||||
|
dut.input <- "" // Sending empty string ensures the previous is added to filter.
|
||||||
|
|
||||||
|
tests = append(tests, struct {
|
||||||
|
in string
|
||||||
|
check []string
|
||||||
|
exist bool
|
||||||
|
}{in: "cycle2/file.txt", exist: true})
|
||||||
|
|
||||||
|
// Shut down
|
||||||
|
cancel()
|
||||||
|
<-dut.saveExited
|
||||||
|
|
||||||
|
if dut.current() != 2 {
|
||||||
|
t.Fatal("wrong current idx after save. want 2, got:", dut.current())
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel = context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Reload...
|
||||||
|
dut = newDataUpdateTracker()
|
||||||
|
dut.start(ctx, tmpDir)
|
||||||
|
|
||||||
|
if dut.current() != 2 {
|
||||||
|
t.Fatal("current idx after load not preserved. want 2, got:", dut.current())
|
||||||
|
}
|
||||||
|
bfr2, err := dut.cycleFilter(ctx, 1, 3)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bfr2.Complete {
|
||||||
|
t.Fatal("Wanted complete, didn't get it")
|
||||||
|
}
|
||||||
|
if bfr2.CurrentIdx != 3 {
|
||||||
|
t.Fatal("wanted index 3, got", bfr2.CurrentIdx)
|
||||||
|
}
|
||||||
|
if bfr2.OldestIdx != 1 {
|
||||||
|
t.Fatal("wanted oldest index 3, got", bfr2.OldestIdx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rerun test with returned bfr2
|
||||||
|
bf := dut.newBloomFilter()
|
||||||
|
_, err = bf.ReadFrom(bytes.NewBuffer(bfr2.Filter))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.in+"-reloaded", func(t *testing.T) {
|
||||||
|
if len(tt.check) == 0 {
|
||||||
|
got := bf.containsDir(tt.in)
|
||||||
|
if got != tt.exist {
|
||||||
|
// For unlimited tests this could lead to false positives,
|
||||||
|
// but it should be deterministic.
|
||||||
|
t.Errorf("entry %q, got: %v, want %v", tt.in, got, tt.exist)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, check := range tt.check {
|
||||||
|
got := bf.containsDir(check)
|
||||||
|
if got != tt.exist {
|
||||||
|
// For unlimited tests this could lead to false positives,
|
||||||
|
// but it should be deterministic.
|
||||||
|
t.Errorf("entry %q, check: %q, got: %v, want %v", tt.in, check, got, tt.exist)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDataUpdateTracker(b *testing.B) {
|
||||||
|
dut := newDataUpdateTracker()
|
||||||
|
// Change some defaults.
|
||||||
|
dut.debug = false
|
||||||
|
dut.input = make(chan string)
|
||||||
|
dut.save = make(chan struct{})
|
||||||
|
|
||||||
|
defer addTestingLogging(b)()
|
||||||
|
|
||||||
|
dut.Current.bf = dut.newBloomFilter()
|
||||||
|
// We do this unbuffered. This will very significantly reduce throughput, so this is a worst case.
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go dut.startCollector(ctx)
|
||||||
|
input := make([]string, 1000)
|
||||||
|
rng := rand.New(rand.NewSource(0xabad1dea))
|
||||||
|
tmp := []string{"bucket", "aprefix", "nextprefixlevel", "maybeobjname", "evendeeper", "ok-one-morelevel", "final.object"}
|
||||||
|
for i := range input {
|
||||||
|
tmp := tmp[:1+rng.Intn(cap(tmp)-1)]
|
||||||
|
input[i] = path.Join(tmp...)
|
||||||
|
}
|
||||||
|
b.SetBytes(1)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
dut.input <- input[rng.Intn(len(input))]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,555 @@
|
|||||||
|
/*
|
||||||
|
* 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"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cespare/xxhash/v2"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/hash"
|
||||||
|
"github.com/tinylib/msgp/msgp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dataUsageHashLen = 8
|
||||||
|
|
||||||
|
//go:generate msgp -file $GOFILE -unexported
|
||||||
|
|
||||||
|
// dataUsageHash is the hash type used.
|
||||||
|
type dataUsageHash uint64
|
||||||
|
|
||||||
|
// sizeHistogram is a size histogram.
|
||||||
|
type sizeHistogram [dataUsageBucketLen]uint64
|
||||||
|
|
||||||
|
//msgp:tuple dataUsageEntry
|
||||||
|
type dataUsageEntry struct {
|
||||||
|
// These fields do no include any children.
|
||||||
|
Size int64
|
||||||
|
Objects uint64
|
||||||
|
ObjSizes sizeHistogram
|
||||||
|
|
||||||
|
Children dataUsageHashMap
|
||||||
|
}
|
||||||
|
|
||||||
|
//msgp:ignore dataUsageEntryInfo
|
||||||
|
type dataUsageEntryInfo struct {
|
||||||
|
Name string
|
||||||
|
Parent string
|
||||||
|
Entry dataUsageEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataUsageCacheInfo struct {
|
||||||
|
// Name of the bucket. Also root element.
|
||||||
|
Name string
|
||||||
|
LastUpdate time.Time
|
||||||
|
NextCycle uint32
|
||||||
|
BloomFilter []byte `msg:"BloomFilter,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge other data usage entry into this, excluding children.
|
||||||
|
func (e *dataUsageEntry) merge(other dataUsageEntry) {
|
||||||
|
e.Objects += other.Objects
|
||||||
|
e.Size += other.Size
|
||||||
|
for i, v := range other.ObjSizes[:] {
|
||||||
|
e.ObjSizes[i] += v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mod returns true if the hash mod cycles == cycle.
|
||||||
|
func (h dataUsageHash) mod(cycle uint32, cycles uint32) bool {
|
||||||
|
return uint32(h)%cycles == cycle%cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
// addChildString will add a child based on its name.
|
||||||
|
// If it already exists it will not be added again.
|
||||||
|
func (e *dataUsageEntry) addChildString(name string) {
|
||||||
|
e.addChild(hashPath(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// addChild will add a child based on its hash.
|
||||||
|
// If it already exists it will not be added again.
|
||||||
|
func (e *dataUsageEntry) addChild(hash dataUsageHash) {
|
||||||
|
if _, ok := e.Children[hash]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e.Children == nil {
|
||||||
|
e.Children = make(dataUsageHashMap, 1)
|
||||||
|
}
|
||||||
|
e.Children[hash] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find a path in the cache.
|
||||||
|
// Returns nil if not found.
|
||||||
|
func (d *dataUsageCache) find(path string) *dataUsageEntry {
|
||||||
|
due, ok := d.Cache[hashPath(path)]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &due
|
||||||
|
}
|
||||||
|
|
||||||
|
// dui converts the flattened version of the path to DataUsageInfo.
|
||||||
|
// As a side effect d will be flattened, use a clone if this is not ok.
|
||||||
|
func (d *dataUsageCache) dui(path string, buckets []BucketInfo) DataUsageInfo {
|
||||||
|
e := d.find(path)
|
||||||
|
if e == nil {
|
||||||
|
return DataUsageInfo{LastUpdate: UTCNow()}
|
||||||
|
}
|
||||||
|
flat := d.flatten(*e)
|
||||||
|
return DataUsageInfo{
|
||||||
|
LastUpdate: d.Info.LastUpdate,
|
||||||
|
ObjectsCount: flat.Objects,
|
||||||
|
ObjectsTotalSize: uint64(flat.Size),
|
||||||
|
ObjectsSizesHistogram: flat.ObjSizes.asMap(),
|
||||||
|
BucketsCount: uint64(len(e.Children)),
|
||||||
|
BucketsSizes: d.pathSizes(buckets),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace will add or replace an entry in the cache.
|
||||||
|
// If a parent is specified it will be added to that if not already there.
|
||||||
|
// If the parent does not exist, it will be added.
|
||||||
|
func (d *dataUsageCache) replace(path, parent string, e dataUsageEntry) {
|
||||||
|
hash := hashPath(path)
|
||||||
|
if d.Cache == nil {
|
||||||
|
d.Cache = make(map[dataUsageHash]dataUsageEntry, 100)
|
||||||
|
}
|
||||||
|
d.Cache[hash] = e
|
||||||
|
if parent != "" {
|
||||||
|
phash := hashPath(parent)
|
||||||
|
p := d.Cache[phash]
|
||||||
|
p.addChild(hash)
|
||||||
|
d.Cache[phash] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replaceHashed add or replaces an entry to the cache based on its hash.
|
||||||
|
// If a parent is specified it will be added to that if not already there.
|
||||||
|
// If the parent does not exist, it will be added.
|
||||||
|
func (d *dataUsageCache) replaceHashed(hash dataUsageHash, parent *dataUsageHash, e dataUsageEntry) {
|
||||||
|
if d.Cache == nil {
|
||||||
|
d.Cache = make(map[dataUsageHash]dataUsageEntry, 100)
|
||||||
|
}
|
||||||
|
d.Cache[hash] = e
|
||||||
|
if parent != nil {
|
||||||
|
p := d.Cache[*parent]
|
||||||
|
p.addChild(hash)
|
||||||
|
d.Cache[*parent] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyWithChildren will copy entry with hash from src if it exists along with any children.
|
||||||
|
// If a parent is specified it will be added to that if not already there.
|
||||||
|
// If the parent does not exist, it will be added.
|
||||||
|
func (d *dataUsageCache) copyWithChildren(src *dataUsageCache, hash dataUsageHash, parent *dataUsageHash) {
|
||||||
|
if d.Cache == nil {
|
||||||
|
d.Cache = make(map[dataUsageHash]dataUsageEntry, 100)
|
||||||
|
}
|
||||||
|
e, ok := src.Cache[hash]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.Cache[hash] = e
|
||||||
|
for ch := range e.Children {
|
||||||
|
if ch == hash {
|
||||||
|
logger.LogIf(GlobalContext, errors.New("dataUsageCache.copyWithChildren: Circular reference"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.copyWithChildren(src, ch, &hash)
|
||||||
|
}
|
||||||
|
if parent != nil {
|
||||||
|
p := d.Cache[*parent]
|
||||||
|
p.addChild(hash)
|
||||||
|
d.Cache[*parent] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringAll returns a detailed string representation of all entries in the cache.
|
||||||
|
func (d *dataUsageCache) StringAll() string {
|
||||||
|
s := fmt.Sprintf("info:%+v\n", d.Info)
|
||||||
|
for k, v := range d.Cache {
|
||||||
|
s += fmt.Sprintf("\t%v: %+v\n", k, v)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert the hash into dst.
|
||||||
|
// dst must be at least dataUsageHashLen bytes long.
|
||||||
|
func (h dataUsageHash) bytes(dst []byte) {
|
||||||
|
binary.LittleEndian.PutUint64(dst, uint64(h))
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a human readable representation of the string.
|
||||||
|
func (h dataUsageHash) String() string {
|
||||||
|
return fmt.Sprintf("%x", uint64(h))
|
||||||
|
}
|
||||||
|
|
||||||
|
// flatten all children of the root into the root element and return it.
|
||||||
|
func (d *dataUsageCache) flatten(root dataUsageEntry) dataUsageEntry {
|
||||||
|
for id := range root.Children {
|
||||||
|
e := d.Cache[id]
|
||||||
|
if len(e.Children) > 0 {
|
||||||
|
e = d.flatten(e)
|
||||||
|
}
|
||||||
|
root.merge(e)
|
||||||
|
}
|
||||||
|
root.Children = nil
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a size to the histogram.
|
||||||
|
func (h *sizeHistogram) add(size int64) {
|
||||||
|
// Fetch the histogram interval corresponding
|
||||||
|
// to the passed object size.
|
||||||
|
for i, interval := range ObjectsHistogramIntervals {
|
||||||
|
if size >= interval.start && size <= interval.end {
|
||||||
|
h[i]++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// asMap returns the map as a map[string]uint64.
|
||||||
|
func (h *sizeHistogram) asMap() map[string]uint64 {
|
||||||
|
res := make(map[string]uint64, 7)
|
||||||
|
for i, count := range h {
|
||||||
|
res[ObjectsHistogramIntervals[i].name] = count
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathSizes returns the path sizes as a map.
|
||||||
|
func (d *dataUsageCache) pathSizes(buckets []BucketInfo) map[string]uint64 {
|
||||||
|
var dst = make(map[string]uint64, len(buckets))
|
||||||
|
for _, bucket := range buckets {
|
||||||
|
e := d.find(bucket.Name)
|
||||||
|
if e == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
flat := d.flatten(*e)
|
||||||
|
dst[bucket.Name] = uint64(flat.Size)
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// sizeRecursive returns the path as a flattened entry.
|
||||||
|
func (d *dataUsageCache) sizeRecursive(path string) *dataUsageEntry {
|
||||||
|
root := d.find(path)
|
||||||
|
if root == nil || len(root.Children) == 0 {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
flat := d.flatten(*root)
|
||||||
|
return &flat
|
||||||
|
}
|
||||||
|
|
||||||
|
// dataUsageCache contains a cache of data usage entries.
|
||||||
|
//msgp:ignore dataUsageCache
|
||||||
|
type dataUsageCache struct {
|
||||||
|
Info dataUsageCacheInfo
|
||||||
|
Cache map[dataUsageHash]dataUsageEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// root returns the root of the cache.
|
||||||
|
func (d *dataUsageCache) root() *dataUsageEntry {
|
||||||
|
return d.find(d.Info.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rootHash returns the root of the cache.
|
||||||
|
func (d *dataUsageCache) rootHash() dataUsageHash {
|
||||||
|
return hashPath(d.Info.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clone returns a copy of the cache with no references to the existing.
|
||||||
|
func (d *dataUsageCache) clone() dataUsageCache {
|
||||||
|
clone := dataUsageCache{
|
||||||
|
Info: d.Info,
|
||||||
|
Cache: make(map[dataUsageHash]dataUsageEntry, len(d.Cache)),
|
||||||
|
}
|
||||||
|
for k, v := range d.Cache {
|
||||||
|
clone.Cache[k] = v
|
||||||
|
}
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge root of other into d.
|
||||||
|
// children of root will be flattened before being merged.
|
||||||
|
// Last update time will be set to the last updated.
|
||||||
|
func (d *dataUsageCache) merge(other dataUsageCache) {
|
||||||
|
existingRoot := d.root()
|
||||||
|
otherRoot := other.root()
|
||||||
|
if existingRoot == nil && otherRoot == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if otherRoot == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if existingRoot == nil {
|
||||||
|
*d = other.clone()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if other.Info.LastUpdate.After(d.Info.LastUpdate) {
|
||||||
|
d.Info.LastUpdate = other.Info.LastUpdate
|
||||||
|
}
|
||||||
|
existingRoot.merge(*otherRoot)
|
||||||
|
eHash := d.rootHash()
|
||||||
|
for key := range otherRoot.Children {
|
||||||
|
entry := other.Cache[key]
|
||||||
|
flat := other.flatten(entry)
|
||||||
|
existing := d.Cache[key]
|
||||||
|
// If not found, merging simply adds.
|
||||||
|
existing.merge(flat)
|
||||||
|
d.replaceHashed(key, &eHash, existing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the cache content with name from minioMetaBackgroundOpsBucket.
|
||||||
|
// Only backend errors are returned as errors.
|
||||||
|
// If the object is not found or unable to deserialize d is cleared and nil error is returned.
|
||||||
|
func (d *dataUsageCache) load(ctx context.Context, store ObjectLayer, name string) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := store.GetObject(ctx, dataUsageBucket, name, 0, -1, &buf, "", ObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if !isErrObjectNotFound(err) && !isErrBucketNotFound(err) {
|
||||||
|
return toObjectErr(err, dataUsageBucket, name)
|
||||||
|
}
|
||||||
|
*d = dataUsageCache{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = d.deserialize(buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
*d = dataUsageCache{}
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the content of the cache to minioMetaBackgroundOpsBucket with the provided name.
|
||||||
|
func (d *dataUsageCache) save(ctx context.Context, store ObjectLayer, name string) error {
|
||||||
|
b := d.serialize()
|
||||||
|
size := int64(len(b))
|
||||||
|
r, err := hash.NewReader(bytes.NewReader(b), size, "", "", size, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = store.PutObject(ctx,
|
||||||
|
dataUsageBucket,
|
||||||
|
name,
|
||||||
|
NewPutObjReader(r, nil, nil),
|
||||||
|
ObjectOptions{})
|
||||||
|
if isErrBucketNotFound(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// dataUsageCacheVer indicates the cache version.
|
||||||
|
// Bumping the cache version will drop data from previous versions
|
||||||
|
// and write new data with the new version.
|
||||||
|
const dataUsageCacheVer = 1
|
||||||
|
|
||||||
|
// serialize the contents of the cache.
|
||||||
|
func (d *dataUsageCache) serialize() []byte {
|
||||||
|
// Alloc pessimistically
|
||||||
|
// dataUsageCacheVer
|
||||||
|
due := dataUsageEntry{}
|
||||||
|
msgLen := 1
|
||||||
|
msgLen += d.Info.Msgsize()
|
||||||
|
// len(d.Cache)
|
||||||
|
msgLen += binary.MaxVarintLen64
|
||||||
|
// Hashes (one for key, assume 1 child/node)
|
||||||
|
msgLen += len(d.Cache) * dataUsageHashLen * 2
|
||||||
|
msgLen += len(d.Cache) * due.Msgsize()
|
||||||
|
|
||||||
|
// Create destination buffer...
|
||||||
|
dst := make([]byte, 0, msgLen)
|
||||||
|
|
||||||
|
var n int
|
||||||
|
tmp := make([]byte, 1024)
|
||||||
|
// byte: version.
|
||||||
|
dst = append(dst, dataUsageCacheVer)
|
||||||
|
// Info...
|
||||||
|
dst, err := d.Info.MarshalMsg(dst)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
n = binary.PutUvarint(tmp, uint64(len(d.Cache)))
|
||||||
|
dst = append(dst, tmp[:n]...)
|
||||||
|
|
||||||
|
for k, v := range d.Cache {
|
||||||
|
// Put key
|
||||||
|
binary.LittleEndian.PutUint64(tmp[:dataUsageHashLen], uint64(k))
|
||||||
|
dst = append(dst, tmp[:8]...)
|
||||||
|
tmp, err = v.MarshalMsg(tmp[:0])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// key, value pairs.
|
||||||
|
dst = append(dst, tmp...)
|
||||||
|
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// deserialize the supplied byte slice into the cache.
|
||||||
|
func (d *dataUsageCache) deserialize(b []byte) error {
|
||||||
|
if len(b) < 1 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
switch b[0] {
|
||||||
|
case 1:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("dataUsageCache: unknown version: %d", int(b[0]))
|
||||||
|
}
|
||||||
|
b = b[1:]
|
||||||
|
|
||||||
|
// Info...
|
||||||
|
b, err := d.Info.UnmarshalMsg(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cacheLen, n := binary.Uvarint(b)
|
||||||
|
if n <= 0 {
|
||||||
|
return fmt.Errorf("dataUsageCache: reading cachelen, n <= 0 ")
|
||||||
|
}
|
||||||
|
b = b[n:]
|
||||||
|
d.Cache = make(map[dataUsageHash]dataUsageEntry, cacheLen)
|
||||||
|
|
||||||
|
for i := 0; i < int(cacheLen); i++ {
|
||||||
|
if len(b) <= dataUsageHashLen {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
k := binary.LittleEndian.Uint64(b[:dataUsageHashLen])
|
||||||
|
b = b[dataUsageHashLen:]
|
||||||
|
var v dataUsageEntry
|
||||||
|
b, err = v.UnmarshalMsg(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.Cache[dataUsageHash(k)] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim this from start+end of hashes.
|
||||||
|
var hashPathCutSet = dataUsageRoot
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if dataUsageRoot != string(filepath.Separator) {
|
||||||
|
hashPathCutSet = dataUsageRoot + string(filepath.Separator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashPath calculates a hash of the provided string.
|
||||||
|
func hashPath(data string) dataUsageHash {
|
||||||
|
if data != dataUsageRoot {
|
||||||
|
data = strings.Trim(data, hashPathCutSet)
|
||||||
|
}
|
||||||
|
data = path.Clean(data)
|
||||||
|
return dataUsageHash(xxhash.Sum64String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
//msgp:ignore dataUsageEntryInfo
|
||||||
|
type dataUsageHashMap map[dataUsageHash]struct{}
|
||||||
|
|
||||||
|
// MarshalMsg implements msgp.Marshaler
|
||||||
|
func (d dataUsageHashMap) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
|
o = msgp.Require(b, d.Msgsize())
|
||||||
|
|
||||||
|
// Write bin header manually
|
||||||
|
const mbin32 uint8 = 0xc6
|
||||||
|
sz := uint32(len(d)) * dataUsageHashLen
|
||||||
|
o = append(o, mbin32, byte(sz>>24), byte(sz>>16), byte(sz>>8), byte(sz))
|
||||||
|
|
||||||
|
var tmp [dataUsageHashLen]byte
|
||||||
|
for k := range d {
|
||||||
|
binary.LittleEndian.PutUint64(tmp[:], uint64(k))
|
||||||
|
o = append(o, tmp[:]...)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||||
|
func (d dataUsageHashMap) Msgsize() (s int) {
|
||||||
|
s = 5 + len(d)*dataUsageHashLen
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalMsg implements msgp.Unmarshaler
|
||||||
|
func (d *dataUsageHashMap) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||||
|
var hashes []byte
|
||||||
|
hashes, bts, err = msgp.ReadBytesZC(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "dataUsageHashMap")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var dst = make(dataUsageHashMap, len(hashes)/dataUsageHashLen)
|
||||||
|
for len(hashes) >= dataUsageHashLen {
|
||||||
|
dst[dataUsageHash(binary.LittleEndian.Uint64(hashes[:dataUsageHashLen]))] = struct{}{}
|
||||||
|
hashes = hashes[dataUsageHashLen:]
|
||||||
|
}
|
||||||
|
*d = dst
|
||||||
|
o = bts
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dataUsageHashMap) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||||
|
var zb0001 uint32
|
||||||
|
zb0001, err = dc.ReadBytesHeader()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var dst = make(dataUsageHashMap, zb0001)
|
||||||
|
var tmp [8]byte
|
||||||
|
for i := uint32(0); i < zb0001; i++ {
|
||||||
|
_, err = io.ReadFull(dc, tmp[:])
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "dataUsageHashMap")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dst[dataUsageHash(binary.LittleEndian.Uint64(tmp[:]))] = struct{}{}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (d dataUsageHashMap) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
|
err = en.WriteBytesHeader(uint32(len(d)) * dataUsageHashLen)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var tmp [dataUsageHashLen]byte
|
||||||
|
for k := range d {
|
||||||
|
binary.LittleEndian.PutUint64(tmp[:], uint64(k))
|
||||||
|
_, err = en.Write(tmp[:])
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tinylib/msgp/msgp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecodeMsg implements msgp.Decodable
|
||||||
|
func (z *dataUsageCacheInfo) 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 "LastUpdate":
|
||||||
|
z.LastUpdate, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "LastUpdate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "NextCycle":
|
||||||
|
z.NextCycle, err = dc.ReadUint32()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "NextCycle")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "BloomFilter":
|
||||||
|
z.BloomFilter, err = dc.ReadBytes(z.BloomFilter)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "BloomFilter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = dc.Skip()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeMsg implements msgp.Encodable
|
||||||
|
func (z *dataUsageCacheInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
|
// omitempty: check for empty values
|
||||||
|
zb0001Len := uint32(4)
|
||||||
|
var zb0001Mask uint8 /* 4 bits */
|
||||||
|
if z.BloomFilter == nil {
|
||||||
|
zb0001Len--
|
||||||
|
zb0001Mask |= 0x8
|
||||||
|
}
|
||||||
|
// variable map header, size zb0001Len
|
||||||
|
err = en.Append(0x80 | uint8(zb0001Len))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if zb0001Len == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "Name"
|
||||||
|
err = en.Append(0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteString(z.Name)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Name")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "LastUpdate"
|
||||||
|
err = en.Append(0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.LastUpdate)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "LastUpdate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "NextCycle"
|
||||||
|
err = en.Append(0xa9, 0x4e, 0x65, 0x78, 0x74, 0x43, 0x79, 0x63, 0x6c, 0x65)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteUint32(z.NextCycle)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "NextCycle")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (zb0001Mask & 0x8) == 0 { // if not empty
|
||||||
|
// write "BloomFilter"
|
||||||
|
err = en.Append(0xab, 0x42, 0x6c, 0x6f, 0x6f, 0x6d, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteBytes(z.BloomFilter)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "BloomFilter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalMsg implements msgp.Marshaler
|
||||||
|
func (z *dataUsageCacheInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
|
o = msgp.Require(b, z.Msgsize())
|
||||||
|
// omitempty: check for empty values
|
||||||
|
zb0001Len := uint32(4)
|
||||||
|
var zb0001Mask uint8 /* 4 bits */
|
||||||
|
if z.BloomFilter == nil {
|
||||||
|
zb0001Len--
|
||||||
|
zb0001Mask |= 0x8
|
||||||
|
}
|
||||||
|
// variable map header, size zb0001Len
|
||||||
|
o = append(o, 0x80|uint8(zb0001Len))
|
||||||
|
if zb0001Len == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// string "Name"
|
||||||
|
o = append(o, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||||
|
o = msgp.AppendString(o, z.Name)
|
||||||
|
// string "LastUpdate"
|
||||||
|
o = append(o, 0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65)
|
||||||
|
o = msgp.AppendTime(o, z.LastUpdate)
|
||||||
|
// string "NextCycle"
|
||||||
|
o = append(o, 0xa9, 0x4e, 0x65, 0x78, 0x74, 0x43, 0x79, 0x63, 0x6c, 0x65)
|
||||||
|
o = msgp.AppendUint32(o, z.NextCycle)
|
||||||
|
if (zb0001Mask & 0x8) == 0 { // if not empty
|
||||||
|
// string "BloomFilter"
|
||||||
|
o = append(o, 0xab, 0x42, 0x6c, 0x6f, 0x6f, 0x6d, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
|
||||||
|
o = msgp.AppendBytes(o, z.BloomFilter)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalMsg implements msgp.Unmarshaler
|
||||||
|
func (z *dataUsageCacheInfo) 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 "LastUpdate":
|
||||||
|
z.LastUpdate, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "LastUpdate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "NextCycle":
|
||||||
|
z.NextCycle, bts, err = msgp.ReadUint32Bytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "NextCycle")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "BloomFilter":
|
||||||
|
z.BloomFilter, bts, err = msgp.ReadBytesBytes(bts, z.BloomFilter)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "BloomFilter")
|
||||||
|
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 *dataUsageCacheInfo) Msgsize() (s int) {
|
||||||
|
s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 11 + msgp.TimeSize + 10 + msgp.Uint32Size + 12 + msgp.BytesPrefixSize + len(z.BloomFilter)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeMsg implements msgp.Decodable
|
||||||
|
func (z *dataUsageEntry) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||||
|
var zb0001 uint32
|
||||||
|
zb0001, err = dc.ReadArrayHeader()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if zb0001 != 4 {
|
||||||
|
err = msgp.ArrayError{Wanted: 4, Got: zb0001}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
z.Size, err = dc.ReadInt64()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Size")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
z.Objects, err = dc.ReadUint64()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Objects")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var zb0002 uint32
|
||||||
|
zb0002, err = dc.ReadArrayHeader()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjSizes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if zb0002 != uint32(dataUsageBucketLen) {
|
||||||
|
err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for za0001 := range z.ObjSizes {
|
||||||
|
z.ObjSizes[za0001], err = dc.ReadUint64()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjSizes", za0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = z.Children.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Children")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeMsg implements msgp.Encodable
|
||||||
|
func (z *dataUsageEntry) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
|
// array header, size 4
|
||||||
|
err = en.Append(0x94)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteInt64(z.Size)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Size")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteUint64(z.Objects)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Objects")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteArrayHeader(uint32(dataUsageBucketLen))
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjSizes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for za0001 := range z.ObjSizes {
|
||||||
|
err = en.WriteUint64(z.ObjSizes[za0001])
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjSizes", za0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = z.Children.EncodeMsg(en)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Children")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalMsg implements msgp.Marshaler
|
||||||
|
func (z *dataUsageEntry) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
|
o = msgp.Require(b, z.Msgsize())
|
||||||
|
// array header, size 4
|
||||||
|
o = append(o, 0x94)
|
||||||
|
o = msgp.AppendInt64(o, z.Size)
|
||||||
|
o = msgp.AppendUint64(o, z.Objects)
|
||||||
|
o = msgp.AppendArrayHeader(o, uint32(dataUsageBucketLen))
|
||||||
|
for za0001 := range z.ObjSizes {
|
||||||
|
o = msgp.AppendUint64(o, z.ObjSizes[za0001])
|
||||||
|
}
|
||||||
|
o, err = z.Children.MarshalMsg(o)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Children")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalMsg implements msgp.Unmarshaler
|
||||||
|
func (z *dataUsageEntry) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||||
|
var zb0001 uint32
|
||||||
|
zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if zb0001 != 4 {
|
||||||
|
err = msgp.ArrayError{Wanted: 4, Got: zb0001}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
z.Size, bts, err = msgp.ReadInt64Bytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Size")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
z.Objects, bts, err = msgp.ReadUint64Bytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Objects")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var zb0002 uint32
|
||||||
|
zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjSizes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if zb0002 != uint32(dataUsageBucketLen) {
|
||||||
|
err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for za0001 := range z.ObjSizes {
|
||||||
|
z.ObjSizes[za0001], bts, err = msgp.ReadUint64Bytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjSizes", za0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bts, err = z.Children.UnmarshalMsg(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "Children")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
o = bts
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||||
|
func (z *dataUsageEntry) Msgsize() (s int) {
|
||||||
|
s = 1 + msgp.Int64Size + msgp.Uint64Size + msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size)) + z.Children.Msgsize()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeMsg implements msgp.Decodable
|
||||||
|
func (z *dataUsageHash) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||||
|
{
|
||||||
|
var zb0001 uint64
|
||||||
|
zb0001, err = dc.ReadUint64()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
(*z) = dataUsageHash(zb0001)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeMsg implements msgp.Encodable
|
||||||
|
func (z dataUsageHash) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
|
err = en.WriteUint64(uint64(z))
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalMsg implements msgp.Marshaler
|
||||||
|
func (z dataUsageHash) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
|
o = msgp.Require(b, z.Msgsize())
|
||||||
|
o = msgp.AppendUint64(o, uint64(z))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalMsg implements msgp.Unmarshaler
|
||||||
|
func (z *dataUsageHash) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||||
|
{
|
||||||
|
var zb0001 uint64
|
||||||
|
zb0001, bts, err = msgp.ReadUint64Bytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
(*z) = dataUsageHash(zb0001)
|
||||||
|
}
|
||||||
|
o = bts
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||||
|
func (z dataUsageHash) Msgsize() (s int) {
|
||||||
|
s = msgp.Uint64Size
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeMsg implements msgp.Decodable
|
||||||
|
func (z *sizeHistogram) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||||
|
var zb0001 uint32
|
||||||
|
zb0001, err = dc.ReadArrayHeader()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if zb0001 != uint32(dataUsageBucketLen) {
|
||||||
|
err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0001}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for za0001 := range z {
|
||||||
|
z[za0001], err = dc.ReadUint64()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, za0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeMsg implements msgp.Encodable
|
||||||
|
func (z *sizeHistogram) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
|
err = en.WriteArrayHeader(uint32(dataUsageBucketLen))
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for za0001 := range z {
|
||||||
|
err = en.WriteUint64(z[za0001])
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, za0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalMsg implements msgp.Marshaler
|
||||||
|
func (z *sizeHistogram) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
|
o = msgp.Require(b, z.Msgsize())
|
||||||
|
o = msgp.AppendArrayHeader(o, uint32(dataUsageBucketLen))
|
||||||
|
for za0001 := range z {
|
||||||
|
o = msgp.AppendUint64(o, z[za0001])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalMsg implements msgp.Unmarshaler
|
||||||
|
func (z *sizeHistogram) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||||
|
var zb0001 uint32
|
||||||
|
zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if zb0001 != uint32(dataUsageBucketLen) {
|
||||||
|
err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0001}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for za0001 := range z {
|
||||||
|
z[za0001], bts, err = msgp.ReadUint64Bytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, za0001)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o = bts
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||||
|
func (z *sizeHistogram) Msgsize() (s int) {
|
||||||
|
s = msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size))
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tinylib/msgp/msgp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarshalUnmarshaldataUsageCacheInfo(t *testing.T) {
|
||||||
|
v := dataUsageCacheInfo{}
|
||||||
|
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 BenchmarkMarshalMsgdataUsageCacheInfo(b *testing.B) {
|
||||||
|
v := dataUsageCacheInfo{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgdataUsageCacheInfo(b *testing.B) {
|
||||||
|
v := dataUsageCacheInfo{}
|
||||||
|
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 BenchmarkUnmarshaldataUsageCacheInfo(b *testing.B) {
|
||||||
|
v := dataUsageCacheInfo{}
|
||||||
|
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 TestEncodeDecodedataUsageCacheInfo(t *testing.T) {
|
||||||
|
v := dataUsageCacheInfo{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodedataUsageCacheInfo Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := dataUsageCacheInfo{}
|
||||||
|
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 BenchmarkEncodedataUsageCacheInfo(b *testing.B) {
|
||||||
|
v := dataUsageCacheInfo{}
|
||||||
|
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 BenchmarkDecodedataUsageCacheInfo(b *testing.B) {
|
||||||
|
v := dataUsageCacheInfo{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||||
|
dc := msgp.NewReader(rd)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := v.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalUnmarshaldataUsageEntry(t *testing.T) {
|
||||||
|
v := dataUsageEntry{}
|
||||||
|
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 BenchmarkMarshalMsgdataUsageEntry(b *testing.B) {
|
||||||
|
v := dataUsageEntry{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgdataUsageEntry(b *testing.B) {
|
||||||
|
v := dataUsageEntry{}
|
||||||
|
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 BenchmarkUnmarshaldataUsageEntry(b *testing.B) {
|
||||||
|
v := dataUsageEntry{}
|
||||||
|
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 TestEncodeDecodedataUsageEntry(t *testing.T) {
|
||||||
|
v := dataUsageEntry{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodedataUsageEntry Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := dataUsageEntry{}
|
||||||
|
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 BenchmarkEncodedataUsageEntry(b *testing.B) {
|
||||||
|
v := dataUsageEntry{}
|
||||||
|
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 BenchmarkDecodedataUsageEntry(b *testing.B) {
|
||||||
|
v := dataUsageEntry{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
b.SetBytes(int64(buf.Len()))
|
||||||
|
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||||
|
dc := msgp.NewReader(rd)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
err := v.DecodeMsg(dc)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalUnmarshalsizeHistogram(t *testing.T) {
|
||||||
|
v := sizeHistogram{}
|
||||||
|
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 BenchmarkMarshalMsgsizeHistogram(b *testing.B) {
|
||||||
|
v := sizeHistogram{}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
v.MarshalMsg(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAppendMsgsizeHistogram(b *testing.B) {
|
||||||
|
v := sizeHistogram{}
|
||||||
|
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 BenchmarkUnmarshalsizeHistogram(b *testing.B) {
|
||||||
|
v := sizeHistogram{}
|
||||||
|
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 TestEncodeDecodesizeHistogram(t *testing.T) {
|
||||||
|
v := sizeHistogram{}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
msgp.Encode(&buf, &v)
|
||||||
|
|
||||||
|
m := v.Msgsize()
|
||||||
|
if buf.Len() > m {
|
||||||
|
t.Log("WARNING: TestEncodeDecodesizeHistogram Msgsize() is inaccurate")
|
||||||
|
}
|
||||||
|
|
||||||
|
vn := sizeHistogram{}
|
||||||
|
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 BenchmarkEncodesizeHistogram(b *testing.B) {
|
||||||
|
v := sizeHistogram{}
|
||||||
|
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 BenchmarkDecodesizeHistogram(b *testing.B) {
|
||||||
|
v := sizeHistogram{}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+423
-125
@@ -19,119 +19,132 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path"
|
||||||
"sync"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"github.com/minio/minio/cmd/config"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/color"
|
||||||
|
"github.com/minio/minio/pkg/env"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
|
"github.com/willf/bloom"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
dataUsageObjName = "data-usage"
|
envDataUsageCrawlConf = "MINIO_DISK_USAGE_CRAWL_ENABLE"
|
||||||
dataUsageCrawlInterval = 12 * time.Hour
|
envDataUsageCrawlDelay = "MINIO_DISK_USAGE_CRAWL_DELAY"
|
||||||
|
envDataUsageCrawlDebug = "MINIO_DISK_USAGE_CRAWL_DEBUG"
|
||||||
|
|
||||||
|
dataUsageRoot = SlashSeparator
|
||||||
|
dataUsageBucket = minioMetaBucket + SlashSeparator + bucketMetaPrefix
|
||||||
|
|
||||||
|
dataUsageObjName = ".usage.json"
|
||||||
|
dataUsageCacheName = ".usage-cache.bin"
|
||||||
|
dataUsageBloomName = ".bloomcycle.bin"
|
||||||
|
dataUsageSleepPerFolder = 1 * time.Millisecond
|
||||||
|
dataUsageSleepDefMult = 10.0
|
||||||
|
dataUsageUpdateDirCycles = 16
|
||||||
|
dataUsageStartDelay = 5 * time.Minute // Time to wait on startup and between cycles.
|
||||||
)
|
)
|
||||||
|
|
||||||
func initDataUsageStats() {
|
// initDataUsageStats will start the crawler unless disabled.
|
||||||
go runDataUsageInfoUpdateRoutine()
|
func initDataUsageStats(ctx context.Context, objAPI ObjectLayer) {
|
||||||
}
|
if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOn {
|
||||||
|
go runDataUsageInfo(ctx, objAPI)
|
||||||
func runDataUsageInfoUpdateRoutine() {
|
|
||||||
// Wait until the object layer is ready
|
|
||||||
var objAPI ObjectLayer
|
|
||||||
for {
|
|
||||||
objAPI = newObjectLayerWithoutSafeModeFn()
|
|
||||||
if objAPI == nil {
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runDataUsageInfo(context.Background(), objAPI, GlobalServiceDoneCh)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// timeToNextCrawl returns the duration until next crawl should occur
|
func runDataUsageInfo(ctx context.Context, objAPI ObjectLayer) {
|
||||||
// this is validated by verifying the LastUpdate time.
|
// Load current bloom cycle
|
||||||
func timeToCrawl(ctx context.Context, objAPI ObjectLayer) time.Duration {
|
nextBloomCycle := intDataUpdateTracker.current() + 1
|
||||||
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objAPI)
|
var buf bytes.Buffer
|
||||||
|
err := objAPI.GetObject(ctx, dataUsageBucket, dataUsageBloomName, 0, -1, &buf, "", ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Upon an error wait for like 10
|
if !isErrObjectNotFound(err) && !isErrBucketNotFound(err) {
|
||||||
// seconds to start the crawler.
|
logger.LogIf(ctx, err)
|
||||||
return 10 * time.Second
|
}
|
||||||
|
} else {
|
||||||
|
if buf.Len() == 8 {
|
||||||
|
nextBloomCycle = binary.LittleEndian.Uint64(buf.Bytes())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// File indeed doesn't exist when LastUpdate is zero
|
|
||||||
// so we have never crawled, start crawl right away.
|
for {
|
||||||
if dataUsageInfo.LastUpdate.IsZero() {
|
select {
|
||||||
return 1 * time.Second
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.NewTimer(dataUsageStartDelay).C:
|
||||||
|
// Wait before starting next cycle and wait on startup.
|
||||||
|
results := make(chan DataUsageInfo, 1)
|
||||||
|
go storeDataUsageInBackend(ctx, objAPI, results)
|
||||||
|
bf, err := globalNotificationSys.updateBloomFilter(ctx, nextBloomCycle)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
err = objAPI.CrawlAndGetDataUsage(ctx, bf, results)
|
||||||
|
close(results)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
if err == nil {
|
||||||
|
// Store new cycle...
|
||||||
|
nextBloomCycle++
|
||||||
|
if nextBloomCycle%dataUpdateTrackerResetEvery == 0 {
|
||||||
|
if intDataUpdateTracker.debug {
|
||||||
|
logger.Info(color.Green("runDataUsageInfo:") + " Resetting bloom filter for next runs.")
|
||||||
|
}
|
||||||
|
nextBloomCycle++
|
||||||
|
}
|
||||||
|
var tmp [8]byte
|
||||||
|
binary.LittleEndian.PutUint64(tmp[:], nextBloomCycle)
|
||||||
|
r, err := hash.NewReader(bytes.NewReader(tmp[:]), int64(len(tmp)), "", "", int64(len(tmp)), false)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = objAPI.PutObject(ctx, dataUsageBucket, dataUsageBloomName, NewPutObjReader(r, nil, nil), ObjectOptions{})
|
||||||
|
if !isErrBucketNotFound(err) {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
waitDuration := dataUsageInfo.LastUpdate.Sub(UTCNow())
|
|
||||||
if waitDuration > dataUsageCrawlInterval {
|
|
||||||
// Waited long enough start crawl in a 1 second
|
|
||||||
return 1 * time.Second
|
|
||||||
}
|
|
||||||
// No crawling needed, ask the routine to wait until
|
|
||||||
// the daily interval 12hrs - delta between last update
|
|
||||||
// with current time.
|
|
||||||
return dataUsageCrawlInterval - waitDuration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDataUsageInfo(ctx context.Context, objAPI ObjectLayer, endCh <-chan struct{}) {
|
// storeDataUsageInBackend will store all objects sent on the gui channel until closed.
|
||||||
locker := objAPI.NewNSLock(ctx, minioMetaBucket, "leader-data-usage-info")
|
func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, gui <-chan DataUsageInfo) {
|
||||||
for {
|
for dataUsageInfo := range gui {
|
||||||
err := locker.GetLock(newDynamicTimeout(time.Millisecond, time.Millisecond))
|
dataUsageJSON, err := json.Marshal(dataUsageInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
time.Sleep(5 * time.Minute)
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
size := int64(len(dataUsageJSON))
|
||||||
|
r, err := hash.NewReader(bytes.NewReader(dataUsageJSON), size, "", "", size, false)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Break without unlocking, this node will acquire
|
|
||||||
// data usage calculator role for its lifetime.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
_, err = objAPI.PutObject(ctx, dataUsageBucket, dataUsageObjName, NewPutObjReader(r, nil, nil), ObjectOptions{})
|
||||||
wait := timeToCrawl(ctx, objAPI)
|
if !isErrBucketNotFound(err) {
|
||||||
select {
|
|
||||||
case <-endCh:
|
|
||||||
locker.Unlock()
|
|
||||||
return
|
|
||||||
case <-time.NewTimer(wait).C:
|
|
||||||
// Crawl only when no previous crawl has occurred,
|
|
||||||
// or its been too long since last crawl.
|
|
||||||
err := storeDataUsageInBackend(ctx, objAPI, objAPI.CrawlAndGetDataUsage(ctx, endCh))
|
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, dataUsageInfo DataUsageInfo) error {
|
|
||||||
dataUsageJSON, err := json.Marshal(dataUsageInfo)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
size := int64(len(dataUsageJSON))
|
|
||||||
r, err := hash.NewReader(bytes.NewReader(dataUsageJSON), size, "", "", size, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = objAPI.PutObject(ctx, minioMetaBackgroundOpsBucket, dataUsageObjName, NewPutObjReader(r, nil, nil), ObjectOptions{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (DataUsageInfo, error) {
|
func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (DataUsageInfo, error) {
|
||||||
var dataUsageInfoJSON bytes.Buffer
|
var dataUsageInfoJSON bytes.Buffer
|
||||||
|
|
||||||
err := objAPI.GetObject(ctx, minioMetaBackgroundOpsBucket, dataUsageObjName, 0, -1, &dataUsageInfoJSON, "", ObjectOptions{})
|
err := objAPI.GetObject(ctx, dataUsageBucket, dataUsageObjName, 0, -1, &dataUsageInfoJSON, "", ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isErrObjectNotFound(err) {
|
if isErrObjectNotFound(err) || isErrBucketNotFound(err) {
|
||||||
return DataUsageInfo{}, nil
|
return DataUsageInfo{}, nil
|
||||||
}
|
}
|
||||||
return DataUsageInfo{}, toObjectErr(err, minioMetaBackgroundOpsBucket, dataUsageObjName)
|
return DataUsageInfo{}, toObjectErr(err, dataUsageBucket, dataUsageObjName)
|
||||||
}
|
}
|
||||||
|
|
||||||
var dataUsageInfo DataUsageInfo
|
var dataUsageInfo DataUsageInfo
|
||||||
@@ -152,60 +165,345 @@ type Item struct {
|
|||||||
|
|
||||||
type getSizeFn func(item Item) (int64, error)
|
type getSizeFn func(item Item) (int64, error)
|
||||||
|
|
||||||
func updateUsage(basePath string, doneCh <-chan struct{}, waitForLowActiveIO func(), getSize getSizeFn) DataUsageInfo {
|
type cachedFolder struct {
|
||||||
var dataUsageInfo = DataUsageInfo{
|
name string
|
||||||
BucketsSizes: make(map[string]uint64),
|
parent *dataUsageHash
|
||||||
ObjectsSizesHistogram: make(map[string]uint64),
|
}
|
||||||
|
|
||||||
|
type folderScanner struct {
|
||||||
|
root string
|
||||||
|
getSize getSizeFn
|
||||||
|
oldCache dataUsageCache
|
||||||
|
newCache dataUsageCache
|
||||||
|
withFilter *bloomFilter
|
||||||
|
waitForLowActiveIO func()
|
||||||
|
|
||||||
|
dataUsageCrawlMult float64
|
||||||
|
dataUsageCrawlDebug bool
|
||||||
|
|
||||||
|
newFolders []cachedFolder
|
||||||
|
existingFolders []cachedFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
// sleepDuration multiplies the duration d by x and sleeps if is more than 100 micro seconds.
|
||||||
|
// sleep is limited to max 1 second.
|
||||||
|
func sleepDuration(d time.Duration, x float64) {
|
||||||
|
// Don't sleep for really small amount of time
|
||||||
|
if d := time.Duration(float64(d) * x); d > time.Microsecond*100 {
|
||||||
|
if d > time.Second {
|
||||||
|
d = time.Second
|
||||||
|
}
|
||||||
|
time.Sleep(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanQueuedLevels will scan the provided folders.
|
||||||
|
// Files found in the folders will be added to f.newCache.
|
||||||
|
// If final is provided folders will be put into f.newFolders or f.existingFolders.
|
||||||
|
// If final is not provided the folders found are returned from the function.
|
||||||
|
func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFolder, final bool) ([]cachedFolder, error) {
|
||||||
|
var nextFolders []cachedFolder
|
||||||
|
done := ctx.Done()
|
||||||
|
for _, folder := range folders {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
thisHash := hashPath(folder.name)
|
||||||
|
|
||||||
|
if _, ok := f.oldCache.Cache[thisHash]; f.withFilter != nil && ok {
|
||||||
|
// If folder isn't in filter and we have data, skip it completely.
|
||||||
|
if folder.name != dataUsageRoot && !f.withFilter.containsDir(folder.name) {
|
||||||
|
f.newCache.copyWithChildren(&f.oldCache, thisHash, folder.parent)
|
||||||
|
if f.dataUsageCrawlDebug {
|
||||||
|
logger.Info(color.Green("data-usage:")+" Skipping non-updated folder: %v", folder.name)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.waitForLowActiveIO()
|
||||||
|
sleepDuration(dataUsageSleepPerFolder, f.dataUsageCrawlMult)
|
||||||
|
|
||||||
|
cache := dataUsageEntry{}
|
||||||
|
|
||||||
|
err := readDirFn(path.Join(f.root, folder.name), func(entName string, typ os.FileMode) error {
|
||||||
|
// Parse
|
||||||
|
entName = path.Clean(path.Join(folder.name, entName))
|
||||||
|
bucket, _ := path2BucketObjectWithBasePath(f.root, entName)
|
||||||
|
if bucket == "" {
|
||||||
|
if f.dataUsageCrawlDebug {
|
||||||
|
logger.Info(color.Green("data-usage:")+" no bucket (%s,%s)", f.root, entName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isReservedOrInvalidBucket(bucket, false) {
|
||||||
|
if f.dataUsageCrawlDebug {
|
||||||
|
logger.Info(color.Green("data-usage:")+" invalid bucket: %v, entry: %v", bucket, entName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if typ&os.ModeDir != 0 {
|
||||||
|
h := hashPath(entName)
|
||||||
|
_, exists := f.oldCache.Cache[h]
|
||||||
|
cache.addChildString(entName)
|
||||||
|
|
||||||
|
this := cachedFolder{name: entName, parent: &thisHash}
|
||||||
|
cache.addChild(h)
|
||||||
|
if final {
|
||||||
|
if exists {
|
||||||
|
f.existingFolders = append(f.existingFolders, this)
|
||||||
|
} else {
|
||||||
|
f.newFolders = append(f.newFolders, this)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nextFolders = append(nextFolders, this)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f.waitForLowActiveIO()
|
||||||
|
// Dynamic time delay.
|
||||||
|
t := UTCNow()
|
||||||
|
|
||||||
|
// Get file size, ignore errors.
|
||||||
|
size, err := f.getSize(Item{Path: path.Join(f.root, entName), Typ: typ})
|
||||||
|
|
||||||
|
sleepDuration(time.Since(t), f.dataUsageCrawlMult)
|
||||||
|
if err == errSkipFile {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
cache.Size += size
|
||||||
|
cache.Objects++
|
||||||
|
cache.ObjSizes.add(size)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f.newCache.replaceHashed(thisHash, folder.parent, cache)
|
||||||
|
}
|
||||||
|
return nextFolders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deepScanFolder will deep scan a folder and return the size if no error occurs.
|
||||||
|
func (f *folderScanner) deepScanFolder(ctx context.Context, folder string) (*dataUsageEntry, error) {
|
||||||
|
var cache dataUsageEntry
|
||||||
|
|
||||||
|
done := ctx.Done()
|
||||||
|
|
||||||
|
var addDir func(entName string, typ os.FileMode) error
|
||||||
|
var dirStack = []string{f.root, folder}
|
||||||
|
|
||||||
|
addDir = func(entName string, typ os.FileMode) error {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
f.waitForLowActiveIO()
|
||||||
|
if typ&os.ModeDir != 0 {
|
||||||
|
dirStack = append(dirStack, entName)
|
||||||
|
err := readDirFn(path.Join(dirStack...), addDir)
|
||||||
|
dirStack = dirStack[:len(dirStack)-1]
|
||||||
|
sleepDuration(dataUsageSleepPerFolder, f.dataUsageCrawlMult)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic time delay.
|
||||||
|
t := UTCNow()
|
||||||
|
|
||||||
|
// Get file size, ignore errors.
|
||||||
|
dirStack = append(dirStack, entName)
|
||||||
|
fileName := path.Join(dirStack...)
|
||||||
|
dirStack = dirStack[:len(dirStack)-1]
|
||||||
|
|
||||||
|
size, err := f.getSize(Item{Path: fileName, Typ: typ})
|
||||||
|
|
||||||
|
// Don't sleep for really small amount of time
|
||||||
|
sleepDuration(time.Since(t), f.dataUsageCrawlMult)
|
||||||
|
|
||||||
|
if err == errSkipFile {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
cache.Size += size
|
||||||
|
cache.Objects++
|
||||||
|
cache.ObjSizes.add(size)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := readDirFn(path.Join(dirStack...), addDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cache, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateUsage will crawl the basepath+cache.Info.Name and return an updated cache.
|
||||||
|
// The returned cache will always be valid, but may not be updated from the existing.
|
||||||
|
// Before each operation waitForLowActiveIO is called which can be used to temporarily halt the crawler.
|
||||||
|
// If the supplied context is canceled the function will return at the first chance.
|
||||||
|
func updateUsage(ctx context.Context, basePath string, cache dataUsageCache, waitForLowActiveIO func(), getSize getSizeFn) (dataUsageCache, error) {
|
||||||
|
t := UTCNow()
|
||||||
|
|
||||||
|
dataUsageDebug := env.Get(envDataUsageCrawlDebug, config.EnableOff) == config.EnableOn
|
||||||
|
logPrefix := color.Green("data-usage: ")
|
||||||
|
logSuffix := color.Blue(" - %v + %v", basePath, cache.Info.Name)
|
||||||
|
if dataUsageDebug {
|
||||||
|
defer func() {
|
||||||
|
logger.Info(logPrefix+" Crawl time: %v"+logSuffix, time.Since(t))
|
||||||
|
}()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
numWorkers := 4
|
if cache.Info.Name == "" {
|
||||||
var mutex sync.Mutex // Mutex to update dataUsageInfo
|
cache.Info.Name = dataUsageRoot
|
||||||
|
}
|
||||||
|
|
||||||
fastWalk(basePath, numWorkers, doneCh, func(path string, typ os.FileMode) error {
|
delayMult, err := strconv.ParseFloat(env.Get(envDataUsageCrawlDelay, "10.0"), 64)
|
||||||
// Wait for I/O to go down.
|
if err != nil {
|
||||||
waitForLowActiveIO()
|
logger.LogIf(ctx, err)
|
||||||
|
delayMult = dataUsageSleepDefMult
|
||||||
|
}
|
||||||
|
|
||||||
bucket, entry := path2BucketObjectWithBasePath(basePath, path)
|
s := folderScanner{
|
||||||
if bucket == "" {
|
root: basePath,
|
||||||
return nil
|
getSize: getSize,
|
||||||
}
|
oldCache: cache,
|
||||||
|
newCache: dataUsageCache{Info: cache.Info},
|
||||||
|
waitForLowActiveIO: waitForLowActiveIO,
|
||||||
|
newFolders: nil,
|
||||||
|
existingFolders: nil,
|
||||||
|
dataUsageCrawlMult: delayMult,
|
||||||
|
dataUsageCrawlDebug: dataUsageDebug,
|
||||||
|
}
|
||||||
|
|
||||||
if isReservedOrInvalidBucket(bucket, false) {
|
if len(cache.Info.BloomFilter) > 0 {
|
||||||
return filepath.SkipDir
|
s.withFilter = &bloomFilter{BloomFilter: &bloom.BloomFilter{}}
|
||||||
}
|
_, err := s.withFilter.ReadFrom(bytes.NewBuffer(cache.Info.BloomFilter))
|
||||||
|
|
||||||
if entry == "" && typ&os.ModeDir != 0 {
|
|
||||||
mutex.Lock()
|
|
||||||
dataUsageInfo.BucketsCount++
|
|
||||||
dataUsageInfo.BucketsSizes[bucket] = 0
|
|
||||||
mutex.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
|
|
||||||
if typ&os.ModeDir != 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
t := time.Now()
|
|
||||||
size, err := getSize(Item{path, typ})
|
|
||||||
// Use the response time of the getSize call to guess system load.
|
|
||||||
// Sleep equivalent time.
|
|
||||||
if d := time.Since(t); d > 100*time.Microsecond {
|
|
||||||
time.Sleep(d)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errSkipFile
|
logger.LogIf(ctx, err, logPrefix+"Error reading bloom filter")
|
||||||
|
s.withFilter = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.dataUsageCrawlDebug {
|
||||||
|
logger.Info(logPrefix+"Start crawling. Bloom filter: %v"+logSuffix, s.withFilter != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
done := ctx.Done()
|
||||||
|
var flattenLevels = 3
|
||||||
|
|
||||||
|
// If we are scanning inside a bucket reduce depth by 1.
|
||||||
|
if cache.Info.Name != dataUsageRoot {
|
||||||
|
flattenLevels--
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.dataUsageCrawlDebug {
|
||||||
|
logger.Info(logPrefix+"Cycle: %v, Entries: %v"+logSuffix, cache.Info.NextCycle, len(cache.Cache))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always scan flattenLevels deep. Cache root is level 0.
|
||||||
|
todo := []cachedFolder{{name: cache.Info.Name}}
|
||||||
|
for i := 0; i < flattenLevels; i++ {
|
||||||
|
if s.dataUsageCrawlDebug {
|
||||||
|
logger.Info(logPrefix+"Level %v, scanning %v directories."+logSuffix, i, len(todo))
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return cache, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
todo, err = s.scanQueuedLevels(ctx, todo, i == flattenLevels-1)
|
||||||
|
if err != nil {
|
||||||
|
// No useful information...
|
||||||
|
return cache, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.dataUsageCrawlDebug {
|
||||||
|
logger.Info(logPrefix+"New folders: %v"+logSuffix, s.newFolders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new folders first
|
||||||
|
for _, folder := range s.newFolders {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return s.newCache, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
du, err := s.deepScanFolder(ctx, folder.name)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if du == nil {
|
||||||
|
logger.Info(logPrefix + "no disk usage provided" + logSuffix)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
dataUsageInfo.ObjectsCount++
|
s.newCache.replace(folder.name, "", *du)
|
||||||
dataUsageInfo.ObjectsTotalSize += uint64(size)
|
// Add to parent manually
|
||||||
dataUsageInfo.BucketsSizes[bucket] += uint64(size)
|
if folder.parent != nil {
|
||||||
dataUsageInfo.ObjectsSizesHistogram[objSizeToHistoInterval(uint64(size))]++
|
parent := s.newCache.Cache[*folder.parent]
|
||||||
return nil
|
parent.addChildString(folder.name)
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return dataUsageInfo
|
if s.dataUsageCrawlDebug {
|
||||||
|
logger.Info(logPrefix+"Existing folders: %v"+logSuffix, len(s.existingFolders))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do selective scanning of existing folders.
|
||||||
|
for _, folder := range s.existingFolders {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return s.newCache, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
h := hashPath(folder.name)
|
||||||
|
if !h.mod(s.oldCache.Info.NextCycle, dataUsageUpdateDirCycles) {
|
||||||
|
s.newCache.replaceHashed(h, folder.parent, s.oldCache.Cache[h])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.withFilter != nil {
|
||||||
|
// If folder isn't in filter, skip it completely.
|
||||||
|
if !s.withFilter.containsDir(folder.name) {
|
||||||
|
if s.dataUsageCrawlDebug {
|
||||||
|
logger.Info(logPrefix+"Skipping non-updated folder: %v"+logSuffix, folder)
|
||||||
|
}
|
||||||
|
s.newCache.replaceHashed(h, folder.parent, s.oldCache.Cache[h])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update on this cycle...
|
||||||
|
du, err := s.deepScanFolder(ctx, folder.name)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if du == nil {
|
||||||
|
logger.LogIf(ctx, errors.New("data-usage: no disk usage provided"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.newCache.replaceHashed(h, folder.parent, *du)
|
||||||
|
}
|
||||||
|
if s.dataUsageCrawlDebug {
|
||||||
|
logger.Info(logPrefix+"Finished crawl, %v entries"+logSuffix, len(s.newCache.Cache))
|
||||||
|
}
|
||||||
|
s.newCache.Info.LastUpdate = UTCNow()
|
||||||
|
s.newCache.Info.NextCycle++
|
||||||
|
return s.newCache, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,651 @@
|
|||||||
|
/*
|
||||||
|
* 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"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type usageTestFile struct {
|
||||||
|
name string
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDataUsageUpdate(t *testing.T) {
|
||||||
|
base, err := ioutil.TempDir("", "TestDataUsageUpdate")
|
||||||
|
if err != nil {
|
||||||
|
t.Skip(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(base)
|
||||||
|
var files = []usageTestFile{
|
||||||
|
{name: "rootfile", size: 10000},
|
||||||
|
{name: "rootfile2", size: 10000},
|
||||||
|
{name: "dir1/d1file", size: 2000},
|
||||||
|
{name: "dir2/d2file", size: 300},
|
||||||
|
{name: "dir1/dira/dafile", size: 100000},
|
||||||
|
{name: "dir1/dira/dbfile", size: 200000},
|
||||||
|
{name: "dir1/dira/dirasub/dcfile", size: 1000000},
|
||||||
|
{name: "dir1/dira/dirasub/sublevel3/dccccfile", size: 10},
|
||||||
|
}
|
||||||
|
createUsageTestFiles(t, base, files)
|
||||||
|
|
||||||
|
getSize := func(item Item) (i int64, err error) {
|
||||||
|
if item.Typ&os.ModeDir == 0 {
|
||||||
|
s, err := os.Stat(item.Path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return s.Size(), nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := updateUsage(context.Background(), base, dataUsageCache{}, func() {}, getSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test dirs
|
||||||
|
var want = []struct {
|
||||||
|
path string
|
||||||
|
isNil bool
|
||||||
|
size, objs int
|
||||||
|
flatten bool
|
||||||
|
oSizes sizeHistogram
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
size: 1322310,
|
||||||
|
flatten: true,
|
||||||
|
objs: 8,
|
||||||
|
oSizes: sizeHistogram{0: 2, 1: 6},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
size: 20000,
|
||||||
|
objs: 2,
|
||||||
|
oSizes: sizeHistogram{1: 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/dir1",
|
||||||
|
size: 2000,
|
||||||
|
objs: 1,
|
||||||
|
oSizes: sizeHistogram{1: 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/dir1/dira",
|
||||||
|
flatten: true,
|
||||||
|
size: 1300010,
|
||||||
|
objs: 4,
|
||||||
|
oSizes: sizeHistogram{0: 1, 1: 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/dir1/dira/",
|
||||||
|
flatten: true,
|
||||||
|
size: 1300010,
|
||||||
|
objs: 4,
|
||||||
|
oSizes: sizeHistogram{0: 1, 1: 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/dir1/dira",
|
||||||
|
size: 300000,
|
||||||
|
objs: 2,
|
||||||
|
oSizes: sizeHistogram{0: 0, 1: 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/dir1/dira/",
|
||||||
|
size: 300000,
|
||||||
|
objs: 2,
|
||||||
|
oSizes: sizeHistogram{0: 0, 1: 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/nonexistying",
|
||||||
|
isNil: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, w := range want {
|
||||||
|
t.Run(w.path, func(t *testing.T) {
|
||||||
|
e := got.find(w.path)
|
||||||
|
if w.isNil {
|
||||||
|
if e != nil {
|
||||||
|
t.Error("want nil, got", e)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e == nil {
|
||||||
|
t.Fatal("got nil result")
|
||||||
|
}
|
||||||
|
if w.flatten {
|
||||||
|
*e = got.flatten(*e)
|
||||||
|
}
|
||||||
|
if e.Size != int64(w.size) {
|
||||||
|
t.Error("got size", e.Size, "want", w.size)
|
||||||
|
}
|
||||||
|
if e.Objects != uint64(w.objs) {
|
||||||
|
t.Error("got objects", e.Objects, "want", w.objs)
|
||||||
|
}
|
||||||
|
if e.ObjSizes != w.oSizes {
|
||||||
|
t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
files = []usageTestFile{
|
||||||
|
{
|
||||||
|
name: "newfolder/afile",
|
||||||
|
size: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "newfolder/anotherone",
|
||||||
|
size: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "newfolder/anemptyone",
|
||||||
|
size: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dir1/fileindir1",
|
||||||
|
size: 20000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dir1/dirc/fileindirc",
|
||||||
|
size: 20000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rootfile3",
|
||||||
|
size: 1000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
createUsageTestFiles(t, base, files)
|
||||||
|
got, err = updateUsage(context.Background(), base, got, func() {}, getSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want = []struct {
|
||||||
|
path string
|
||||||
|
isNil bool
|
||||||
|
size, objs int
|
||||||
|
flatten bool
|
||||||
|
oSizes sizeHistogram
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
size: 1363315,
|
||||||
|
flatten: true,
|
||||||
|
objs: 14,
|
||||||
|
oSizes: sizeHistogram{0: 6, 1: 8},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
size: 21000,
|
||||||
|
objs: 3,
|
||||||
|
oSizes: sizeHistogram{0: 1, 1: 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/newfolder",
|
||||||
|
size: 5,
|
||||||
|
objs: 3,
|
||||||
|
oSizes: sizeHistogram{0: 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/dir1/dira",
|
||||||
|
size: 1300010,
|
||||||
|
flatten: true,
|
||||||
|
objs: 4,
|
||||||
|
oSizes: sizeHistogram{0: 1, 1: 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/nonexistying",
|
||||||
|
isNil: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, w := range want {
|
||||||
|
t.Run(w.path, func(t *testing.T) {
|
||||||
|
e := got.find(w.path)
|
||||||
|
if w.isNil {
|
||||||
|
if e != nil {
|
||||||
|
t.Error("want nil, got", e)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e == nil {
|
||||||
|
t.Fatal("got nil result")
|
||||||
|
}
|
||||||
|
if w.flatten {
|
||||||
|
*e = got.flatten(*e)
|
||||||
|
}
|
||||||
|
if e.Size != int64(w.size) {
|
||||||
|
t.Error("got size", e.Size, "want", w.size)
|
||||||
|
}
|
||||||
|
if e.Objects != uint64(w.objs) {
|
||||||
|
t.Error("got objects", e.Objects, "want", w.objs)
|
||||||
|
}
|
||||||
|
if e.ObjSizes != w.oSizes {
|
||||||
|
t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
files = []usageTestFile{
|
||||||
|
{
|
||||||
|
name: "dir1/dira/dirasub/fileindira2",
|
||||||
|
size: 200,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
createUsageTestFiles(t, base, files)
|
||||||
|
err = os.RemoveAll(filepath.Join(base, "dir1/dira/dirasub/dcfile"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Changed dir must be picked up in this many cycles.
|
||||||
|
for i := 0; i < dataUsageUpdateDirCycles; i++ {
|
||||||
|
got, err = updateUsage(context.Background(), base, got, func() {}, getSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
want = []struct {
|
||||||
|
path string
|
||||||
|
isNil bool
|
||||||
|
size, objs int
|
||||||
|
flatten bool
|
||||||
|
oSizes sizeHistogram
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
size: 363515,
|
||||||
|
flatten: true,
|
||||||
|
objs: 14,
|
||||||
|
oSizes: sizeHistogram{0: 7, 1: 7},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/dir1/dira",
|
||||||
|
size: 300210,
|
||||||
|
objs: 4,
|
||||||
|
flatten: true,
|
||||||
|
oSizes: sizeHistogram{0: 2, 1: 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, w := range want {
|
||||||
|
t.Run(w.path, func(t *testing.T) {
|
||||||
|
e := got.find(w.path)
|
||||||
|
if w.isNil {
|
||||||
|
if e != nil {
|
||||||
|
t.Error("want nil, got", e)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e == nil {
|
||||||
|
t.Fatal("got nil result")
|
||||||
|
}
|
||||||
|
if w.flatten {
|
||||||
|
*e = got.flatten(*e)
|
||||||
|
}
|
||||||
|
if e.Size != int64(w.size) {
|
||||||
|
t.Error("got size", e.Size, "want", w.size)
|
||||||
|
}
|
||||||
|
if e.Objects != uint64(w.objs) {
|
||||||
|
t.Error("got objects", e.Objects, "want", w.objs)
|
||||||
|
}
|
||||||
|
if e.ObjSizes != w.oSizes {
|
||||||
|
t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDataUsageUpdatePrefix(t *testing.T) {
|
||||||
|
base, err := ioutil.TempDir("", "TestDataUpdateUsagePrefix")
|
||||||
|
if err != nil {
|
||||||
|
t.Skip(err)
|
||||||
|
}
|
||||||
|
base = filepath.Join(base, "bucket")
|
||||||
|
defer os.RemoveAll(base)
|
||||||
|
var files = []usageTestFile{
|
||||||
|
{name: "bucket/rootfile", size: 10000},
|
||||||
|
{name: "bucket/rootfile2", size: 10000},
|
||||||
|
{name: "bucket/dir1/d1file", size: 2000},
|
||||||
|
{name: "bucket/dir2/d2file", size: 300},
|
||||||
|
{name: "bucket/dir1/dira/dafile", size: 100000},
|
||||||
|
{name: "bucket/dir1/dira/dbfile", size: 200000},
|
||||||
|
{name: "bucket/dir1/dira/dirasub/dcfile", size: 1000000},
|
||||||
|
{name: "bucket/dir1/dira/dirasub/sublevel3/dccccfile", size: 10},
|
||||||
|
}
|
||||||
|
createUsageTestFiles(t, base, files)
|
||||||
|
|
||||||
|
getSize := func(item Item) (i int64, err error) {
|
||||||
|
if item.Typ&os.ModeDir == 0 {
|
||||||
|
s, err := os.Stat(item.Path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return s.Size(), nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
got, err := updateUsage(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: "bucket"}}, func() {}, getSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test dirs
|
||||||
|
var want = []struct {
|
||||||
|
path string
|
||||||
|
isNil bool
|
||||||
|
size, objs int
|
||||||
|
oSizes sizeHistogram
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
path: "flat",
|
||||||
|
size: 1322310,
|
||||||
|
objs: 8,
|
||||||
|
oSizes: sizeHistogram{0: 2, 1: 6},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "bucket/",
|
||||||
|
size: 20000,
|
||||||
|
objs: 2,
|
||||||
|
oSizes: sizeHistogram{1: 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "bucket/dir1",
|
||||||
|
size: 2000,
|
||||||
|
objs: 1,
|
||||||
|
oSizes: sizeHistogram{1: 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "bucket/dir1/dira",
|
||||||
|
size: 1300010,
|
||||||
|
objs: 4,
|
||||||
|
oSizes: sizeHistogram{0: 1, 1: 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "bucket/dir1/dira/",
|
||||||
|
size: 1300010,
|
||||||
|
objs: 4,
|
||||||
|
oSizes: sizeHistogram{0: 1, 1: 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "bucket/nonexistying",
|
||||||
|
isNil: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, w := range want {
|
||||||
|
t.Run(w.path, func(t *testing.T) {
|
||||||
|
e := got.find(w.path)
|
||||||
|
if w.path == "flat" {
|
||||||
|
f := got.flatten(*got.root())
|
||||||
|
e = &f
|
||||||
|
}
|
||||||
|
if w.isNil {
|
||||||
|
if e != nil {
|
||||||
|
t.Error("want nil, got", e)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e == nil {
|
||||||
|
t.Fatal("got nil result")
|
||||||
|
}
|
||||||
|
if e.Size != int64(w.size) {
|
||||||
|
t.Error("got size", e.Size, "want", w.size)
|
||||||
|
}
|
||||||
|
if e.Objects != uint64(w.objs) {
|
||||||
|
t.Error("got objects", e.Objects, "want", w.objs)
|
||||||
|
}
|
||||||
|
if e.ObjSizes != w.oSizes {
|
||||||
|
t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
files = []usageTestFile{
|
||||||
|
{
|
||||||
|
name: "bucket/newfolder/afile",
|
||||||
|
size: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bucket/newfolder/anotherone",
|
||||||
|
size: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bucket/newfolder/anemptyone",
|
||||||
|
size: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bucket/dir1/fileindir1",
|
||||||
|
size: 20000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bucket/dir1/dirc/fileindirc",
|
||||||
|
size: 20000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bucket/rootfile3",
|
||||||
|
size: 1000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
createUsageTestFiles(t, base, files)
|
||||||
|
got, err = updateUsage(context.Background(), base, got, func() {}, getSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want = []struct {
|
||||||
|
path string
|
||||||
|
isNil bool
|
||||||
|
size, objs int
|
||||||
|
oSizes sizeHistogram
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
path: "flat",
|
||||||
|
size: 1363315,
|
||||||
|
objs: 14,
|
||||||
|
oSizes: sizeHistogram{0: 6, 1: 8},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "bucket/",
|
||||||
|
size: 21000,
|
||||||
|
objs: 3,
|
||||||
|
oSizes: sizeHistogram{0: 1, 1: 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "bucket/newfolder",
|
||||||
|
size: 5,
|
||||||
|
objs: 3,
|
||||||
|
oSizes: sizeHistogram{0: 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "bucket/dir1/dira",
|
||||||
|
size: 1300010,
|
||||||
|
objs: 4,
|
||||||
|
oSizes: sizeHistogram{0: 1, 1: 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "bucket/nonexistying",
|
||||||
|
isNil: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, w := range want {
|
||||||
|
t.Run(w.path, func(t *testing.T) {
|
||||||
|
e := got.find(w.path)
|
||||||
|
if w.path == "flat" {
|
||||||
|
f := got.flatten(*got.root())
|
||||||
|
e = &f
|
||||||
|
}
|
||||||
|
if w.isNil {
|
||||||
|
if e != nil {
|
||||||
|
t.Error("want nil, got", e)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e == nil {
|
||||||
|
t.Fatal("got nil result")
|
||||||
|
}
|
||||||
|
if e.Size != int64(w.size) {
|
||||||
|
t.Error("got size", e.Size, "want", w.size)
|
||||||
|
}
|
||||||
|
if e.Objects != uint64(w.objs) {
|
||||||
|
t.Error("got objects", e.Objects, "want", w.objs)
|
||||||
|
}
|
||||||
|
if e.ObjSizes != w.oSizes {
|
||||||
|
t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
files = []usageTestFile{
|
||||||
|
{
|
||||||
|
name: "bucket/dir1/dira/dirasub/fileindira2",
|
||||||
|
size: 200,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
createUsageTestFiles(t, base, files)
|
||||||
|
err = os.RemoveAll(filepath.Join(base, "bucket/dir1/dira/dirasub/dcfile"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Changed dir must be picked up in this many cycles.
|
||||||
|
for i := 0; i < dataUsageUpdateDirCycles; i++ {
|
||||||
|
got, err = updateUsage(context.Background(), base, got, func() {}, getSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
want = []struct {
|
||||||
|
path string
|
||||||
|
isNil bool
|
||||||
|
size, objs int
|
||||||
|
oSizes sizeHistogram
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
path: "flat",
|
||||||
|
size: 363515,
|
||||||
|
objs: 14,
|
||||||
|
oSizes: sizeHistogram{0: 7, 1: 7},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "bucket/dir1/dira",
|
||||||
|
size: 300210,
|
||||||
|
objs: 4,
|
||||||
|
oSizes: sizeHistogram{0: 2, 1: 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, w := range want {
|
||||||
|
t.Run(w.path, func(t *testing.T) {
|
||||||
|
e := got.find(w.path)
|
||||||
|
if w.path == "flat" {
|
||||||
|
f := got.flatten(*got.root())
|
||||||
|
e = &f
|
||||||
|
}
|
||||||
|
if w.isNil {
|
||||||
|
if e != nil {
|
||||||
|
t.Error("want nil, got", e)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e == nil {
|
||||||
|
t.Fatal("got nil result")
|
||||||
|
}
|
||||||
|
if e.Size != int64(w.size) {
|
||||||
|
t.Error("got size", e.Size, "want", w.size)
|
||||||
|
}
|
||||||
|
if e.Objects != uint64(w.objs) {
|
||||||
|
t.Error("got objects", e.Objects, "want", w.objs)
|
||||||
|
}
|
||||||
|
if e.ObjSizes != w.oSizes {
|
||||||
|
t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUsageTestFiles(t *testing.T, base string, files []usageTestFile) {
|
||||||
|
for _, f := range files {
|
||||||
|
err := os.MkdirAll(filepath.Dir(filepath.Join(base, f.name)), os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(filepath.Join(base, f.name), make([]byte, f.size), os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDataUsageCacheSerialize(t *testing.T) {
|
||||||
|
base, err := ioutil.TempDir("", "TestDataUsageCacheSerialize")
|
||||||
|
if err != nil {
|
||||||
|
t.Skip(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(base)
|
||||||
|
var files = []usageTestFile{
|
||||||
|
{name: "rootfile", size: 10000},
|
||||||
|
{name: "rootfile2", size: 10000},
|
||||||
|
{name: "dir1/d1file", size: 2000},
|
||||||
|
{name: "dir2/d2file", size: 300},
|
||||||
|
{name: "dir1/dira/dafile", size: 100000},
|
||||||
|
{name: "dir1/dira/dbfile", size: 200000},
|
||||||
|
{name: "dir1/dira/dirasub/dcfile", size: 1000000},
|
||||||
|
{name: "dir1/dira/dirasub/sublevel3/dccccfile", size: 10},
|
||||||
|
}
|
||||||
|
createUsageTestFiles(t, base, files)
|
||||||
|
|
||||||
|
getSize := func(item Item) (i int64, err error) {
|
||||||
|
if item.Typ&os.ModeDir == 0 {
|
||||||
|
s, err := os.Stat(item.Path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return s.Size(), nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
want, err := updateUsage(context.Background(), base, dataUsageCache{}, func() {}, getSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := want.serialize()
|
||||||
|
var got dataUsageCache
|
||||||
|
err = got.deserialize(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got.Info.LastUpdate.IsZero() {
|
||||||
|
t.Error("lastupdate not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !want.Info.LastUpdate.Equal(got.Info.LastUpdate) {
|
||||||
|
t.Fatalf("deserialize mismatch\nwant: %+v\ngot: %+v", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
+150
-90
@@ -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.
|
||||||
@@ -21,12 +21,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ import (
|
|||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/disk"
|
"github.com/minio/minio/pkg/disk"
|
||||||
"github.com/minio/sio"
|
"github.com/minio/sio"
|
||||||
"github.com/ncw/directio"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -44,7 +45,7 @@ const (
|
|||||||
cacheMetaJSONFile = "cache.json"
|
cacheMetaJSONFile = "cache.json"
|
||||||
cacheDataFile = "part.1"
|
cacheDataFile = "part.1"
|
||||||
cacheMetaVersion = "1.0.0"
|
cacheMetaVersion = "1.0.0"
|
||||||
|
cacheExpiryDays = time.Duration(90 * time.Hour * 24) // defaults to 90 days
|
||||||
// SSECacheEncrypted is the metadata key indicating that the object
|
// SSECacheEncrypted is the metadata key indicating that the object
|
||||||
// is a cache entry encrypted with cache KMS master key in globalCacheKMS.
|
// is a cache entry encrypted with cache KMS master key in globalCacheKMS.
|
||||||
SSECacheEncrypted = "X-Minio-Internal-Encrypted-Cache"
|
SSECacheEncrypted = "X-Minio-Internal-Encrypted-Cache"
|
||||||
@@ -126,15 +127,15 @@ func (m *cacheMeta) ToObjectInfo(bucket, object string) (o ObjectInfo) {
|
|||||||
type diskCache struct {
|
type diskCache struct {
|
||||||
dir string // caching directory
|
dir string // caching directory
|
||||||
quotaPct int // max usage in %
|
quotaPct int // max usage in %
|
||||||
expiry int // cache expiry in days
|
|
||||||
// mark false if drive is offline
|
// mark false if drive is offline
|
||||||
online bool
|
online bool
|
||||||
// mutex to protect updates to online variable
|
// mutex to protect updates to online variable
|
||||||
onlineMutex *sync.RWMutex
|
onlineMutex *sync.RWMutex
|
||||||
// purge() listens on this channel to start the cache-purge process
|
pool sync.Pool
|
||||||
purgeChan chan struct{}
|
after int // minimum accesses before an object is cached.
|
||||||
pool sync.Pool
|
lowWatermark int
|
||||||
after int // minimum accesses before an object is cached.
|
highWatermark int
|
||||||
|
gcCounter atomic.Uint64
|
||||||
// nsMutex namespace lock
|
// nsMutex namespace lock
|
||||||
nsMutex *nsLockMap
|
nsMutex *nsLockMap
|
||||||
// Object functions pointing to the corresponding functions of backend implementation.
|
// Object functions pointing to the corresponding functions of backend implementation.
|
||||||
@@ -142,21 +143,21 @@ type diskCache struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inits the disk cache dir if it is not initialized already.
|
// Inits the disk cache dir if it is not initialized already.
|
||||||
func newDiskCache(dir string, expiry int, quotaPct, after int) (*diskCache, error) {
|
func newDiskCache(dir string, quotaPct, after, lowWatermark, highWatermark int) (*diskCache, error) {
|
||||||
if err := os.MkdirAll(dir, 0777); err != nil {
|
if err := os.MkdirAll(dir, 0777); err != nil {
|
||||||
return nil, fmt.Errorf("Unable to initialize '%s' dir, %w", dir, err)
|
return nil, fmt.Errorf("Unable to initialize '%s' dir, %w", dir, err)
|
||||||
}
|
}
|
||||||
cache := diskCache{
|
cache := diskCache{
|
||||||
dir: dir,
|
dir: dir,
|
||||||
expiry: expiry,
|
quotaPct: quotaPct,
|
||||||
quotaPct: quotaPct,
|
after: after,
|
||||||
after: after,
|
lowWatermark: lowWatermark,
|
||||||
purgeChan: make(chan struct{}),
|
highWatermark: highWatermark,
|
||||||
online: true,
|
online: true,
|
||||||
onlineMutex: &sync.RWMutex{},
|
onlineMutex: &sync.RWMutex{},
|
||||||
pool: sync.Pool{
|
pool: sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() interface{} {
|
||||||
b := directio.AlignedBlock(int(cacheBlkSize))
|
b := disk.AlignedBlock(int(cacheBlkSize))
|
||||||
return &b
|
return &b
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -168,35 +169,36 @@ func newDiskCache(dir string, expiry int, quotaPct, after int) (*diskCache, erro
|
|||||||
return &cache, nil
|
return &cache, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns if the disk usage is low.
|
// diskUsageLow() returns true if disk usage falls below the low watermark w.r.t configured cache quota.
|
||||||
// Disk usage is low if usage is < 80% of cacheMaxDiskUsagePct
|
// Ex. for a 100GB disk, if quota is configured as 70% and watermark_low = 80% and
|
||||||
// Ex. for a 100GB disk, if maxUsage is configured as 70% then cacheMaxDiskUsagePct is 70G
|
// watermark_high = 90% then garbage collection starts when 63% of disk is used and
|
||||||
// hence disk usage is low if the disk usage is less than 56G (because 80% of 70G is 56G)
|
// stops when disk usage drops to 56%
|
||||||
func (c *diskCache) diskUsageLow() bool {
|
func (c *diskCache) diskUsageLow() bool {
|
||||||
minUsage := c.quotaPct * 80 / 100
|
gcStopPct := c.quotaPct * c.lowWatermark / 100
|
||||||
di, err := disk.GetInfo(c.dir)
|
di, err := disk.GetInfo(c.dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
|
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
usedPercent := (di.Total - di.Free) * 100 / di.Total
|
usedPercent := (di.Total - di.Free) * 100 / di.Total
|
||||||
return int(usedPercent) < minUsage
|
return int(usedPercent) < gcStopPct
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return if the disk usage is high.
|
// Returns if the disk usage reaches high water mark w.r.t the configured cache quota.
|
||||||
// Disk usage is high if disk used is > cacheMaxDiskUsagePct
|
// gc starts if high water mark reached.
|
||||||
func (c *diskCache) diskUsageHigh() bool {
|
func (c *diskCache) diskUsageHigh() bool {
|
||||||
|
gcTriggerPct := c.quotaPct * c.highWatermark / 100
|
||||||
di, err := disk.GetInfo(c.dir)
|
di, err := disk.GetInfo(c.dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
|
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
usedPercent := (di.Total - di.Free) * 100 / di.Total
|
usedPercent := (di.Total - di.Free) * 100 / di.Total
|
||||||
return int(usedPercent) > c.quotaPct
|
return int(usedPercent) >= gcTriggerPct
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns if size space can be allocated without exceeding
|
// Returns if size space can be allocated without exceeding
|
||||||
@@ -205,7 +207,7 @@ func (c *diskCache) diskAvailable(size int64) bool {
|
|||||||
di, err := disk.GetInfo(c.dir)
|
di, err := disk.GetInfo(c.dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
|
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -213,12 +215,46 @@ func (c *diskCache) diskAvailable(size int64) bool {
|
|||||||
return int(usedPercent) < c.quotaPct
|
return int(usedPercent) < c.quotaPct
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toClear returns how many bytes should be cleared to reach the low watermark quota.
|
||||||
|
// returns 0 if below quota.
|
||||||
|
func (c *diskCache) toClear() uint64 {
|
||||||
|
di, err := disk.GetInfo(c.dir)
|
||||||
|
if err != nil {
|
||||||
|
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
|
||||||
|
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return bytesToClear(int64(di.Total), int64(di.Free), uint64(c.quotaPct), uint64(c.lowWatermark))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errDoneForNow = errors.New("done for now")
|
||||||
|
)
|
||||||
|
|
||||||
// Purge cache entries that were not accessed.
|
// Purge cache entries that were not accessed.
|
||||||
func (c *diskCache) purge() {
|
func (c *diskCache) purge(ctx context.Context) {
|
||||||
|
if c.diskUsageLow() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toFree := c.toClear()
|
||||||
|
if toFree == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// expiry for cleaning up old cache.json files that
|
||||||
|
// need to be cleaned up.
|
||||||
|
expiry := UTCNow().Add(-cacheExpiryDays)
|
||||||
|
// defaulting max hits count to 100
|
||||||
|
scorer, err := newFileScorer(int64(toFree), time.Now().Unix(), 100)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// this function returns FileInfo for cached range files and cache data file.
|
// this function returns FileInfo for cached range files and cache data file.
|
||||||
fiStatFn := func(ranges map[string]string, dataFile, pathPrefix string) map[string]os.FileInfo {
|
fiStatFn := func(ranges map[string]string, dataFile, pathPrefix string) map[string]os.FileInfo {
|
||||||
fm := make(map[string]os.FileInfo)
|
fm := make(map[string]os.FileInfo)
|
||||||
fname := pathJoin(pathPrefix, cacheDataFile)
|
fname := pathJoin(pathPrefix, dataFile)
|
||||||
if fi, err := os.Stat(fname); err == nil {
|
if fi, err := os.Stat(fname); err == nil {
|
||||||
fm[fname] = fi
|
fm[fname] = fi
|
||||||
}
|
}
|
||||||
@@ -231,65 +267,88 @@ func (c *diskCache) purge() {
|
|||||||
}
|
}
|
||||||
return fm
|
return fm
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
|
||||||
for {
|
|
||||||
olderThan := c.expiry * 24
|
|
||||||
for !c.diskUsageLow() {
|
|
||||||
// delete unaccessed objects older than expiry duration
|
|
||||||
expiry := UTCNow().Add(time.Hour * time.Duration(-1*olderThan))
|
|
||||||
olderThan /= 2
|
|
||||||
if olderThan < 1 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
deletedCount := 0
|
|
||||||
|
|
||||||
objDirs, err := ioutil.ReadDir(c.dir)
|
filterFn := func(name string, typ os.FileMode) error {
|
||||||
if err != nil {
|
if name == minioMetaBucket {
|
||||||
log.Fatal(err)
|
// Proceed to next file.
|
||||||
}
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
for _, obj := range objDirs {
|
cacheDir := pathJoin(c.dir, name)
|
||||||
if obj.Name() == minioMetaBucket {
|
meta, _, numHits, err := c.statCachedMeta(ctx, cacheDir)
|
||||||
continue
|
if err != nil {
|
||||||
}
|
// delete any partially filled cache entry left behind.
|
||||||
meta, _, _, err := c.statCachedMeta(context.Background(), pathJoin(c.dir, obj.Name()))
|
removeAll(cacheDir)
|
||||||
if err != nil {
|
// Proceed to next file.
|
||||||
// delete any partially filled cache entry left behind.
|
return nil
|
||||||
removeAll(pathJoin(c.dir, obj.Name()))
|
}
|
||||||
continue
|
|
||||||
}
|
|
||||||
// stat all cached file ranges and cacheDataFile.
|
|
||||||
fis := fiStatFn(meta.Ranges, cacheDataFile, pathJoin(c.dir, obj.Name()))
|
|
||||||
objInfo := meta.ToObjectInfo("", "")
|
|
||||||
cc := cacheControlOpts(objInfo)
|
|
||||||
|
|
||||||
for fname, fi := range fis {
|
// stat all cached file ranges and cacheDataFile.
|
||||||
if atime.Get(fi).Before(expiry) ||
|
cachedFiles := fiStatFn(meta.Ranges, cacheDataFile, pathJoin(c.dir, name))
|
||||||
cc.isStale(objInfo.ModTime) {
|
objInfo := meta.ToObjectInfo("", "")
|
||||||
if err = removeAll(fname); err != nil {
|
cc := cacheControlOpts(objInfo)
|
||||||
logger.LogIf(ctx, err)
|
for fname, fi := range cachedFiles {
|
||||||
}
|
if cc != nil {
|
||||||
deletedCount++
|
if cc.isStale(objInfo.ModTime) {
|
||||||
// break early if sufficient disk space reclaimed.
|
if err = removeAll(fname); err != nil {
|
||||||
if !c.diskUsageLow() {
|
logger.LogIf(ctx, err)
|
||||||
break
|
}
|
||||||
}
|
scorer.adjustSaveBytes(-fi.Size())
|
||||||
|
|
||||||
|
// break early if sufficient disk space reclaimed.
|
||||||
|
if c.diskUsageLow() {
|
||||||
|
// if we found disk usage is already low, we return nil filtering is complete.
|
||||||
|
return errDoneForNow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if deletedCount == 0 {
|
scorer.addFile(fname, atime.Get(fi), fi.Size(), numHits)
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for {
|
// clean up stale cache.json files for objects that never got cached but access count was maintained in cache.json
|
||||||
<-c.purgeChan
|
fi, err := os.Stat(pathJoin(cacheDir, cacheMetaJSONFile))
|
||||||
if c.diskUsageHigh() {
|
if err != nil || (fi.ModTime().Before(expiry) && len(cachedFiles) == 0) {
|
||||||
break
|
removeAll(cacheDir)
|
||||||
}
|
scorer.adjustSaveBytes(-fi.Size())
|
||||||
|
// Proceed to next file.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we found disk usage is already low, we return nil filtering is complete.
|
||||||
|
if c.diskUsageLow() {
|
||||||
|
return errDoneForNow
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed to next file.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := readDirFilterFn(c.dir, filterFn); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range scorer.fileNames() {
|
||||||
|
removeAll(path)
|
||||||
|
slashIdx := strings.LastIndex(path, SlashSeparator)
|
||||||
|
pathPrefix := path[0:slashIdx]
|
||||||
|
fname := path[slashIdx+1:]
|
||||||
|
if fname == cacheDataFile {
|
||||||
|
removeAll(pathPrefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *diskCache) incGCCounter() {
|
||||||
|
c.gcCounter.Add(uint64(1))
|
||||||
|
}
|
||||||
|
func (c *diskCache) resetGCCounter() {
|
||||||
|
c.gcCounter.Store(uint64(0))
|
||||||
|
}
|
||||||
|
func (c *diskCache) gcCount() uint64 {
|
||||||
|
return c.gcCounter.Load()
|
||||||
|
}
|
||||||
|
|
||||||
// sets cache drive status
|
// sets cache drive status
|
||||||
func (c *diskCache) setOnline(status bool) {
|
func (c *diskCache) setOnline(status bool) {
|
||||||
c.onlineMutex.Lock()
|
c.onlineMutex.Lock()
|
||||||
@@ -378,6 +437,9 @@ func (c *diskCache) statRange(ctx context.Context, bucket, object string, rs *HT
|
|||||||
if !ok {
|
if !ok {
|
||||||
return oi, rngInfo, numHits, ObjectNotFound{Bucket: bucket, Object: object}
|
return oi, rngInfo, numHits, ObjectNotFound{Bucket: bucket, Object: object}
|
||||||
}
|
}
|
||||||
|
if _, err = os.Stat(pathJoin(cacheObjPath, rngFile)); err != nil {
|
||||||
|
return oi, rngInfo, numHits, ObjectNotFound{Bucket: bucket, Object: object}
|
||||||
|
}
|
||||||
rngInfo = RangeInfo{Range: rng, File: rngFile, Size: int64(actualRngSize)}
|
rngInfo = RangeInfo{Range: rng, File: rngFile, Size: int64(actualRngSize)}
|
||||||
|
|
||||||
err = decryptCacheObjectETag(&oi)
|
err = decryptCacheObjectETag(&oi)
|
||||||
@@ -568,10 +630,8 @@ func newCacheEncryptMetadata(bucket, object string, metadata map[string]string)
|
|||||||
// Caches the object to disk
|
// Caches the object to disk
|
||||||
func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Reader, size int64, rs *HTTPRangeSpec, opts ObjectOptions, incHitsOnly bool) error {
|
func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Reader, size int64, rs *HTTPRangeSpec, opts ObjectOptions, incHitsOnly bool) error {
|
||||||
if c.diskUsageHigh() {
|
if c.diskUsageHigh() {
|
||||||
select {
|
c.incGCCounter()
|
||||||
case c.purgeChan <- struct{}{}:
|
io.Copy(ioutil.Discard, data)
|
||||||
default:
|
|
||||||
}
|
|
||||||
return errDiskFull
|
return errDiskFull
|
||||||
}
|
}
|
||||||
cachePath := getCacheSHADir(c.dir, bucket, object)
|
cachePath := getCacheSHADir(c.dir, bucket, object)
|
||||||
@@ -622,11 +682,11 @@ func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Read
|
|||||||
if IsErr(err, baseErrs...) {
|
if IsErr(err, baseErrs...) {
|
||||||
c.setOnline(false)
|
c.setOnline(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
removeAll(cachePath)
|
removeAll(cachePath)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if actualSize != uint64(n) {
|
if actualSize != uint64(n) {
|
||||||
removeAll(cachePath)
|
removeAll(cachePath)
|
||||||
return IncompleteBody{}
|
return IncompleteBody{}
|
||||||
@@ -757,7 +817,7 @@ func (c *diskCache) bitrotReadFromCache(ctx context.Context, filePath string, of
|
|||||||
if !bytes.Equal(hashBytes, checksumHash) {
|
if !bytes.Equal(hashBytes, checksumHash) {
|
||||||
err = fmt.Errorf("hashes do not match expected %s, got %s",
|
err = fmt.Errorf("hashes do not match expected %s, got %s",
|
||||||
hex.EncodeToString(checksumHash), hex.EncodeToString(hashBytes))
|
hex.EncodeToString(checksumHash), hex.EncodeToString(hashBytes))
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(GlobalContext, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,11 +864,11 @@ func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRang
|
|||||||
if HasSuffix(object, SlashSeparator) {
|
if HasSuffix(object, SlashSeparator) {
|
||||||
// The lock taken above is released when
|
// The lock taken above is released when
|
||||||
// objReader.Close() is called by the caller.
|
// objReader.Close() is called by the caller.
|
||||||
gr, gerr := NewGetObjectReaderFromReader(bytes.NewBuffer(nil), objInfo, opts.CheckCopyPrecondFn, nsUnlocker)
|
gr, gerr := NewGetObjectReaderFromReader(bytes.NewBuffer(nil), objInfo, opts, nsUnlocker)
|
||||||
return gr, numHits, gerr
|
return gr, numHits, gerr
|
||||||
}
|
}
|
||||||
|
|
||||||
fn, off, length, nErr := NewGetObjectReader(rs, objInfo, opts.CheckCopyPrecondFn, nsUnlocker)
|
fn, off, length, nErr := NewGetObjectReader(rs, objInfo, opts, nsUnlocker)
|
||||||
if nErr != nil {
|
if nErr != nil {
|
||||||
return nil, numHits, nErr
|
return nil, numHits, nErr
|
||||||
}
|
}
|
||||||
|
|||||||
+169
-11
@@ -17,8 +17,12 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"container/list"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -57,13 +61,8 @@ type cacheControl struct {
|
|||||||
noCache bool
|
noCache bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c cacheControl) isEmpty() bool {
|
func (c *cacheControl) isStale(modTime time.Time) bool {
|
||||||
return c == cacheControl{}
|
if c == nil {
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c cacheControl) isStale(modTime time.Time) bool {
|
|
||||||
if c.isEmpty() {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// response will never be stale if only-if-cached is set
|
// response will never be stale if only-if-cached is set
|
||||||
@@ -100,7 +99,8 @@ func (c cacheControl) isStale(modTime time.Time) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns struct with cache-control settings from user metadata.
|
// returns struct with cache-control settings from user metadata.
|
||||||
func cacheControlOpts(o ObjectInfo) (c cacheControl) {
|
func cacheControlOpts(o ObjectInfo) *cacheControl {
|
||||||
|
c := cacheControl{}
|
||||||
m := o.UserDefined
|
m := o.UserDefined
|
||||||
if o.Expires != timeSentinel {
|
if o.Expires != timeSentinel {
|
||||||
c.expiry = o.Expires
|
c.expiry = o.Expires
|
||||||
@@ -114,7 +114,7 @@ func cacheControlOpts(o ObjectInfo) (c cacheControl) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
if headerVal == "" {
|
if headerVal == "" {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
headerVal = strings.ToLower(headerVal)
|
headerVal = strings.ToLower(headerVal)
|
||||||
headerVal = strings.TrimSpace(headerVal)
|
headerVal = strings.TrimSpace(headerVal)
|
||||||
@@ -146,7 +146,7 @@ func cacheControlOpts(o ObjectInfo) (c cacheControl) {
|
|||||||
p[0] == "max-stale" {
|
p[0] == "max-stale" {
|
||||||
i, err := strconv.Atoi(p[1])
|
i, err := strconv.Atoi(p[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cacheControl{}
|
return nil
|
||||||
}
|
}
|
||||||
if p[0] == "max-age" {
|
if p[0] == "max-age" {
|
||||||
c.maxAge = i
|
c.maxAge = i
|
||||||
@@ -162,7 +162,7 @@ func cacheControlOpts(o ObjectInfo) (c cacheControl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return c
|
return &c
|
||||||
}
|
}
|
||||||
|
|
||||||
// backendDownError returns true if err is due to backend failure or faulty disk if in server mode
|
// backendDownError returns true if err is due to backend failure or faulty disk if in server mode
|
||||||
@@ -283,3 +283,161 @@ func isMetadataSame(m1, m2 map[string]string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fileScorer struct {
|
||||||
|
saveBytes int64
|
||||||
|
now int64
|
||||||
|
maxHits int
|
||||||
|
// 1/size for consistent score.
|
||||||
|
sizeMult float64
|
||||||
|
|
||||||
|
// queue is a linked list of files we want to delete.
|
||||||
|
// The list is kept sorted according to score, highest at top, lowest at bottom.
|
||||||
|
queue list.List
|
||||||
|
queuedBytes int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type queuedFile struct {
|
||||||
|
name string
|
||||||
|
size int64
|
||||||
|
score float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// newFileScorer allows to collect files to save a specific number of bytes.
|
||||||
|
// Each file is assigned a score based on its age, size and number of hits.
|
||||||
|
// A list of files is maintained
|
||||||
|
func newFileScorer(saveBytes int64, now int64, maxHits int) (*fileScorer, error) {
|
||||||
|
if saveBytes <= 0 {
|
||||||
|
return nil, errors.New("newFileScorer: saveBytes <= 0")
|
||||||
|
}
|
||||||
|
if now < 0 {
|
||||||
|
return nil, errors.New("newFileScorer: now < 0")
|
||||||
|
}
|
||||||
|
if maxHits <= 0 {
|
||||||
|
return nil, errors.New("newFileScorer: maxHits <= 0")
|
||||||
|
}
|
||||||
|
f := fileScorer{saveBytes: saveBytes, maxHits: maxHits, now: now, sizeMult: 1 / float64(saveBytes)}
|
||||||
|
f.queue.Init()
|
||||||
|
return &f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileScorer) addFile(name string, lastAccess time.Time, size int64, hits int) {
|
||||||
|
// Calculate how much we want to delete this object.
|
||||||
|
file := queuedFile{
|
||||||
|
name: name,
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
score := float64(f.now - lastAccess.Unix())
|
||||||
|
// Size as fraction of how much we want to save, 0->1.
|
||||||
|
szWeight := math.Max(0, (math.Min(1, float64(size)*f.sizeMult)))
|
||||||
|
// 0 at f.maxHits, 1 at 0.
|
||||||
|
hitsWeight := (1.0 - math.Max(0, math.Min(1.0, float64(hits)/float64(f.maxHits))))
|
||||||
|
file.score = score * (1 + 0.25*szWeight + 0.25*hitsWeight)
|
||||||
|
// If we still haven't saved enough, just add the file
|
||||||
|
if f.queuedBytes < f.saveBytes {
|
||||||
|
f.insertFile(file)
|
||||||
|
f.trimQueue()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// If we score less than the worst, don't insert.
|
||||||
|
worstE := f.queue.Back()
|
||||||
|
if worstE != nil && file.score < worstE.Value.(queuedFile).score {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.insertFile(file)
|
||||||
|
f.trimQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjustSaveBytes allows to adjust the number of bytes to save.
|
||||||
|
// This can be used to adjust the count on the fly.
|
||||||
|
// Returns true if there still is a need to delete files (saveBytes >0),
|
||||||
|
// false if no more bytes needs to be saved.
|
||||||
|
func (f *fileScorer) adjustSaveBytes(n int64) bool {
|
||||||
|
f.saveBytes += n
|
||||||
|
if f.saveBytes <= 0 {
|
||||||
|
f.queue.Init()
|
||||||
|
f.saveBytes = 0
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if n < 0 {
|
||||||
|
f.trimQueue()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertFile will insert a file into the list, sorted by its score.
|
||||||
|
func (f *fileScorer) insertFile(file queuedFile) {
|
||||||
|
e := f.queue.Front()
|
||||||
|
for e != nil {
|
||||||
|
v := e.Value.(queuedFile)
|
||||||
|
if v.score < file.score {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
e = e.Next()
|
||||||
|
}
|
||||||
|
f.queuedBytes += file.size
|
||||||
|
// We reached the end.
|
||||||
|
if e == nil {
|
||||||
|
f.queue.PushBack(file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.queue.InsertBefore(file, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// trimQueue will trim the back of queue and still keep below wantSave.
|
||||||
|
func (f *fileScorer) trimQueue() {
|
||||||
|
for {
|
||||||
|
e := f.queue.Back()
|
||||||
|
if e == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v := e.Value.(queuedFile)
|
||||||
|
if f.queuedBytes-v.size < f.saveBytes {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.queue.Remove(e)
|
||||||
|
f.queuedBytes -= v.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileNames returns all queued file names.
|
||||||
|
func (f *fileScorer) fileNames() []string {
|
||||||
|
res := make([]string, 0, f.queue.Len())
|
||||||
|
e := f.queue.Front()
|
||||||
|
for e != nil {
|
||||||
|
res = append(res, e.Value.(queuedFile).name)
|
||||||
|
e = e.Next()
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileScorer) reset() {
|
||||||
|
f.queue.Init()
|
||||||
|
f.queuedBytes = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileScorer) queueString() string {
|
||||||
|
var res strings.Builder
|
||||||
|
e := f.queue.Front()
|
||||||
|
i := 0
|
||||||
|
for e != nil {
|
||||||
|
v := e.Value.(queuedFile)
|
||||||
|
if i > 0 {
|
||||||
|
res.WriteByte('\n')
|
||||||
|
}
|
||||||
|
res.WriteString(fmt.Sprintf("%03d: %s (score: %.3f, bytes: %d)", i, v.name, v.score, v.size))
|
||||||
|
i++
|
||||||
|
e = e.Next()
|
||||||
|
}
|
||||||
|
return res.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// bytesToClear() returns the number of bytes to clear to reach low watermark
|
||||||
|
// w.r.t quota given disk total and free space, quota in % allocated to cache
|
||||||
|
// and low watermark % w.r.t allowed quota.
|
||||||
|
func bytesToClear(total, free int64, quotaPct, lowWatermark uint64) uint64 {
|
||||||
|
used := (total - free)
|
||||||
|
quotaAllowed := total * (int64)(quotaPct) / 100
|
||||||
|
lowWMUsage := (total * (int64)(lowWatermark*quotaPct) / (100 * 100))
|
||||||
|
return (uint64)(math.Min(float64(quotaAllowed), math.Max(0.0, float64(used-lowWMUsage))))
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,16 +29,16 @@ func TestGetCacheControlOpts(t *testing.T) {
|
|||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
cacheControlHeaderVal string
|
cacheControlHeaderVal string
|
||||||
expiryHeaderVal time.Time
|
expiryHeaderVal time.Time
|
||||||
expectedCacheControl cacheControl
|
expectedCacheControl *cacheControl
|
||||||
expectedErr bool
|
expectedErr bool
|
||||||
}{
|
}{
|
||||||
{"", timeSentinel, cacheControl{}, false},
|
{"", timeSentinel, nil, false},
|
||||||
{"max-age=2592000, public", timeSentinel, cacheControl{maxAge: 2592000, sMaxAge: 0, minFresh: 0, expiry: time.Time{}}, false},
|
{"max-age=2592000, public", timeSentinel, &cacheControl{maxAge: 2592000, sMaxAge: 0, minFresh: 0, expiry: time.Time{}}, false},
|
||||||
{"max-age=2592000, no-store", timeSentinel, cacheControl{maxAge: 2592000, sMaxAge: 0, noStore: true, minFresh: 0, expiry: time.Time{}}, false},
|
{"max-age=2592000, no-store", timeSentinel, &cacheControl{maxAge: 2592000, sMaxAge: 0, noStore: true, minFresh: 0, expiry: time.Time{}}, false},
|
||||||
{"must-revalidate, max-age=600", timeSentinel, cacheControl{maxAge: 600, sMaxAge: 0, minFresh: 0, expiry: time.Time{}}, false},
|
{"must-revalidate, max-age=600", timeSentinel, &cacheControl{maxAge: 600, sMaxAge: 0, minFresh: 0, expiry: time.Time{}}, false},
|
||||||
{"s-maxAge=2500, max-age=600", timeSentinel, cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Time{}}, false},
|
{"s-maxAge=2500, max-age=600", timeSentinel, &cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Time{}}, false},
|
||||||
{"s-maxAge=2500, max-age=600", expiry, cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Date(2015, time.October, 21, 07, 28, 00, 00, time.UTC)}, false},
|
{"s-maxAge=2500, max-age=600", expiry, &cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Date(2015, time.October, 21, 07, 28, 00, 00, time.UTC)}, false},
|
||||||
{"s-maxAge=2500, max-age=600s", timeSentinel, cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Time{}}, true},
|
{"s-maxAge=2500, max-age=600s", timeSentinel, &cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Time{}}, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
@@ -49,7 +49,7 @@ func TestGetCacheControlOpts(t *testing.T) {
|
|||||||
m["expires"] = testCase.expiryHeaderVal.String()
|
m["expires"] = testCase.expiryHeaderVal.String()
|
||||||
}
|
}
|
||||||
c := cacheControlOpts(ObjectInfo{UserDefined: m, Expires: testCase.expiryHeaderVal})
|
c := cacheControlOpts(ObjectInfo{UserDefined: m, Expires: testCase.expiryHeaderVal})
|
||||||
if testCase.expectedErr && (c != cacheControl{}) {
|
if testCase.expectedErr && (c != nil) {
|
||||||
t.Errorf("expected err, got <nil>")
|
t.Errorf("expected err, got <nil>")
|
||||||
}
|
}
|
||||||
if !testCase.expectedErr && !reflect.DeepEqual(c, testCase.expectedCacheControl) {
|
if !testCase.expectedErr && !reflect.DeepEqual(c, testCase.expectedCacheControl) {
|
||||||
@@ -83,3 +83,90 @@ func TestIsMetadataSame(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewFileScorer(t *testing.T) {
|
||||||
|
fs, err := newFileScorer(1000, time.Now().Unix(), 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(fs.fileNames()) != 0 {
|
||||||
|
t.Fatal("non zero files??")
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
fs.addFile("recent", now.Add(-time.Minute), 1000, 10)
|
||||||
|
fs.addFile("older", now.Add(-time.Hour), 1000, 10)
|
||||||
|
if !reflect.DeepEqual(fs.fileNames(), []string{"older"}) {
|
||||||
|
t.Fatal("unexpected file list", fs.queueString())
|
||||||
|
}
|
||||||
|
fs.reset()
|
||||||
|
fs.addFile("bigger", now.Add(-time.Minute), 2000, 10)
|
||||||
|
fs.addFile("recent", now.Add(-time.Minute), 1000, 10)
|
||||||
|
if !reflect.DeepEqual(fs.fileNames(), []string{"bigger"}) {
|
||||||
|
t.Fatal("unexpected file list", fs.queueString())
|
||||||
|
}
|
||||||
|
fs.reset()
|
||||||
|
fs.addFile("less", now.Add(-time.Minute), 1000, 5)
|
||||||
|
fs.addFile("recent", now.Add(-time.Minute), 1000, 10)
|
||||||
|
if !reflect.DeepEqual(fs.fileNames(), []string{"less"}) {
|
||||||
|
t.Fatal("unexpected file list", fs.queueString())
|
||||||
|
}
|
||||||
|
fs.reset()
|
||||||
|
fs.addFile("small", now.Add(-time.Minute), 200, 10)
|
||||||
|
fs.addFile("medium", now.Add(-time.Minute), 300, 10)
|
||||||
|
if !reflect.DeepEqual(fs.fileNames(), []string{"medium", "small"}) {
|
||||||
|
t.Fatal("unexpected file list", fs.queueString())
|
||||||
|
}
|
||||||
|
fs.addFile("large", now.Add(-time.Minute), 700, 10)
|
||||||
|
fs.addFile("xsmol", now.Add(-time.Minute), 7, 10)
|
||||||
|
if !reflect.DeepEqual(fs.fileNames(), []string{"large", "medium"}) {
|
||||||
|
t.Fatal("unexpected file list", fs.queueString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.reset()
|
||||||
|
fs.addFile("less", now.Add(-time.Minute), 500, 5)
|
||||||
|
fs.addFile("recent", now.Add(-time.Minute), 500, 10)
|
||||||
|
if !fs.adjustSaveBytes(-500) {
|
||||||
|
t.Fatal("we should still need more bytes, got false")
|
||||||
|
}
|
||||||
|
// We should only need 500 bytes now.
|
||||||
|
if !reflect.DeepEqual(fs.fileNames(), []string{"less"}) {
|
||||||
|
t.Fatal("unexpected file list", fs.queueString())
|
||||||
|
}
|
||||||
|
if fs.adjustSaveBytes(-500) {
|
||||||
|
t.Fatal("we shouldn't need any more bytes, got true")
|
||||||
|
}
|
||||||
|
fs, err = newFileScorer(1000, time.Now().Unix(), 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fs.addFile("bigger", now.Add(-time.Minute), 50, 10)
|
||||||
|
// sorting should be consistent after adjusting savebytes.
|
||||||
|
fs.adjustSaveBytes(-800)
|
||||||
|
fs.addFile("smaller", now.Add(-time.Minute), 40, 10)
|
||||||
|
if !reflect.DeepEqual(fs.fileNames(), []string{"bigger", "smaller"}) {
|
||||||
|
t.Fatal("unexpected file list", fs.queueString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestBytesToClear(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
total int64
|
||||||
|
free int64
|
||||||
|
quotaPct uint64
|
||||||
|
watermarkLow uint64
|
||||||
|
expected uint64
|
||||||
|
}{
|
||||||
|
{1000, 800, 40, 90, 0},
|
||||||
|
{1000, 200, 40, 90, 400},
|
||||||
|
{1000, 400, 40, 90, 240},
|
||||||
|
{1000, 600, 40, 90, 40},
|
||||||
|
{1000, 600, 40, 70, 120},
|
||||||
|
{1000, 1000, 90, 70, 0},
|
||||||
|
{1000, 0, 90, 70, 370},
|
||||||
|
}
|
||||||
|
for i, tc := range testCases {
|
||||||
|
toClear := bytesToClear(tc.total, tc.free, tc.quotaPct, tc.watermarkLow)
|
||||||
|
if tc.expected != toClear {
|
||||||
|
t.Errorf("test %d expected %v, got %v", i, tc.expected, toClear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+57
-26
@@ -38,7 +38,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
cacheBlkSize = int64(1 * 1024 * 1024)
|
cacheBlkSize = int64(1 * 1024 * 1024)
|
||||||
|
cacheGCInterval = time.Minute * 30
|
||||||
)
|
)
|
||||||
|
|
||||||
// CacheStorageInfo - represents total, free capacity of
|
// CacheStorageInfo - represents total, free capacity of
|
||||||
@@ -174,7 +175,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
|||||||
if c.isCacheExclude(bucket, object) || c.skipCache() {
|
if c.isCacheExclude(bucket, object) || c.skipCache() {
|
||||||
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
||||||
}
|
}
|
||||||
var cc cacheControl
|
var cc *cacheControl
|
||||||
var cacheObjSize int64
|
var cacheObjSize int64
|
||||||
// fetch diskCache if object is currently cached or nearest available cache drive
|
// fetch diskCache if object is currently cached or nearest available cache drive
|
||||||
dcache, err := c.getCacheToLoc(ctx, bucket, object)
|
dcache, err := c.getCacheToLoc(ctx, bucket, object)
|
||||||
@@ -191,8 +192,8 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cc = cacheControlOpts(cacheReader.ObjInfo)
|
cc = cacheControlOpts(cacheReader.ObjInfo)
|
||||||
if (!cc.isEmpty() && !cc.isStale(cacheReader.ObjInfo.ModTime)) ||
|
if cc != nil && (!cc.isStale(cacheReader.ObjInfo.ModTime) ||
|
||||||
cc.onlyIfCached {
|
cc.onlyIfCached) {
|
||||||
// This is a cache hit, mark it so
|
// This is a cache hit, mark it so
|
||||||
bytesServed := cacheReader.ObjInfo.Size
|
bytesServed := cacheReader.ObjInfo.Size
|
||||||
if rs != nil {
|
if rs != nil {
|
||||||
@@ -205,7 +206,8 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
|||||||
c.incHitsToMeta(ctx, dcache, bucket, object, cacheReader.ObjInfo.Size, cacheReader.ObjInfo.ETag)
|
c.incHitsToMeta(ctx, dcache, bucket, object, cacheReader.ObjInfo.Size, cacheReader.ObjInfo.ETag)
|
||||||
return cacheReader, nil
|
return cacheReader, nil
|
||||||
}
|
}
|
||||||
if cc.noStore {
|
if cc != nil && cc.noStore {
|
||||||
|
cacheReader.Close()
|
||||||
c.cacheStats.incMiss()
|
c.cacheStats.incMiss()
|
||||||
bReader, err := c.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
bReader, err := c.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
||||||
bReader.ObjInfo.CacheLookupStatus = CacheHit
|
bReader.ObjInfo.CacheLookupStatus = CacheHit
|
||||||
@@ -219,9 +221,11 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
|||||||
c.incCacheStats(cacheObjSize)
|
c.incCacheStats(cacheObjSize)
|
||||||
return cacheReader, nil
|
return cacheReader, nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
if cacheErr == nil {
|
||||||
|
cacheReader.Close()
|
||||||
|
}
|
||||||
if _, ok := err.(ObjectNotFound); ok {
|
if _, ok := err.(ObjectNotFound); ok {
|
||||||
if cacheErr == nil {
|
if cacheErr == nil {
|
||||||
cacheReader.Close()
|
|
||||||
// Delete cached entry if backend object
|
// Delete cached entry if backend object
|
||||||
// was deleted.
|
// was deleted.
|
||||||
dcache.Delete(ctx, bucket, object)
|
dcache.Delete(ctx, bucket, object)
|
||||||
@@ -232,13 +236,19 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !objInfo.IsCacheable() {
|
if !objInfo.IsCacheable() {
|
||||||
|
if cacheErr == nil {
|
||||||
|
cacheReader.Close()
|
||||||
|
}
|
||||||
c.cacheStats.incMiss()
|
c.cacheStats.incMiss()
|
||||||
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
||||||
}
|
}
|
||||||
// skip cache for objects with locks
|
// skip cache for objects with locks
|
||||||
objRetention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
|
objRetention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
|
||||||
legalHold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
|
legalHold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
|
||||||
if objRetention.Mode != objectlock.Invalid || legalHold.Status != "" {
|
if objRetention.Mode.Valid() || legalHold.Status.Valid() {
|
||||||
|
if cacheErr == nil {
|
||||||
|
cacheReader.Close()
|
||||||
|
}
|
||||||
c.cacheStats.incMiss()
|
c.cacheStats.incMiss()
|
||||||
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
||||||
}
|
}
|
||||||
@@ -259,11 +269,8 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
|||||||
c.cacheStats.incMiss()
|
c.cacheStats.incMiss()
|
||||||
// Since we got here, we are serving the request from backend,
|
// Since we got here, we are serving the request from backend,
|
||||||
// and also adding the object to the cache.
|
// and also adding the object to the cache.
|
||||||
if !dcache.diskUsageLow() {
|
if dcache.diskUsageHigh() {
|
||||||
select {
|
dcache.incGCCounter()
|
||||||
case dcache.purgeChan <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bkReader, bkErr := c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
bkReader, bkErr := c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
||||||
@@ -314,7 +321,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
|||||||
}()
|
}()
|
||||||
cleanupBackend := func() { bkReader.Close() }
|
cleanupBackend := func() { bkReader.Close() }
|
||||||
cleanupPipe := func() { pipeWriter.Close() }
|
cleanupPipe := func() { pipeWriter.Close() }
|
||||||
return NewGetObjectReaderFromReader(teeReader, bkReader.ObjInfo, opts.CheckCopyPrecondFn, cleanupBackend, cleanupPipe)
|
return NewGetObjectReaderFromReader(teeReader, bkReader.ObjInfo, opts, cleanupBackend, cleanupPipe)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns ObjectInfo from cache if available.
|
// Returns ObjectInfo from cache if available.
|
||||||
@@ -330,12 +337,12 @@ func (c *cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return getObjectInfoFn(ctx, bucket, object, opts)
|
return getObjectInfoFn(ctx, bucket, object, opts)
|
||||||
}
|
}
|
||||||
var cc cacheControl
|
var cc *cacheControl
|
||||||
// if cache control setting is valid, avoid HEAD operation to backend
|
// if cache control setting is valid, avoid HEAD operation to backend
|
||||||
cachedObjInfo, _, cerr := dcache.Stat(ctx, bucket, object)
|
cachedObjInfo, _, cerr := dcache.Stat(ctx, bucket, object)
|
||||||
if cerr == nil {
|
if cerr == nil {
|
||||||
cc = cacheControlOpts(cachedObjInfo)
|
cc = cacheControlOpts(cachedObjInfo)
|
||||||
if !cc.isStale(cachedObjInfo.ModTime) {
|
if cc == nil || (cc != nil && !cc.isStale(cachedObjInfo.ModTime)) {
|
||||||
// This is a cache hit, mark it so
|
// This is a cache hit, mark it so
|
||||||
c.cacheStats.incHit()
|
c.cacheStats.incHit()
|
||||||
return cachedObjInfo, nil
|
return cachedObjInfo, nil
|
||||||
@@ -392,7 +399,7 @@ func (c *cacheObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dst
|
|||||||
// if currently cached, evict old entry and revert to backend.
|
// if currently cached, evict old entry and revert to backend.
|
||||||
if cachedObjInfo, _, cerr := dcache.Stat(ctx, srcBucket, srcObject); cerr == nil {
|
if cachedObjInfo, _, cerr := dcache.Stat(ctx, srcBucket, srcObject); cerr == nil {
|
||||||
cc := cacheControlOpts(cachedObjInfo)
|
cc := cacheControlOpts(cachedObjInfo)
|
||||||
if !cc.isStale(cachedObjInfo.ModTime) {
|
if cc == nil || !cc.isStale(cachedObjInfo.ModTime) {
|
||||||
dcache.Delete(ctx, srcBucket, srcObject)
|
dcache.Delete(ctx, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -503,7 +510,7 @@ func (c *cacheObjects) hashIndex(bucket, object string) int {
|
|||||||
// or the global env overrides.
|
// or the global env overrides.
|
||||||
func newCache(config cache.Config) ([]*diskCache, bool, error) {
|
func newCache(config cache.Config) ([]*diskCache, bool, error) {
|
||||||
var caches []*diskCache
|
var caches []*diskCache
|
||||||
ctx := logger.SetReqInfo(context.Background(), &logger.ReqInfo{})
|
ctx := logger.SetReqInfo(GlobalContext, &logger.ReqInfo{})
|
||||||
formats, migrating, err := loadAndValidateCacheFormat(ctx, config.Drives)
|
formats, migrating, err := loadAndValidateCacheFormat(ctx, config.Drives)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
@@ -522,15 +529,10 @@ func newCache(config cache.Config) ([]*diskCache, bool, error) {
|
|||||||
if quota == 0 {
|
if quota == 0 {
|
||||||
quota = config.Quota
|
quota = config.Quota
|
||||||
}
|
}
|
||||||
|
cache, err := newDiskCache(dir, quota, config.After, config.WatermarkLow, config.WatermarkHigh)
|
||||||
cache, err := newDiskCache(dir, config.Expiry, quota, config.After)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
// Start the purging go-routine for entries that have expired if no migration in progress
|
|
||||||
if !migrating {
|
|
||||||
go cache.purge()
|
|
||||||
}
|
|
||||||
caches = append(caches, cache)
|
caches = append(caches, cache)
|
||||||
}
|
}
|
||||||
return caches, migrating, nil
|
return caches, migrating, nil
|
||||||
@@ -577,13 +579,12 @@ func (c *cacheObjects) migrateCacheFromV1toV2(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errCnt := 0
|
errCnt := 0
|
||||||
for index, err := range g.Wait() {
|
for _, err := range g.Wait() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCnt++
|
errCnt++
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go c.cache[index].purge()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if errCnt > 0 {
|
if errCnt > 0 {
|
||||||
@@ -622,7 +623,7 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
|
|||||||
// skip cache for objects with locks
|
// skip cache for objects with locks
|
||||||
objRetention := objectlock.GetObjectRetentionMeta(opts.UserDefined)
|
objRetention := objectlock.GetObjectRetentionMeta(opts.UserDefined)
|
||||||
legalHold := objectlock.GetObjectLegalHoldMeta(opts.UserDefined)
|
legalHold := objectlock.GetObjectLegalHoldMeta(opts.UserDefined)
|
||||||
if objRetention.Mode != objectlock.Invalid || legalHold.Status != "" {
|
if objRetention.Mode.Valid() || legalHold.Status.Valid() {
|
||||||
dcache.Delete(ctx, bucket, object)
|
dcache.Delete(ctx, bucket, object)
|
||||||
return putObjectFn(ctx, bucket, object, r, opts)
|
return putObjectFn(ctx, bucket, object, r, opts)
|
||||||
}
|
}
|
||||||
@@ -697,5 +698,35 @@ func newServerCacheObjects(ctx context.Context, config cache.Config) (CacheObjec
|
|||||||
if migrateSw {
|
if migrateSw {
|
||||||
go c.migrateCacheFromV1toV2(ctx)
|
go c.migrateCacheFromV1toV2(ctx)
|
||||||
}
|
}
|
||||||
|
go c.gc(ctx)
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *cacheObjects) gc(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(cacheGCInterval)
|
||||||
|
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if c.migrating {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, dcache := range c.cache {
|
||||||
|
if dcache.gcCount() == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
go func(d *diskCache) {
|
||||||
|
defer wg.Done()
|
||||||
|
d.resetGCCounter()
|
||||||
|
d.purge(ctx)
|
||||||
|
}(dcache)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+8
-88
@@ -18,24 +18,22 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize cache objects.
|
// Initialize cache objects.
|
||||||
func initCacheObjects(disk string, cacheMaxUse, cacheAfter int) (*diskCache, error) {
|
func initCacheObjects(disk string, cacheMaxUse, cacheAfter, cacheWatermarkLow, cacheWatermarkHigh int) (*diskCache, error) {
|
||||||
return newDiskCache(disk, 80, cacheMaxUse, cacheAfter)
|
return newDiskCache(disk, cacheMaxUse, cacheAfter, cacheWatermarkLow, cacheWatermarkHigh)
|
||||||
}
|
}
|
||||||
|
|
||||||
// inits diskCache struct for nDisks
|
// inits diskCache struct for nDisks
|
||||||
func initDiskCaches(drives []string, cacheMaxUse, cacheAfter int, t *testing.T) ([]*diskCache, error) {
|
func initDiskCaches(drives []string, cacheMaxUse, cacheAfter, cacheWatermarkLow, cacheWatermarkHigh int, t *testing.T) ([]*diskCache, error) {
|
||||||
var cb []*diskCache
|
var cb []*diskCache
|
||||||
for _, d := range drives {
|
for _, d := range drives {
|
||||||
obj, err := initCacheObjects(d, cacheMaxUse, cacheAfter)
|
obj, err := initCacheObjects(d, cacheMaxUse, cacheAfter, cacheWatermarkLow, cacheWatermarkHigh)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -70,7 +68,7 @@ func TestGetCachedLoc(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
d, err := initDiskCaches(fsDirs, 100, 1, t)
|
d, err := initDiskCaches(fsDirs, 100, 1, 80, 90, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -109,7 +107,7 @@ func TestGetCacheMaxUse(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
d, err := initDiskCaches(fsDirs, 80, 1, t)
|
d, err := initDiskCaches(fsDirs, 80, 1, 80, 90, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -174,96 +172,18 @@ func TestCacheExclusion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test diskCache.
|
|
||||||
func TestDiskCache(t *testing.T) {
|
|
||||||
fsDirs, err := getRandomDisks(1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
d, err := initDiskCaches(fsDirs, 100, 0, t)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
c := cacheObjects{cache: d}
|
|
||||||
|
|
||||||
cache := c.cache[0]
|
|
||||||
ctx := context.Background()
|
|
||||||
bucketName := "testbucket"
|
|
||||||
objectName := "testobject"
|
|
||||||
content := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
||||||
etag := "061208c10af71a30c6dcd6cf5d89f0fe"
|
|
||||||
contentType := "application/zip"
|
|
||||||
size := len(content)
|
|
||||||
|
|
||||||
httpMeta := make(map[string]string)
|
|
||||||
httpMeta["etag"] = etag
|
|
||||||
httpMeta["content-type"] = contentType
|
|
||||||
|
|
||||||
objInfo := ObjectInfo{}
|
|
||||||
objInfo.Bucket = bucketName
|
|
||||||
objInfo.Name = objectName
|
|
||||||
objInfo.Size = int64(size)
|
|
||||||
objInfo.ContentType = contentType
|
|
||||||
objInfo.ETag = etag
|
|
||||||
objInfo.UserDefined = httpMeta
|
|
||||||
var opts ObjectOptions
|
|
||||||
byteReader := bytes.NewReader([]byte(content))
|
|
||||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "", int64(size), globalCLIContext.StrictS3Compat)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
err = cache.Put(ctx, bucketName, objectName, hashReader, hashReader.Size(), nil, ObjectOptions{UserDefined: httpMeta}, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
cReader, _, err := cache.Get(ctx, bucketName, objectName, nil, http.Header{
|
|
||||||
"Content-Type": []string{"application/json"},
|
|
||||||
}, opts)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
cachedObjInfo := cReader.ObjInfo
|
|
||||||
if !cache.Exists(ctx, bucketName, objectName) {
|
|
||||||
t.Fatal("Expected object to exist on cache")
|
|
||||||
}
|
|
||||||
if cachedObjInfo.ETag != objInfo.ETag {
|
|
||||||
t.Fatal("Expected ETag to match")
|
|
||||||
}
|
|
||||||
if cachedObjInfo.Size != objInfo.Size {
|
|
||||||
t.Fatal("Size mismatch")
|
|
||||||
}
|
|
||||||
if cachedObjInfo.ContentType != objInfo.ContentType {
|
|
||||||
t.Fatal("Cached content-type does not match")
|
|
||||||
}
|
|
||||||
writer := bytes.NewBuffer(nil)
|
|
||||||
_, err = io.Copy(writer, cReader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if ccontent := writer.Bytes(); !bytes.Equal([]byte(content), ccontent) {
|
|
||||||
t.Errorf("wrong cached file content")
|
|
||||||
}
|
|
||||||
cReader.Close()
|
|
||||||
|
|
||||||
cache.Delete(ctx, bucketName, objectName)
|
|
||||||
online := cache.IsOnline()
|
|
||||||
if !online {
|
|
||||||
t.Errorf("expected cache drive to be online")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test diskCache with upper bound on max cache use.
|
// Test diskCache with upper bound on max cache use.
|
||||||
func TestDiskCacheMaxUse(t *testing.T) {
|
func TestDiskCacheMaxUse(t *testing.T) {
|
||||||
fsDirs, err := getRandomDisks(1)
|
fsDirs, err := getRandomDisks(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
d, err := initDiskCaches(fsDirs, 80, 0, t)
|
d, err := initDiskCaches(fsDirs, 90, 0, 90, 99, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
cache := d[0]
|
cache := d[0]
|
||||||
ctx := context.Background()
|
ctx := GlobalContext
|
||||||
bucketName := "testbucket"
|
bucketName := "testbucket"
|
||||||
objectName := "testobject"
|
objectName := "testobject"
|
||||||
content := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
content := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
|||||||
+84
-22
@@ -29,6 +29,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v6/pkg/encrypt"
|
"github.com/minio/minio-go/v6/pkg/encrypt"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
@@ -168,39 +169,39 @@ func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string, sseS3 bool) ([]byte, error) {
|
func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string, sseS3 bool) (crypto.ObjectKey, error) {
|
||||||
var sealedKey crypto.SealedKey
|
var sealedKey crypto.SealedKey
|
||||||
if sseS3 {
|
if sseS3 {
|
||||||
if GlobalKMS == nil {
|
if GlobalKMS == nil {
|
||||||
return nil, errKMSNotConfigured
|
return crypto.ObjectKey{}, errKMSNotConfigured
|
||||||
}
|
}
|
||||||
key, encKey, err := GlobalKMS.GenerateKey(GlobalKMS.KeyID(), crypto.Context{bucket: path.Join(bucket, object)})
|
key, encKey, err := GlobalKMS.GenerateKey(GlobalKMS.KeyID(), crypto.Context{bucket: path.Join(bucket, object)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return crypto.ObjectKey{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
objectKey := crypto.GenerateKey(key, rand.Reader)
|
objectKey := crypto.GenerateKey(key, rand.Reader)
|
||||||
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
|
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
|
||||||
crypto.S3.CreateMetadata(metadata, GlobalKMS.KeyID(), encKey, sealedKey)
|
crypto.S3.CreateMetadata(metadata, GlobalKMS.KeyID(), encKey, sealedKey)
|
||||||
return objectKey[:], nil
|
return objectKey, nil
|
||||||
}
|
}
|
||||||
var extKey [32]byte
|
var extKey [32]byte
|
||||||
copy(extKey[:], key)
|
copy(extKey[:], key)
|
||||||
objectKey := crypto.GenerateKey(extKey, rand.Reader)
|
objectKey := crypto.GenerateKey(extKey, rand.Reader)
|
||||||
sealedKey = objectKey.Seal(extKey, crypto.GenerateIV(rand.Reader), crypto.SSEC.String(), bucket, object)
|
sealedKey = objectKey.Seal(extKey, crypto.GenerateIV(rand.Reader), crypto.SSEC.String(), bucket, object)
|
||||||
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
||||||
return objectKey[:], nil
|
return objectKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEncryptReader(content io.Reader, key []byte, bucket, object string, metadata map[string]string, sseS3 bool) (r io.Reader, encKey []byte, err error) {
|
func newEncryptReader(content io.Reader, key []byte, bucket, object string, metadata map[string]string, sseS3 bool) (io.Reader, crypto.ObjectKey, error) {
|
||||||
objectEncryptionKey, err := newEncryptMetadata(key, bucket, object, metadata, sseS3)
|
objectEncryptionKey, err := newEncryptMetadata(key, bucket, object, metadata, sseS3)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, encKey, err
|
return nil, crypto.ObjectKey{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey[:], MinVersion: sio.Version20})
|
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey[:], MinVersion: sio.Version20})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, encKey, crypto.ErrInvalidCustomerKey
|
return nil, crypto.ObjectKey{}, crypto.ErrInvalidCustomerKey
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader, objectEncryptionKey, nil
|
return reader, objectEncryptionKey, nil
|
||||||
@@ -225,23 +226,24 @@ func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[
|
|||||||
// EncryptRequest takes the client provided content and encrypts the data
|
// EncryptRequest takes the client provided content and encrypts the data
|
||||||
// with the client provided key. It also marks the object as client-side-encrypted
|
// with the client provided key. It also marks the object as client-side-encrypted
|
||||||
// and sets the correct headers.
|
// and sets the correct headers.
|
||||||
func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, metadata map[string]string) (reader io.Reader, objEncKey []byte, err error) {
|
func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, metadata map[string]string) (io.Reader, crypto.ObjectKey, error) {
|
||||||
var key []byte
|
|
||||||
|
|
||||||
if crypto.S3.IsRequested(r.Header) && crypto.SSEC.IsRequested(r.Header) {
|
if crypto.S3.IsRequested(r.Header) && crypto.SSEC.IsRequested(r.Header) {
|
||||||
return nil, objEncKey, crypto.ErrIncompatibleEncryptionMethod
|
return nil, crypto.ObjectKey{}, crypto.ErrIncompatibleEncryptionMethod
|
||||||
}
|
|
||||||
if crypto.SSEC.IsRequested(r.Header) {
|
|
||||||
key, err = ParseSSECustomerRequest(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, objEncKey, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if r.ContentLength > encryptBufferThreshold {
|
if r.ContentLength > encryptBufferThreshold {
|
||||||
// The encryption reads in blocks of 64KB.
|
// The encryption reads in blocks of 64KB.
|
||||||
// We add a buffer on bigger files to reduce the number of syscalls upstream.
|
// We add a buffer on bigger files to reduce the number of syscalls upstream.
|
||||||
content = bufio.NewReaderSize(content, encryptBufferSize)
|
content = bufio.NewReaderSize(content, encryptBufferSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var key []byte
|
||||||
|
if crypto.SSEC.IsRequested(r.Header) {
|
||||||
|
var err error
|
||||||
|
key, err = ParseSSECustomerRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, crypto.ObjectKey{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return newEncryptReader(content, key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
|
return newEncryptReader(content, key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,6 +636,47 @@ func (o *ObjectInfo) DecryptedSize() (int64, error) {
|
|||||||
return size, nil
|
return size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DecryptETag decrypts the ETag that is part of given object
|
||||||
|
// with the given object encryption key.
|
||||||
|
//
|
||||||
|
// However, DecryptETag does not try to decrypt the ETag if
|
||||||
|
// it consists of a 128 bit hex value (32 hex chars) and exactly
|
||||||
|
// one '-' followed by a 32-bit number.
|
||||||
|
// This special case adresses randomly-generated ETags generated
|
||||||
|
// by the MinIO server when running in non-compat mode. These
|
||||||
|
// random ETags are not encrypt.
|
||||||
|
//
|
||||||
|
// Calling DecryptETag with a non-randomly generated ETag will
|
||||||
|
// fail.
|
||||||
|
func DecryptETag(key crypto.ObjectKey, object ObjectInfo) (string, error) {
|
||||||
|
if n := strings.Count(object.ETag, "-"); n > 0 {
|
||||||
|
if n != 1 {
|
||||||
|
return "", errObjectTampered
|
||||||
|
}
|
||||||
|
i := strings.IndexByte(object.ETag, '-')
|
||||||
|
if len(object.ETag[:i]) != 32 {
|
||||||
|
return "", errObjectTampered
|
||||||
|
}
|
||||||
|
if _, err := hex.DecodeString(object.ETag[:32]); err != nil {
|
||||||
|
return "", errObjectTampered
|
||||||
|
}
|
||||||
|
if _, err := strconv.ParseInt(object.ETag[i+1:], 10, 32); err != nil {
|
||||||
|
return "", errObjectTampered
|
||||||
|
}
|
||||||
|
return object.ETag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
etag, err := hex.DecodeString(object.ETag)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
etag, err = key.UnsealETag(etag)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(etag), nil
|
||||||
|
}
|
||||||
|
|
||||||
// For encrypted objects, the ETag sent by client if available
|
// For encrypted objects, the ETag sent by client if available
|
||||||
// is stored in encrypted form in the backend. Decrypt the ETag
|
// is stored in encrypted form in the backend. Decrypt the ETag
|
||||||
// if ETag was previously encrypted.
|
// if ETag was previously encrypted.
|
||||||
@@ -817,7 +860,7 @@ func (o *ObjectInfo) EncryptedSize() int64 {
|
|||||||
// This cannot happen since AWS S3 allows parts to be 5GB at most
|
// This cannot happen since AWS S3 allows parts to be 5GB at most
|
||||||
// sio max. size is 256 TB
|
// sio max. size is 256 TB
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("size", strconv.FormatUint(size, 10))
|
reqInfo := (&logger.ReqInfo{}).AppendTags("size", strconv.FormatUint(size, 10))
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||||
logger.CriticalIf(ctx, err)
|
logger.CriticalIf(ctx, err)
|
||||||
}
|
}
|
||||||
return int64(size)
|
return int64(size)
|
||||||
@@ -845,7 +888,7 @@ func DecryptCopyObjectInfo(info *ObjectInfo, headers http.Header) (errCode APIEr
|
|||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
if info.Size, err = info.DecryptedSize(); err != nil {
|
if info.Size, err = info.DecryptedSize(); err != nil {
|
||||||
errCode = toAPIErrorCode(context.Background(), err)
|
errCode = toAPIErrorCode(GlobalContext, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -939,6 +982,19 @@ func getOpts(ctx context.Context, r *http.Request, bucket, object string) (Objec
|
|||||||
encryption encrypt.ServerSide
|
encryption encrypt.ServerSide
|
||||||
opts ObjectOptions
|
opts ObjectOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var partNumber int
|
||||||
|
var err error
|
||||||
|
if pn := r.URL.Query().Get("partNumber"); pn != "" {
|
||||||
|
partNumber, err = strconv.Atoi(pn)
|
||||||
|
if err != nil {
|
||||||
|
return opts, err
|
||||||
|
}
|
||||||
|
if partNumber < 0 {
|
||||||
|
return opts, errInvalidArgument
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if GlobalGatewaySSE.SSEC() && crypto.SSEC.IsRequested(r.Header) {
|
if GlobalGatewaySSE.SSEC() && crypto.SSEC.IsRequested(r.Header) {
|
||||||
key, err := crypto.SSEC.ParseHTTP(r.Header)
|
key, err := crypto.SSEC.ParseHTTP(r.Header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -947,10 +1003,16 @@ func getOpts(ctx context.Context, r *http.Request, bucket, object string) (Objec
|
|||||||
derivedKey := deriveClientKey(key, bucket, object)
|
derivedKey := deriveClientKey(key, bucket, object)
|
||||||
encryption, err = encrypt.NewSSEC(derivedKey[:])
|
encryption, err = encrypt.NewSSEC(derivedKey[:])
|
||||||
logger.CriticalIf(ctx, err)
|
logger.CriticalIf(ctx, err)
|
||||||
return ObjectOptions{ServerSideEncryption: encryption}, nil
|
return ObjectOptions{ServerSideEncryption: encryption, PartNumber: partNumber}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// default case of passing encryption headers to backend
|
// default case of passing encryption headers to backend
|
||||||
return getDefaultOpts(r.Header, false, nil)
|
opts, err = getDefaultOpts(r.Header, false, nil)
|
||||||
|
if err != nil {
|
||||||
|
return opts, err
|
||||||
|
}
|
||||||
|
opts.PartNumber = partNumber
|
||||||
|
return opts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// get ObjectOptions for PUT calls from encryption headers and metadata
|
// get ObjectOptions for PUT calls from encryption headers and metadata
|
||||||
|
|||||||
@@ -256,6 +256,78 @@ func TestDecryptObjectInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var decryptETagTests = []struct {
|
||||||
|
ObjectKey crypto.ObjectKey
|
||||||
|
ObjectInfo ObjectInfo
|
||||||
|
ShouldFail bool
|
||||||
|
ETag string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ObjectKey: [32]byte{},
|
||||||
|
ObjectInfo: ObjectInfo{ETag: "20000f00f27834c9a2654927546df57f9e998187496394d4ee80f3d9978f85f3c7d81f72600cdbe03d80dc5a13d69354"},
|
||||||
|
ETag: "8ad3fe6b84bf38489e95c701c84355b6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectKey: [32]byte{},
|
||||||
|
ObjectInfo: ObjectInfo{ETag: "20000f00f27834c9a2654927546df57f9e998187496394d4ee80f3d9978f85f3c7d81f72600cdbe03d80dc5a13d6935"},
|
||||||
|
ETag: "",
|
||||||
|
ShouldFail: true, // ETag is not a valid hex value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectKey: [32]byte{},
|
||||||
|
ObjectInfo: ObjectInfo{ETag: "00000f00f27834c9a2654927546df57f9e998187496394d4ee80f3d9978f85f3c7d81f72600cdbe03d80dc5a13d69354"},
|
||||||
|
ETag: "",
|
||||||
|
ShouldFail: true, // modified ETag
|
||||||
|
},
|
||||||
|
|
||||||
|
// Special tests for ETags that end with a '-x'
|
||||||
|
{
|
||||||
|
ObjectKey: [32]byte{},
|
||||||
|
ObjectInfo: ObjectInfo{ETag: "916516b396f0f4d4f2a0e7177557bec4-1"},
|
||||||
|
ETag: "916516b396f0f4d4f2a0e7177557bec4-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectKey: [32]byte{},
|
||||||
|
ObjectInfo: ObjectInfo{ETag: "916516b396f0f4d4f2a0e7177557bec4-738"},
|
||||||
|
ETag: "916516b396f0f4d4f2a0e7177557bec4-738",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectKey: [32]byte{},
|
||||||
|
ObjectInfo: ObjectInfo{ETag: "916516b396f0f4d4f2a0e7177557bec4-Q"},
|
||||||
|
ETag: "",
|
||||||
|
ShouldFail: true, // Q is not a number
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectKey: [32]byte{},
|
||||||
|
ObjectInfo: ObjectInfo{ETag: "16516b396f0f4d4f2a0e7177557bec4-1"},
|
||||||
|
ETag: "",
|
||||||
|
ShouldFail: true, // ETag prefix is not a valid hex value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectKey: [32]byte{},
|
||||||
|
ObjectInfo: ObjectInfo{ETag: "16516b396f0f4d4f2a0e7177557bec4-1-2"},
|
||||||
|
ETag: "",
|
||||||
|
ShouldFail: true, // ETag contains multiple: -
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecryptETag(t *testing.T) {
|
||||||
|
for i, test := range decryptETagTests {
|
||||||
|
etag, err := DecryptETag(test.ObjectKey, test.ObjectInfo)
|
||||||
|
if err != nil && !test.ShouldFail {
|
||||||
|
t.Fatalf("Test %d: should succeed but failed: %v", i, err)
|
||||||
|
}
|
||||||
|
if err == nil && test.ShouldFail {
|
||||||
|
t.Fatalf("Test %d: should fail but succeeded", i)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
if etag != test.ETag {
|
||||||
|
t.Fatalf("Test %d: ETag mismatch: got %s - want %s", i, etag, test.ETag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tests for issue reproduced when getting the right encrypted
|
// Tests for issue reproduced when getting the right encrypted
|
||||||
// offset of the object.
|
// offset of the object.
|
||||||
func TestGetDecryptedRange_Issue50(t *testing.T) {
|
func TestGetDecryptedRange_Issue50(t *testing.T) {
|
||||||
@@ -280,15 +352,11 @@ func TestGetDecryptedRange_Issue50(t *testing.T) {
|
|||||||
Parts: []ObjectPartInfo{
|
Parts: []ObjectPartInfo{
|
||||||
{
|
{
|
||||||
Number: 1,
|
Number: 1,
|
||||||
Name: "part.1",
|
|
||||||
ETag: "etag1",
|
|
||||||
Size: 297580380,
|
Size: 297580380,
|
||||||
ActualSize: 297435132,
|
ActualSize: 297435132,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Number: 2,
|
Number: 2,
|
||||||
Name: "part.2",
|
|
||||||
ETag: "etag2",
|
|
||||||
Size: 297580380,
|
Size: 297580380,
|
||||||
ActualSize: 297435132,
|
ActualSize: 297435132,
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user