Compare commits

..

62 Commits

Author SHA1 Message Date
Anis Elleuch c5ac901e8d xl: Fix healing empty directories (#8013)
After some extensive refactors, it turned out empty directories
are not healed and heal status is also not reported correctly.

This commit fixes it and adds the appropriate unit tests
2019-08-01 14:13:06 -07:00
Aditya Manthramurthy 4101d4917c Fix IAM users migration regression (#8008) 2019-08-01 12:31:04 -07:00
maihde d966d29fed fix: add integer code for Windows Subsystem for Linux filesystem (#8010) 2019-08-01 06:00:57 -07:00
Minio Trusted c301f5882d Update yaml files to latest version RELEASE.2019-07-31T18-57-56Z 2019-07-31 19:06:20 +00:00
Harshavardhana 123cccaed1 Honor connection pooling while tracing (#7979)
This PR fixes relying on r.Context().Done()
by setting

```
Connection: "close"
```

HTTP Header, this has detrimental issues for
client side connection pooling. Since this
header explicitly tells clients to turn-off
connection pooling. This causing pro-active
connections to be closed leaving many conn's
in TIME_WAIT state. This can be observed with
`mc admin trace -a` when running distributed
setup.

This PR also fixes tracing filtering issue
when bucket names have `minio` as prefixes,
trace was erroneously ignoring them.
2019-07-31 11:08:39 -07:00
Anis Elleuch cbd02c58be federation: Avoid printing context canceled error (#7997)
Golang proactively prints this error
        `http: proxy error: context canceled`

when a request arrived to the current deployment and
redirected to another deployment in a federated setup.

Since this error can confuse users, this commit will
just hide it.
2019-07-31 11:08:10 -07:00
Aditya Manthramurthy c71895f225 Listen for PolicyDB events from etcd and fix etcd watch handling (#7992) 2019-07-30 18:50:49 -07:00
Harshavardhana b83413b167 Use GOPROXY to speed up builds (#7984)
Read more here https://proxy.golang.org proposal 
for go1.13
2019-07-30 22:27:11 +05:30
Praveen raj Mani 63e0a81760 Ignore stale notification queues in notification.xml (#7673)
Allow renaming/editing a notification config. By replying with 
a successful GetBucketNotification response, without checking 
for any missing config ARN in targetList.

Fixes #7650
2019-07-30 14:19:06 +05:30
Harshavardhana 8d47ef503c Fix crash observed in OPA initialization (#7990)
Related to #7982, this PR refactors the code
such that we validate the OPA or JWKS in a
common place.

This is also a refactor which is already done
in the new config migration change. Attempt
to avoid any network I/O during Unmarshal of
JSON from disk, instead do it later when
updating the in-memory data structure.
2019-07-29 15:58:25 -07:00
Harshavardhana 54eded2e6f Do not assume all HTTP errors as Network errors (#7983)
In situations such as when client uploading data,
prematurely disconnects from server such as pressing
ctrl-c before uploading all the data. Under this
situation in distributed setup we prematurely
disconnect disks causing a reconnect loop. This has
an adverse affect we end up leaving a lot of files
in temporary location which ideally should have been
cleaned up when Put() prematurely fails.

This is also a regression which got introduced in #7610
2019-07-29 14:48:18 -07:00
Harshavardhana 94c88890b8 Add additional logging for OPA connections (#7982) 2019-07-28 08:33:25 +05:30
Harshavardhana e871e27562 Refactor and simplify etcd helpers used in IAM subsystem (#7980) 2019-07-26 13:42:54 -07:00
Harshavardhana 007a52b546 Add common validation for compression and encryption (#7978) 2019-07-26 02:41:16 -07:00
Praveen raj Mani efb8b00db0 Preserve tailing backslash in URL paths (#7678)
Fixes #7649
2019-07-25 20:55:09 -07:00
Harshavardhana d744865dc6 Enable config for NAS gateway mode (#7948)
Starting with #7751 we don't store config
in etcd anymore, allow NAS to honor config
on disk.
2019-07-25 17:41:25 -07:00
Harshavardhana e40c29e834 Fail appropriately if the disk has I/O errors (#7972)
If the disk has I/O errors, we should simply ignore
such a disk and not be bothered about it - until
it is replaced.
2019-07-25 13:35:27 -07:00
Praveen raj Mani b0cea1c0f3 Enable event persistence in AMQP (#7565) 2019-07-25 11:20:24 -07:00
Harshavardhana 6f2b4675fa Add krb5 support for HDFS gateway (#7933) 2019-07-24 18:05:48 -07:00
Harshavardhana a4ce1daf99 docs: Use --user to start container in non-root (#7966) 2019-07-24 17:35:52 -07:00
Aditya Manthramurthy 7bdaf9bc50 Update on-disk storage format for users system (#7949)
- Policy mapping is now at `config/iam/policydb/users/myuser1.json`
  and includes version.

- User identity file is now versioned.

- Migrate old data to the new format.
2019-07-24 17:34:23 -07:00
Praveen raj Mani 55d4eee6f1 Enable event persistence in MySQL and PostgreSQL (#7629) 2019-07-24 10:18:29 -07:00
Harshavardhana ac82798d0a Remove uneeded calls on FS (#7967) 2019-07-24 15:59:13 +05:30
Minio Trusted 5b71c21330 Update yaml files to latest version RELEASE.2019-07-24T02-02-23Z 2019-07-24 02:09:19 +00:00
kannappanr 3e3fbdf8e6 Remove file added inadvertently (#7968) 2019-07-23 18:51:54 -07:00
Praveen raj Mani c9349747ca Enable event-persistence in NATS and NATS-Streaming (#7612) 2019-07-23 10:37:25 -07:00
Praveen raj Mani 2b9b907f9c Enable event persistence in Redis (#7601) 2019-07-23 10:22:08 -07:00
Daryl Finlay 9389a55e5d Cancel PutObjectPart on upload abort (#7940)
Calling ListMultipartUploads fails if an upload is aborted while a
part is being uploaded because the directory for the upload exists
(since fsRenameFile ends up calling os.MkdirAll) but the meta JSON file
doesn't. To fix this we make sure an upload hasn't been aborted during
PutObjectPart by checking the existence of the directory for the upload
while moving the temporary part file into it.
2019-07-22 22:36:15 -07:00
Harshavardhana 87e6533cf3 Add some design docs for distributed setup (#7950) 2019-07-23 07:48:10 +05:30
Christian Muehlhaeuser 38bc3a45db Fixed tautological conditions (#7959)
We already check for err being equal to nil above, no need
to check again.
2019-07-22 17:06:08 -07:00
Christian Muehlhaeuser c5faba55c1 Comment: Typo Fix (#7958) 2019-07-21 05:55:09 +01:00
Harshavardhana 8b5e6e338c Fix: Only add SRV records that match the bucket name exactly (#7957)
Problem: MinIO incorrectly appends DNS SRV records of buckets that have a prefix match with a given bucket. E.g bucket1 would incorrectly get bucket's DNS records too.
Solution: This fix ensures that we only add SRV records that match the key exactly
2019-07-20 11:29:05 +01:00
poornas 0373a1699b Add error filter to admin trace API (#7923)
This allows MinIO to have the ability to send back only error trace
2019-07-20 01:38:26 +01:00
Krishnan Parthasarathi 559a59220e Add initial support for bucket lifecycle (#7563)
This PR is based off @sinhaashish's PR for object lifecycle
management, which includes support only for,
- Expiration of object
- Filter using object prefix (_not_ object tags)

N B the code for actual expiration of objects will be included in a
subsequent PR.
2019-07-19 21:20:33 +01:00
Yao Zongyou 59e1763816 doc: use make instead of go command to test changes (#7951) 2019-07-19 14:40:39 +01:00
poornas 041a812ba0 trace api: add call stats to trace (#7915)
Stats such as call latency, bytes received and sent have been added
2019-07-18 23:29:17 +01:00
Krishnan Parthasarathi fbfc9a61ec Add node address information to logs (#7941) 2019-07-18 09:58:37 -07:00
Philipp Dallig be9baa1464 Fix startup without MINIO_USERNAME and MINIO_GROUPNAME (#7944) 2019-07-18 21:49:49 +05:30
Minio Trusted b058e32348 Update yaml files to latest version RELEASE.2019-07-17T22-54-12Z 2019-07-17 22:59:33 +00:00
Lucas ea66a52ed1 Add KMS master key from Docker secret (#7825) 2019-07-17 20:55:26 +01:00
Harshavardhana 55dd017e62 Deprecate auto detection of container user (#7930)
There is no reliable way to handle fallbacks for
MinIO deployments, due to various command line
options and multiple locations which require
access inside container.

Parsing command line options is tricky to figure
out which is the backend disk etc, we did try
to fix this in implementations of check-user.go
but it wasn't complete and introduced more bugs.

This PR simplifies the entire approach to rather
than running Docker container as non-root by default
always, it allows users to opt-in. Such that they
are aware that that is what they are planning to do.

In-fact there are other ways docker containers can
be run as regular users, without modifying our
internal behavior and adding more complexities.
2019-07-17 19:20:55 +01:00
Kanagaraj M 12353caf35 Fix: Support Unicode delimiters in s3 select (#7931) 2019-07-17 19:10:17 +01:00
Harshavardhana a57c747667 Document vault in prod mode instead of dev mode (#7928) 2019-07-16 01:32:15 +01:00
Anis Elleuch 28661c0413 heal: Trigger auto-heal once each month instead of 24 hours (#7934) 2019-07-16 00:03:42 +01:00
Harshavardhana 04a152be12 Redirect to browser only if browser is enabled (#7914) 2019-07-15 20:01:17 +01:00
Harshavardhana bce3f8237d Allow users to give anonymous access (#7926)
Current code already allows users to GetPolicy/SetPolicy
there was a missing code in ListAllBucketPolicies to allow
access, this fixes this behavior.

Fixes #7913
2019-07-15 20:00:41 +01:00
Harshavardhana 16a45e5aff Fix dynamic help vars for sub-commands (#7925)
The fix in #7646 introduced a regression which
was left unnoticed, the fix didn't work for
sub-commands unfortunately. This fixes it
by moving v1.21.0 version of the minio/cli
package.

Fixes #7924
2019-07-12 23:32:27 -07:00
Anis Elleuch 000a60f238 xl: Heal empty parts (#7860)
posix.VerifyFile() doesn't know how to check if a file
is corrupted if that file is empty. We do have the part
size in xl.json so we pass it to VerifyFile to return
an error so healing empty parts can work properly.
2019-07-13 00:29:44 +01:00
Praveen raj Mani bf278ca36f Enable event persistence in NSQ (#7579) 2019-07-12 10:41:57 +01:00
Ashish Kumar Sinha 97f2bc26b9 Add validations for object name length and prefix (#7746)
fixes #7717
2019-07-12 10:08:12 +05:30
Praveen raj Mani bba562235b Enable persistent event store in elasticsearch (#7564) 2019-07-12 08:23:20 +05:30
dependabot[bot] 2337e5f803 Bump lodash from 4.17.4 to 4.17.14 in /browser (#7912)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.4 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.4...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-11 16:44:47 -07:00
Krishnan Parthasarathi ffd7b7059c Pass on web-handler arguments properly to log entries (#7894) 2019-07-11 22:37:13 +01:00
Harshavardhana 5c0acbc6fc Add text/event-stream for long running http connections (#7909)
When MinIO is behind a proxy, proxies end up killing
clients when no data is seen on the connection, adding
the right content-type ensures that proxies do not come
in the way.
2019-07-11 13:19:25 -07:00
Harshavardhana 5a52bc7ff6 Fix mint hub.docker.com builds (#7908) 2019-07-11 11:45:57 -07:00
poornas 045e1fed2b Fix dotnet tests build to be project structure agnostic (#7906) 2019-07-11 12:51:30 +05:30
dependabot[bot] a861d38532 Bump lodash-es from 4.17.4 to 4.17.14 in /browser (#7910)
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.4 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.4...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-11 10:55:13 +05:30
poornas 20a15567b8 Fix atime support check for disk cache (#7891)
- add a sleep between Stat operations to
accurately detect atime
2019-07-10 23:41:11 +01:00
Krishnan Parthasarathi 94f67ad224 Log error response even if a handler doesn't logBody (#7867) 2019-07-10 11:49:02 -07:00
ebozduman 36ee110563 Regression fix to bring back checkPolicyCond function call (#7897)
Fixes #7895
2019-07-10 10:48:43 +05:30
Harshavardhana 1dc25bcf5f Add mint tests into MinIO repo (#7886) 2019-07-09 18:32:39 -07:00
Minio Trusted 2d96745156 Update yaml files to latest version RELEASE.2019-07-10T00-34-56Z 2019-07-10 00:39:45 +00:00
216 changed files with 13297 additions and 1740 deletions
+1 -3
View File
@@ -9,7 +9,6 @@ site/
/.idea/
/Minio.iml
**/access.log
build/
vendor/**/*.js
vendor/**/*.json
release
@@ -23,5 +22,4 @@ prime/
stage/
.sia_temp/
config.json
healthcheck
check-user
dockerscripts/healthcheck
+14 -2
View File
@@ -2,6 +2,14 @@ go_import_path: github.com/minio/minio
language: go
addons:
apt:
packages:
- shellcheck
services:
- docker
# this ensures PRs based on a local branch are not built twice
# the downside is that a PR targeting a different branch is not built
# but as a workaround you can add the branch to this list
@@ -18,6 +26,7 @@ matrix:
- ARCH=x86_64
- CGO_ENABLED=0
- GO111MODULE=on
- GOPROXY=https://proxy.golang.org
# Enable build cache
# https://restic.net/blog/2018-09-02/travis-build-cache
cache:
@@ -26,7 +35,7 @@ matrix:
- $HOME/gopath/pkg/mod
- $HOME/go/pkg/mod
go: 1.12.1
go: 1.12.5
script:
- make
- diff -au <(gofmt -s -d cmd) <(printf "")
@@ -37,12 +46,15 @@ matrix:
- make verify
- make coverage
- cd browser && yarn && yarn test && cd ..
- bash -c 'shopt -s globstar; shellcheck mint/**/*.sh'
- os: windows
env:
- ARCH=x86_64
- CGO_ENABLED=0
- GO111MODULE=on
go: 1.12.1
- GOPROXY=https://proxy.golang.org
go: 1.12.5
script:
- go build --ldflags="$(go run buildscripts/gen-ldflags.go)" -o %GOPATH%\bin\minio.exe
- bash buildscripts/go-coverage.sh
+1 -1
View File
@@ -37,7 +37,7 @@ After your code changes, make sure
- To add test cases for the new code. If you have questions about how to do it, please ask on our [Slack](https://slack.min.io) channel.
- To run `make verifiers`
- To squash your commits into a single commit. `git rebase -i`. It's okay to force update your pull request.
- To run `go test -race ./...` and `go build` completes.
- To run `make test` and `make build` completes.
### Commit changes
After verification, commit your changes. This is a [great post](https://chris.beams.io/posts/git-commit/) on how to write useful commit messages
+3 -4
View File
@@ -10,20 +10,19 @@ RUN \
apk add --no-cache git && \
git clone https://github.com/minio/minio && cd minio && \
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
cd dockerscripts; go build -tags kqueue -ldflags "-s -w" -o /usr/bin/healthcheck healthcheck.go && \
go build -tags kqueue -ldflags "-s -w" -o /usr/bin/check-user check-user.go
cd dockerscripts; go build -tags kqueue -ldflags "-s -w" -o /usr/bin/healthcheck healthcheck.go
FROM alpine:3.9
ENV MINIO_UPDATE off
ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_SECRET_KEY_FILE=secret_key
MINIO_SECRET_KEY_FILE=secret_key \
MINIO_SSE_MASTER_KEY_FILE=sse_master_key
EXPOSE 9000
COPY --from=0 /go/bin/minio /usr/bin/minio
COPY --from=0 /usr/bin/healthcheck /usr/bin/healthcheck
COPY --from=0 /usr/bin/check-user /usr/bin/check-user
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
RUN \
+4 -4
View File
@@ -2,20 +2,20 @@ FROM alpine:3.9
LABEL maintainer="MinIO Inc <dev@min.io>"
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck dockerscripts/check-user /usr/bin/
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck /usr/bin/
COPY minio /usr/bin/
ENV MINIO_UPDATE off
ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_SECRET_KEY_FILE=secret_key
MINIO_SECRET_KEY_FILE=secret_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 && \
chmod +x /usr/bin/minio && \
chmod +x /usr/bin/docker-entrypoint.sh && \
chmod +x /usr/bin/healthcheck && \
chmod +x /usr/bin/check-user
chmod +x /usr/bin/healthcheck
EXPOSE 9000
+23
View File
@@ -0,0 +1,23 @@
FROM ubuntu:16.04
ENV DEBIAN_FRONTEND noninteractive
ENV LANG C.UTF-8
ENV GOROOT /usr/local/go
ENV GOPATH /usr/local
ENV PATH $GOPATH/bin:$GOROOT/bin:$PATH
ENV MINT_ROOT_DIR /mint
COPY mint /mint
RUN apt-get --yes update && apt-get --yes upgrade && \
apt-get --yes --quiet install wget jq curl git dnsmasq && \
cd /mint && /mint/release.sh
WORKDIR /mint
ENTRYPOINT ["/mint/entrypoint.sh"]
+4 -6
View File
@@ -7,20 +7,19 @@ ENV GO111MODULE on
RUN \
apk add --no-cache git && \
git clone https://github.com/minio/minio && cd minio/dockerscripts && \
go build -tags kqueue -ldflags "-s -w" -o /usr/bin/healthcheck healthcheck.go && \
go build -tags kqueue -ldflags "-s -w" -o /usr/bin/check-user check-user.go
go build -tags kqueue -ldflags "-s -w" -o /usr/bin/healthcheck healthcheck.go
FROM alpine:3.9
LABEL maintainer="MinIO Inc <dev@min.io>"
COPY --from=0 /usr/bin/healthcheck /usr/bin/healthcheck
COPY --from=0 /usr/bin/check-user /usr/bin/check-user
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
ENV MINIO_UPDATE off
ENV MINIO_ACCESS_KEY_FILE=access_key \
MINIO_SECRET_KEY_FILE=secret_key
MINIO_SECRET_KEY_FILE=secret_key \
MINIO_SSE_MASTER_KEY_FILE=sse_master_key
RUN \
apk add --no-cache ca-certificates 'curl>7.61.0' 'su-exec>=0.2' && \
@@ -28,8 +27,7 @@ RUN \
curl https://dl.min.io/server/minio/release/linux-amd64/minio > /usr/bin/minio && \
chmod +x /usr/bin/minio && \
chmod +x /usr/bin/docker-entrypoint.sh && \
chmod +x /usr/bin/healthcheck && \
chmod +x /usr/bin/check-user
chmod +x /usr/bin/healthcheck
EXPOSE 9000
+6 -3
View File
@@ -8,6 +8,7 @@ WORKDIR /go/src/github.com/minio/minio
RUN apt-get update && apt-get install -y jq
ENV GO111MODULE=on
ENV GOPROXY=https://proxy.golang.org
RUN git config --global http.cookiefile /gitcookie/.gitcookie
@@ -33,7 +34,7 @@ USER ci
RUN make
RUN bash -c 'diff -au <(gofmt -s -d cmd) <(printf "")'
RUN bash -c 'diff -au <(gofmt -s -d pkg) <(printf "")'
RUN for d in $(go list ./... | grep -v browser); do go test -v -race --timeout 15m "$d"; done
RUN for d in $(go list ./... | grep -v browser); do go test -v -race --timeout 20m "$d"; done
RUN make verifiers
RUN make crosscompile
RUN make verify
@@ -56,15 +57,17 @@ FROM ubuntu:16.04
COPY --from=0 /go/src/github.com/minio/minio/minio /usr/bin/minio
COPY buildscripts/gateway-tests.sh /usr/bin/gateway-tests.sh
COPY mint /mint
ENV DEBIAN_FRONTEND noninteractive
ENV LANG C.UTF-8
ENV GOROOT /usr/local/go
ENV GOPATH /usr/local
ENV PATH $GOPATH/bin:$GOROOT/bin:$PATH
ENV MINT_ROOT_DIR /mint
RUN apt-get --yes update && apt-get --yes upgrade && apt-get --yes --quiet install wget jq curl git dnsmasq && \
git clone https://github.com/minio/mint.git /mint && \
RUN apt-get --yes update && apt-get --yes upgrade && \
apt-get --yes --quiet install wget jq curl git dnsmasq && \
cd /mint && /mint/release.sh
WORKDIR /mint
+15 -16
View File
@@ -30,35 +30,35 @@ verifiers: getdeps vet fmt lint staticcheck spelling
vet:
@echo "Running $@"
@GO111MODULE=on go vet github.com/minio/minio/...
@GOPROXY=https://proxy.golang.org GO111MODULE=on go vet github.com/minio/minio/...
fmt:
@echo "Running $@"
@GO111MODULE=on gofmt -d cmd/
@GO111MODULE=on gofmt -d pkg/
@GOPROXY=https://proxy.golang.org GO111MODULE=on gofmt -d cmd/
@GOPROXY=https://proxy.golang.org GO111MODULE=on gofmt -d pkg/
lint:
@echo "Running $@"
@GO111MODULE=on ${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/cmd/...
@GO111MODULE=on ${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/pkg/...
@GOPROXY=https://proxy.golang.org GO111MODULE=on ${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/cmd/...
@GOPROXY=https://proxy.golang.org GO111MODULE=on ${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/pkg/...
staticcheck:
@echo "Running $@"
@GO111MODULE=on ${GOPATH}/bin/staticcheck github.com/minio/minio/cmd/...
@GO111MODULE=on ${GOPATH}/bin/staticcheck github.com/minio/minio/pkg/...
@GOPROXY=https://proxy.golang.org GO111MODULE=on ${GOPATH}/bin/staticcheck github.com/minio/minio/cmd/...
@GOPROXY=https://proxy.golang.org GO111MODULE=on ${GOPATH}/bin/staticcheck github.com/minio/minio/pkg/...
spelling:
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find cmd/`
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find pkg/`
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find docs/`
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find buildscripts/`
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find dockerscripts/`
@GOPROXY=https://proxy.golang.org GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find cmd/`
@GOPROXY=https://proxy.golang.org GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find pkg/`
@GOPROXY=https://proxy.golang.org GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find docs/`
@GOPROXY=https://proxy.golang.org GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find buildscripts/`
@GOPROXY=https://proxy.golang.org GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find dockerscripts/`
# Builds minio, runs the verifiers then runs the tests.
check: test
test: verifiers build
@echo "Running unit tests"
@GO111MODULE=on CGO_ENABLED=0 go test -tags kqueue ./... 1>/dev/null
@GOPROXY=https://proxy.golang.org GO111MODULE=on CGO_ENABLED=0 go test -tags kqueue ./... 1>/dev/null
verify: build
@echo "Verifying build"
@@ -71,9 +71,8 @@ coverage: build
# Builds minio locally.
build: checks
@echo "Building minio binary to './minio'"
@GO111MODULE=on GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
@GO111MODULE=on GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/dockerscripts/healthcheck $(PWD)/dockerscripts/healthcheck.go 1>/dev/null
@GO111MODULE=on GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/dockerscripts/check-user $(PWD)/dockerscripts/check-user.go 1>/dev/null
@GOPROXY=https://proxy.golang.org GO111MODULE=on GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
@GOPROXY=https://proxy.golang.org GO111MODULE=on GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/dockerscripts/healthcheck $(PWD)/dockerscripts/healthcheck.go 1>/dev/null
docker: build
@docker build -t $(TAG) . -f Dockerfile.dev
+1 -1
View File
@@ -86,7 +86,7 @@ service minio start
Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://golang.org/doc/install). Minimum version required is [go1.12](https://golang.org/dl/#stable)
```sh
GO111MODULE=on go get github.com/minio/minio
GOPROXY=https://proxy.golang.org GO111MODULE=on go get github.com/minio/minio
```
## Allow port access for Firewalls
+6 -26
View File
@@ -5817,12 +5817,6 @@
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
"dev": true
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
},
"micromatch": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
@@ -6408,12 +6402,6 @@
"requires": {
"lodash": "^4.17.11"
}
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
}
}
},
@@ -7645,14 +7633,14 @@
}
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
"version": "4.17.14",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
"integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw=="
},
"lodash-es": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz",
"integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q=="
"version": "4.17.14",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.14.tgz",
"integrity": "sha512-7zchRrGa8UZXjD/4ivUWP1867jDkhzTG2c/uj739utSd7O/pFFdxspCemIFKEEjErbcqRzn8nKnGsi7mvTgRPA=="
},
"lodash._baseisequal": {
"version": "3.0.7",
@@ -11078,14 +11066,6 @@
"dev": true,
"requires": {
"lodash": "^4.17.11"
},
"dependencies": {
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
}
}
},
"request-promise-native": {
+7 -10
View File
@@ -5075,8 +5075,9 @@ locate-path@^3.0.0:
path-exists "^3.0.0"
lodash-es@^4.2.0, lodash-es@^4.2.1:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"
version "4.17.14"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.14.tgz#12a95a963cc5955683cee3b74e85458954f37ecc"
integrity sha512-7zchRrGa8UZXjD/4ivUWP1867jDkhzTG2c/uj739utSd7O/pFFdxspCemIFKEEjErbcqRzn8nKnGsi7mvTgRPA==
lodash._baseisequal@^3.0.0:
version "3.0.7"
@@ -5185,14 +5186,10 @@ lodash.words@^3.0.0:
dependencies:
lodash._root "^3.0.0"
lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
lodash@^4.17.3, lodash@^4.17.5, lodash@^4.3.0:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
version "4.17.14"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
loglevel@^1.4.1:
version "1.6.1"
+1
View File
@@ -23,6 +23,7 @@ function _build() {
export GOOS=$os
export GOARCH=$arch
export GO111MODULE=on
export GOPROXY=https://proxy.golang.org
go build -tags kqueue -o /dev/null
}
+1 -1
View File
@@ -2,4 +2,4 @@
set -e
GO111MODULE=on CGO_ENABLED=0 go test -v -coverprofile=coverage.txt -covermode=atomic ./...
GOPROXY=https://proxy.golang.org GO111MODULE=on CGO_ENABLED=0 go test -v -coverprofile=coverage.txt -covermode=atomic ./...
+1
View File
@@ -33,6 +33,7 @@ export ACCESS_KEY="minio"
export SECRET_KEY="minio123"
export ENABLE_HTTPS=0
export GO111MODULE=on
export GOPROXY=https://proxy.golang.org
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" )
+38 -32
View File
@@ -241,17 +241,12 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
if objectAPI == nil {
return
}
hostName, err := getHostName(r)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
serverInfo := globalNotificationSys.ServerInfo(ctx)
// Once we have received all the ServerInfo from peers
// add the local peer server info as well.
serverInfo = append(serverInfo, ServerInfo{
Addr: hostName,
Addr: getHostName(r),
Data: &ServerInfoData{
StorageInfo: objectAPI.StorageInfo(ctx),
ConnStats: globalConnStats.toServerConnStats(),
@@ -442,18 +437,13 @@ func (a adminAPIHandlers) TopLocksHandler(w http.ResponseWriter, r *http.Request
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
return
}
hostName, err := getHostName(r)
if err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return
}
peerLocks := globalNotificationSys.GetLocks(ctx)
// Once we have received all the locks currently used from peers
// add the local peer locks list as well.
localLocks := globalLockServer.ll.DupLockMap()
peerLocks = append(peerLocks, &PeerLocks{
Addr: hostName,
Addr: getHostName(r),
Locks: localLocks,
})
@@ -681,12 +671,12 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
// Start writing response to client
started = true
setCommonHeaders(w)
w.Header().Set(xhttp.ContentType, string(mimeJSON))
w.Header().Set(xhttp.ContentType, "text/event-stream")
// Set 200 OK status
w.WriteHeader(200)
}
// Send whitespace and keep connection open
w.Write([]byte("\n\r"))
w.Write([]byte(" "))
w.(http.Flusher).Flush()
case hr := <-respCh:
switch hr.apiErr {
@@ -1263,7 +1253,7 @@ func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request)
return
}
if err := globalIAMSys.SetUserPolicy(accessKey, policyName); err != nil {
if err := globalIAMSys.PolicyDBSet(accessKey, policyName); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
}
@@ -1469,12 +1459,30 @@ func (a adminAPIHandlers) SetConfigKeysHandler(w http.ResponseWriter, r *http.Re
writeSuccessResponseHeadersOnly(w)
}
// Returns true if the trace.Info should be traced,
// false if certain conditions are not met.
// - input entry is not of the type *trace.Info*
// - errOnly entries are to be traced, not status code 2xx, 3xx.
// - all entries to be traced, if not trace only S3 API requests.
func mustTrace(entry interface{}, trcAll, errOnly bool) bool {
trcInfo, ok := entry.(trace.Info)
if !ok {
return false
}
trace := trcAll || !hasPrefix(trcInfo.ReqInfo.Path, minioReservedBucketPath+slashSeparator)
if errOnly {
return trace && trcInfo.RespInfo.StatusCode >= http.StatusBadRequest
}
return trace
}
// TraceHandler - POST /minio/admin/v1/trace
// ----------
// The handler sends http trace to the connected HTTP client.
func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "HTTPTrace")
trcAll := r.URL.Query().Get("all") == "true"
trcErr := r.URL.Query().Get("err") == "true"
// Validate request signature.
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
@@ -1483,10 +1491,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Avoid reusing tcp connection if read timeout is hit
// This is needed to make r.Context().Done() work as
// expected in case of read timeout
w.Header().Add(xhttp.Connection, "close")
w.Header().Set(xhttp.ContentType, "text/event-stream")
doneCh := make(chan struct{})
defer close(doneCh)
@@ -1495,24 +1500,22 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
// Use buffered channel to take care of burst sends or slow w.Write()
traceCh := make(chan interface{}, 4000)
filter := func(entry interface{}) bool {
if trcAll {
return true
}
trcInfo := entry.(trace.Info)
return !strings.HasPrefix(trcInfo.ReqInfo.Path, minioReservedBucketPath)
}
remoteHosts := getRemoteHosts(globalEndpoints)
peers, err := getRestClients(remoteHosts)
peers, err := getRestClients(getRemoteHosts(globalEndpoints))
if err != nil {
return
}
globalHTTPTrace.Subscribe(traceCh, doneCh, filter)
globalHTTPTrace.Subscribe(traceCh, doneCh, func(entry interface{}) bool {
return mustTrace(entry, trcAll, trcErr)
})
for _, peer := range peers {
peer.Trace(traceCh, doneCh, trcAll)
peer.Trace(traceCh, doneCh, trcAll, trcErr)
}
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
defer keepAliveTicker.Stop()
enc := json.NewEncoder(w)
for {
select {
@@ -1521,8 +1524,11 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
return
}
w.(http.Flusher).Flush()
case <-r.Context().Done():
return
case <-keepAliveTicker.C:
if _, err := w.Write([]byte(" ")); err != nil {
return
}
w.(http.Flusher).Flush()
case <-GlobalServiceDoneCh:
return
}
+20 -6
View File
@@ -83,7 +83,9 @@ var (
"durable": false,
"internal": false,
"noWait": false,
"autoDeleted": false
"autoDeleted": false,
"queueDir": "",
"queueLimit": 0
}
},
"elasticsearch": {
@@ -91,7 +93,9 @@ var (
"enable": false,
"format": "namespace",
"url": "",
"index": ""
"index": "",
"queueDir": "",
"queueLimit": 0
}
},
"kafka": {
@@ -137,7 +141,9 @@ var (
"port": "",
"user": "",
"password": "",
"database": ""
"database": "",
"queueDir": "",
"queueLimit": 0
}
},
"nats": {
@@ -150,6 +156,8 @@ var (
"token": "",
"secure": false,
"pingInterval": 0,
"queueDir": "",
"queueLimit": 0,
"streaming": {
"enable": false,
"clusterID": "",
@@ -166,7 +174,9 @@ var (
"tls": {
"enable": false,
"skipVerify": false
}
},
"queueDir": "",
"queueLimit": 0
}
},
"postgresql": {
@@ -179,7 +189,9 @@ var (
"port": "",
"user": "",
"password": "",
"database": ""
"database": "",
"queueDir": "",
"queueLimit": 0
}
},
"redis": {
@@ -188,7 +200,9 @@ var (
"format": "namespace",
"address": "",
"password": "",
"key": ""
"key": "",
"queueDir": "",
"queueLimit": 0
}
},
"webhook": {
+24
View File
@@ -92,6 +92,7 @@ const (
ErrMissingRequestBodyError
ErrNoSuchBucket
ErrNoSuchBucketPolicy
ErrNoSuchBucketLifecycle
ErrNoSuchKey
ErrNoSuchUpload
ErrNoSuchVersion
@@ -139,6 +140,7 @@ const (
ErrSlowDown
ErrInvalidPrefixMarker
ErrBadRequest
ErrKeyTooLongError
// Add new error codes here.
// SSE-S3 related API errors
@@ -187,6 +189,7 @@ const (
ErrRequestBodyParse
ErrObjectExistsAsDirectory
ErrInvalidObjectName
ErrInvalidObjectNamePrefixSlash
ErrInvalidResourceName
ErrServerNotInitialized
ErrOperationTimedOut
@@ -465,6 +468,11 @@ var errorCodes = errorCodeMap{
Description: "The bucket policy does not exist",
HTTPStatusCode: http.StatusNotFound,
},
ErrNoSuchBucketLifecycle: {
Code: "NoSuchBucketLifecycle",
Description: "The bucket lifecycle configuration does not exist",
HTTPStatusCode: http.StatusNotFound,
},
ErrNoSuchKey: {
Code: "NoSuchKey",
Description: "The specified key does not exist.",
@@ -682,6 +690,11 @@ var errorCodes = errorCodeMap{
Description: "400 BadRequest",
HTTPStatusCode: http.StatusBadRequest,
},
ErrKeyTooLongError: {
Code: "KeyTooLongError",
Description: "Your key is too long",
HTTPStatusCode: http.StatusBadRequest,
},
// FIXME: Actual XML error response also contains the header which missed in list of signed header parameters.
ErrUnsignedHeaders: {
@@ -885,6 +898,11 @@ var errorCodes = errorCodeMap{
Description: "Object name contains unsupported characters.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidObjectNamePrefixSlash: {
Code: "XMinioInvalidObjectName",
Description: "Object name contains a leading slash.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidResourceName: {
Code: "XMinioInvalidResourceName",
Description: "Resource name contains bad components such as \"..\" or \".\".",
@@ -1579,6 +1597,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrMethodNotAllowed
case ObjectNameInvalid:
apiErr = ErrInvalidObjectName
case ObjectNamePrefixAsSlash:
apiErr = ErrInvalidObjectNamePrefixSlash
case InvalidUploadID:
apiErr = ErrNoSuchUpload
case InvalidPart:
@@ -1613,6 +1633,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrUnsupportedMetadata
case BucketPolicyNotFound:
apiErr = ErrNoSuchBucketPolicy
case BucketLifecycleNotFound:
apiErr = ErrNoSuchBucketLifecycle
case *event.ErrInvalidEventName:
apiErr = ErrEventNotification
case *event.ErrInvalidARN:
@@ -1639,6 +1661,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
apiErr = ErrBackendDown
case crypto.Error:
apiErr = ErrObjectTampered
case ObjectNameTooLong:
apiErr = ErrKeyTooLongError
default:
var ie, iw int
// This work-around is to handle the issue golang/go#30648
+1 -1
View File
@@ -582,7 +582,7 @@ func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError
case "AccessDenied":
// The request is from browser and also if browser
// is enabled we need to redirect.
if browser {
if browser && globalIsBrowserEnabled {
w.Header().Set(xhttp.Location, minioReservedBucketPath+reqURL.Path)
w.WriteHeader(http.StatusTemporaryRedirect)
return
+11 -4
View File
@@ -91,7 +91,9 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
// GetBucketLocation
bucket.Methods(http.MethodGet).HandlerFunc(httpTraceAll(api.GetBucketLocationHandler)).Queries("location", "")
// GetBucketPolicy
bucket.Methods(http.MethodGet).HandlerFunc(httpTraceAll(api.GetBucketPolicyHandler)).Queries("policy", "")
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketPolicyHandler)).Queries("policy", "")
// GetBucketLifecycle
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketLifecycleHandler)).Queries("lifecycle", "")
// Dummy Bucket Calls
// GetBucketACL -- this is a dummy call.
@@ -128,9 +130,12 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
// ListObjectsV2
bucket.Methods(http.MethodGet).HandlerFunc(httpTraceAll(api.ListObjectsV2Handler)).Queries("list-type", "2")
// ListObjectsV1 (Legacy)
bucket.Methods(http.MethodGet).HandlerFunc(httpTraceAll(api.ListObjectsV1Handler))
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV1Handler))
// PutBucketLifecycle
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketLifecycleHandler)).Queries("lifecycle", "")
// PutBucketPolicy
bucket.Methods(http.MethodPut).HandlerFunc(httpTraceAll(api.PutBucketPolicyHandler)).Queries("policy", "")
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketPolicyHandler)).Queries("policy", "")
// PutBucketNotification
bucket.Methods(http.MethodPut).HandlerFunc(httpTraceAll(api.PutBucketNotificationHandler)).Queries("notification", "")
// PutBucket
@@ -142,7 +147,9 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
// DeleteMultipleObjects
bucket.Methods(http.MethodPost).HandlerFunc(httpTraceAll(api.DeleteMultipleObjectsHandler)).Queries("delete", "")
// DeleteBucketPolicy
bucket.Methods(http.MethodDelete).HandlerFunc(httpTraceAll(api.DeleteBucketPolicyHandler)).Queries("policy", "")
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketPolicyHandler)).Queries("policy", "")
// DeleteBucketLifecycle
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketLifecycleHandler)).Queries("lifecycle", "")
// DeleteBucket
bucket.Methods(http.MethodDelete).HandlerFunc(httpTraceAll(api.DeleteBucketHandler))
}
+3 -1
View File
@@ -537,7 +537,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return
}
// Remove all tmp files creating during multipart upload
// Remove all tmp files created during multipart upload
defer form.RemoveAll()
// Extract all form fields
@@ -800,6 +800,8 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
globalNotificationSys.RemoveNotification(bucket)
globalPolicySys.Remove(bucket)
globalNotificationSys.DeleteBucket(ctx, bucket)
globalLifecycleSys.Remove(bucket)
globalNotificationSys.RemoveBucketLifecycle(ctx, bucket)
// Write success response.
writeSuccessNoContent(w)
+164
View File
@@ -0,0 +1,164 @@
/*
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"encoding/xml"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/lifecycle"
)
const (
// Lifecycle configuration file.
bucketLifecycleConfig = "lifecycle.xml"
)
// PutBucketLifecycleHandler - This HTTP handler stores given bucket lifecycle configuration as per
// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lifecycle-mgmt.html
func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "PutBucketLifecycle")
defer logger.AuditLog(w, r, "PutBucketLifecycle", mustGetClaimsFromToken(r))
objAPI := api.ObjectAPI()
if objAPI == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
if s3Error := checkRequestAuthType(ctx, r, lifecycle.PutBucketLifecycleAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
// Check if bucket exists.
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
// PutBucketLifecycle always needs a Content-Md5
if _, ok := r.Header["Content-Md5"]; !ok {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL, guessIsBrowserReq(r))
return
}
bucketLifecycle, err := lifecycle.ParseLifecycleConfig(io.LimitReader(r.Body, r.ContentLength))
if err != nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL, guessIsBrowserReq(r))
return
}
if err = objAPI.SetBucketLifecycle(ctx, bucket, bucketLifecycle); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
globalLifecycleSys.Set(bucket, *bucketLifecycle)
globalNotificationSys.SetBucketLifecycle(ctx, bucket, bucketLifecycle)
// Success.
writeSuccessNoContent(w)
}
// GetBucketLifecycleHandler - This HTTP handler returns bucket policy configuration.
func (api objectAPIHandlers) GetBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "GetBucketLifecycle")
defer logger.AuditLog(w, r, "GetBucketLifecycle", mustGetClaimsFromToken(r))
objAPI := api.ObjectAPI()
if objAPI == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
if s3Error := checkRequestAuthType(ctx, r, lifecycle.GetBucketLifecycleAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
// Check if bucket exists.
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
// Read bucket access lifecycle.
bucketLifecycle, err := objAPI.GetBucketLifecycle(ctx, bucket)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
lifecycleData, err := xml.Marshal(bucketLifecycle)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
// Write lifecycle configuration to client.
writeSuccessResponseXML(w, lifecycleData)
}
// DeleteBucketLifecycleHandler - This HTTP handler removes bucket lifecycle configuration.
func (api objectAPIHandlers) DeleteBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "DeleteBucketLifecycle")
defer logger.AuditLog(w, r, "DeleteBucketLifecycle", mustGetClaimsFromToken(r))
objAPI := api.ObjectAPI()
if objAPI == nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
return
}
vars := mux.Vars(r)
bucket := vars["bucket"]
if s3Error := checkRequestAuthType(ctx, r, lifecycle.DeleteBucketLifecycleAction, bucket, ""); s3Error != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
return
}
// Check if bucket exists.
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
if err := objAPI.DeleteBucketLifecycle(ctx, bucket); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
globalLifecycleSys.Remove(bucket)
globalNotificationSys.RemoveBucketLifecycle(ctx, bucket)
// Success.
writeSuccessNoContent(w)
}
+26 -12
View File
@@ -17,12 +17,15 @@
package cmd
import (
"bytes"
"encoding/xml"
"errors"
"io"
"net/http"
"path"
"github.com/gorilla/mux"
xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/event/target"
@@ -48,6 +51,7 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
vars := mux.Vars(r)
bucketName := vars["bucket"]
var config *event.Config
objAPI := api.ObjectAPI()
if objAPI == nil {
@@ -71,24 +75,31 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
return
}
// Attempt to successfully load notification config.
nConfig, err := readNotificationConfig(ctx, objAPI, bucketName)
// Construct path to notification.xml for the given bucket.
configFile := path.Join(bucketConfigPrefix, bucketName, bucketNotificationConfig)
configData, err := readConfig(ctx, objAPI, configFile)
if err != nil {
// Ignore errNoSuchNotifications to comply with AWS S3.
if err != errNoSuchNotifications {
if err != errConfigNotFound {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
config = &event.Config{}
} else {
if err = xml.NewDecoder(bytes.NewReader(configData)).Decode(&config); err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
}
nConfig = &event.Config{}
}
config.SetRegion(globalServerConfig.GetRegion())
// If xml namespace is empty, set a default value before returning.
if nConfig.XMLNS == "" {
nConfig.XMLNS = "http://s3.amazonaws.com/doc/2006-03-01/"
if config.XMLNS == "" {
config.XMLNS = "http://s3.amazonaws.com/doc/2006-03-01/"
}
notificationBytes, err := xml.Marshal(nConfig)
notificationBytes, err := xml.Marshal(config)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
return
@@ -142,9 +153,10 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
if event.IsEventError(err) {
apiErr = toAPIError(ctx, err)
}
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
return
if _, ok := err.(*event.ErrARNNotFound); !ok {
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
return
}
}
if err = saveNotificationConfig(ctx, objectAPI, bucketName, config); err != nil {
@@ -246,6 +258,8 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
return
}
w.Header().Set(xhttp.ContentType, "text/event-stream")
target, err := target.NewHTTPClientTarget(*host, w)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
+19
View File
@@ -38,6 +38,25 @@ import (
xnet "github.com/minio/minio/pkg/net"
)
func verifyObjectLayerFeatures(name string, objAPI ObjectLayer) {
if (globalAutoEncryption || GlobalKMS != nil) && !objAPI.IsEncryptionSupported() {
logger.Fatal(errInvalidArgument,
"Encryption support is requested but '%s' does not support encryption", name)
}
if strings.HasPrefix(name, "gateway") {
if GlobalGatewaySSE.IsSet() && GlobalKMS == nil {
uiErr := uiErrInvalidGWSSEEnvValue(nil).Msg("MINIO_GATEWAY_SSE set but KMS is not configured")
logger.Fatal(uiErr, "Unable to start gateway with SSE")
}
}
if globalIsCompressionEnabled && !objAPI.IsCompressionSupported() {
logger.Fatal(errInvalidArgument,
"Compression support is requested but '%s' does not support compression", name)
}
}
// Check for updates and print a notification message
func checkUpdate(mode string) {
// Its OK to ignore any errors during doUpdate() here.
-53
View File
@@ -20,9 +20,7 @@ import (
"bytes"
"context"
"errors"
"fmt"
etcd "github.com/coreos/etcd/clientv3"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/hash"
)
@@ -51,38 +49,10 @@ func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) ([]b
return buffer.Bytes(), nil
}
func deleteConfigEtcd(ctx context.Context, client *etcd.Client, configFile string) error {
timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
defer cancel()
_, err := client.Delete(timeoutCtx, configFile)
if err != nil {
if err == context.DeadlineExceeded {
return fmt.Errorf("etcd setup is unreachable, please check your endpoints %s",
client.Endpoints())
}
return fmt.Errorf("unexpected error %s returned by etcd setup, please check your endpoints %s",
err, client.Endpoints())
}
return nil
}
func deleteConfig(ctx context.Context, objAPI ObjectLayer, configFile string) error {
return objAPI.DeleteObject(ctx, minioMetaBucket, configFile)
}
func saveConfigEtcd(ctx context.Context, client *etcd.Client, configFile string, data []byte) error {
timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
defer cancel()
_, err := client.Put(timeoutCtx, configFile, string(data))
if err == context.DeadlineExceeded {
return fmt.Errorf("etcd setup is unreachable, please check your endpoints %s", client.Endpoints())
} else if err != nil {
return fmt.Errorf("unexpected error %s returned by etcd setup, please check your endpoints %s", err, client.Endpoints())
}
return nil
}
func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data []byte) error {
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data)), globalCLIContext.StrictS3Compat)
if err != nil {
@@ -93,29 +63,6 @@ func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data
return err
}
func readConfigEtcd(ctx context.Context, client *etcd.Client, configFile string) ([]byte, error) {
timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
defer cancel()
resp, err := client.Get(timeoutCtx, configFile)
if err != nil {
if err == context.DeadlineExceeded {
return nil, fmt.Errorf("etcd setup is unreachable, please check your endpoints %s",
client.Endpoints())
}
return nil, fmt.Errorf("unexpected error %s returned by etcd setup, please check your endpoints %s",
err, client.Endpoints())
}
if resp.Count == 0 {
return nil, errConfigNotFound
}
for _, ev := range resp.Kvs {
if string(ev.Key) == configFile {
return ev.Value, nil
}
}
return nil, errConfigNotFound
}
func checkConfig(ctx context.Context, objAPI ObjectLayer, configFile string) error {
if _, err := objAPI.GetObjectInfo(ctx, minioMetaBucket, configFile, ObjectOptions{}); err != nil {
// Treat object not found as config not found.
+41 -24
View File
@@ -30,7 +30,7 @@ import (
"github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/event/target"
"github.com/minio/minio/pkg/iam/policy"
iampolicy "github.com/minio/minio/pkg/iam/policy"
"github.com/minio/minio/pkg/iam/validator"
xnet "github.com/minio/minio/pkg/net"
)
@@ -281,17 +281,27 @@ func (s *serverConfig) loadFromEnvs() {
}
if jwksURL, ok := os.LookupEnv("MINIO_IAM_JWKS_URL"); ok {
if u, err := xnet.ParseURL(jwksURL); err == nil {
s.OpenID.JWKS.URL = u
logger.FatalIf(s.OpenID.JWKS.PopulatePublicKey(), "Unable to populate public key from JWKS URL")
u, err := xnet.ParseURL(jwksURL)
if err != nil {
logger.FatalIf(err, "Unable to parse MINIO_IAM_JWKS_URL %s", jwksURL)
}
s.OpenID.JWKS.URL = u
}
if opaURL, ok := os.LookupEnv("MINIO_IAM_OPA_URL"); ok {
if u, err := xnet.ParseURL(opaURL); err == nil {
s.Policy.OPA.URL = u
s.Policy.OPA.AuthToken = os.Getenv("MINIO_IAM_OPA_AUTHTOKEN")
u, err := xnet.ParseURL(opaURL)
if err != nil {
logger.FatalIf(err, "Unable to parse MINIO_IAM_OPA_URL %s", opaURL)
}
opaArgs := iampolicy.OpaArgs{
URL: u,
AuthToken: os.Getenv("MINIO_IAM_OPA_AUTHTOKEN"),
Transport: NewCustomHTTPTransport(),
CloseRespFn: xhttp.DrainBody,
}
logger.FatalIf(opaArgs.Validate(), "Unable to reach MINIO_IAM_OPA_URL %s", opaURL)
s.Policy.OPA.URL = opaArgs.URL
s.Policy.OPA.AuthToken = opaArgs.AuthToken
}
}
@@ -303,7 +313,7 @@ func (s *serverConfig) TestNotificationTargets() error {
if !v.Enable {
continue
}
t, err := target.NewAMQPTarget(k, v)
t, err := target.NewAMQPTarget(k, v, GlobalServiceDoneCh)
if err != nil {
return fmt.Errorf("amqp(%s): %s", k, err.Error())
}
@@ -314,7 +324,7 @@ func (s *serverConfig) TestNotificationTargets() error {
if !v.Enable {
continue
}
t, err := target.NewElasticsearchTarget(k, v)
t, err := target.NewElasticsearchTarget(k, v, GlobalServiceDoneCh)
if err != nil {
return fmt.Errorf("elasticsearch(%s): %s", k, err.Error())
}
@@ -347,7 +357,7 @@ func (s *serverConfig) TestNotificationTargets() error {
if !v.Enable {
continue
}
t, err := target.NewMySQLTarget(k, v)
t, err := target.NewMySQLTarget(k, v, GlobalServiceDoneCh)
if err != nil {
return fmt.Errorf("mysql(%s): %s", k, err.Error())
}
@@ -358,7 +368,7 @@ func (s *serverConfig) TestNotificationTargets() error {
if !v.Enable {
continue
}
t, err := target.NewNATSTarget(k, v)
t, err := target.NewNATSTarget(k, v, GlobalServiceDoneCh)
if err != nil {
return fmt.Errorf("nats(%s): %s", k, err.Error())
}
@@ -369,7 +379,7 @@ func (s *serverConfig) TestNotificationTargets() error {
if !v.Enable {
continue
}
t, err := target.NewNSQTarget(k, v)
t, err := target.NewNSQTarget(k, v, GlobalServiceDoneCh)
if err != nil {
return fmt.Errorf("nsq(%s): %s", k, err.Error())
}
@@ -380,7 +390,7 @@ func (s *serverConfig) TestNotificationTargets() error {
if !v.Enable {
continue
}
t, err := target.NewPostgreSQLTarget(k, v)
t, err := target.NewPostgreSQLTarget(k, v, GlobalServiceDoneCh)
if err != nil {
return fmt.Errorf("postgreSQL(%s): %s", k, err.Error())
}
@@ -391,7 +401,7 @@ func (s *serverConfig) TestNotificationTargets() error {
if !v.Enable {
continue
}
t, err := target.NewRedisTarget(k, v)
t, err := target.NewRedisTarget(k, v, GlobalServiceDoneCh)
if err != nil {
return fmt.Errorf("redis(%s): %s", k, err.Error())
}
@@ -536,7 +546,7 @@ func (s *serverConfig) loadToCachedConfigs() {
globalCacheMaxUse = cacheConf.MaxUse
}
if err := Environment.LookupKMSConfig(s.KMS); err != nil {
logger.FatalIf(err, "Unable to setup the KMS")
logger.FatalIf(err, "Unable to setup the KMS %s", s.KMS.Vault.Endpoint)
}
if !globalIsCompressionEnabled {
@@ -546,15 +556,22 @@ func (s *serverConfig) loadToCachedConfigs() {
globalIsCompressionEnabled = compressionConf.Enabled
}
if s.OpenID.JWKS.URL != nil && s.OpenID.JWKS.URL.String() != "" {
logger.FatalIf(s.OpenID.JWKS.PopulatePublicKey(),
"Unable to populate public key from JWKS URL %s", s.OpenID.JWKS.URL)
}
globalIAMValidators = getAuthValidators(s)
if s.Policy.OPA.URL != nil && s.Policy.OPA.URL.String() != "" {
globalPolicyOPA = iampolicy.NewOpa(iampolicy.OpaArgs{
opaArgs := iampolicy.OpaArgs{
URL: s.Policy.OPA.URL,
AuthToken: s.Policy.OPA.AuthToken,
Transport: NewCustomHTTPTransport(),
CloseRespFn: xhttp.DrainBody,
})
}
logger.FatalIf(opaArgs.Validate(), "Unable to reach OPA URL %s", s.Policy.OPA.URL)
globalPolicyOPA = iampolicy.NewOpa(opaArgs)
}
}
@@ -637,7 +654,7 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
}
for id, args := range config.Notify.AMQP {
if args.Enable {
newTarget, err := target.NewAMQPTarget(id, args)
newTarget, err := target.NewAMQPTarget(id, args, GlobalServiceDoneCh)
if err != nil {
logger.LogIf(context.Background(), err)
continue
@@ -651,7 +668,7 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
for id, args := range config.Notify.Elasticsearch {
if args.Enable {
newTarget, err := target.NewElasticsearchTarget(id, args)
newTarget, err := target.NewElasticsearchTarget(id, args, GlobalServiceDoneCh)
if err != nil {
logger.LogIf(context.Background(), err)
continue
@@ -696,7 +713,7 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
for id, args := range config.Notify.MySQL {
if args.Enable {
newTarget, err := target.NewMySQLTarget(id, args)
newTarget, err := target.NewMySQLTarget(id, args, GlobalServiceDoneCh)
if err != nil {
logger.LogIf(context.Background(), err)
continue
@@ -710,7 +727,7 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
for id, args := range config.Notify.NATS {
if args.Enable {
newTarget, err := target.NewNATSTarget(id, args)
newTarget, err := target.NewNATSTarget(id, args, GlobalServiceDoneCh)
if err != nil {
logger.LogIf(context.Background(), err)
continue
@@ -724,7 +741,7 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
for id, args := range config.Notify.NSQ {
if args.Enable {
newTarget, err := target.NewNSQTarget(id, args)
newTarget, err := target.NewNSQTarget(id, args, GlobalServiceDoneCh)
if err != nil {
logger.LogIf(context.Background(), err)
continue
@@ -738,7 +755,7 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
for id, args := range config.Notify.PostgreSQL {
if args.Enable {
newTarget, err := target.NewPostgreSQLTarget(id, args)
newTarget, err := target.NewPostgreSQLTarget(id, args, GlobalServiceDoneCh)
if err != nil {
logger.LogIf(context.Background(), err)
continue
@@ -752,7 +769,7 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
for id, args := range config.Notify.Redis {
if args.Enable {
newTarget, err := target.NewRedisTarget(id, args)
newTarget, err := target.NewRedisTarget(id, args, GlobalServiceDoneCh)
if err != nil {
logger.LogIf(context.Background(), err)
continue
+7 -7
View File
@@ -185,10 +185,10 @@ func TestValidateConfig(t *testing.T) {
{`{"version": "` + v + `", "browser": "on", "browser": "on", "region":"us-east-1", "credential" : {"accessKey":"minio", "secretKey":"minio123"}}`, false},
// Test 11 - Test AMQP
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "amqp": { "1": { "enable": true, "url": "", "exchange": "", "routingKey": "", "exchangeType": "", "mandatory": false, "immediate": false, "durable": false, "internal": false, "noWait": false, "autoDeleted": false }}}}`, false},
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "amqp": { "1": { "enable": true, "url": "", "exchange": "", "routingKey": "", "exchangeType": "", "mandatory": false, "immediate": false, "durable": false, "internal": false, "noWait": false, "autoDeleted": false, "queueDir": "", "queueLimit": 0}}}}`, false},
// Test 12 - Test NATS
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nats": { "1": { "enable": true, "address": "", "subject": "", "username": "", "password": "", "token": "", "secure": false, "pingInterval": 0, "streaming": { "enable": false, "clusterID": "", "async": false, "maxPubAcksInflight": 0 } } }}}`, false},
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nats": { "1": { "enable": true, "address": "", "subject": "", "username": "", "password": "", "token": "", "secure": false, "pingInterval": 0, "queueDir": "", "queueLimit": 0, "streaming": { "enable": false, "clusterID": "", "async": false, "maxPubAcksInflight": 0 } } }}}`, false},
// Test 13 - Test ElasticSearch
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "url": "", "index": "" } }}}`, false},
@@ -197,7 +197,7 @@ func TestValidateConfig(t *testing.T) {
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "address": "", "password": "", "key": "" } }}}`, false},
// Test 15 - Test PostgreSQL
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, false},
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "", "queueDir": "", "queueLimit": 0 }}}}`, false},
// Test 16 - Test Kafka
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "kafka": { "1": { "enable": true, "brokers": null, "topic": "", "queueDir": "", "queueLimit": 0 } }}}`, false},
@@ -206,7 +206,7 @@ func TestValidateConfig(t *testing.T) {
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "webhook": { "1": { "enable": true, "endpoint": "", "queueDir": "", "queueLimit": 0} }}}`, false},
// Test 18 - Test MySQL
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, false},
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "", "queueDir": "", "queueLimit": 0 }}}}`, false},
// Test 19 - Test Format for MySQL
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "invalid", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, false},
@@ -224,10 +224,10 @@ func TestValidateConfig(t *testing.T) {
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "invalid", "url": "example.com", "index": "myindex" } }}}`, false},
// Test 24 - Test valid Format for ElasticSearch
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "namespace", "url": "example.com", "index": "myindex" } }}}`, true},
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "namespace", "url": "example.com", "index": "myindex", "queueDir": "", "queueLimit": 0 } }}}`, true},
// Test 25 - Test Format for Redis
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "invalid", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, false},
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "invalid", "address": "example.com:80", "password": "xxx", "key": "key1", "queueDir": "", "queueLimit": 0 } }}}`, false},
// Test 26 - Test valid Format for Redis
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
@@ -236,7 +236,7 @@ func TestValidateConfig(t *testing.T) {
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "username": "", "password": "", "queueDir": "", "queueLimit": 0}}}}`, false},
// Test 28 - Test NSQ
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nsq": { "1": { "enable": true, "nsqdAddress": "", "topic": ""} }}}`, false},
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nsq": { "1": { "enable": true, "nsqdAddress": "", "topic": "", "queueDir": "", "queueLimit": 0} }}}`, false},
}
for i, testCase := range testCases {
+1 -1
View File
@@ -2426,7 +2426,7 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
defer func() {
if err == nil {
if globalEtcdClient != nil {
deleteConfigEtcd(context.Background(), globalEtcdClient, configFile)
deleteKeyEtcd(context.Background(), globalEtcdClient, configFile)
} else {
// Rename config.json to config.json.deprecated only upon
// success of this function.
+2 -2
View File
@@ -22,7 +22,7 @@ import (
"github.com/minio/minio/cmd/crypto"
"github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/event/target"
"github.com/minio/minio/pkg/iam/policy"
iampolicy "github.com/minio/minio/pkg/iam/policy"
"github.com/minio/minio/pkg/iam/validator"
"github.com/minio/minio/pkg/quick"
)
@@ -884,7 +884,7 @@ type serverConfigV32 struct {
} `json:"policy"`
}
// serverConfigV33 is just like version '32', removes clientID from NATS and MQTT, and adds queueDir, queueLimit in notification targets.
// serverConfigV33 is just like version '32', removes clientID from NATS and MQTT, and adds queueDir, queueLimit in all notification targets.
type serverConfigV33 struct {
quick.Config `json:"-"` // ignore interfaces
+20
View File
@@ -23,6 +23,7 @@ import (
"path"
"runtime"
"strings"
"time"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/quick"
@@ -101,6 +102,25 @@ func (sys *ConfigSys) Load(objAPI ObjectLayer) error {
return sys.Init(objAPI)
}
// WatchConfigNASDisk - watches nas disk on periodic basis.
func (sys *ConfigSys) WatchConfigNASDisk(objAPI ObjectLayer) {
configInterval := globalRefreshIAMInterval
watchDisk := func() {
ticker := time.NewTicker(configInterval)
defer ticker.Stop()
for {
select {
case <-GlobalServiceDoneCh:
return
case <-ticker.C:
loadConfig(objAPI)
}
}
}
// Refresh configSys in background for NAS gateway.
go watchDisk()
}
// Init - initializes config system from config.json.
func (sys *ConfigSys) Init(objAPI ObjectLayer) error {
if objAPI == nil {
+2 -2
View File
@@ -119,9 +119,9 @@ func dailySweeper() {
break
}
// Perform a sweep round each 24 hours
// Perform a sweep round each month
for {
if time.Since(lastSweepTime) < 24*time.Hour {
if time.Since(lastSweepTime) < 30*24*time.Hour {
time.Sleep(time.Hour)
continue
}
+4 -1
View File
@@ -945,7 +945,10 @@ func checkAtimeSupport(dir string) (err error) {
if err != nil {
return
}
if _, err = io.Copy(ioutil.Discard, file); err != io.EOF {
// add a sleep to ensure atime change is detected
time.Sleep(10 * time.Millisecond)
if _, err = io.Copy(ioutil.Discard, file); err != nil {
return
}
+5 -12
View File
@@ -124,17 +124,10 @@ func TestGetCacheFSMaxUse(t *testing.T) {
// test wildcard patterns for excluding entries from cache
func TestCacheExclusion(t *testing.T) {
fsDirs, err := getRandomDisks(1)
if err != nil {
t.Fatal(err)
cobjects := &cacheObjects{
cache: nil,
}
cconfig := CacheConfig{Expiry: 30, Drives: fsDirs}
cobjects, err := newServerCacheObjects(cconfig)
if err != nil {
t.Fatal(err)
}
cobj := cobjects.(*cacheObjects)
GlobalServiceDoneCh <- struct{}{}
testCases := []struct {
bucketName string
objectName string
@@ -155,8 +148,8 @@ func TestCacheExclusion(t *testing.T) {
}
for i, testCase := range testCases {
cobj.exclude = []string{testCase.excludePattern}
if cobj.isCacheExclude(testCase.bucketName, testCase.objectName) != testCase.expectedResult {
cobjects.exclude = []string{testCase.excludePattern}
if cobjects.isCacheExclude(testCase.bucketName, testCase.objectName) != testCase.expectedResult {
t.Fatal("Cache exclusion test failed for case ", i)
}
}
-6
View File
@@ -72,12 +72,6 @@ func (api objectAPIHandlers) GetBucketLoggingHandler(w http.ResponseWriter, r *h
w.(http.Flusher).Flush()
}
// GetBucketLifecycleHandler - GET bucket lifecycle, a dummy api
func (api objectAPIHandlers) GetBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
writeSuccessResponseHeadersOnly(w)
w.(http.Flusher).Flush()
}
// GetBucketReplicationHandler - GET bucket replication, a dummy api
func (api objectAPIHandlers) GetBucketReplicationHandler(w http.ResponseWriter, r *http.Request) {
writeSuccessResponseHeadersOnly(w)
+72
View File
@@ -0,0 +1,72 @@
/*
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"context"
"errors"
"fmt"
etcd "github.com/coreos/etcd/clientv3"
)
var errEtcdUnreachable = errors.New("etcd is unreachable, please check your endpoints")
func etcdErrToErr(err error, etcdEndpoints []string) error {
if err == nil {
return nil
}
switch err {
case context.DeadlineExceeded:
return fmt.Errorf("%s %s", errEtcdUnreachable, etcdEndpoints)
default:
return fmt.Errorf("unexpected error %s from etcd, please check your endpoints %s", err, etcdEndpoints)
}
}
func saveKeyEtcd(ctx context.Context, client *etcd.Client, key string, data []byte) error {
timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
defer cancel()
_, err := client.Put(timeoutCtx, key, string(data))
return etcdErrToErr(err, client.Endpoints())
}
func deleteKeyEtcd(ctx context.Context, client *etcd.Client, key string) error {
timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
defer cancel()
_, err := client.Delete(timeoutCtx, key)
return etcdErrToErr(err, client.Endpoints())
}
func readKeyEtcd(ctx context.Context, client *etcd.Client, key string) ([]byte, error) {
timeoutCtx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
defer cancel()
resp, err := client.Get(timeoutCtx, key)
if err != nil {
return nil, etcdErrToErr(err, client.Endpoints())
}
if resp.Count == 0 {
return nil, errConfigNotFound
}
for _, ev := range resp.Kvs {
if string(ev.Key) == key {
return ev.Value, nil
}
}
return nil, errConfigNotFound
}
+21 -15
View File
@@ -228,15 +228,6 @@ func fsStatDir(ctx context.Context, statDir string) (os.FileInfo, error) {
return fi, nil
}
// Returns if the dirPath is a directory.
func fsIsDir(ctx context.Context, dirPath string) bool {
fi, err := fsStat(ctx, dirPath)
if err != nil {
return false
}
return fi.IsDir()
}
// Lookup if file exists, returns file attributes upon success.
func fsStatFile(ctx context.Context, statFile string) (os.FileInfo, error) {
fi, err := fsStat(ctx, statFile)
@@ -280,7 +271,7 @@ func fsOpenFile(ctx context.Context, readPath string, offset int64) (io.ReadClos
}
// Stat to get the size of the file at path.
st, err := os.Stat(readPath)
st, err := fr.Stat()
if err != nil {
err = osErrToFSFileErr(err)
if err != errFileNotFound {
@@ -386,6 +377,26 @@ func fsFAllocate(fd int, offset int64, len int64) (err error) {
return nil
}
// Renames source path to destination path, fails if the destination path
// parents are not already created.
func fsSimpleRenameFile(ctx context.Context, sourcePath, destPath string) error {
if err := checkPathLength(sourcePath); err != nil {
logger.LogIf(ctx, err)
return err
}
if err := checkPathLength(destPath); err != nil {
logger.LogIf(ctx, err)
return err
}
if err := os.Rename(sourcePath, destPath); err != nil {
logger.LogIf(ctx, err)
return osErrToFSFileErr(err)
}
return nil
}
// Renames source path to destination path, creates all the
// missing parents if they don't exist.
func fsRenameFile(ctx context.Context, sourcePath, destPath string) error {
@@ -398,11 +409,6 @@ func fsRenameFile(ctx context.Context, sourcePath, destPath string) error {
return err
}
// Verify if source path exists.
if _, err := os.Stat(sourcePath); err != nil {
return osErrToFSFileErr(err)
}
if err := renameAll(sourcePath, destPath); err != nil {
logger.LogIf(ctx, err)
return err
-12
View File
@@ -548,18 +548,6 @@ func TestFSRemoveMeta(t *testing.T) {
}
}
func TestFSIsDir(t *testing.T) {
dirPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
if err != nil {
t.Fatalf("Unable to create tmp directory %s", err)
}
defer os.RemoveAll(dirPath)
if !fsIsDir(context.Background(), dirPath) {
t.Fatalf("Expected %s to be a directory", dirPath)
}
}
func TestFSIsFile(t *testing.T) {
dirPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
if err != nil {
+6 -2
View File
@@ -152,7 +152,7 @@ func (fs *FSObjects) ListMultipartUploads(ctx context.Context, bucket, object, k
return result, toObjectErr(err)
}
// S3 spec says uploaIDs should be sorted based on initiated time. ModTime of fs.json
// S3 spec says uploadIDs should be sorted based on initiated time. ModTime of fs.json
// is the creation time of the uploadID, hence we will use that.
var uploads []MultipartInfo
for _, uploadID := range uploadIDs {
@@ -326,7 +326,11 @@ func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
partPath := pathJoin(uploadIDDir, fs.encodePartFile(partID, etag, data.ActualSize()))
if err = fsRenameFile(ctx, tmpPartPath, partPath); err != nil {
// Make sure not to create parent directories if they don't exist - the upload might have been aborted.
if err = fsSimpleRenameFile(ctx, tmpPartPath, partPath); err != nil {
if err == errFileNotFound || err == errFileAccessDenied {
return pi, InvalidUploadID{UploadID: uploadID}
}
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
}
+16 -4
View File
@@ -32,6 +32,7 @@ import (
"github.com/minio/minio-go/v6/pkg/s3utils"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/lifecycle"
"github.com/minio/minio/pkg/lock"
"github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/mimedb"
@@ -690,10 +691,6 @@ func (fs *FSObjects) defaultFsJSON(object string) fsMetaV1 {
func (fs *FSObjects) getObjectInfo(ctx context.Context, bucket, object string) (oi ObjectInfo, e error) {
fsMeta := fsMetaV1{}
if hasSuffix(object, slashSeparator) {
// Since we support PUT of a "directory" object, we allow HEAD.
if !fsIsDir(ctx, pathJoin(fs.fsPath, bucket, object)) {
return oi, errFileNotFound
}
fi, err := fsStatDir(ctx, pathJoin(fs.fsPath, bucket, object))
if err != nil {
return oi, err
@@ -1170,6 +1167,21 @@ func (fs *FSObjects) DeleteBucketPolicy(ctx context.Context, bucket string) erro
return removePolicyConfig(ctx, fs, bucket)
}
// SetBucketLifecycle sets lifecycle on bucket
func (fs *FSObjects) SetBucketLifecycle(ctx context.Context, bucket string, lifecycle *lifecycle.Lifecycle) error {
return saveLifecycleConfig(ctx, fs, bucket, lifecycle)
}
// GetBucketLifecycle will get lifecycle on bucket
func (fs *FSObjects) GetBucketLifecycle(ctx context.Context, bucket string) (*lifecycle.Lifecycle, error) {
return getLifecycleConfig(fs, bucket)
}
// DeleteBucketLifecycle deletes all lifecycle on bucket
func (fs *FSObjects) DeleteBucketLifecycle(ctx context.Context, bucket string) error {
return removeLifecycleConfig(ctx, fs, bucket)
}
// ListObjectsV2 lists all blobs in bucket filtered by prefix
func (fs *FSObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result ListObjectsV2Info, err error) {
marker := continuationToken
+2
View File
@@ -317,6 +317,8 @@ func ErrorRespToObjectError(err error, params ...string) error {
err = BucketNotEmpty{}
case "NoSuchBucketPolicy":
err = BucketPolicyNotFound{}
case "NoSuchBucketLifecycle":
err = BucketLifecycleNotFound{}
case "InvalidBucketName":
err = BucketNameInvalid{Bucket: bucket}
case "InvalidPart":
+13 -15
View File
@@ -158,7 +158,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
registerSTSRouter(router)
}
enableConfigOps := globalEtcdClient != nil && gatewayName == "nas"
enableConfigOps := gatewayName == "nas"
enableIAMOps := globalEtcdClient != nil
// Enable IAM admin APIs if etcd is enabled, if not just enable basic
@@ -236,6 +236,10 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
// Load globalServerConfig from etcd
logger.LogIf(context.Background(), globalConfigSys.Init(newObject))
// Start watching disk for reloading config, this
// is only enabled for "NAS" gateway.
globalConfigSys.WatchConfigNASDisk(newObject)
}
// Load logger subsystem
@@ -269,25 +273,19 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
// Initialize policy system.
go globalPolicySys.Init(newObject)
// Create new lifecycle system
globalLifecycleSys = NewLifecycleSys()
// Create new notification system.
globalNotificationSys = NewNotificationSys(globalServerConfig, globalEndpoints)
if globalEtcdClient != nil && newObject.IsNotificationSupported() {
if enableConfigOps && newObject.IsNotificationSupported() {
logger.LogIf(context.Background(), globalNotificationSys.Init(newObject))
}
// Encryption support checks in gateway mode.
{
if (globalAutoEncryption || GlobalKMS != nil) && !newObject.IsEncryptionSupported() {
logger.Fatal(errInvalidArgument,
"Encryption support is requested but (%s) gateway does not support encryption", gw.Name())
}
if GlobalGatewaySSE.IsSet() && GlobalKMS == nil {
logger.Fatal(uiErrInvalidGWSSEEnvValue(nil).Msg("MINIO_GATEWAY_SSE set but KMS is not configured"),
"Unable to start gateway with SSE")
}
}
// Verify if object layer supports
// - encryption
// - compression
verifyObjectLayerFeatures("gateway "+gatewayName, newObject)
// Once endpoints are finalized, initialize the new object api.
globalObjLayerMutex.Lock()
+17
View File
@@ -20,6 +20,7 @@ import (
"context"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/lifecycle"
"github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/policy"
)
@@ -81,6 +82,22 @@ func (a GatewayUnsupported) DeleteBucketPolicy(ctx context.Context, bucket strin
return NotImplemented{}
}
// SetBucketLifecycle sets lifecycle on bucket
func (a GatewayUnsupported) SetBucketLifecycle(ctx context.Context, bucket string, lifecycle *lifecycle.Lifecycle) error {
logger.LogIf(ctx, NotImplemented{})
return NotImplemented{}
}
// GetBucketLifecycle will get lifecycle on bucket
func (a GatewayUnsupported) GetBucketLifecycle(ctx context.Context, bucket string) (*lifecycle.Lifecycle, error) {
return nil, NotImplemented{}
}
// DeleteBucketLifecycle deletes all lifecycle on bucket
func (a GatewayUnsupported) DeleteBucketLifecycle(ctx context.Context, bucket string) error {
return NotImplemented{}
}
// ReloadFormat - Not implemented stub.
func (a GatewayUnsupported) ReloadFormat(ctx context.Context, dryRun bool) error {
return NotImplemented{}
+1 -1
View File
@@ -111,7 +111,7 @@ EXAMPLES:
minio.RegisterGatewayCommand(cli.Command{
Name: azureBackend,
Usage: "Microsoft Azure Blob Storage.",
Usage: "Microsoft Azure Blob Storage",
Action: azureGatewayMain,
CustomHelpTemplate: azureGatewayTemplate,
HideHelpCommand: true,
+75 -16
View File
@@ -18,6 +18,7 @@ package hdfs
import (
"context"
"fmt"
"io"
"net"
"net/http"
@@ -30,7 +31,11 @@ import (
"time"
"github.com/minio/cli"
krb "github.com/minio/gokrb5/v7/client"
"github.com/minio/gokrb5/v7/config"
"github.com/minio/gokrb5/v7/credentials"
"github.com/minio/hdfs/v3"
"github.com/minio/hdfs/v3/hadoopconf"
"github.com/minio/minio-go/v6/pkg/s3utils"
minio "github.com/minio/minio/cmd"
"github.com/minio/minio/cmd/logger"
@@ -102,7 +107,7 @@ EXAMPLES:
// Handler for 'minio gateway hdfs' command line.
func hdfsGatewayMain(ctx *cli.Context) {
// Validate gateway arguments.
if !ctx.Args().Present() || ctx.Args().First() == "help" {
if ctx.Args().First() == "help" {
cli.ShowCommandHelpAndExit(ctx, hdfsBackend, 1)
}
@@ -119,6 +124,43 @@ func (g *HDFS) Name() string {
return hdfsBackend
}
func getKerberosClient() (*krb.Client, error) {
configPath := os.Getenv("KRB5_CONFIG")
if configPath == "" {
configPath = "/etc/krb5.conf"
}
cfg, err := config.Load(configPath)
if err != nil {
return nil, err
}
// Determine the ccache location from the environment,
// falling back to the default location.
ccachePath := os.Getenv("KRB5CCNAME")
if strings.Contains(ccachePath, ":") {
if strings.HasPrefix(ccachePath, "FILE:") {
ccachePath = strings.TrimPrefix(ccachePath, "FILE:")
} else {
return nil, fmt.Errorf("unable to use kerberos ccache: %s", ccachePath)
}
} else if ccachePath == "" {
u, err := user.Current()
if err != nil {
return nil, err
}
ccachePath = fmt.Sprintf("/tmp/krb5cc_%s", u.Uid)
}
ccache, err := credentials.LoadCCache(ccachePath)
if err != nil {
return nil, err
}
return krb.NewClientFromCCache(ccache, cfg)
}
// NewGatewayLayer returns hdfs gatewaylayer.
func (g *HDFS) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error) {
dialFunc := (&net.Dialer{
@@ -127,25 +169,42 @@ func (g *HDFS) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error
DualStack: true,
}).DialContext
var addresses []string
for _, s := range g.args {
u, err := xnet.ParseURL(s)
if err != nil {
return nil, err
}
addresses = append(addresses, u.Host)
}
user, err := user.Current()
hconfig, err := hadoopconf.LoadFromEnvironment()
if err != nil {
return nil, err
}
opts := hdfs.ClientOptions{
Addresses: addresses,
User: user.Username,
NamenodeDialFunc: dialFunc,
DatanodeDialFunc: dialFunc,
opts := hdfs.ClientOptionsFromConf(hconfig)
opts.NamenodeDialFunc = dialFunc
opts.DatanodeDialFunc = dialFunc
// Not addresses found, load it from command line.
if len(opts.Addresses) == 0 {
var addresses []string
for _, s := range g.args {
u, err := xnet.ParseURL(s)
if err != nil {
return nil, err
}
addresses = append(addresses, u.Host)
}
opts.Addresses = addresses
}
if opts.KerberosClient != nil {
opts.KerberosClient, err = getKerberosClient()
if err != nil {
return nil, fmt.Errorf("Unable to initialize kerberos client: %s", err)
}
} else {
opts.User = os.Getenv("HADOOP_USER_NAME")
if opts.User == "" {
u, err := user.Current()
if err != nil {
return nil, fmt.Errorf("Unable to lookup local user: %s", err)
}
opts.User = u.Username
}
}
clnt, err := hdfs.NewClient(opts)
+17 -17
View File
@@ -77,37 +77,37 @@ ENVIRONMENT VARIABLES:
EXAMPLES:
1. Start minio gateway server for AWS S3 backend.
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}accesskey
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}secretkey
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accesskey
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}secretkey
{{.Prompt}} {{.HelpName}}
2. Start minio gateway server for S3 backend on custom endpoint.
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}Q3AM3UQ867SPQQA43P2F
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}Q3AM3UQ867SPQQA43P2F
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
{{.Prompt}} {{.HelpName}} https://play.min.io:9000
3. Start minio gateway server for AWS S3 backend logging all requests to http endpoint.
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}Q3AM3UQ867SPQQA43P2F
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}"http://localhost:8000/"
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}Q3AM3UQ867SPQQA43P2F
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_LOGGER_HTTP_ENDPOINT{{.AssignmentOperator}}"http://localhost:8000/"
{{.Prompt}} {{.HelpName}} https://play.min.io:9000
4. Start minio gateway server for AWS S3 backend with edge caching enabled.
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}accesskey
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}secretkey
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}"/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}"bucket1/*;*.png"
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}40
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}80
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accesskey
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}secretkey
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_DRIVES{{.AssignmentOperator}}"/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXCLUDE{{.AssignmentOperator}}"bucket1/*;*.png"
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXPIRY{{.AssignmentOperator}}40
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_MAXUSE{{.AssignmentOperator}}80
{{.Prompt}} {{.HelpName}}
4. Start minio gateway server for AWS S3 backend using AWS environment variables.
NOTE: The access and secret key in this case will authenticate with MinIO instead
of AWS and AWS envs will be used to authenticate to AWS S3.
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}aws_access_key
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}aws_secret_key
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}accesskey
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}secretkey
{{.Prompt}} {{.EnvVarSetCommand}} AWS_ACCESS_KEY_ID{{.AssignmentOperator}}aws_access_key
{{.Prompt}} {{.EnvVarSetCommand}} AWS_SECRET_ACCESS_KEY{{.AssignmentOperator}}aws_secret_key
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accesskey
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}secretkey
{{.Prompt}} {{.HelpName}}
`
+3 -1
View File
@@ -473,7 +473,6 @@ var notimplementedBucketResourceNames = map[string]bool{
"acl": true,
"cors": true,
"inventory": true,
"lifecycle": true,
"logging": true,
"metrics": true,
"replication": true,
@@ -747,6 +746,9 @@ func setBucketForwardingHandler(h http.Handler) http.Handler {
fwd := handlers.NewForwarder(&handlers.Forwarder{
PassHost: true,
RoundTripper: NewCustomHTTPTransport(),
Logger: func(err error) {
logger.LogIf(context.Background(), err)
},
})
return bucketForwardingHandler{fwd, h}
}
+7
View File
@@ -83,6 +83,11 @@ const (
// GlobalMultipartCleanupInterval - Cleanup interval when the stale multipart cleanup is initiated.
GlobalMultipartCleanupInterval = time.Hour * 24 // 24 hrs.
// GlobalServiceExecutionInterval - Executes the Lifecycle events.
GlobalServiceExecutionInterval = time.Hour * 24 // 24 hrs.
// Refresh interval to update in-memory bucket lifecycle cache.
globalRefreshBucketLifecycleInterval = 5 * time.Minute
// Refresh interval to update in-memory iam config cache.
globalRefreshIAMInterval = 5 * time.Minute
@@ -148,6 +153,8 @@ var (
globalPolicySys *PolicySys
globalIAMSys *IAMSys
globalLifecycleSys *LifecycleSys
// CA root certificates, a nil value means system certs pool will be used
globalRootCAs *x509.CertPool
+4 -9
View File
@@ -31,7 +31,6 @@ import (
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/handlers"
xnet "github.com/minio/minio/pkg/net"
)
// Parses location constraint from the incoming reader.
@@ -387,15 +386,11 @@ func notFoundHandler(w http.ResponseWriter, r *http.Request) {
}
// gets host name for current node
func getHostName(r *http.Request) (hostName string, err error) {
var thisAddr *xnet.Host
hostName = r.Host
func getHostName(r *http.Request) (hostName string) {
if globalIsDistXL {
thisAddr, err = xnet.ParseHost(GetLocalPeer(globalEndpoints))
if err != nil {
return
}
hostName = thisAddr.String()
hostName = GetLocalPeer(globalEndpoints)
} else {
hostName = r.Host
}
return
}
+69 -36
View File
@@ -32,6 +32,8 @@ import (
trace "github.com/minio/minio/pkg/trace"
)
var traceBodyPlaceHolder = []byte("<BODY>")
// recordRequest - records the first recLen bytes
// of a given io.Reader
type recordRequest struct {
@@ -41,10 +43,16 @@ type recordRequest struct {
logBody bool
// Internal recording buffer
buf bytes.Buffer
// request headers
headers http.Header
// total bytes read including header size
bytesRead int
}
func (r *recordRequest) Read(p []byte) (n int, err error) {
n, err = r.Reader.Read(p)
r.bytesRead += n
if r.logBody {
r.buf.Write(p[:n])
}
@@ -53,10 +61,22 @@ func (r *recordRequest) Read(p []byte) (n int, err error) {
}
return n, err
}
func (r *recordRequest) Size() int {
sz := r.bytesRead
for k, v := range r.headers {
sz += len(k) + len(v)
}
return sz
}
// Return the bytes that were recorded.
func (r *recordRequest) Data() []byte {
return r.buf.Bytes()
// If body logging is enabled then we return the actual body
if r.logBody {
return r.buf.Bytes()
}
// ... otherwise we return <BODY> placeholder
return traceBodyPlaceHolder
}
// recordResponseWriter - records the first recLen bytes
@@ -73,13 +93,17 @@ type recordResponseWriter struct {
statusCode int
// Indicate if headers are written in the log
headersLogged bool
// number of bytes written
bytesWritten int
}
// Write the headers into the given buffer
func writeHeaders(w io.Writer, statusCode int, headers http.Header) {
fmt.Fprintf(w, "%d %s\n", statusCode, http.StatusText(statusCode))
func (r *recordResponseWriter) writeHeaders(w io.Writer, statusCode int, headers http.Header) {
n, _ := fmt.Fprintf(w, "%d %s\n", statusCode, http.StatusText(statusCode))
r.bytesWritten += n
for k, v := range headers {
fmt.Fprintf(w, "%s: %s\n", k, v[0])
n, _ := fmt.Fprintf(w, "%s: %s\n", k, v[0])
r.bytesWritten += n
}
}
@@ -87,7 +111,7 @@ func writeHeaders(w io.Writer, statusCode int, headers http.Header) {
func (r *recordResponseWriter) WriteHeader(i int) {
r.statusCode = i
if !r.headersLogged {
writeHeaders(&r.headers, i, r.ResponseWriter.Header())
r.writeHeaders(&r.headers, i, r.ResponseWriter.Header())
r.headersLogged = true
}
r.ResponseWriter.WriteHeader(i)
@@ -95,10 +119,11 @@ func (r *recordResponseWriter) WriteHeader(i int) {
func (r *recordResponseWriter) Write(p []byte) (n int, err error) {
n, err = r.ResponseWriter.Write(p)
r.bytesWritten += n
if !r.headersLogged {
// We assume the response code to be '200 OK' when WriteHeader() is not called,
// that way following Golang HTTP response behavior.
writeHeaders(&r.headers, http.StatusOK, r.ResponseWriter.Header())
r.writeHeaders(&r.headers, http.StatusOK, r.ResponseWriter.Header())
r.headersLogged = true
}
if (r.statusCode != http.StatusOK && r.statusCode != http.StatusPartialContent && r.statusCode != 0) || r.logBody {
@@ -108,6 +133,10 @@ func (r *recordResponseWriter) Write(p []byte) (n int, err error) {
return n, err
}
func (r *recordResponseWriter) Size() int {
return r.bytesWritten
}
// Calls the underlying Flush.
func (r *recordResponseWriter) Flush() {
r.ResponseWriter.(http.Flusher).Flush()
@@ -115,7 +144,13 @@ func (r *recordResponseWriter) Flush() {
// Return response body.
func (r *recordResponseWriter) Body() []byte {
return r.body.Bytes()
// If there was an error response or body logging is enabled
// then we return the body contents
if r.statusCode >= 400 || r.logBody {
return r.body.Bytes()
}
// ... otherwise we return the <BODY> place holder
return traceBodyPlaceHolder
}
// getOpName sanitizes the operation name for mc
@@ -136,14 +171,19 @@ func getOpName(name string) (op string) {
// Trace gets trace of http request
func Trace(f http.HandlerFunc, logBody bool, w http.ResponseWriter, r *http.Request) trace.Info {
name := getOpName(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
bodyPlaceHolder := []byte("<BODY>")
var reqBodyRecorder *recordRequest
// Setup a http request body recorder
reqHeaders := cloneHeader(r.Header)
reqHeaders.Set("Content-Length", strconv.Itoa(int(r.ContentLength)))
reqHeaders.Set("Host", r.Host)
for _, enc := range r.TransferEncoding {
reqHeaders.Add("Transfer-Encoding", enc)
}
var reqBodyRecorder *recordRequest
t := trace.Info{FuncName: name}
reqBodyRecorder = &recordRequest{Reader: r.Body, logBody: logBody}
reqBodyRecorder = &recordRequest{Reader: r.Body, logBody: logBody, headers: reqHeaders}
r.Body = ioutil.NopCloser(reqBodyRecorder)
t.NodeName = r.Host
if globalIsDistXL {
@@ -154,41 +194,34 @@ func Trace(f http.HandlerFunc, logBody bool, w http.ResponseWriter, r *http.Requ
t.NodeName = host
}
rq := trace.RequestInfo{Time: time.Now().UTC(), Method: r.Method, Path: r.URL.Path, RawQuery: r.URL.RawQuery, Client: r.RemoteAddr}
rq.Headers = cloneHeader(r.Header)
rq.Headers.Set("Content-Length", strconv.Itoa(int(r.ContentLength)))
rq.Headers.Set("Host", r.Host)
for _, enc := range r.TransferEncoding {
rq.Headers.Add("Transfer-Encoding", enc)
rq := trace.RequestInfo{
Time: time.Now().UTC(),
Method: r.Method,
Path: r.URL.Path,
RawQuery: r.URL.RawQuery,
Client: r.RemoteAddr,
Headers: reqHeaders,
Body: reqBodyRecorder.Data(),
}
if logBody {
// If body logging is disabled then we print <BODY> as a placeholder
// for the actual body.
rq.Body = reqBodyRecorder.Data()
} else {
rq.Body = bodyPlaceHolder
}
// Setup a http response body recorder
respBodyRecorder := &recordResponseWriter{ResponseWriter: w, logBody: logBody}
f(respBodyRecorder, r)
rs := trace.ResponseInfo{Time: time.Now().UTC()}
rs.Headers = cloneHeader(respBodyRecorder.Header())
rs.StatusCode = respBodyRecorder.statusCode
rs := trace.ResponseInfo{
Time: time.Now().UTC(),
Headers: cloneHeader(respBodyRecorder.Header()),
StatusCode: respBodyRecorder.statusCode,
Body: respBodyRecorder.Body(),
}
if rs.StatusCode == 0 {
rs.StatusCode = http.StatusOK
}
bodyContents := respBodyRecorder.Body()
if bodyContents != nil {
rs.Body = bodyContents
}
if !logBody {
// If there was no error response and body logging is disabled
// then we print <BODY> as a placeholder for the actual body.
rs.Body = bodyPlaceHolder
}
t.ReqInfo = rq
t.RespInfo = rs
t.CallStats = trace.CallStats{Latency: rs.Time.Sub(rq.Time), InputBytes: reqBodyRecorder.Size(), OutputBytes: respBodyRecorder.Size()}
return t
}
+887 -404
View File
File diff suppressed because it is too large Load Diff
+191
View File
@@ -0,0 +1,191 @@
/*
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"bytes"
"context"
"encoding/xml"
"path"
"strings"
"sync"
"time"
"github.com/minio/minio-go/pkg/set"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/lifecycle"
)
const (
// Disabled means the lifecycle rule is inactive
Disabled = "Disabled"
)
// LifecycleSys - Bucket lifecycle subsystem.
type LifecycleSys struct {
sync.RWMutex
bucketLifecycleMap map[string]lifecycle.Lifecycle
}
// Set - sets lifecycle config to given bucket name.
func (sys *LifecycleSys) Set(bucketName string, lifecycle lifecycle.Lifecycle) {
sys.Lock()
defer sys.Unlock()
sys.bucketLifecycleMap[bucketName] = lifecycle
}
func saveLifecycleConfig(ctx context.Context, objAPI ObjectLayer, bucketName string, bucketLifecycle *lifecycle.Lifecycle) error {
data, err := xml.Marshal(bucketLifecycle)
if err != nil {
return err
}
// Construct path to lifecycle.xml for the given bucket.
configFile := path.Join(bucketConfigPrefix, bucketName, bucketLifecycleConfig)
return saveConfig(ctx, objAPI, configFile, data)
}
// getLifecycleConfig - get lifecycle config for given bucket name.
func getLifecycleConfig(objAPI ObjectLayer, bucketName string) (*lifecycle.Lifecycle, error) {
// Construct path to lifecycle.xml for the given bucket.
configFile := path.Join(bucketConfigPrefix, bucketName, bucketLifecycleConfig)
configData, err := readConfig(context.Background(), objAPI, configFile)
if err != nil {
if err == errConfigNotFound {
err = BucketLifecycleNotFound{Bucket: bucketName}
}
return nil, err
}
return lifecycle.ParseLifecycleConfig(bytes.NewReader(configData))
}
func removeLifecycleConfig(ctx context.Context, objAPI ObjectLayer, bucketName string) error {
// Construct path to lifecycle.xml for the given bucket.
configFile := path.Join(bucketConfigPrefix, bucketName, bucketLifecycleConfig)
if err := objAPI.DeleteObject(ctx, minioMetaBucket, configFile); err != nil {
if _, ok := err.(ObjectNotFound); ok {
return BucketLifecycleNotFound{Bucket: bucketName}
}
return err
}
return nil
}
// NewLifecycleSys - creates new lifecycle system.
func NewLifecycleSys() *LifecycleSys {
return &LifecycleSys{
bucketLifecycleMap: make(map[string]lifecycle.Lifecycle),
}
}
// Init - initializes lifecycle system from lifecycle.xml of all buckets.
func (sys *LifecycleSys) Init(objAPI ObjectLayer) error {
if objAPI == nil {
return errServerNotInitialized
}
defer func() {
// Refresh LifecycleSys in background.
go func() {
ticker := time.NewTicker(globalRefreshBucketLifecycleInterval)
defer ticker.Stop()
for {
select {
case <-GlobalServiceDoneCh:
return
case <-ticker.C:
sys.refresh(objAPI)
}
}
}()
}()
doneCh := make(chan struct{})
defer close(doneCh)
// Initializing lifecycle needs a retry mechanism for
// the following reasons:
// - Read quorum is lost just after the initialization
// of the object layer.
for range newRetryTimerSimple(doneCh) {
// Load LifecycleSys once during boot.
if err := sys.refresh(objAPI); err != nil {
if err == errDiskNotFound ||
strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) ||
strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) {
logger.Info("Waiting for lifecycle subsystem to be initialized..")
continue
}
return err
}
break
}
return nil
}
// Refresh LifecycleSys.
func (sys *LifecycleSys) refresh(objAPI ObjectLayer) error {
buckets, err := objAPI.ListBuckets(context.Background())
if err != nil {
logger.LogIf(context.Background(), err)
return err
}
sys.removeDeletedBuckets(buckets)
for _, bucket := range buckets {
config, err := objAPI.GetBucketLifecycle(context.Background(), bucket.Name)
if err != nil {
if _, ok := err.(BucketLifecycleNotFound); ok {
sys.Remove(bucket.Name)
}
continue
}
sys.Set(bucket.Name, *config)
}
return nil
}
// removeDeletedBuckets - to handle a corner case where we have cached the lifecycle for a deleted
// bucket. i.e if we miss a delete-bucket notification we should delete the corresponding
// bucket policy during sys.refresh()
func (sys *LifecycleSys) removeDeletedBuckets(bucketInfos []BucketInfo) {
buckets := set.NewStringSet()
for _, info := range bucketInfos {
buckets.Add(info.Name)
}
sys.Lock()
defer sys.Unlock()
for bucket := range sys.bucketLifecycleMap {
if !buckets.Contains(bucket) {
delete(sys.bucketLifecycleMap, bucket)
}
}
}
// Remove - removes policy for given bucket name.
func (sys *LifecycleSys) Remove(bucketName string) {
sys.Lock()
defer sys.Unlock()
delete(sys.bucketLifecycleMap, bucketName)
}
+1
View File
@@ -331,6 +331,7 @@ func logIf(ctx context.Context, err error) {
DeploymentID: req.DeploymentID,
Level: ErrorLvl.String(),
RemoteHost: req.RemoteHost,
Host: req.Host,
RequestID: req.RequestID,
UserAgent: req.UserAgent,
Time: time.Now().UTC().Format(time.RFC3339Nano),
+1
View File
@@ -43,6 +43,7 @@ type Entry struct {
Time string `json:"time"`
API *API `json:"api,omitempty"`
RemoteHost string `json:"remotehost,omitempty"`
Host string `json:"host,omitempty"`
RequestID string `json:"requestID,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
Message string `json:"message,omitempty"`
+1
View File
@@ -36,6 +36,7 @@ type KeyVal struct {
// ReqInfo stores the request info.
type ReqInfo struct {
RemoteHost string // Client Host/IP
Host string // Node Host/IP
UserAgent string // User Agent
DeploymentID string // x-minio-deployment-id
RequestID string // x-amz-request-id
+7 -2
View File
@@ -89,6 +89,11 @@ func (c *Target) Send(e interface{}) error {
remoteHost = "\nRemoteHost: " + entry.RemoteHost
}
var host string
if entry.Host != "" {
host = "\nHost: " + entry.Host
}
var userAgent string
if entry.UserAgent != "" {
userAgent = "\nUserAgent: " + entry.UserAgent
@@ -99,8 +104,8 @@ func (c *Target) Send(e interface{}) error {
}
var msg = logger.ColorFgRed(logger.ColorBold(entry.Trace.Message))
var output = fmt.Sprintf("\n%s\n%s%s%s%s%s\nError: %s%s\n%s",
apiString, timeString, deploymentID, requestID, remoteHost, userAgent,
var output = fmt.Sprintf("\n%s\n%s%s%s%s%s%s\nError: %s%s\n%s",
apiString, timeString, deploymentID, requestID, remoteHost, host, userAgent,
msg, tagString, strings.Join(trace, "\n"))
fmt.Println(output)
+2 -2
View File
@@ -196,9 +196,9 @@ func (d *naughtyDisk) ReadAll(volume string, path string) (buf []byte, err error
return d.disk.ReadAll(volume, path)
}
func (d *naughtyDisk) VerifyFile(volume, path string, algo BitrotAlgorithm, sum []byte, shardSize int64) error {
func (d *naughtyDisk) VerifyFile(volume, path string, empty bool, algo BitrotAlgorithm, sum []byte, shardSize int64) error {
if err := d.calcError(); err != nil {
return err
}
return d.disk.VerifyFile(volume, path, algo, sum, shardSize)
return d.disk.VerifyFile(volume, path, empty, algo, sum, shardSize)
}
+43
View File
@@ -34,6 +34,7 @@ import (
"github.com/minio/minio/cmd/crypto"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/lifecycle"
"github.com/minio/minio/pkg/madmin"
xnet "github.com/minio/minio/pkg/net"
"github.com/minio/minio/pkg/policy"
@@ -498,6 +499,48 @@ func (sys *NotificationSys) RemoveBucketPolicy(ctx context.Context, bucketName s
}()
}
// SetBucketLifecycle - calls SetBucketLifecycle on all peers.
func (sys *NotificationSys) SetBucketLifecycle(ctx context.Context, bucketName string, bucketLifecycle *lifecycle.Lifecycle) {
go func() {
var wg sync.WaitGroup
for _, client := range sys.peerClients {
if client == nil {
continue
}
wg.Add(1)
go func(client *peerRESTClient) {
defer wg.Done()
if err := client.SetBucketLifecycle(bucketName, bucketLifecycle); err != nil {
logger.GetReqInfo(ctx).AppendTags("remotePeer", client.host.Name)
logger.LogIf(ctx, err)
}
}(client)
}
wg.Wait()
}()
}
// RemoveBucketLifecycle - calls RemoveLifecycle on all peers.
func (sys *NotificationSys) RemoveBucketLifecycle(ctx context.Context, bucketName string) {
go func() {
var wg sync.WaitGroup
for _, client := range sys.peerClients {
if client == nil {
continue
}
wg.Add(1)
go func(client *peerRESTClient) {
defer wg.Done()
if err := client.RemoveBucketLifecycle(bucketName); err != nil {
logger.GetReqInfo(ctx).AppendTags("remotePeer", client.host.Name)
logger.LogIf(ctx, err)
}
}(client)
}
wg.Wait()
}()
}
// PutBucketNotification - calls PutBucketNotification RPC call on all peers.
func (sys *NotificationSys) PutBucketNotification(ctx context.Context, bucketName string, rulesMap event.RulesMap) {
go func() {
+1 -1
View File
@@ -129,7 +129,7 @@ func cleanupDir(ctx context.Context, storage StorageAPI, volume, dirPath string)
// Entry path is empty, just delete it.
if len(entries) == 0 {
err = storage.DeleteFile(volume, path.Clean(entryPath))
err = storage.DeleteFile(volume, entryPath)
logger.LogIf(ctx, err)
return err
}
+23
View File
@@ -254,6 +254,13 @@ func (e BucketPolicyNotFound) Error() string {
return "No bucket policy found for bucket: " + e.Bucket
}
// BucketLifecycleNotFound - no bucket lifecycle found.
type BucketLifecycleNotFound GenericError
func (e BucketLifecycleNotFound) Error() string {
return "No bucket life cycle found for bucket : " + e.Bucket
}
/// Bucket related errors.
// BucketNameInvalid - bucketname provided is invalid.
@@ -269,11 +276,27 @@ func (e BucketNameInvalid) Error() string {
// ObjectNameInvalid - object name provided is invalid.
type ObjectNameInvalid GenericError
// ObjectNameTooLong - object name too long.
type ObjectNameTooLong GenericError
// ObjectNamePrefixAsSlash - object name has a slash as prefix.
type ObjectNamePrefixAsSlash GenericError
// Return string an error formatted as the given text.
func (e ObjectNameInvalid) Error() string {
return "Object name invalid: " + e.Bucket + "#" + e.Object
}
// Return string an error formatted as the given text.
func (e ObjectNameTooLong) Error() string {
return "Object name too long: " + e.Bucket + "#" + e.Object
}
// Return string an error formatted as the given text.
func (e ObjectNamePrefixAsSlash) Error() string {
return "Object name contains forward slash as pefix: " + e.Bucket + "#" + e.Object
}
// AllAccessDisabled All access to this object has been disabled
type AllAccessDisabled GenericError
+8 -6
View File
@@ -166,18 +166,18 @@ func checkObjectArgs(ctx context.Context, bucket, object string, obj ObjectLayer
if err := checkBucketExist(ctx, bucket, obj); err != nil {
return err
}
if err := checkObjectNameForLengthAndSlash(bucket, object); err != nil {
return err
}
// Validates object name validity after bucket exists.
if !IsValidObjectName(object) {
logger.LogIf(ctx, ObjectNameInvalid{
Bucket: bucket,
Object: object,
})
return ObjectNameInvalid{
Bucket: bucket,
Object: object,
}
}
return nil
}
@@ -192,8 +192,10 @@ func checkPutObjectArgs(ctx context.Context, bucket, object string, obj ObjectLa
return err
}
if err := checkObjectNameForLengthAndSlash(bucket, object); err != nil {
return err
}
if len(object) == 0 ||
hasPrefix(object, slashSeparator) ||
(hasSuffix(object, slashSeparator) && size != 0) ||
!IsValidObjectPrefix(object) {
return ObjectNameInvalid{
+6
View File
@@ -22,6 +22,7 @@ import (
"net/http"
"github.com/minio/minio-go/v6/pkg/encrypt"
"github.com/minio/minio/pkg/lifecycle"
"github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/policy"
)
@@ -107,4 +108,9 @@ type ObjectLayer interface {
// Compression support check.
IsCompressionSupported() bool
// Lifecycle operations
SetBucketLifecycle(context.Context, string, *lifecycle.Lifecycle) error
GetBucketLifecycle(context.Context, string) (*lifecycle.Lifecycle, error)
DeleteBucketLifecycle(context.Context, string) error
}
+20 -4
View File
@@ -136,7 +136,7 @@ func IsValidObjectName(object string) bool {
if len(object) == 0 {
return false
}
if hasSuffix(object, slashSeparator) || hasPrefix(object, slashSeparator) {
if hasSuffix(object, slashSeparator) {
return false
}
return IsValidObjectPrefix(object)
@@ -148,9 +148,6 @@ func IsValidObjectPrefix(object string) bool {
if hasBadPathComponent(object) {
return false
}
if len(object) > 1024 {
return false
}
if !utf8.ValidString(object) {
return false
}
@@ -161,6 +158,25 @@ func IsValidObjectPrefix(object string) bool {
return true
}
// checkObjectNameForLengthAndSlash -check for the validity of object name length and prefis as slash
func checkObjectNameForLengthAndSlash(bucket, object string) error {
// Check for the length of object name
if len(object) > 1024 {
return ObjectNameTooLong{
Bucket: bucket,
Object: object,
}
}
// Check for slash as prefix in object name
if hasPrefix(object, slashSeparator) {
return ObjectNamePrefixAsSlash{
Bucket: bucket,
Object: object,
}
}
return nil
}
// Slash separator.
const slashSeparator = "/"
-1
View File
@@ -113,7 +113,6 @@ func TestIsValidObjectName(t *testing.T) {
// passing invalid object names.
{"", false},
{"a/b/c/", false},
{"/a/b/c", false},
{"../../etc", false},
{"../../", false},
{"/../../etc", false},
+1
View File
@@ -2280,6 +2280,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
w.Write(encodedErrorResponse)
w.(http.Flusher).Flush()
}
w.Header().Set(xhttp.ContentType, "text/event-stream")
w = &whiteSpaceWriter{ResponseWriter: w, Flusher: w.(http.Flusher)}
completeDoneCh := sendWhiteSpace(ctx, w)
objInfo, err := completeMultiPartUpload(ctx, bucket, object, uploadID, completeParts, opts)
+3 -3
View File
@@ -156,11 +156,11 @@ func renameAll(srcFilePath, dstFilePath string) (err error) {
// Reliably retries os.RenameAll if for some reason os.RenameAll returns
// syscall.ENOENT (parent does not exist).
func reliableRename(srcFilePath, dstFilePath string) (err error) {
if err = reliableMkdirAll(path.Dir(dstFilePath), 0777); err != nil {
return err
}
i := 0
for {
if err = reliableMkdirAll(path.Dir(dstFilePath), 0777); err != nil {
return err
}
// After a successful parent directory create attempt a renameAll.
if err = os.Rename(srcFilePath, dstFilePath); err != nil {
// Retry only for the first retryable error.
+42 -7
View File
@@ -30,6 +30,7 @@ import (
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/cmd/rest"
"github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/lifecycle"
"github.com/minio/minio/pkg/madmin"
xnet "github.com/minio/minio/pkg/net"
"github.com/minio/minio/pkg/policy"
@@ -329,6 +330,37 @@ func (client *peerRESTClient) SetBucketPolicy(bucket string, bucketPolicy *polic
return nil
}
// RemoveBucketLifecycle - Remove bucket lifecycle configuration on the peer node
func (client *peerRESTClient) RemoveBucketLifecycle(bucket string) error {
values := make(url.Values)
values.Set(peerRESTBucket, bucket)
respBody, err := client.call(peerRESTMethodBucketLifecycleRemove, values, nil, -1)
if err != nil {
return err
}
defer http.DrainBody(respBody)
return nil
}
// SetBucketLifecycle - Set bucket lifecycle configuration on the peer node
func (client *peerRESTClient) SetBucketLifecycle(bucket string, bucketLifecycle *lifecycle.Lifecycle) error {
values := make(url.Values)
values.Set(peerRESTBucket, bucket)
var reader bytes.Buffer
err := gob.NewEncoder(&reader).Encode(bucketLifecycle)
if err != nil {
return err
}
respBody, err := client.call(peerRESTMethodBucketLifecycleSet, values, &reader, -1)
if err != nil {
return err
}
defer http.DrainBody(respBody)
return nil
}
// PutBucketNotification - Put bucket notification on the peer node.
func (client *peerRESTClient) PutBucketNotification(bucket string, rulesMap event.RulesMap) error {
values := make(url.Values)
@@ -435,9 +467,10 @@ func (client *peerRESTClient) BackgroundHealStatus() (madmin.BgHealState, error)
return state, err
}
func (client *peerRESTClient) doTrace(traceCh chan interface{}, doneCh chan struct{}, trcAll bool) {
func (client *peerRESTClient) doTrace(traceCh chan interface{}, doneCh chan struct{}, trcAll, trcErr bool) {
values := make(url.Values)
values.Set(peerRESTTraceAll, strconv.FormatBool(trcAll))
values.Set(peerRESTTraceErr, strconv.FormatBool(trcErr))
// To cancel the REST request in case doneCh gets closed.
ctx, cancel := context.WithCancel(context.Background())
@@ -466,19 +499,21 @@ func (client *peerRESTClient) doTrace(traceCh chan interface{}, doneCh chan stru
if err = dec.Decode(&info); err != nil {
return
}
select {
case traceCh <- info:
default:
// Do not block on slow receivers.
if len(info.NodeName) > 0 {
select {
case traceCh <- info:
default:
// Do not block on slow receivers.
}
}
}
}
// Trace - send http trace request to peer nodes
func (client *peerRESTClient) Trace(traceCh chan interface{}, doneCh chan struct{}, trcAll bool) {
func (client *peerRESTClient) Trace(traceCh chan interface{}, doneCh chan struct{}, trcAll, trcErr bool) {
go func() {
for {
client.doTrace(traceCh, doneCh, trcAll)
client.doTrace(traceCh, doneCh, trcAll, trcErr)
select {
case <-doneCh:
return
+3
View File
@@ -43,6 +43,8 @@ const (
peerRESTMethodTargetExists = "targetexists"
peerRESTMethodSendEvent = "sendevent"
peerRESTMethodTrace = "trace"
peerRESTMethodBucketLifecycleSet = "setbucketlifecycle"
peerRESTMethodBucketLifecycleRemove = "removebucketlifecycle"
)
const (
@@ -54,4 +56,5 @@ const (
peerRESTProfiler = "profiler"
peerRESTDryRun = "dry-run"
peerRESTTraceAll = "all"
peerRESTTraceErr = "err"
)
+57 -13
View File
@@ -28,9 +28,9 @@ import (
"time"
"github.com/gorilla/mux"
xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/event"
"github.com/minio/minio/pkg/lifecycle"
xnet "github.com/minio/minio/pkg/net"
"github.com/minio/minio/pkg/policy"
trace "github.com/minio/minio/pkg/trace"
@@ -469,6 +469,47 @@ func (s *peerRESTServer) SetBucketPolicyHandler(w http.ResponseWriter, r *http.R
w.(http.Flusher).Flush()
}
// RemoveBucketLifecycleHandler - Remove bucket lifecycle.
func (s *peerRESTServer) RemoveBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
if !s.IsValid(w, r) {
s.writeErrorResponse(w, errors.New("Invalid request"))
return
}
vars := mux.Vars(r)
bucketName := vars[peerRESTBucket]
if bucketName == "" {
s.writeErrorResponse(w, errors.New("Bucket name is missing"))
return
}
globalLifecycleSys.Remove(bucketName)
w.(http.Flusher).Flush()
}
// SetBucketLifecycleHandler - Set bucket lifecycle.
func (s *peerRESTServer) SetBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bucketName := vars[peerRESTBucket]
if bucketName == "" {
s.writeErrorResponse(w, errors.New("Bucket name is missing"))
return
}
var lifecycleData lifecycle.Lifecycle
if r.ContentLength < 0 {
s.writeErrorResponse(w, errInvalidArgument)
return
}
err := gob.NewDecoder(r.Body).Decode(&lifecycleData)
if err != nil {
s.writeErrorResponse(w, err)
return
}
globalLifecycleSys.Set(bucketName, lifecycleData)
w.(http.Flusher).Flush()
}
type remoteTargetExistsResp struct {
Exists bool
}
@@ -675,26 +716,24 @@ func (s *peerRESTServer) TraceHandler(w http.ResponseWriter, r *http.Request) {
return
}
trcAll := r.URL.Query().Get(peerRESTTraceAll) == "true"
trcErr := r.URL.Query().Get(peerRESTTraceErr) == "true"
w.Header().Set(xhttp.Connection, "close")
w.WriteHeader(http.StatusOK)
w.(http.Flusher).Flush()
filter := func(entry interface{}) bool {
if trcAll {
return true
}
trcInfo := entry.(trace.Info)
return !strings.HasPrefix(trcInfo.ReqInfo.Path, minioReservedBucketPath)
}
doneCh := make(chan struct{})
defer close(doneCh)
// Trace Publisher uses nonblocking publish and hence does not wait for slow subscribers.
// Use buffered channel to take care of burst sends or slow w.Write()
ch := make(chan interface{}, 2000)
globalHTTPTrace.Subscribe(ch, doneCh, filter)
globalHTTPTrace.Subscribe(ch, doneCh, func(entry interface{}) bool {
return mustTrace(entry, trcAll, trcErr)
})
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
defer keepAliveTicker.Stop()
enc := gob.NewEncoder(w)
for {
@@ -704,8 +743,11 @@ func (s *peerRESTServer) TraceHandler(w http.ResponseWriter, r *http.Request) {
return
}
w.(http.Flusher).Flush()
case <-r.Context().Done():
return
case <-keepAliveTicker.C:
if err := enc.Encode(&trace.Info{}); err != nil {
return
}
w.(http.Flusher).Flush()
}
}
}
@@ -768,6 +810,8 @@ func registerPeerRESTHandlers(router *mux.Router) {
subrouter.Methods(http.MethodPost).Path("/" + peerRESTMethodBucketNotificationListen).HandlerFunc(httpTraceHdrs(server.ListenBucketNotificationHandler)).Queries(restQueries(peerRESTBucket)...)
subrouter.Methods(http.MethodPost).Path("/" + peerRESTMethodReloadFormat).HandlerFunc(httpTraceHdrs(server.ReloadFormatHandler)).Queries(restQueries(peerRESTDryRun)...)
subrouter.Methods(http.MethodPost).Path("/" + peerRESTMethodBucketLifecycleSet).HandlerFunc(httpTraceHdrs(server.SetBucketLifecycleHandler)).Queries(restQueries(peerRESTBucket)...)
subrouter.Methods(http.MethodPost).Path("/" + peerRESTMethodBucketLifecycleRemove).HandlerFunc(httpTraceHdrs(server.RemoveBucketLifecycleHandler)).Queries(restQueries(peerRESTBucket)...)
subrouter.Methods(http.MethodPost).Path("/" + peerRESTMethodTrace).HandlerFunc(server.TraceHandler)
subrouter.Methods(http.MethodPost).Path("/" + peerRESTMethodBackgroundHealStatus).HandlerFunc(server.BackgroundHealStatusHandler)
+30 -5
View File
@@ -214,8 +214,13 @@ func getDiskInfo(diskPath string) (di disk.Info, err error) {
di, err = disk.GetInfo(diskPath)
}
if os.IsNotExist(err) {
switch {
case os.IsNotExist(err):
err = errDiskNotFound
case isSysErrTooLong(err):
err = errFileNameTooLong
case isSysErrIO(err):
err = errFaultyDisk
}
return di, err
@@ -285,6 +290,9 @@ func (s *posix) String() string {
}
func (s *posix) LastError() error {
if atomic.LoadInt32(&s.ioErrCount) > maxAllowedIOError {
return errFaultyDisk
}
return nil
}
@@ -310,10 +318,21 @@ type DiskInfo struct {
// DiskInfo provides current information about disk space usage,
// total free inodes and underlying filesystem.
func (s *posix) DiskInfo() (info DiskInfo, err error) {
defer func() {
if err == errFaultyDisk {
atomic.AddInt32(&s.ioErrCount, 1)
}
}()
if atomic.LoadInt32(&s.ioErrCount) > maxAllowedIOError {
return info, errFaultyDisk
}
di, err := getDiskInfo(s.diskPath)
if err != nil {
return info, err
}
used := di.Total - di.Free
if !s.diskMount {
used = atomic.LoadUint64(&s.totalUsed)
@@ -323,6 +342,7 @@ func (s *posix) DiskInfo() (info DiskInfo, err error) {
if err != nil {
return info, err
}
return DiskInfo{
Total: di.Total,
Free: di.Free,
@@ -1526,7 +1546,7 @@ func (s *posix) RenameFile(srcVolume, srcPath, dstVolume, dstPath string) (err e
return nil
}
func (s *posix) VerifyFile(volume, path string, algo BitrotAlgorithm, sum []byte, shardSize int64) (err error) {
func (s *posix) VerifyFile(volume, path string, empty bool, algo BitrotAlgorithm, sum []byte, shardSize int64) (err error) {
defer func() {
if err == errFaultyDisk {
atomic.AddInt32(&s.ioErrCount, 1)
@@ -1546,7 +1566,7 @@ func (s *posix) VerifyFile(volume, path string, algo BitrotAlgorithm, sum []byte
return err
}
// Stat a volume entry.
_, err = os.Stat((volumeDir))
_, err = os.Stat(volumeDir)
if err != nil {
if os.IsNotExist(err) {
return errVolumeNotFound
@@ -1556,12 +1576,12 @@ func (s *posix) VerifyFile(volume, path string, algo BitrotAlgorithm, sum []byte
// Validate effective path length before reading.
filePath := pathJoin(volumeDir, path)
if err = checkPathLength((filePath)); err != nil {
if err = checkPathLength(filePath); err != nil {
return err
}
// Open the file for reading.
file, err := os.Open((filePath))
file, err := os.Open(filePath)
if err != nil {
switch {
case os.IsNotExist(err):
@@ -1601,6 +1621,11 @@ func (s *posix) VerifyFile(volume, path string, algo BitrotAlgorithm, sum []byte
if err != nil {
return err
}
if empty && fi.Size() != 0 || !empty && fi.Size() == 0 {
return errFileUnexpectedSize
}
size := fi.Size()
for {
if size == 0 {
+5 -7
View File
@@ -1797,7 +1797,7 @@ func TestPosixVerifyFile(t *testing.T) {
if err := posixStorage.WriteAll(volName, fileName, bytes.NewBuffer(data)); err != nil {
t.Fatal(err)
}
if err := posixStorage.VerifyFile(volName, fileName, algo, hashBytes, 0); err != nil {
if err := posixStorage.VerifyFile(volName, fileName, false, algo, hashBytes, 0); err != nil {
t.Fatal(err)
}
@@ -1805,7 +1805,7 @@ func TestPosixVerifyFile(t *testing.T) {
if err := posixStorage.AppendFile(volName, fileName, []byte("a")); err != nil {
t.Fatal(err)
}
if err := posixStorage.VerifyFile(volName, fileName, algo, hashBytes, 0); err == nil {
if err := posixStorage.VerifyFile(volName, fileName, false, algo, hashBytes, 0); err == nil {
t.Fatal("expected to fail bitrot check")
}
@@ -1830,12 +1830,10 @@ func TestPosixVerifyFile(t *testing.T) {
if err == io.EOF {
break
}
if err != nil {
t.Fatal(err)
}
t.Fatal(err)
}
w.Close()
if err := posixStorage.VerifyFile(volName, fileName, algo, nil, shardSize); err != nil {
if err := posixStorage.VerifyFile(volName, fileName, false, algo, nil, shardSize); err != nil {
t.Fatal(err)
}
@@ -1849,7 +1847,7 @@ func TestPosixVerifyFile(t *testing.T) {
t.Fatal(err)
}
f.Close()
if err := posixStorage.VerifyFile(volName, fileName, algo, nil, shardSize); err == nil {
if err := posixStorage.VerifyFile(volName, fileName, false, algo, nil, shardSize); err == nil {
t.Fatal("expected to fail bitrot check")
}
}
+2 -1
View File
@@ -109,7 +109,7 @@ type PostPolicyForm struct {
}
}
// parsePostPolicyForm - Parse JSON policy string into typed POostPolicyForm structure.
// parsePostPolicyForm - Parse JSON policy string into typed PostPolicyForm structure.
func parsePostPolicyForm(policy string) (ppf PostPolicyForm, e error) {
// Convert po into interfaces and
// perform strict type conversion using reflection.
@@ -256,6 +256,7 @@ func checkPostPolicy(formValues http.Header, postPolicyForm PostPolicyForm) erro
return fmt.Errorf("Invalid according to Policy: Policy Condition failed")
}
// Check if current policy condition is satisfied
condPassed = checkPolicyCond(op, formValues.Get(formCanonicalName), v.Value)
if !condPassed {
return fmt.Errorf("Invalid according to Policy: Policy Condition failed")
}
+13 -4
View File
@@ -19,7 +19,6 @@ package cmd
import (
"context"
"encoding/gob"
"errors"
"fmt"
"net/http"
"os"
@@ -365,6 +364,14 @@ func serverMain(ctx *cli.Context) {
logger.Fatal(err, "Unable to initialize policy system")
}
// Create new lifecycle system.
globalLifecycleSys = NewLifecycleSys()
// Initialize lifecycle system.
if err = globalLifecycleSys.Init(newObject); err != nil {
logger.Fatal(err, "Unable to initialize lifecycle system")
}
// Create new notification system.
globalNotificationSys = NewNotificationSys(globalServerConfig, globalEndpoints)
@@ -372,9 +379,11 @@ func serverMain(ctx *cli.Context) {
if err = globalNotificationSys.Init(newObject); err != nil {
logger.LogIf(context.Background(), err)
}
if globalAutoEncryption && !newObject.IsEncryptionSupported() {
logger.Fatal(errors.New("Invalid KMS configuration"), "auto-encryption is enabled but server does not support encryption")
}
// Verify if object layer supports
// - encryption
// - compression
verifyObjectLayerFeatures("server", newObject)
if globalIsXL {
initBackgroundHealing()
+33 -3
View File
@@ -1177,7 +1177,7 @@ func (s *TestSuiteCommon) TestPutObject(c *check) {
c.Assert(response.StatusCode, http.StatusOK)
c.Assert(response.ContentLength, int64(len([]byte("hello world"))))
var buffer2 bytes.Buffer
// retrive the contents of response body.
// retrieve the contents of response body.
n, err := io.Copy(&buffer2, response.Body)
c.Assert(err, nil)
c.Assert(n, int64(len([]byte("hello world"))))
@@ -1354,7 +1354,37 @@ func (s *TestSuiteCommon) TestPutObjectLongName(c *check) {
response, err = client.Do(request)
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusOK)
// make long object name.
//make long object name.
longObjName = fmt.Sprintf("%0255d/%0255d/%0255d/%0255d/%0255d", 1, 1, 1, 1, 1)
if IsDocker() || IsKubernetes() {
longObjName = fmt.Sprintf("%0242d/%0242d/%0242d/%0242d/%0242d", 1, 1, 1, 1, 1)
}
// create new HTTP request to insert the object.
buffer = bytes.NewReader([]byte("hello world"))
request, err = newTestSignedRequest("PUT", getPutObjectURL(s.endPoint, bucketName, longObjName),
int64(buffer.Len()), buffer, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
// execute the HTTP request.
response, err = client.Do(request)
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusBadRequest)
verifyError(c, response, "KeyTooLongError", "Your key is too long", http.StatusBadRequest)
// make object name with prefix as slash
longObjName = fmt.Sprintf("/%0255d/%0255d", 1, 1)
buffer = bytes.NewReader([]byte("hello world"))
// create new HTTP request to insert the object.
request, err = newTestSignedRequest("PUT", getPutObjectURL(s.endPoint, bucketName, longObjName),
int64(buffer.Len()), buffer, s.accessKey, s.secretKey, s.signer)
c.Assert(err, nil)
// execute the HTTP request.
response, err = client.Do(request)
c.Assert(err, nil)
c.Assert(response.StatusCode, http.StatusBadRequest)
verifyError(c, response, "XMinioInvalidObjectName", "Object name contains a leading slash.", http.StatusBadRequest)
//make object name as unsuported
longObjName = fmt.Sprintf("%0256d", 1)
buffer = bytes.NewReader([]byte("hello world"))
request, err = newTestSignedRequest("PUT", getPutObjectURL(s.endPoint, bucketName, longObjName),
@@ -1431,7 +1461,7 @@ func (s *TestSuiteCommon) TestHeadOnObjectLastModified(c *check) {
// verify the status of the HTTP response.
c.Assert(response.StatusCode, http.StatusOK)
// retrive the info of last modification time of the object from the response header.
// retrieve the info of last modification time of the object from the response header.
lastModified := response.Header.Get("Last-Modified")
// Parse it into time.Time structure.
t, err := time.Parse(http.TimeFormat, lastModified)
+3
View File
@@ -72,6 +72,9 @@ var errVolumeAccessDenied = errors.New("volume access denied")
// errFileAccessDenied - cannot access file, insufficient permissions.
var errFileAccessDenied = errors.New("file access denied")
// errFileUnexpectedSize - file has an unexpected size
var errFileUnexpectedSize = errors.New("file has unexpected size")
// errFileParentIsFile - cannot have overlapping objects, parent is already a file.
var errFileParentIsFile = errors.New("parent is a file")
+1 -1
View File
@@ -52,7 +52,7 @@ type StorageAPI interface {
StatFile(volume string, path string) (file FileInfo, err error)
DeleteFile(volume string, path string) (err error)
DeleteFileBulk(volume string, paths []string) (errs []error, err error)
VerifyFile(volume, path string, algo BitrotAlgorithm, sum []byte, shardSize int64) error
VerifyFile(volume, path string, empty bool, algo BitrotAlgorithm, sum []byte, shardSize int64) error
// Write all data, syncs the data to disk.
WriteAll(volume string, path string, reader io.Reader) (err error)
+6 -3
View File
@@ -44,8 +44,8 @@ func isNetworkError(err error) bool {
if err.Error() == errConnectionStale.Error() {
return true
}
if _, ok := err.(*rest.NetworkError); ok {
return true
if nerr, ok := err.(*rest.NetworkError); ok {
return isNetworkOrHostDown(nerr.Err)
}
return false
}
@@ -67,6 +67,8 @@ func toStorageErr(err error) error {
return io.EOF
case io.ErrUnexpectedEOF.Error():
return io.ErrUnexpectedEOF
case errFileUnexpectedSize.Error():
return errFileUnexpectedSize
case errUnexpected.Error():
return errUnexpected
case errDiskFull.Error():
@@ -434,11 +436,12 @@ func (client *storageRESTClient) getInstanceID() (err error) {
return nil
}
func (client *storageRESTClient) VerifyFile(volume, path string, algo BitrotAlgorithm, sum []byte, shardSize int64) error {
func (client *storageRESTClient) VerifyFile(volume, path string, empty bool, algo BitrotAlgorithm, sum []byte, shardSize int64) error {
values := make(url.Values)
values.Set(storageRESTVolume, volume)
values.Set(storageRESTFilePath, path)
values.Set(storageRESTBitrotAlgo, algo.String())
values.Set(storageRESTEmpty, strconv.FormatBool(empty))
values.Set(storageRESTLength, strconv.Itoa(int(shardSize)))
if len(sum) != 0 {
values.Set(storageRESTBitrotHash, hex.EncodeToString(sum))
+2 -1
View File
@@ -16,7 +16,7 @@
package cmd
const storageRESTVersion = "v7"
const storageRESTVersion = "v8"
const storageRESTPath = minioReservedBucketPath + "/storage/" + storageRESTVersion + "/"
const (
@@ -52,6 +52,7 @@ const (
storageRESTDstPath = "destination-path"
storageRESTOffset = "offset"
storageRESTLength = "length"
storageRESTEmpty = "empty"
storageRESTCount = "count"
storageRESTMarkerPath = "marker"
storageRESTLeafFile = "leaf-file"
+7 -1
View File
@@ -520,6 +520,11 @@ func (s *storageRESTServer) VerifyFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
volume := vars[storageRESTVolume]
filePath := vars[storageRESTFilePath]
empty, err := strconv.ParseBool(vars[storageRESTEmpty])
if err != nil {
s.writeErrorResponse(w, err)
return
}
shardSize, err := strconv.Atoi(vars[storageRESTLength])
if err != nil {
s.writeErrorResponse(w, err)
@@ -540,8 +545,9 @@ func (s *storageRESTServer) VerifyFile(w http.ResponseWriter, r *http.Request) {
return
}
algo := BitrotAlgorithmFromString(algoStr)
w.Header().Set(xhttp.ContentType, "text/event-stream")
doneCh := sendWhiteSpaceVerifyFile(w)
err = s.storage.VerifyFile(volume, filePath, algo, hash, int64(shardSize))
err = s.storage.VerifyFile(volume, filePath, empty, algo, hash, int64(shardSize))
<-doneCh
gob.NewEncoder(w).Encode(VerifyFileResp{err})
}
+1 -1
View File
@@ -183,7 +183,7 @@ func (sts *stsAPIHandlers) AssumeRole(w http.ResponseWriter, r *http.Request) {
return
}
policyName, err := globalIAMSys.GetUserPolicy(user.AccessKey)
policyName, err := globalIAMSys.PolicyDBGet(user.AccessKey)
if err != nil {
logger.LogIf(ctx, err)
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
+3
View File
@@ -365,6 +365,9 @@ func UnstartedTestServer(t TestErrHandler, instanceType string) TestServer {
globalNotificationSys = NewNotificationSys(globalServerConfig, testServer.Disks)
globalNotificationSys.Init(objLayer)
globalLifecycleSys = NewLifecycleSys()
globalLifecycleSys.Init(objLayer)
return testServer
}
+15 -24
View File
@@ -29,7 +29,6 @@ import (
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
@@ -429,6 +428,7 @@ func newContext(r *http.Request, w http.ResponseWriter, api string) context.Cont
DeploymentID: globalDeploymentID,
RequestID: w.Header().Get(xhttp.AmzRequestID),
RemoteHost: handlers.GetSourceIP(r),
Host: getHostName(r),
UserAgent: r.UserAgent(),
API: api,
BucketName: bucket,
@@ -442,30 +442,21 @@ func isNetworkOrHostDown(err error) bool {
if err == nil {
return false
}
switch err.(type) {
case *net.DNSError, *net.OpError, net.UnknownNetworkError:
return true
case *url.Error:
// For a URL error, where it replies back "connection closed"
if strings.Contains(err.Error(), "Connection closed by foreign host") {
return true
}
return true
default:
if strings.Contains(err.Error(), "net/http: TLS handshake timeout") {
// If error is - tlsHandshakeTimeoutError,.
return true
} else if strings.Contains(err.Error(), "i/o timeout") {
// If error is - tcp timeoutError.
return true
} else if strings.Contains(err.Error(), "connection timed out") {
// If err is a net.Dial timeout.
return true
} else if strings.Contains(err.Error(), "net/http: HTTP/1.x transport connection broken") {
return true
}
// We need to figure if the error either a timeout
// or a non-temporary error.
e, ok := err.(net.Error)
if ok {
return e.Timeout()
}
return false
// Fallback to other mechanisms.
if strings.Contains(err.Error(), "i/o timeout") {
// If error is - tcp timeoutError.
ok = true
} else if strings.Contains(err.Error(), "connection timed out") {
// If err is a net.Dial timeout.
ok = true
}
return ok
}
// Used for registering with rest handlers (have a look at registerStorageRESTHandlers for usage example)
+245
View File
@@ -0,0 +1,245 @@
/*
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/handlers"
)
const (
kmBucket = "BucketName"
kmObject = "ObjectName"
kmObjects = "Objects"
kmPrefix = "Prefix"
kmMarker = "Marker"
kmUsername = "UserName"
kmHostname = "HostName"
kmPolicy = "Policy"
)
// KeyValueMap extends builtin map to support setting and getting
// select fields like BucketName, ObjectName, Prefix, etc.
type KeyValueMap map[string]string
// Bucket returns the BucketName
func (km KeyValueMap) Bucket() string {
return km[kmBucket]
}
// Object returns the ObjectName
func (km KeyValueMap) Object() string {
return km[kmObject]
}
// Prefix returns the Prefix
func (km KeyValueMap) Prefix() string {
return km[kmPrefix]
}
// Username returns the Username
func (km KeyValueMap) Username() string {
return km[kmUsername]
}
// Hostname returns the Hostname
func (km KeyValueMap) Hostname() string {
return km[kmHostname]
}
// Policy returns the Policy
func (km KeyValueMap) Policy() string {
return km[kmPolicy]
}
// Objects returns the Objects
func (km KeyValueMap) Objects() []string {
var objects []string
_ = json.Unmarshal([]byte(km[kmObjects]), &objects)
return objects
}
// SetBucket sets the given bucket to the KeyValueMap
func (km *KeyValueMap) SetBucket(bucket string) {
(*km)[kmBucket] = bucket
}
// SetPrefix sets the given prefix to the KeyValueMap
func (km *KeyValueMap) SetPrefix(prefix string) {
(*km)[kmPrefix] = prefix
}
// SetObject sets the given object to the KeyValueMap
func (km *KeyValueMap) SetObject(object string) {
(*km)[kmObject] = object
}
// SetMarker sets the given marker to the KeyValueMap
func (km *KeyValueMap) SetMarker(marker string) {
(*km)[kmMarker] = marker
}
// SetPolicy sets the given policy to the KeyValueMap
func (km *KeyValueMap) SetPolicy(policy string) {
(*km)[kmPolicy] = policy
}
// SetExpiry sets the expiry to the KeyValueMap
func (km *KeyValueMap) SetExpiry(expiry int64) {
(*km)[kmPolicy] = fmt.Sprintf("%d", expiry)
}
// SetObjects sets the list of objects to the KeyValueMap
func (km *KeyValueMap) SetObjects(objects []string) {
objsVal, err := json.Marshal(objects)
if err != nil {
// NB this can only happen when we can't marshal a Go
// slice to its json representation.
objsVal = []byte("[]")
}
(*km)[kmObjects] = string(objsVal)
}
// SetUsername sets the username to the KeyValueMap
func (km *KeyValueMap) SetUsername(username string) {
(*km)[kmUsername] = username
}
// SetHostname sets the hostname to the KeyValueMap
func (km *KeyValueMap) SetHostname(hostname string) {
(*km)[kmHostname] = hostname
}
// ToKeyValuer interface wraps ToKeyValue method that allows types to
// marshal their values as a map of structure member names to their
// values, as strings
type ToKeyValuer interface {
ToKeyValue() KeyValueMap
}
// ToKeyValue implementation for WebGenericArgs
func (args *WebGenericArgs) ToKeyValue() KeyValueMap {
return KeyValueMap{}
}
// ToKeyValue implementation for MakeBucketArgs
func (args *MakeBucketArgs) ToKeyValue() KeyValueMap {
km := KeyValueMap{}
km.SetBucket(args.BucketName)
return km
}
// ToKeyValue implementation for RemoveBucketArgs
func (args *RemoveBucketArgs) ToKeyValue() KeyValueMap {
km := KeyValueMap{}
km.SetBucket(args.BucketName)
return km
}
// ToKeyValue implementation for ListObjectsArgs
func (args *ListObjectsArgs) ToKeyValue() KeyValueMap {
km := KeyValueMap{}
km.SetBucket(args.BucketName)
km.SetPrefix(args.Prefix)
km.SetMarker(args.Marker)
return km
}
// ToKeyValue implementation for RemoveObjectArgs
func (args *RemoveObjectArgs) ToKeyValue() KeyValueMap {
km := KeyValueMap{}
km.SetBucket(args.BucketName)
km.SetObjects(args.Objects)
return km
}
// ToKeyValue implementation for LoginArgs
func (args *LoginArgs) ToKeyValue() KeyValueMap {
km := KeyValueMap{}
km.SetUsername(args.Username)
return km
}
// ToKeyValue implementation for GetBucketPolicyArgs
func (args *GetBucketPolicyArgs) ToKeyValue() KeyValueMap {
km := KeyValueMap{}
km.SetBucket(args.BucketName)
km.SetPrefix(args.Prefix)
return km
}
// ToKeyValue implementation for ListAllBucketPoliciesArgs
func (args *ListAllBucketPoliciesArgs) ToKeyValue() KeyValueMap {
km := KeyValueMap{}
km.SetBucket(args.BucketName)
return km
}
// ToKeyValue implementation for SetBucketPolicyWebArgs
func (args *SetBucketPolicyWebArgs) ToKeyValue() KeyValueMap {
km := KeyValueMap{}
km.SetBucket(args.BucketName)
km.SetPrefix(args.Prefix)
km.SetPolicy(args.Policy)
return km
}
// ToKeyValue implementation for SetAuthArgs
// SetAuthArgs doesn't implement the ToKeyValue interface that will be
// used by logger subsystem down the line, to avoid leaking
// credentials to an external log target
func (args *SetAuthArgs) ToKeyValue() KeyValueMap {
return KeyValueMap{}
}
// ToKeyValue implementation for PresignedGetArgs
func (args *PresignedGetArgs) ToKeyValue() KeyValueMap {
km := KeyValueMap{}
km.SetHostname(args.HostName)
km.SetBucket(args.BucketName)
km.SetObject(args.ObjectName)
km.SetExpiry(args.Expiry)
return km
}
// newWebContext creates a context with ReqInfo values from the given
// http request and api name.
func newWebContext(r *http.Request, args ToKeyValuer, api string) context.Context {
argsMap := args.ToKeyValue()
bucket := argsMap.Bucket()
object := argsMap.Object()
prefix := argsMap.Prefix()
if prefix != "" {
object = prefix
}
reqInfo := &logger.ReqInfo{
DeploymentID: globalDeploymentID,
RemoteHost: handlers.GetSourceIP(r),
Host: getHostName(r),
UserAgent: r.UserAgent(),
API: api,
BucketName: bucket,
ObjectName: object,
}
return logger.SetReqInfo(context.Background(), reqInfo)
}
+111
View File
@@ -0,0 +1,111 @@
/*
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"bytes"
"net/http"
"testing"
"github.com/minio/minio/cmd/logger"
)
func TestKeyValueMap(t *testing.T) {
bucket := "bucket"
object := "object"
prefix := "prefix"
username := "username"
policy := "policy"
host := "min.io"
objects := []string{object, object}
km := KeyValueMap{}
km.SetBucket(bucket)
km.SetPrefix(prefix)
km.SetUsername(username)
km.SetHostname(host)
km.SetObject(object)
km.SetObjects(objects)
km.SetPolicy(policy)
if got := km.Bucket(); got != bucket {
t.Errorf("Expected %s but got %s", bucket, got)
}
if got := km.Object(); got != object {
t.Errorf("Expected %s but got %s", object, got)
}
areEqualObjects := func(as, bs []string) bool {
if len(as) != len(bs) {
return false
}
for i, a := range as {
b := bs[i]
if a != b {
return false
}
}
return true
}
if got := km.Objects(); !areEqualObjects(got, objects) {
t.Errorf("Expected %s but got %s", objects, got)
}
if got := km.Policy(); got != policy {
t.Errorf("Expected %s but got %s", policy, got)
}
if got := km.Prefix(); got != prefix {
t.Errorf("Expected %s but got %s", prefix, got)
}
if got := km.Username(); got != username {
t.Errorf("Expected %s but got %s", username, got)
}
if got := km.Hostname(); got != host {
t.Errorf("Expected %s but got %s", host, got)
}
}
func TestNewWebContext(t *testing.T) {
api := "Test API"
args := ListObjectsArgs{
BucketName: "bucket",
Prefix: "prefix",
Marker: "marker",
}
req, err := http.NewRequest(http.MethodPost, "http://min.io", bytes.NewReader([]byte("nothing")))
if err != nil {
t.Fatal("Unexpected failure while creating a test request")
}
ctx := newWebContext(req, &args, api)
reqInfo := logger.GetReqInfo(ctx)
if reqInfo.API != api {
t.Errorf("Expected %s got %s", api, reqInfo.API)
}
if reqInfo.BucketName != args.BucketName {
t.Errorf("Expected %s got %s", args.BucketName, reqInfo.BucketName)
}
}
+25 -39
View File
@@ -74,31 +74,9 @@ type ServerInfoRep struct {
UIVersion string `json:"uiVersion"`
}
// newWebContext creates a context with ReqInfo values from the given
// http request and api name.
func newWebContext(r *http.Request, api string) context.Context {
vars := mux.Vars(r)
bucket := vars["bucket"]
object := vars["object"]
prefix := vars["prefix"]
if prefix != "" {
object = prefix
}
reqInfo := &logger.ReqInfo{
DeploymentID: globalDeploymentID,
RemoteHost: handlers.GetSourceIP(r),
UserAgent: r.UserAgent(),
API: api,
BucketName: bucket,
ObjectName: object,
}
return logger.SetReqInfo(context.Background(), reqInfo)
}
// ServerInfo - get server info.
func (web *webAPIHandlers) ServerInfo(r *http.Request, args *WebGenericArgs, reply *ServerInfoRep) error {
ctx := newWebContext(r, "webServerInfo")
ctx := newWebContext(r, args, "webServerInfo")
_, owner, authErr := webRequestAuthenticate(r)
if authErr != nil {
return toJSONError(ctx, authErr)
@@ -148,7 +126,7 @@ type StorageInfoRep struct {
// StorageInfo - web call to gather storage usage statistics.
func (web *webAPIHandlers) StorageInfo(r *http.Request, args *WebGenericArgs, reply *StorageInfoRep) error {
ctx := newWebContext(r, "webStorageInfo")
ctx := newWebContext(r, args, "webStorageInfo")
objectAPI := web.ObjectAPI()
if objectAPI == nil {
return toJSONError(ctx, errServerNotInitialized)
@@ -169,7 +147,7 @@ type MakeBucketArgs struct {
// MakeBucket - creates a new bucket.
func (web *webAPIHandlers) MakeBucket(r *http.Request, args *MakeBucketArgs, reply *WebGenericRep) error {
ctx := newWebContext(r, "webMakeBucket")
ctx := newWebContext(r, args, "webMakeBucket")
objectAPI := web.ObjectAPI()
if objectAPI == nil {
return toJSONError(ctx, errServerNotInitialized)
@@ -230,7 +208,7 @@ type RemoveBucketArgs struct {
// DeleteBucket - removes a bucket, must be empty.
func (web *webAPIHandlers) DeleteBucket(r *http.Request, args *RemoveBucketArgs, reply *WebGenericRep) error {
ctx := newWebContext(r, "webDeleteBucket")
ctx := newWebContext(r, args, "webDeleteBucket")
objectAPI := web.ObjectAPI()
if objectAPI == nil {
return toJSONError(ctx, errServerNotInitialized)
@@ -318,7 +296,7 @@ type WebBucketInfo struct {
// ListBuckets - list buckets api.
func (web *webAPIHandlers) ListBuckets(r *http.Request, args *WebGenericArgs, reply *ListBucketsRep) error {
ctx := newWebContext(r, "webListBuckets")
ctx := newWebContext(r, args, "webListBuckets")
objectAPI := web.ObjectAPI()
if objectAPI == nil {
return toJSONError(ctx, errServerNotInitialized)
@@ -421,7 +399,7 @@ type WebObjectInfo struct {
// ListObjects - list objects api.
func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, reply *ListObjectsRep) error {
ctx := newWebContext(r, "webListObjects")
ctx := newWebContext(r, args, "webListObjects")
reply.UIVersion = browser.UIVersion
objectAPI := web.ObjectAPI()
if objectAPI == nil {
@@ -616,7 +594,7 @@ type RemoveObjectArgs struct {
// RemoveObject - removes an object, or all the objects at a given prefix.
func (web *webAPIHandlers) RemoveObject(r *http.Request, args *RemoveObjectArgs, reply *WebGenericRep) error {
ctx := newWebContext(r, "webRemoveObject")
ctx := newWebContext(r, args, "webRemoveObject")
objectAPI := web.ObjectAPI()
if objectAPI == nil {
return toJSONError(ctx, errServerNotInitialized)
@@ -776,7 +754,7 @@ type LoginRep struct {
// Login - user login handler.
func (web *webAPIHandlers) Login(r *http.Request, args *LoginArgs, reply *LoginRep) error {
ctx := newWebContext(r, "webLogin")
ctx := newWebContext(r, args, "webLogin")
token, err := authenticateWeb(args.Username, args.Password)
if err != nil {
return toJSONError(ctx, err)
@@ -795,7 +773,7 @@ type GenerateAuthReply struct {
}
func (web webAPIHandlers) GenerateAuth(r *http.Request, args *WebGenericArgs, reply *GenerateAuthReply) error {
ctx := newWebContext(r, "webGenerateAuth")
ctx := newWebContext(r, args, "webGenerateAuth")
_, owner, authErr := webRequestAuthenticate(r)
if authErr != nil {
return toJSONError(ctx, authErr)
@@ -830,7 +808,7 @@ type SetAuthReply struct {
// SetAuth - Set accessKey and secretKey credentials.
func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *SetAuthReply) error {
ctx := newWebContext(r, "webSetAuth")
ctx := newWebContext(r, args, "webSetAuth")
claims, owner, authErr := webRequestAuthenticate(r)
if authErr != nil {
return toJSONError(ctx, authErr)
@@ -919,7 +897,7 @@ type URLTokenReply struct {
// CreateURLToken creates a URL token (short-lived) for GET requests.
func (web *webAPIHandlers) CreateURLToken(r *http.Request, args *WebGenericArgs, reply *URLTokenReply) error {
ctx := newWebContext(r, "webCreateURLToken")
ctx := newWebContext(r, args, "webCreateURLToken")
claims, owner, authErr := webRequestAuthenticate(r)
if authErr != nil {
return toJSONError(ctx, authErr)
@@ -1503,7 +1481,7 @@ type GetBucketPolicyRep struct {
// GetBucketPolicy - get bucket policy for the requested prefix.
func (web *webAPIHandlers) GetBucketPolicy(r *http.Request, args *GetBucketPolicyArgs, reply *GetBucketPolicyRep) error {
ctx := newWebContext(r, "webGetBucketPolicy")
ctx := newWebContext(r, args, "webGetBucketPolicy")
objectAPI := web.ObjectAPI()
if objectAPI == nil {
return toJSONError(ctx, errServerNotInitialized)
@@ -1513,6 +1491,7 @@ func (web *webAPIHandlers) GetBucketPolicy(r *http.Request, args *GetBucketPolic
if authErr != nil {
return toJSONError(ctx, authErr)
}
// For authenticated users apply IAM policy.
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.Subject,
@@ -1599,18 +1578,25 @@ type ListAllBucketPoliciesRep struct {
// ListAllBucketPolicies - get all bucket policy.
func (web *webAPIHandlers) ListAllBucketPolicies(r *http.Request, args *ListAllBucketPoliciesArgs, reply *ListAllBucketPoliciesRep) error {
ctx := newWebContext(r, "WebListAllBucketPolicies")
ctx := newWebContext(r, args, "WebListAllBucketPolicies")
objectAPI := web.ObjectAPI()
if objectAPI == nil {
return toJSONError(ctx, errServerNotInitialized)
}
_, owner, authErr := webRequestAuthenticate(r)
claims, owner, authErr := webRequestAuthenticate(r)
if authErr != nil {
return toJSONError(ctx, authErr)
}
if !owner {
// For authenticated users apply IAM policy.
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: claims.Subject,
Action: iampolicy.GetBucketPolicyAction,
BucketName: args.BucketName,
ConditionValues: getConditionValues(r, "", claims.Subject),
IsOwner: owner,
}) {
return toJSONError(ctx, errAccessDenied)
}
@@ -1680,7 +1666,7 @@ type SetBucketPolicyWebArgs struct {
// SetBucketPolicy - set bucket policy.
func (web *webAPIHandlers) SetBucketPolicy(r *http.Request, args *SetBucketPolicyWebArgs, reply *WebGenericRep) error {
ctx := newWebContext(r, "webSetBucketPolicy")
ctx := newWebContext(r, args, "webSetBucketPolicy")
objectAPI := web.ObjectAPI()
reply.UIVersion = browser.UIVersion
@@ -1832,7 +1818,7 @@ type PresignedGetRep struct {
// PresignedGET - returns presigned-Get url.
func (web *webAPIHandlers) PresignedGet(r *http.Request, args *PresignedGetArgs, reply *PresignedGetRep) error {
ctx := newWebContext(r, "webPresignedGet")
ctx := newWebContext(r, args, "webPresignedGet")
claims, owner, authErr := webRequestAuthenticate(r)
if authErr != nil {
return toJSONError(ctx, authErr)
+16
View File
@@ -29,6 +29,7 @@ import (
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/bpool"
"github.com/minio/minio/pkg/lifecycle"
"github.com/minio/minio/pkg/madmin"
"github.com/minio/minio/pkg/policy"
"github.com/minio/minio/pkg/sync/errgroup"
@@ -518,6 +519,21 @@ func (s *xlSets) DeleteBucketPolicy(ctx context.Context, bucket string) error {
return removePolicyConfig(ctx, s, bucket)
}
// SetBucketLifecycle sets lifecycle on bucket
func (s *xlSets) SetBucketLifecycle(ctx context.Context, bucket string, lifecycle *lifecycle.Lifecycle) error {
return saveLifecycleConfig(ctx, s, bucket, lifecycle)
}
// GetBucketLifecycle will get lifecycle on bucket
func (s *xlSets) GetBucketLifecycle(ctx context.Context, bucket string) (*lifecycle.Lifecycle, error) {
return getLifecycleConfig(s, bucket)
}
// DeleteBucketLifecycle deletes all lifecycle on bucket
func (s *xlSets) DeleteBucketLifecycle(ctx context.Context, bucket string) error {
return removeLifecycleConfig(ctx, s, bucket)
}
// IsNotificationSupported returns whether bucket notification is applicable for this layer.
func (s *xlSets) IsNotificationSupported() bool {
return s.getHashedSet("").IsNotificationSupported()
+16
View File
@@ -23,6 +23,7 @@ import (
"github.com/minio/minio-go/v6/pkg/s3utils"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/lifecycle"
"github.com/minio/minio/pkg/policy"
)
@@ -308,6 +309,21 @@ func (xl xlObjects) DeleteBucketPolicy(ctx context.Context, bucket string) error
return removePolicyConfig(ctx, xl, bucket)
}
// SetBucketLifecycle sets lifecycle on bucket
func (xl xlObjects) SetBucketLifecycle(ctx context.Context, bucket string, lifecycle *lifecycle.Lifecycle) error {
return saveLifecycleConfig(ctx, xl, bucket, lifecycle)
}
// GetBucketLifecycle will get lifecycle on bucket
func (xl xlObjects) GetBucketLifecycle(ctx context.Context, bucket string) (*lifecycle.Lifecycle, error) {
return getLifecycleConfig(xl, bucket)
}
// DeleteBucketLifecycle deletes all lifecycle on bucket
func (xl xlObjects) DeleteBucketLifecycle(ctx context.Context, bucket string) error {
return removeLifecycleConfig(ctx, xl, bucket)
}
// IsNotificationSupported returns whether bucket notification is applicable for this layer.
func (xl xlObjects) IsNotificationSupported() bool {
return true
+2 -2
View File
@@ -183,10 +183,10 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
// it needs healing too.
for _, part := range partsMetadata[i].Parts {
checksumInfo := erasureInfo.GetChecksumInfo(part.Name)
err = onlineDisk.VerifyFile(bucket, pathJoin(object, part.Name), checksumInfo.Algorithm, checksumInfo.Hash, erasure.ShardSize())
err = onlineDisk.VerifyFile(bucket, pathJoin(object, part.Name), part.Size == 0, checksumInfo.Algorithm, checksumInfo.Hash, erasure.ShardSize())
if err != nil {
isCorrupt := strings.HasPrefix(err.Error(), "Bitrot verification mismatch - expected ")
if !isCorrupt && err != errFileNotFound && err != errVolumeNotFound {
if !isCorrupt && err != errFileNotFound && err != errVolumeNotFound && err != errFileUnexpectedSize {
logger.LogIf(ctx, err)
}
dataErrs[i] = err
+10 -2
View File
@@ -195,6 +195,9 @@ func shouldHealObjectOnDisk(xlErr, dataErr error, meta xlMetaV1, quorumModTime t
if dataErr == errFileNotFound {
return true
}
if dataErr == errFileUnexpectedSize {
return true
}
if _, ok := dataErr.(HashMismatchError); ok {
return true
}
@@ -498,10 +501,14 @@ func (xl xlObjects) healObjectDir(ctx context.Context, bucket, object string, dr
drive = storageDisks[i].String()
}
switch err {
case nil:
hr.Before.Drives[i] = madmin.HealDriveInfo{State: madmin.DriveStateOk}
hr.After.Drives[i] = madmin.HealDriveInfo{State: madmin.DriveStateOk}
case errDiskNotFound:
hr.Before.Drives[i] = madmin.HealDriveInfo{State: madmin.DriveStateOffline}
hr.After.Drives[i] = madmin.HealDriveInfo{State: madmin.DriveStateOffline}
case errVolumeNotFound:
case errVolumeNotFound, errFileNotFound:
// Bucket or prefix/directory not found
hr.Before.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateMissing}
hr.After.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateMissing}
default:
@@ -514,7 +521,8 @@ func (xl xlObjects) healObjectDir(ctx context.Context, bucket, object string, dr
}
for i, err := range errs {
switch err {
case errVolumeNotFound:
case errVolumeNotFound, errFileNotFound:
// Bucket or prefix/directory not found
merr := storageDisks[i].MakeVol(pathJoin(bucket, object))
switch merr {
case nil, errVolumeExists:
+105 -2
View File
@@ -107,7 +107,7 @@ func TestHealObjectCorrupted(t *testing.T) {
t.Fatalf("Failed to complete multipart upload - %v", err)
}
// Remove the object backend files from the first disk.
// Test 1: Remove the object backend files from the first disk.
xl := obj.(*xlObjects)
firstDisk := xl.storageDisks[0]
err = firstDisk.DeleteFile(bucket, filepath.Join(object, xlMetaJSONFile))
@@ -125,7 +125,34 @@ func TestHealObjectCorrupted(t *testing.T) {
t.Errorf("Expected xl.json file to be present but stat failed - %v", err)
}
// Delete xl.json from more than read quorum number of disks, to create a corrupted situation.
// Test 2: Heal when part.1 is empty
partSt1, err := firstDisk.StatFile(bucket, filepath.Join(object, "part.1"))
if err != nil {
t.Errorf("Expected part.1 file to be present but stat failed - %v", err)
}
err = firstDisk.DeleteFile(bucket, filepath.Join(object, "part.1"))
if err != nil {
t.Errorf("Failure during part.1 removal - %v", err)
}
err = firstDisk.AppendFile(bucket, filepath.Join(object, "part.1"), []byte{})
if err != nil {
t.Errorf("Failure during creating part.1 - %v", err)
}
_, err = obj.HealObject(context.Background(), bucket, object, false, true, madmin.HealDeepScan)
if err != nil {
t.Errorf("Expected nil but received %v", err)
}
partSt2, err := firstDisk.StatFile(bucket, filepath.Join(object, "part.1"))
if err != nil {
t.Errorf("Expected from part.1 file to be present but stat failed - %v", err)
}
if partSt1.Size != partSt2.Size {
t.Errorf("part.1 file size is not the same before and after heal")
}
// Test 3: checks if HealObject returns an error when xl.json is not found
// in more than read quorum number of disks, to create a corrupted situation.
for i := 0; i <= len(xl.storageDisks)/2; i++ {
xl.storageDisks[i].DeleteFile(bucket, filepath.Join(object, xlMetaJSONFile))
}
@@ -223,3 +250,79 @@ func TestHealObjectXL(t *testing.T) {
t.Errorf("Expected %v but received %v", InsufficientReadQuorum{}, err)
}
}
// Tests healing of empty directories
func TestHealEmptyDirectoryXL(t *testing.T) {
nDisks := 16
fsDirs, err := getRandomDisks(nDisks)
if err != nil {
t.Fatal(err)
}
defer removeRoots(fsDirs)
// Everything is fine, should return nil
obj, _, err := initObjectLayer(mustGetNewEndpointList(fsDirs...))
if err != nil {
t.Fatal(err)
}
bucket := "bucket"
object := "empty-dir/"
var opts ObjectOptions
err = obj.MakeBucketWithLocation(context.Background(), bucket, "")
if err != nil {
t.Fatalf("Failed to make a bucket - %v", err)
}
// Upload an empty directory
_, err = obj.PutObject(context.Background(), bucket, object, mustGetPutObjReader(t, bytes.NewReader([]byte{}), 0, "", ""), opts)
if err != nil {
t.Fatal(err)
}
// Remove the object backend files from the first disk.
xl := obj.(*xlObjects)
firstDisk := xl.storageDisks[0]
err = firstDisk.DeleteFile(bucket, object)
if err != nil {
t.Fatalf("Failed to delete a file - %v", err)
}
// Heal the object
hr, err := obj.HealObject(context.Background(), bucket, object, false, false, madmin.HealNormalScan)
if err != nil {
t.Fatalf("Failed to heal object - %v", err)
}
// Check if the empty directory is restored in the first disk
_, err = firstDisk.StatVol(pathJoin(bucket, object))
if err != nil {
t.Fatalf("Expected object to be present but stat failed - %v", err)
}
// Check the state of the object in the first disk (should be missing)
if hr.Before.Drives[0].State != madmin.DriveStateMissing {
t.Fatalf("Unexpected drive state: %v", hr.Before.Drives[0].State)
}
// Check the state of all other disks (should be ok)
for i, h := range append(hr.Before.Drives[1:], hr.After.Drives...) {
if h.State != madmin.DriveStateOk {
t.Fatalf("Unexpected drive state (%d): %v", i+1, h.State)
}
}
// Heal the same object again
hr, err = obj.HealObject(context.Background(), bucket, object, false, false, madmin.HealNormalScan)
if err != nil {
t.Fatalf("Failed to heal object - %v", err)
}
// Check that Before & After states are all okay
for i, h := range append(hr.Before.Drives, hr.After.Drives...) {
if h.State != madmin.DriveStateOk {
t.Fatalf("Unexpected drive state (%d): %v", i+1, h.State)
}
}
}
-135
View File
@@ -1,135 +0,0 @@
// +build ignore
/*
* 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 main
import (
"fmt"
"log"
"os"
"os/user"
"syscall"
"github.com/minio/cli"
minio "github.com/minio/minio/cmd"
)
var defaultUserGroup string
func init() {
username := os.Getenv("MINIO_USERNAME")
groupname := os.Getenv("MINIO_GROUPNAME")
defaultUserGroup = username + ":" + groupname
}
func getUserGroup(path string) (string, error) {
fi, err := os.Stat(minio.PathJoin(path, ".minio.sys"))
if err != nil {
// Fresh directory we should default to what was requested by user.
if os.IsNotExist(err) {
fi, err = os.Stat(path)
if err != nil {
return "", err
}
} else {
return "", err
}
}
stat, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
// Unable to figure out uid/gid, default to defaultUserGroup
return defaultUserGroup, nil
}
u, err := user.LookupId(fmt.Sprintf("%d", stat.Uid))
if err != nil {
return fmt.Sprintf("%d:%d", stat.Uid, stat.Gid), nil
}
g, err := user.LookupGroupId(fmt.Sprintf("%d", stat.Gid))
if err != nil {
return fmt.Sprintf("%d:%d", stat.Uid, stat.Gid), nil
}
return fmt.Sprintf("%s:%s", u.Username, g.Name), nil
}
func main() {
app := cli.NewApp()
app.Flags = append(minio.ServerFlags, minio.GlobalFlags...)
app.Action = func(ctx *cli.Context) {
// Fetch address option
serverAddr := ctx.GlobalString("address")
if serverAddr == "" || serverAddr == ":9000" {
serverAddr = ctx.String("address")
}
if ctx.Args().First() == "help" {
cli.ShowCommandHelpAndExit(ctx, "check-user", 1)
}
if ctx.Args().First() != "minio" {
cli.ShowCommandHelpAndExit(ctx, "check-user", 1)
}
args := cli.Args(ctx.Args().Tail())
if !args.Present() {
cli.ShowCommandHelpAndExit(ctx, "check-user", 1)
}
var ug string
var err error
switch args.First() {
case "gateway":
args = cli.Args(args.Tail())
if args.First() != "nas" {
fmt.Println(defaultUserGroup)
return
}
args = cli.Args(args.Tail())
if args.First() == "" {
fmt.Println("")
return
}
ug, err = getUserGroup(args.First())
if err != nil {
log.Fatalln(err)
}
case "server":
var setArgs [][]string
setArgs, err = minio.GetAllSets(args.Tail()...)
if err != nil {
log.Fatalln(err)
}
var endpoints minio.EndpointList
_, endpoints, _, err = minio.CreateEndpoints(serverAddr, setArgs...)
if err != nil {
log.Fatalln(err)
}
for _, endpoint := range endpoints {
if !endpoint.IsLocal {
continue
}
ug, err = getUserGroup(endpoint.Path)
if err != nil {
log.Fatalln(err)
}
break
}
default:
cli.ShowCommandHelpAndExit(ctx, "check-user", 1)
}
fmt.Println(ug)
}
if err := app.Run(os.Args); err != nil {
log.Fatalln(err)
}
}
+20 -25
View File
@@ -15,9 +15,6 @@
# limitations under the License.
#
export MINIO_USERNAME=${MINIO_USERNAME:-"minio"}
export MINIO_GROUPNAME=${MINIO_GROUPNAME:-"minio"}
# If command starts with an option, prepend minio.
if [ "${1}" != "minio" ]; then
if [ -n "${1}" ]; then
@@ -42,37 +39,35 @@ docker_secrets_env() {
fi
}
## Create UID/GID based on available environment variables.
docker_set_uid_gid() {
addgroup -S "$MINIO_GROUPNAME" >/dev/null 2>&1 && \
adduser -S -G "$MINIO_GROUPNAME" "$MINIO_USERNAME" >/dev/null 2>&1
## Set SSE_MASTER_KEY from docker secrets if provided
docker_sse_encryption_env() {
SSE_MASTER_KEY_FILE="/run/secrets/$MINIO_SSE_MASTER_KEY_FILE"
if [ -f "$SSE_MASTER_KEY_FILE" ]; then
MINIO_SSE_MASTER_KEY="$(cat "$SSE_MASTER_KEY_FILE")"
export MINIO_SSE_MASTER_KEY
fi
}
# su-exec to requested user, if user cannot be requested
# existing user is used automatically.
# su-exec to requested user, if service cannot run exec will fail.
docker_switch_user() {
owner=$(check-user "$@")
if [ "${owner}" != "${MINIO_USERNAME}:${MINIO_GROUPNAME}" ]; then
## Print the message only if we are not using non-default username:groupname.
if [ "${MINIO_USERNAME}:${MINIO_GROUPNAME}" != "minio:minio" ]; then
echo "Requested username/group ${MINIO_USERNAME}:${MINIO_GROUPNAME} cannot be used"
echo "Found existing data with user ${owner}, we will continue and use ${owner} instead."
return
fi
if [ ! -z "${MINIO_USERNAME}" ] && [ ! -z "${MINIO_GROUPNAME}" ]; then
addgroup -S "$MINIO_GROUPNAME" >/dev/null 2>&1 && \
adduser -S -G "$MINIO_GROUPNAME" "$MINIO_USERNAME" >/dev/null 2>&1
exec su-exec "${MINIO_USERNAME}:${MINIO_GROUPNAME}" "$@"
else
# fallback
exec "$@"
fi
# check if su-exec is allowed, if yes proceed proceed.
if su-exec "${owner}" "/bin/ls" >/dev/null 2>&1; then
exec su-exec "${owner}" "$@"
fi
# fallback
exec "$@"
}
## Set access env from secrets if necessary.
docker_secrets_env
## User Input UID and GID
docker_set_uid_gid
## Set sse encryption from secrets if necessary.
docker_sse_encryption_env
## Switch to user if applicable.
docker_switch_user "$@"
+111 -49
View File
@@ -34,20 +34,22 @@ Install RabbitMQ from [here](https://www.rabbitmq.com/).
The MinIO server configuration file is stored on the backend in json format. The AMQP configuration is located in the `amqp` key under the `notify` top-level key. Create a configuration key-value pair here for your AMQP instance. The key is a name for your AMQP endpoint, and the value is a collection of key-value parameters described in the table below.
| Parameter | Type | Description |
| :------------- | :------- | :------------------------------------------------------------------------------ |
| `enable` | _bool_ | (Required) Is this server endpoint configuration active/enabled? |
| `url` | _string_ | (Required) AMQP server endpoint, e.g. `amqp://myuser:mypassword@localhost:5672` |
| `exchange` | _string_ | Name of the exchange. |
| `routingKey` | _string_ | Routing key for publishing. |
| `exchangeType` | _string_ | Kind of exchange. |
| `deliveryMode` | _uint8_ | Delivery mode for publishing. 0 or 1 - transient; 2 - persistent. |
| `mandatory` | _bool_ | Publishing related bool. |
| `immediate` | _bool_ | Publishing related bool. |
| `durable` | _bool_ | Exchange declaration related bool. |
| `internal` | _bool_ | Exchange declaration related bool. |
| `noWait` | _bool_ | Exchange declaration related bool. |
| `autoDeleted` | _bool_ | Exchange declaration related bool. |
| Parameter | Type | Description |
| :------------- | :------- | :------------------------------------------------------------------------------- |
| `enable` | _bool_ | (Required) Is this server endpoint configuration active/enabled? |
| `url` | _string_ | (Required) AMQP server endpoint, e.g. `amqp://myuser:mypassword@localhost:5672` |
| `exchange` | _string_ | Name of the exchange. |
| `routingKey` | _string_ | Routing key for publishing. |
| `exchangeType` | _string_ | Kind of exchange. |
| `deliveryMode` | _uint8_ | Delivery mode for publishing. 0 or 1 - transient; 2 - persistent. |
| `mandatory` | _bool_ | Publishing related bool. |
| `immediate` | _bool_ | Publishing related bool. |
| `durable` | _bool_ | Exchange declaration related bool. |
| `internal` | _bool_ | Exchange declaration related bool. |
| `noWait` | _bool_ | Exchange declaration related bool. |
| `autoDeleted` | _bool_ | Exchange declaration related bool. |
| `queueDir` | _string_ | Persistent store for events when AMQP broker is offline |
| `queueLimit` | _int_ | Set the maximum event limit for the persistent store. The default limit is 10000 |
An example configuration for RabbitMQ is shown below:
@@ -65,10 +67,13 @@ An example configuration for RabbitMQ is shown below:
"durable": false,
"internal": false,
"noWait": false,
"autoDeleted": false
"autoDeleted": false,
"queueDir": "",
"queueLimit": 0
}
}
```
MinIO supports persistent event store. The persistent store will backup events when the AMQP broker goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field and the maximum limit of events in the queueDir in `queueLimit` field. For eg, the `queueDir` can be `/home/events` and `queueLimit` can be `1000`. By default, the `queueLimit` is set to 10000.
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally.
@@ -286,12 +291,14 @@ MinIO requires a 5.x series version of Elasticsearch. This is the latest major r
The MinIO server configuration file is stored on the backend in json format. The Elasticsearch configuration is located in the `elasticsearch` key under the `notify` top-level key. Create a configuration key-value pair here for your Elasticsearch instance. The key is a name for your Elasticsearch endpoint, and the value is a collection of key-value parameters described in the table below.
| Parameter | Type | Description |
| :-------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enable` | _bool_ | (Required) Is this server endpoint configuration active/enabled? |
| `format` | _string_ | (Required) Either `namespace` or `access`. |
| `url` | _string_ | (Required) The Elasticsearch server's address, with optional authentication info. For example: `http://localhost:9200` or with authentication info `http://elastic:MagicWord@127.0.0.1:9200`. |
| `index` | _string_ | (Required) The name of an Elasticsearch index in which MinIO will store documents. |
| Parameter | Type | Description |
| :----------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enable` | _bool_ | (Required) Is this server endpoint configuration active/enabled? |
| `format` | _string_ | (Required) Either `namespace` or `access`. |
| `url` | _string_ | (Required) The Elasticsearch server's address, with optional authentication info. For example: `http://localhost:9200` or with authentication info `http://elastic:MagicWord@127.0.0.1:9200`. |
| `index` | _string_ | (Required) The name of an Elasticsearch index in which MinIO will store documents. |
| `queueDir` | _string_ | Persistent store for events when Elasticsearch broker is offline |
| `queueLimit` | _int_ | Set the maximum event limit for the persistent store. The default limit is 10000 |
An example of Elasticsearch configuration is as follows:
@@ -301,11 +308,15 @@ An example of Elasticsearch configuration is as follows:
"enable": true,
"format": "namespace",
"url": "http://127.0.0.1:9200",
"index": "minio_events"
"index": "minio_events",
"queueDir": "",
"queueLimit": 0
}
},
```
MinIO supports persistent event store. The persistent store will backup events when the Elasticsearch broker goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field and the maximum limit of events in the queueDir in `queueLimit` field. For eg, the `queueDir` can be `/home/events` and `queueLimit` can be `1000`. By default, the `queueLimit` is set to 10000.
If Elasticsearch has authentication enabled, the credentials can be supplied to MinIO via the `url` parameter formatted as `PROTO://USERNAME:PASSWORD@ELASTICSEARCH_HOST:PORT`.
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally.
@@ -454,11 +465,15 @@ An example of Redis configuration is as follows:
"format": "namespace",
"address": "127.0.0.1:6379",
"password": "yoursecret",
"key": "bucketevents"
"key": "bucketevents",
"queueDir": "",
"queueLimit": 0
}
}
```
MinIO supports persistent event store. The persistent store will backup events when the Redis broker goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field and the maximum limit of events in the queueDir in `queueLimit` field. For eg, the `queueDir` can be `/home/events` and `queueLimit` can be `1000`. By default, the `queueLimit` is set to 10000.
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally.
```sh
@@ -537,11 +552,12 @@ The NATS configuration block in `config.json` is as follows:
"password": "yoursecret",
"token": "",
"secure": false,
"pingInterval": 0
"pingInterval": 0,
"queueDir": "",
"queueLimit": 0,
"streaming": {
"enable": false,
"clusterID": "",
"clientID": "",
"async": false,
"maxPubAcksInflight": 0
}
@@ -549,6 +565,8 @@ The NATS configuration block in `config.json` is as follows:
},
```
MinIO supports persistent event store. The persistent store will backup events when the NATS broker goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field and the maximum limit of events in the queueDir in `queueLimit` field. For eg, the `queueDir` can be `/home/events` and `queueLimit` can be `1000`. By default, the `queueLimit` is set to 10000.
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally.
```sh
@@ -561,7 +579,7 @@ After updating the NATS configuration in /tmp/myconfig , use `mc admin config se
$ mc admin config set myminio < /tmp/myconfig
```
MinIO server also supports [NATS Streaming mode](http://nats.io/documentation/streaming/nats-streaming-intro/) that offers additional functionality like `Message/event persistence`, `At-least-once-delivery`, and `Publisher rate limiting`. To configure MinIO server to send notifications to NATS Streaming server, update the MinIO server configuration file as follows:
MinIO server also supports [NATS Streaming mode](http://nats.io/documentation/streaming/nats-streaming-intro/) that offers additional functionality like `At-least-once-delivery`, and `Publisher rate limiting`. To configure MinIO server to send notifications to NATS Streaming server, update the MinIO server configuration file as follows:
```
"nats": {
@@ -574,10 +592,11 @@ MinIO server also supports [NATS Streaming mode](http://nats.io/documentation/st
"token": "",
"secure": false,
"pingInterval": 0,
"queueDir": "",
"queueLimit": 0,
"streaming": {
"enable": true,
"clusterID": "test-cluster",
"clientID": "minio-client",
"async": true,
"maxPubAcksInflight": 10
}
@@ -667,20 +686,47 @@ import (
)
func main() {
natsConnection, _ := stan.Connect("test-cluster", "test-client")
log.Println("Connected")
var stanConnection stan.Conn
subscribe := func() {
fmt.Printf("Subscribing to subject 'bucketevents'\n")
stanConnection.Subscribe("bucketevents", func(m *stan.Msg) {
// Handle the message
fmt.Printf("Received a message: %s\n", string(m.Data))
})
}
stanConnection, _ = stan.Connect("test-cluster", "test-client", stan.NatsURL("nats://yourusername:yoursecret@0.0.0.0:4222"), stan.SetConnectionLostHandler(func(c stan.Conn, _ error) {
go func() {
for {
// Reconnect if the connection is lost.
if stanConnection == nil || stanConnection.NatsConn() == nil || !stanConnection.NatsConn().IsConnected() {
stanConnection, _ = stan.Connect("test-cluster", "test-client", stan.NatsURL("nats://yourusername:yoursecret@0.0.0.0:4222"), stan.SetConnectionLostHandler(func(c stan.Conn, _ error) {
if c.NatsConn() != nil {
c.NatsConn().Close()
}
_ = c.Close()
}))
if stanConnection != nil {
subscribe()
}
}
}
}()
}))
// Subscribe to subject
log.Printf("Subscribing to subject 'bucketevents'\n")
natsConnection.Subscribe("bucketevents", func(m *stan.Msg) {
// Handle the message
fmt.Printf("Received a message: %s\n", string(m.Data))
})
subscribe()
// Keep the connection alive
runtime.Goexit()
}
```
```
@@ -748,11 +794,15 @@ An example of PostgreSQL configuration is as follows:
"port": "5432",
"user": "postgres",
"password": "password",
"database": "minio_events"
"database": "minio_events",
"queueDir": "",
"queueLimit": 0
}
}
```
MinIO supports persistent event store. The persistent store will backup events when the PostgreSQL connection goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field and the maximum limit of events in the queueDir in `queueLimit` field. For eg, the `queueDir` can be `/home/events` and `queueLimit` can be `1000`. By default, the `queueLimit` is set to 10000.
Note that for illustration here, we have disabled SSL. In the interest of security, for production this is not recommended.
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally.
@@ -843,21 +893,27 @@ The MinIO server configuration file is stored on the backend in json format. The
An example of MySQL configuration is as follows:
```
```json
"mysql": {
"1": {
"enable": true,
"dsnString": "",
"table": "minio_images",
"host": "172.17.0.1",
"port": "3306",
"user": "root",
"password": "password",
"database": "miniodb"
}
"1": {
"enable": true,
"dsnString": "",
"format": "namespace",
"table": "minio_images",
"host": "172.17.0.1",
"port": "3306",
"user": "root",
"password": "password",
"database": "miniodb",
"queueDir": "",
"queueLimit": 0
}
}
```
MinIO supports persistent event store. The persistent store will backup events when the MySQL connection goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field and the maximum limit of events in the queueDir in `queueLimit` field. For eg, the `queueDir` can be `/home/events` and `queueLimit` can be `1000`. By default, the `queueLimit` is set to 10000.
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally.
```sh
@@ -947,6 +1003,7 @@ The MinIO server configuration file is stored on the backend in json format. Upd
}
}
```
MinIO supports persistent event store. The persistent store will backup events when the kafka broker goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field and the maximum limit of events in the queueDir in `queueLimit` field. For eg, the `queueDir` can be `/home/events` and `queueLimit` can be `1000`. By default, the `queueLimit` is set to 10000.
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally.
@@ -1104,12 +1161,17 @@ An example configuration for NSQ is shown below:
"tls": {
"enable": false,
"skipVerify": true
}
},
"queueDir": "",
"queueLimit": 0
}
}
MinIO supports persistent event store. The persistent store will backup events when the NSQ broker goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field and the maximum limit of events in the queueDir in `queueLimit` field. For eg, the `queueDir` can be `/home/events` and `queueLimit` can be `1000`. By default, the `queueLimit` is set to 10000.
```
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally.
To update the configuration, use `mc admin config get` command to get the current configuration file for the MinIO deployment in json format, and save it locally.
```sh
$ mc admin config get myminio/ > /tmp/myconfig
+20 -6
View File
@@ -48,7 +48,9 @@
"durable": false,
"internal": false,
"noWait": false,
"autoDeleted": false
"autoDeleted": false,
"queueDir": "",
"queueLimit": 0
}
},
"elasticsearch": {
@@ -56,7 +58,9 @@
"enable": false,
"format": "",
"url": "",
"index": ""
"index": "",
"queueDir": "",
"queueLimit": 0
}
},
"kafka": {
@@ -102,7 +106,9 @@
"port": "",
"user": "",
"password": "",
"database": ""
"database": "",
"queueDir": "",
"queueLimit": 0
}
},
"nats": {
@@ -115,6 +121,8 @@
"token": "",
"secure": false,
"pingInterval": 0,
"queueDir": "",
"queueLimit": 0,
"streaming": {
"enable": false,
"clusterID": "",
@@ -131,7 +139,9 @@
"tls": {
"enable": false,
"skipVerify": true
}
},
"queueDir": "",
"queueLimit": 0
}
},
"postgresql": {
@@ -144,7 +154,9 @@
"port": "",
"user": "",
"password": "",
"database": ""
"database": "",
"queueDir": "",
"queueLimit": 0
}
},
"redis": {
@@ -153,7 +165,9 @@
"format": "",
"address": "",
"password": "",
"key": ""
"key": "",
"queueDir": "",
"queueLimit": 0
}
},
"webhook": {
+61 -36
View File
@@ -1,28 +1,26 @@
# Large Bucket Support Design Guide [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
This document explains the design approach, advanced use cases and limits of the large bucket feature. If you're looking to get started with large bucket support, we suggest you go through the [getting started document](https://github.com/minio/minio/blob/master/docs/large-bucket/README.md) first.
# Distributed Server Design Guide [![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io)
This document explains the design approach, advanced use cases and limits of the MinIO distributed server.
## Command-line
```
NAME:
minio server - Start object storage server.
minio server - start object storage server.
USAGE:
minio server [FLAGS] DIR1 [DIR2..]
minio server [FLAGS] DIR{1...64}
DIR:
DIR points to a directory on a filesystem. When you want to combine multiple drives
into a single large system, pass one directory per filesystem separated by space.
You may also use a `...` convention to abbreviate the directory arguments. Remote
directories in a distributed setup are encoded as HTTP(s) URIs.
DIR points to a directory on a filesystem. When you want to combine
multiple drives into a single large system, pass one directory per
filesystem separated by space. You may also use a '...' convention
to abbreviate the directory arguments. Remote directories in a
distributed setup are encoded as HTTP(s) URIs.
```
## Common usage
Standalone erasure coded configuration with 4 sets with 16 disks each.
```
minio server dir{1...64}
```
@@ -33,38 +31,21 @@ Distributed erasure coded configuration with 64 sets with 16 disks each.
minio server http://host{1...16}/export{1...64}
```
## Other usages
## Architecture
### Advanced use cases with multiple ellipses
Expansion of ellipses and choice of erasure sets based on this expansion is an automated process in MinIO. Here are some of the details of our underlying erasure coding behavior.
Standalone erasure coded configuration with 4 sets with 16 disks each, which spawns disks across controllers.
```
minio server /mnt/controller{1...4}/data{1...16}
```
- Erasure coding used by MinIO is [Reed-Solomon](https://github.com/klauspost/reedsolomon) erasure coding scheme, which has a total shard maximum of 256 i.e 128 data and 128 parity. MinIO design goes beyond this limitation by doing some practical architecture choices.
Standalone erasure coded configuration with 16 sets 16 disks per set, across mnts, across controllers.
```
minio server /mnt{1..4}/controller{1...4}/data{1...16}
```
- Erasure set is a single erasure coding unit within a MinIO deployment. An object is sharded within an erasure set. Erasure set size is automatically calculated based on the number of disks. MinIO supports unlimited number of disks but each erasure set can be upto 16 disks and a minimum of 4 disks.
Distributed erasure coded configuration with 2 sets 16 disks per set across hosts.
```
minio server http://host{1...32}/disk1
```
- We limited the number of drives to 16 for erasure set because, erasure code shards more than 16 can become chatty and do not have any performance advantages. Additionally since 16 drive erasure set gives you tolerance of 8 disks per object by default which is plenty in any practical scenario.
Distributed erasure coded configuration with rack level redundancy 32 sets in total, 16 disks per set.
```
minio server http://rack{1...4}-host{1...8}.example.net/export{1...16}
```
- Choice of erasure set size is automatic based on the number of disks available, let's say for example if there are 32 servers and 32 disks which is a total of 1024 disks. In this scenario 16 becomes the erasure set size. This is decided based on the greatest common divisor (GCD) of acceptable erasure set sizes ranging from *4, 6, 8, 10, 12, 14, 16*.
Distributed erasure coded configuration with no rack level redundancy but redundancy with in the rack we split the arguments, 32 sets in total, 16 disks per set.
```
minio server http://rack1-host{1...8}.example.net/export{1...16} http://rack2-host{1...8}.example.net/export{1...16} http://rack3-host{1...8}.example.net/export{1...16} http://rack4-host{1...8}.example.net/export{1...16}
```
- *If total disks has many common divisors the algorithm chooses the minimum amounts of erasure sets possible for a erasure set size of any N*. In the example with 1024 disks - 4, 8, 16 are GCD factors. With 16 disks we get a total of 64 possible sets, with 8 disks we get a total of 128 possible sets, with 4 disks we get a total of 256 possible sets. So algorithm automatically chooses 64 sets, which is *16 * 64 = 1024* disks in total.
### Expected expansion for double ellipses
MinIO server internally expands ellipses passed as arguments. Here is a sample expansion to demonstrate the process
- In this algorithm, we also make sure that we spread the disks out evenly. MinIO server expands ellipses passed as arguments. Here is a sample expansion to demonstrate the process.
```
minio server http://host{1...4}/export{1...8}
@@ -106,6 +87,50 @@ Expected expansion
> http://host4/export8
```
A noticeable trait of this expansion is that it chooses unique hosts such that the erasure code is efficient across drives and hosts.
- Choosing an erasure set for the object is decided during `PutObject()`, object names are used to find the right erasure set using the following pseudo code.
```go
// hashes the key returning an integer.
func crcHashMod(key string, cardinality int) int {
keyCrc := crc32.Checksum([]byte(key), crc32.IEEETable)
return int(keyCrc % uint32(cardinality))
}
```
Input for the key is the object name specified in `PutObject()`, returns a unique index. This index is one of the erasure sets where the object will reside. This function is a consistent hash for a given object name i.e for a given object name the index returned is always the same.
- Write and Read quorum are required to be satisfied only across the erasure set for an object. Healing is also done per object within the erasure set which contains the object.
- MinIO does erasure coding at the object level not at the volume level, unlike other object storage vendors. This allows applications to choose different storage class by setting `x-amz-storage-class=STANDARD/REDUCED_REDUNDANCY` for each object uploads so effectively utilizing the capacity of the cluster. Additionally these can also be enforced using IAM policies to make sure the client uploads with correct HTTP headers.
## Other usages
### Advanced use cases with multiple ellipses
Standalone erasure coded configuration with 4 sets with 16 disks each, which spawns disks across controllers.
```
minio server /mnt/controller{1...4}/data{1...16}
```
Standalone erasure coded configuration with 16 sets, 16 disks per set, across mounts and controllers.
```
minio server /mnt{1..4}/controller{1...4}/data{1...16}
```
Distributed erasure coded configuration with 2 sets, 16 disks per set across hosts.
```
minio server http://host{1...32}/disk1
```
Distributed erasure coded configuration with rack level redundancy 32 sets in total, 16 disks per set.
```
minio server http://rack{1...4}-host{1...8}.example.net/export{1...16}
```
Distributed erasure coded configuration with no rack level redundancy but redundancy with in the rack we split the arguments, 32 sets in total, 16 disks per set.
```
minio server http://rack1-host{1...8}.example.net/export{1...16} http://rack2-host{1...8}.example.net/export{1...16} http://rack3-host{1...8}.example.net/export{1...16} http://rack4-host{1...8}.example.net/export{1...16}
```
## Backend `format.json` changes
`format.json` has new fields
@@ -186,5 +211,5 @@ type formatXLV2 struct {
## Limits
- Minimum of 4 disks are needed for erasure coded configuration.
- Minimum of 4 disks are needed for any erasure coded configuration.
- Maximum of 32 distinct nodes are supported in distributed configuration.
-1
View File
@@ -67,7 +67,6 @@ __NOTE:__ `{1...n}` shown have 3 dots! Using only 2 dots `{1..32}` will be inter
To test this setup, access the MinIO server via browser or [`mc`](https://docs.min.io/docs/minio-client-quickstart-guide).
## Explore Further
- [MinIO Large Bucket Support Guide](https://docs.min.io/docs/minio-large-bucket-support-quickstart-guide)
- [MinIO Erasure Code QuickStart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide)
- [Use `mc` with MinIO Server](https://docs.min.io/docs/minio-client-quickstart-guide)
- [Use `aws-cli` with MinIO Server](https://docs.min.io/docs/aws-cli-with-minio)
+19 -19
View File
@@ -16,7 +16,6 @@ To create a MinIO container with persistent storage, you need to map local persi
```sh
docker run -p 9000:9000 --name minio1 \
-v /mnt/data:/data \
-v /mnt/config:/root/.minio \
minio/minio server /data
```
@@ -24,7 +23,6 @@ docker run -p 9000:9000 --name minio1 \
```sh
docker run -p 9000:9000 --name minio1 \
-v D:\data:/data \
-v D:\minio\config:/root/.minio \
minio/minio server /data
```
@@ -44,7 +42,6 @@ docker run -p 9000:9000 --name minio1 \
-e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" \
-e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \
-v /mnt/data:/data \
-v /mnt/config:/root/.minio \
minio/minio server /data
```
@@ -54,36 +51,39 @@ docker run -p 9000:9000 --name minio1 \
-e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" \
-e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \
-v D:\data:/data \
-v D:\minio\config:/root/.minio \
minio/minio server /data
```
### Run MinIO Docker as non root user
MinIO server runs as non-root within the container by default. However, this is applicable only if you're deploying new MinIO instance (not upgrading from older releases). Deployments upgrading from older MinIO deployments, will continue to run as the user previously used if any.
By default `minio` is username and groupname. Use environment variables `MINIO_USERNAME` and `MINIO_GROUPNAME` to override these default values.
### Run MinIO Docker as a regular user
Docker provides standardized mechanisms to run docker containers as non-root users.
#### GNU/Linux and macOS
On Linux and macOS you can use `--user` to run the container as regular user.
> NOTE: make sure --user has write permission to *${HOME}/data* prior to using `--user`.
```sh
docker run -p 9000:9000 --name minio1 \
mkdir -p ${HOME}/data
docker run -p 9000:9000 \
--user $(id -u):$(id -g) \
--name minio1 \
-e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" \
-e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \
-e "MINIO_USERNAME=custom" \
-e "MINIO_GROUPNAME=custom" \
-v /mnt/data:/data \
-v /mnt/config:/root/.minio \
-e "MINIO_SECRET_KEY=wJalrXUtnFEMIK7MDENGbPxRfiCYEXAMPLEKEY" \
-v ${HOME}/data:/data \
minio/minio server /data
```
#### Windows
On windows you would need to use [Docker integrated windows authentication](https://success.docker.com/article/modernizing-traditional-dot-net-applications#integratedwindowsauthentication) and [Create a container with Active Directory Support](https://blogs.msdn.microsoft.com/containerstuff/2017/01/30/create-a-container-with-active-directory-support/)
> NOTE: make sure your AD/Windows user has write permissions to *D:\data* prior to using `credentialspec=`.
```powershell
docker run -p 9000:9000 --name minio1 \
docker run -p 9000:9000 \
--name minio1 \
--security-opt "credentialspec=file://myuser.json"
-e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" \
-e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \
-e "MINIO_USERNAME=custom" \
-e "MINIO_GROUPNAME=custom" \
-e "MINIO_SECRET_KEY=wJalrXUtnFEMIK7MDENGbPxRfiCYEXAMPLEKEY" \
-v D:\data:/data \
-v D:\minio\config:/root/.minio \
minio/minio server /data
```
+19 -12
View File
@@ -3,27 +3,32 @@ MinIO HDFS gateway adds Amazon S3 API support to Hadoop HDFS filesystem. Applica
## Run MinIO Gateway for HDFS Storage
### Using Docker
Please ensure to replace `hdfs://namenode:8200` with *an* actual name node ip/hostname and port.
```
docker run -p 9000:9000 --name hdfs-s3 \
-e "MINIO_ACCESS_KEY=minio" \
-e "MINIO_SECRET_KEY=minio123" \
minio/minio gateway hdfs hdfs://namenode:8200
```
### Using Binary
Namenode information is obtained by reading `core-site.xml` automatically from your hadoop environment variables *$HADOOP_HOME*
```
export MINIO_ACCESS_KEY=minio
export MINIO_SECRET_KEY=minio123
minio gateway hdfs
```
You can also override the namenode endpoint as shown below.
```
export MINIO_ACCESS_KEY=minio
export MINIO_SECRET_KEY=minio123
minio gateway hdfs hdfs://namenode:8200
```
## Test using MinIO Browser
### Using Docker
Using docker is experimental, most Hadoop environments are not dockerized and may require additional steps in getting this to work properly. You are better off just using the binary in this situation.
```
docker run -p 9000:9000 \
--name hdfs-s3 \
-e "MINIO_ACCESS_KEY=minio" \
-e "MINIO_SECRET_KEY=minio123" \
minio/minio gateway hdfs hdfs://namenode:8200
```
## Test using MinIO Browser
*MinIO gateway* comes with an embedded web based object browser. Point your web browser to http://127.0.0.1:9000 to ensure that your server has started successfully.
![Screenshot](https://raw.githubusercontent.com/minio/minio/master/docs/screenshots/minio-browser-gateway.png)
@@ -59,6 +64,8 @@ Gateway inherits the following limitations of HDFS storage layer:
- Additional metadata support for Multipart operations
- Background append to provide concurrency support for multipart operations
Please open a GitHub issue if you wish these to be fixed https://github.com/minio/minio/issues
## Explore Further
- [`mc` command-line interface](https://docs.minio.io/docs/minio-client-quickstart-guide)
- [`aws` command-line interface](https://docs.minio.io/docs/aws-cli-with-minio)
+138 -32
View File
@@ -12,17 +12,17 @@ MinIO supports two different KMS concepts:
by enabling or disabling the corresponding master keys on demand.
- Direct KMS master keys:
MinIO can also be configured to directly use a master key specified by the environment variable `MINIO_SSE_MASTER_KEY`.
MinIO can also be configured to directly use a master key specified by the environment variable `MINIO_SSE_MASTER_KEY` or with a docker secret key.
Direct master keys are useful if the storage backend is not on the same machine as the MinIO server, e.g.,
if network drives or MinIO gateway is used and an external KMS would cause too much management overhead.
if network drives or MinIO gateway is used and an external KMS would cause too much management overhead.
Note: KMS master keys are mainly for testing purposes. It's not recommended to use them for production deployments.
Further if the MinIO server machine is ever compromised, then the master key must also be treated as compromised.
**Important:**
**Important:**
If multiple MinIO servers are configured as [gateways](https://github.com/minio/minio/blob/master/docs/gateway/README.md)
pointing to the *same* backend - for example the same NAS storage - then the KMS configuration **must** be the same for
all gateways. Otherwise one gateway may not be able to decrypt objects created by another gateway. It is the operators'
all gateways. Otherwise one gateway may not be able to decrypt objects created by another gateway. It is the operators'
responsibility to ensure consistency.
## Get started
@@ -36,47 +36,135 @@ Either use Hashicorp Vault as external KMS or specify a master key directly depe
#### 2.1 Setup Hashicorp Vault
Here is a sample quick start for configuring vault with a transit backend and Approle with correct policy
Here is a sample quick start for configuring vault with a transit backend and Approle with correct policy
MinIO requires the following Vault setup:
- The [transit backend](https://www.vaultproject.io/api/secret/transit/index.html) configured with a named encryption key-ring
- [AppRole](https://www.vaultproject.io/docs/auth/approle.html) based authentication with read/update policy for transit backend. In particular, read and update policy are required for the [Generate Data Key](https://www.vaultproject.io/api/secret/transit/index.html#generate-data-key) endpoint and [Decrypt Data](https://www.vaultproject.io/api/secret/transit/index.html#decrypt-data) endpoint.
**2.1.1 Start Vault server in dev mode**
In dev mode, Vault server runs in-memory and starts unsealed. Note that running Vault in dev mode is insecure and any data stored in the Vault is lost upon restart.
**2.1.1 Start Vault server**
Vault requires access to `mlock` syscall by default. Use `setcap` to give the Vault executable the ability to use the `mlock` syscall without running the process as root. To do so run:
```
vault server -dev
sudo setcap cap_ipc_lock=+ep $(readlink -f $(which vault))
```
**2.1.2 Set up vault transit backend and create an app role**
Create `vault-config.json` to use file backend and start the server.
```
cat > vault-config.json <<EOF
{
"api_addr": "http://127.0.0.1:8200",
"backend": {
"file": {
"path": "vault/file"
}
},
"default_lease_ttl": "168h",
"max_lease_ttl": "720h",
"listener": {
"tcp": {
"address": "0.0.0.0:8200",
"tls_disable": true
}
},
"ui": true
}
EOF
vault server -config vault-config.json
```
> NOTE: In this example we use `"tls_disable": true` for demonstration purposes only,
> in production setup you should generate proper TLS certificates by specifying
> - [`tls_cert_file`](https://www.vaultproject.io/docs/configuration/listener/tcp.html#tls_cert_file)
> - [`tls_key_file`](https://www.vaultproject.io/docs/configuration/listener/tcp.html#tls_key_file)
**2.1.2 Initialize vault and unseal it**
```
export VAULT_ADDR='http://127.0.0.1:8200'
vault operator init
Unseal Key 1: eyW/+8ZtsgT81Cb0e8OVxzJAQP5lY7Dcamnze+JnWEDT
Unseal Key 2: 0tZn+7QQCxphpHwTm6/dC3LpP5JGIbYl6PK8Sy79R+P2
Unseal Key 3: cmhs+AUMXUuB6Lzsvgcbp3bRT6VDGQjgCBwB2xm0ANeF
Unseal Key 4: /fTPpec5fWpGqWHK+uhnnTNMQyAbl5alUi4iq2yNgyqj
Unseal Key 5: UPdDVPto+H6ko+20NKmagK40MOskqOBw4y/S51WpgVy/
Initial Root Token: s.zaU4Gbcu0Wh46uj2V3VuUde0
Vault is initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.
Vault does not store the generated master key. Without at least 3 key to
reconstruct the master key, Vault will remain permanently sealed!
It is possible to generate new unseal keys, provided you have a quorum of
existing unseal keys shares. See "vault operator rekey" for more information.
```
Use any of the previously generated keys to unseal the vault
```
vault operator unseal eyW/+8ZtsgT81Cb0e8OVxzJAQP5lY7Dcamnze+JnWEDT
vault operator unseal 0tZn+7QQCxphpHwTm6/dC3LpP5JGIbYl6PK8Sy79R+P2
vault operator unseal cmhs+AUMXUuB6Lzsvgcbp3bRT6VDGQjgCBwB2xm0ANeF
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false ---> NOTE: vault is unsealed
Total Shares 5
Threshold 3
Version 1.1.3
Cluster Name vault-cluster-3f084948
Cluster ID 8c92e999-7062-4da6-4434-0fc05f34824d
HA Enabled false
```
Obtain root token from the `vault operator init` output above. It is displayed as `Initial Root Token: s.zaU4Gbcu0Wh46uj2V3VuUde0`
**2.1.3 Set up vault transit backend and create an app role**
```
export VAULT_TOKEN=s.zaU4Gbcu0Wh46uj2V3VuUde0
vault auth enable approle # enable approle style auth
vault secrets enable transit # enable transit secrets engine
# define an encryption key-ring for the transit path
vault write -f transit/keys/my-minio-key
cat > vaultpolicy.hcl <<EOF
path "transit/datakey/plaintext/my-minio-key" {
path "transit/datakey/plaintext/my-minio-key" {
capabilities = [ "read", "update"]
}
path "transit/decrypt/my-minio-key" {
path "transit/decrypt/my-minio-key" {
capabilities = [ "read", "update"]
}
path "transit/encrypt/my-minio-key" {
path "transit/encrypt/my-minio-key" {
capabilities = [ "read", "update"]
}
EOF
export VAULT_ADDR='http://127.0.0.1:8200'
vault auth enable approle # enable approle style auth
vault secrets enable transit # enable transit secrets engine
vault write -f transit/keys/my-minio-key #define a encryption key-ring for the transit path
vault policy write minio-policy ./vaultpolicy.hcl #define a policy for AppRole to access transit path
vault write auth/approle/role/my-role token_num_uses=0 secret_id_num_uses=0 period=5m # period indicates it is renewable if token is renewed before the period is over
# define a policy for AppRole to access transit path
vault policy write minio-policy ./vaultpolicy.hcl
# period indicates it is renewable if token is renewed before the period is over
vault write auth/approle/role/my-role token_num_uses=0 secret_id_num_uses=0 period=5m
# define an AppRole
vault write auth/approle/role/my-role policies=minio-policy # apply policy to role
vault read auth/approle/role/my-role/role-id # get Approle ID
vault write -f auth/approle/role/my-role/secret-id
Key Value
--- -----
role_id 8c03926c-6c51-7a1d-cf7d-62e48ab8d6d7
vault write -f auth/approle/role/my-role/secret-id
Key Value
--- -----
secret_id edd8738c-6efe-c226-74f9-ef5b66e119d7
secret_id_accessor 57d1db64-6350-c321-4a3e-fc6aeb7d00b6
```
The AppRole ID, AppRole Secret Id, Vault endpoint and Vault key name can now be used to start minio server with Vault as KMS.
@@ -85,10 +173,10 @@ The AppRole ID, AppRole Secret Id, Vault endpoint and Vault key name can now be
You'll need the Vault endpoint, AppRole ID, AppRole SecretID and encryption key-ring name defined in step 2.1.2
```sh
export MINIO_SSE_VAULT_APPROLE_ID=9b56cc08-8258-45d5-24a3-679876769126
export MINIO_SSE_VAULT_APPROLE_SECRET=4e30c52f-13e4-a6f5-0763-d50e8cb4321f
export MINIO_SSE_VAULT_ENDPOINT=https://vault-endpoint-ip:8200
```
export MINIO_SSE_VAULT_APPROLE_ID=8c03926c-6c51-7a1d-cf7d-62e48ab8d6d7
export MINIO_SSE_VAULT_APPROLE_SECRET=edd8738c-6efe-c226-74f9-ef5b66e119d7
export MINIO_SSE_VAULT_ENDPOINT=http://vault-endpoint-ip:8200
export MINIO_SSE_VAULT_KEY_NAME=my-minio-key
export MINIO_SSE_VAULT_AUTH_TYPE=approle
minio server ~/export
@@ -127,19 +215,38 @@ minio gateway s3
#### 2.2 Specify a master key
**2.2.1 KMS master key from environment variables**
A KMS master key consists of a master-key ID (CMK) and the 256 bit master key encoded as HEX value separated by a `:`.
A KMS master key can be specified directly using:
```sh
```
export MINIO_SSE_MASTER_KEY=my-minio-key:6368616e676520746869732070617373776f726420746f206120736563726574
```
Please use your own master key. A random master key can be generated using e.g. this command on Linux/Mac/BSD* systems:
```sh
```
head -c 32 /dev/urandom | xxd -c 32 -ps
```
**2.2.2 KMS master key from docker secret**
Alternatively, you may pass a master key as a [Docker secret](https://docs.docker.com/engine/swarm/secrets/).
```bash
echo "my-minio-key:6368616e676520746869732070617373776f726420746f206120736563726574" | docker secret create sse_master_key
```
Obviously, do not use this demo key for anything real!
To use another secret name, follow the instructions above and replace sse_master_key with your custom names (e.g. my_sse_master_key).
Then, set the MINIO_SSE_MASTER_KEY_FILE environment variable to your secret name:
```bash
export MINIO_SSE_MASTER_KEY_FILE=my_sse_master_key
```
### 3. Test your setup
To test this setup, start minio server with environment variables set in Step 3, and server is ready to handle SSE-S3 requests.
@@ -149,18 +256,18 @@ MinIO can also enable auto-encryption **if** a valid KMS configuration is specif
encrypted objects. Auto-Encryption, if enabled, ensures that all uploaded objects are encrypted using the specified
KMS configuration.
Auto-Encryption is useful especially if the MinIO operator wants to ensure that objects are **never** stored in
Auto-Encryption is useful especially if the MinIO operator wants to ensure that objects are **never** stored in
plaintext - for example if sensitive data is stored on public cloud storage.
To enable auto-encryption set the environment variable to `on`:
```sh
```
export MINIO_SSE_AUTO_ENCRYPTION=on
```
To verify auto-encryption, use the `mc` command:
```sh
```
mc cp test.file myminio/crypt/
test.file: 5 B / 5 B ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 100.00% 337 B/s 0s
mc stat myminio/crypt/test.file
@@ -171,8 +278,7 @@ Encrypted :
```
Note: Auto-Encryption only affects non-SSE-C requests since objects uploaded using SSE-C are already encrypted
and S3 only allows either SSE-S3 or SSE-C but not both for the same object.
and S3 only allows either SSE-S3 or SSE-C but not both for the same object.
# Explore Further
+17
View File
@@ -0,0 +1,17 @@
{
"api_addr": "http://127.0.0.1:8200",
"backend": {
"file": {
"path": "vault/file"
}
},
"default_lease_ttl": "168h",
"max_lease_ttl": "720h",
"listener": {
"tcp": {
"address": "0.0.0.0:8200",
"tls_disable": true
}
},
"ui": true
}
@@ -5,7 +5,7 @@ version: '2'
# 9001 through 9004.
services:
minio1:
image: minio/minio:RELEASE.2019-07-05T21-20-21Z
image: minio/minio:RELEASE.2019-07-31T18-57-56Z
volumes:
- data1:/data
ports:
@@ -15,7 +15,7 @@ services:
MINIO_SECRET_KEY: minio123
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
minio2:
image: minio/minio:RELEASE.2019-07-05T21-20-21Z
image: minio/minio:RELEASE.2019-07-31T18-57-56Z
volumes:
- data2:/data
ports:
@@ -25,7 +25,7 @@ services:
MINIO_SECRET_KEY: minio123
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
minio3:
image: minio/minio:RELEASE.2019-07-05T21-20-21Z
image: minio/minio:RELEASE.2019-07-31T18-57-56Z
volumes:
- data3:/data
ports:
@@ -35,7 +35,7 @@ services:
MINIO_SECRET_KEY: minio123
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
minio4:
image: minio/minio:RELEASE.2019-07-05T21-20-21Z
image: minio/minio:RELEASE.2019-07-31T18-57-56Z
volumes:
- data4:/data
ports:

Some files were not shown because too many files have changed in this diff Show More