mirror of
https://github.com/pgsty/minio.git
synced 2026-11-04 23:02:04 +03:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 456ce4cc92 | |||
| 696f4ceee2 | |||
| dac1cf5a9a | |||
| cb01516a26 | |||
| dfa1b417a8 | |||
| 31bee6b6ed | |||
| 04b92124c5 | |||
| 5392eee250 | |||
| fa32c71a56 | |||
| c9b8bd8de2 | |||
| 017456df63 | |||
| 14b137aa66 | |||
| 3064da7b08 | |||
| 76df027264 | |||
| 208efb843b | |||
| 9ac12cf898 | |||
| ddea0bdf11 | |||
| e7f491a14b | |||
| 8700945cdf | |||
| ff6aabd9c0 | |||
| 6ba323b009 | |||
| e7b3f39064 | |||
| 9fa727d154 | |||
| 73e4e99942 | |||
| a87fc7d09b | |||
| 475df52a19 | |||
| 5512baab21 | |||
| 77963078a2 | |||
| 3f258062d8 | |||
| 3d65dc8d94 | |||
| 53e4887e02 | |||
| a7be313230 | |||
| e12f52e2c6 | |||
| 432cb38dbd | |||
| 18fedc67d5 | |||
| a0456ce940 | |||
| 94e5cb7576 | |||
| 33aec08e8c | |||
| 5bde31d021 | |||
| b52a3e523c | |||
| 428836d4e1 | |||
| 259a5d825b | |||
| 29f64355ce | |||
| 496fba3e9a | |||
| 810a44e951 | |||
| 341d61e3d8 | |||
| 8a71b0ec5a | |||
| 0772438125 | |||
| b3c19e2d4b | |||
| ec9bfd3aef | |||
| fa3546bb03 | |||
| 42e716a094 | |||
| 6b2ed0fc47 | |||
| 0cd0f6c255 | |||
| 4553db3872 | |||
| 847a3ea0a2 | |||
| 1f3d270de8 | |||
| daa57e8e5a | |||
| eb18c82976 | |||
| 2ab0681c0c | |||
| 83d4c5763c | |||
| c6f86d35d3 | |||
| d65a2c6725 | |||
| 70136fb55b | |||
| d6dd98e597 | |||
| c38ada1a26 | |||
| 48bc3f1d53 | |||
| e211f6f52e | |||
| f13f421e84 | |||
| cd03bfb3cf | |||
| add1b6cb6b | |||
| 4f3c9d7677 | |||
| 2e19619e79 | |||
| de736f2864 |
+2
-15
@@ -26,16 +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:
|
||||
directories:
|
||||
- $HOME/.cache/go-build
|
||||
- $HOME/gopath/pkg/mod
|
||||
- $HOME/go/pkg/mod
|
||||
|
||||
go: 1.12.5
|
||||
go: 1.13.x
|
||||
script:
|
||||
- make
|
||||
- diff -au <(gofmt -s -d cmd) <(printf "")
|
||||
@@ -53,8 +44,7 @@ matrix:
|
||||
- ARCH=x86_64
|
||||
- CGO_ENABLED=0
|
||||
- GO111MODULE=on
|
||||
- GOPROXY=https://proxy.golang.org
|
||||
go: 1.12.5
|
||||
go: 1.13.x
|
||||
script:
|
||||
- go build --ldflags="$(go run buildscripts/gen-ldflags.go)" -o %GOPATH%\bin\minio.exe
|
||||
- bash buildscripts/go-coverage.sh
|
||||
@@ -66,6 +56,3 @@ before_script:
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then nvm install 11.10.1 ; fi
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
FROM golang:1.12-alpine
|
||||
FROM golang:1.13-alpine
|
||||
|
||||
LABEL maintainer="MinIO Inc <dev@min.io>"
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV CGO_ENABLED 0
|
||||
ENV GO111MODULE on
|
||||
ENV GOPROXY https://proxy.golang.org
|
||||
|
||||
RUN \
|
||||
apk add --no-cache git && \
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.12-alpine
|
||||
FROM golang:1.13-alpine
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV CGO_ENABLED 0
|
||||
|
||||
+1
-2
@@ -1,14 +1,13 @@
|
||||
#-------------------------------------------------------------
|
||||
# Stage 1: Build and Unit tests
|
||||
#-------------------------------------------------------------
|
||||
FROM golang:1.12
|
||||
FROM golang:1.13
|
||||
|
||||
COPY . /go/src/github.com/minio/minio
|
||||
WORKDIR /go/src/github.com/minio/minio
|
||||
|
||||
RUN apt-get update && apt-get install -y jq
|
||||
ENV GO111MODULE=on
|
||||
ENV GOPROXY=https://proxy.golang.org
|
||||
|
||||
RUN git config --global http.cookiefile /gitcookie/.gitcookie
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# For maintainers only
|
||||
|
||||
### Setup your minio GitHub Repository
|
||||
|
||||
Fork [minio upstream](https://github.com/minio/minio/fork) source repository to your own personal repository.
|
||||
```bash
|
||||
$ mkdir -p $GOPATH/src/github.com/minio
|
||||
$ cd $GOPATH/src/github.com/minio
|
||||
$ git clone https://github.com/$USER_ID/minio
|
||||
$
|
||||
```
|
||||
|
||||
``minio`` uses [govendor](https://github.com/kardianos/govendor) for its dependency management.
|
||||
|
||||
### To manage dependencies
|
||||
|
||||
#### Add new dependencies
|
||||
|
||||
- Run `go get foo/bar`
|
||||
- Edit your code to import foo/bar
|
||||
- Run `govendor add foo/bar` from top-level directory
|
||||
|
||||
#### Remove dependencies
|
||||
|
||||
- Run `govendor remove foo/bar`
|
||||
|
||||
#### Update dependencies
|
||||
|
||||
- Run `govendor remove +vendor`
|
||||
- Run to update the dependent package `go get -u foo/bar`
|
||||
- Run `govendor add +external`
|
||||
|
||||
### Making new releases
|
||||
|
||||
`minio` doesn't follow semantic versioning style, `minio` instead uses the release date and time as the release versions.
|
||||
|
||||
`make release` will generate new binary into `release` directory.
|
||||
@@ -2,11 +2,8 @@ PWD := $(shell pwd)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
||||
|
||||
GOARCH := $(shell go env GOARCH)
|
||||
GOOS := $(shell go env GOOS)
|
||||
GOOSALT ?= 'linux'
|
||||
ifeq ($(GOOS),'darwin')
|
||||
GOOSALT = 'mac'
|
||||
endif
|
||||
|
||||
TAG ?= $(USER)
|
||||
BUILD_LDFLAGS := '$(LDFLAGS)'
|
||||
@@ -19,9 +16,13 @@ checks:
|
||||
|
||||
getdeps:
|
||||
@mkdir -p ${GOPATH}/bin
|
||||
@which golint 1>/dev/null || (echo "Installing golint" && go get -u golang.org/x/lint/golint)
|
||||
@which staticcheck 1>/dev/null || (echo "Installing staticcheck" && wget --quiet -O ${GOPATH}/bin/staticcheck https://github.com/dominikh/go-tools/releases/download/2019.1/staticcheck_${GOOS}_amd64 && chmod +x ${GOPATH}/bin/staticcheck)
|
||||
@which misspell 1>/dev/null || (echo "Installing misspell" && wget --quiet https://github.com/client9/misspell/releases/download/v0.3.4/misspell_0.3.4_${GOOSALT}_64bit.tar.gz && tar xf misspell_0.3.4_${GOOSALT}_64bit.tar.gz && mv misspell ${GOPATH}/bin/misspell && chmod +x ${GOPATH}/bin/misspell && rm -f misspell_0.3.4_${GOOSALT}_64bit.tar.gz)
|
||||
@which golint 1>/dev/null || (echo "Installing golint" && GO111MODULE=off go get -u golang.org/x/lint/golint)
|
||||
ifeq ($(GOARCH),s390x)
|
||||
@which staticcheck 1>/dev/null || (echo "Installing staticcheck" && GO111MODULE=off go get honnef.co/go/tools/cmd/staticcheck)
|
||||
else
|
||||
@which staticcheck 1>/dev/null || (echo "Installing staticcheck" && wget --quiet https://github.com/dominikh/go-tools/releases/download/2019.2.3/staticcheck_${GOOS}_${GOARCH}.tar.gz && tar xf staticcheck_${GOOS}_${GOARCH}.tar.gz && mv staticcheck/staticcheck ${GOPATH}/bin/staticcheck && chmod +x ${GOPATH}/bin/staticcheck && rm -f staticcheck_${GOOS}_${GOARCH}.tar.gz && rm -rf staticcheck)
|
||||
endif
|
||||
@which misspell 1>/dev/null || (echo "Installing misspell" && GO111MODULE=off go get -u github.com/client9/misspell/cmd/misspell)
|
||||
|
||||
crosscompile:
|
||||
@(env bash $(PWD)/buildscripts/cross-compile.sh)
|
||||
@@ -29,36 +30,37 @@ crosscompile:
|
||||
verifiers: getdeps vet fmt lint staticcheck spelling
|
||||
|
||||
vet:
|
||||
@echo "Running $@"
|
||||
@GOPROXY=https://proxy.golang.org GO111MODULE=on go vet github.com/minio/minio/...
|
||||
@echo "Running $@ check"
|
||||
@GO111MODULE=on go vet github.com/minio/minio/...
|
||||
|
||||
fmt:
|
||||
@echo "Running $@"
|
||||
@GOPROXY=https://proxy.golang.org GO111MODULE=on gofmt -d cmd/
|
||||
@GOPROXY=https://proxy.golang.org GO111MODULE=on gofmt -d pkg/
|
||||
@echo "Running $@ check"
|
||||
@GO111MODULE=on gofmt -d cmd/
|
||||
@GO111MODULE=on gofmt -d pkg/
|
||||
|
||||
lint:
|
||||
@echo "Running $@"
|
||||
@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/...
|
||||
@echo "Running $@ check"
|
||||
@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/...
|
||||
|
||||
staticcheck:
|
||||
@echo "Running $@"
|
||||
@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/...
|
||||
@echo "Running $@ check"
|
||||
@GO111MODULE=on ${GOPATH}/bin/staticcheck github.com/minio/minio/cmd/...
|
||||
@GO111MODULE=on ${GOPATH}/bin/staticcheck github.com/minio/minio/pkg/...
|
||||
|
||||
spelling:
|
||||
@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/`
|
||||
@echo "Running $@ check"
|
||||
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find cmd/`
|
||||
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find pkg/`
|
||||
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find docs/`
|
||||
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find buildscripts/`
|
||||
@GO111MODULE=on ${GOPATH}/bin/misspell -locale US -error `find dockerscripts/`
|
||||
|
||||
# Builds minio, runs the verifiers then runs the tests.
|
||||
check: test
|
||||
test: verifiers build
|
||||
@echo "Running unit tests"
|
||||
@GOPROXY=https://proxy.golang.org GO111MODULE=on CGO_ENABLED=0 go test -tags kqueue ./... 1>/dev/null
|
||||
@GO111MODULE=on CGO_ENABLED=0 go test -tags kqueue ./... 1>/dev/null
|
||||
|
||||
verify: build
|
||||
@echo "Verifying build"
|
||||
@@ -71,7 +73,7 @@ coverage: build
|
||||
# Builds minio locally.
|
||||
build: checks
|
||||
@echo "Building minio binary to './minio'"
|
||||
@GOPROXY=https://proxy.golang.org GO111MODULE=on GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
||||
@GO111MODULE=on CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
||||
|
||||
docker: build
|
||||
@docker build -t $(TAG) . -f Dockerfile.dev
|
||||
|
||||
@@ -5,7 +5,7 @@ MinIO is an object storage server released under Apache License v2.0. It is comp
|
||||
|
||||
MinIO server is light enough to be bundled with the application stack, similar to NodeJS, Redis and MySQL.
|
||||
|
||||
[1]: MinIO in its default mode is faster and does not calculate MD5Sum unless passed by client. This may lead to incompatibility with few S3 clients like s3ql that heavily depend on MD5Sum. For full compatibility with Amazon S3 API, start MinIO with `--compat` option.
|
||||
[1] MinIO in its default mode is faster and does not calculate MD5Sum unless passed by client. This may lead to incompatibility with some S3 clients like s3ql that heavily depend on MD5Sum. For such applications start MinIO with `--compat` option.
|
||||
```sh
|
||||
minio --compat server /data
|
||||
```
|
||||
@@ -91,7 +91,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
|
||||
GOPROXY=https://proxy.golang.org GO111MODULE=on go get github.com/minio/minio
|
||||
GO111MODULE=on go get github.com/minio/minio
|
||||
```
|
||||
|
||||
## Allow port access for Firewalls
|
||||
@@ -162,6 +162,22 @@ When deployed on a single drive, MinIO server lets clients access any pre-existi
|
||||
|
||||
The above statement is also valid for all gateway backends.
|
||||
|
||||
## Upgrading MinIO
|
||||
MinIO server supports rolling upgrades, i.e. you can update one MinIO instance at a time in a distributed cluster. This allows upgrades with no downtime. Upgrades can be done manually by replacing the binary with the latest release and restarting all servers in a rolling fashion. However, we recommend all our users to use [`mc admin update`](https://docs.min.io/docs/minio-admin-complete-guide.html#update) from the client. This will update all the nodes in the cluster and restart them, as shown in the following command from the MinIO client (mc):
|
||||
|
||||
```
|
||||
mc admin update <minio alias, e.g., myminio>
|
||||
```
|
||||
|
||||
**Important things to remember during upgrades**:
|
||||
|
||||
- `mc admin update` will only work if the user running MinIO has write access to the parent directory where the binary is located, for example if the current binary is at `/usr/local/bin/minio`, you would need write access to `/usr/local/bin`.
|
||||
- In the case of federated setups `mc admin update` should be run against each cluster individually. Avoid updating `mc` until all clusters have been updated.
|
||||
- If you are updating the server it is always recommended (unless explicitly mentioned in MinIO server release notes), to update `mc` once all the servers have been upgraded using `mc update`.
|
||||
- `mc admin update` is disabled in docker/container environments, container environments provide their own mechanisms for updating running containers.
|
||||
- If you are using Vault as KMS with MinIO, ensure you have followed the Vault upgrade procedure outlined here: https://www.vaultproject.io/docs/upgrading/index.html
|
||||
- If you are using etcd with MinIO for the federation, ensure you have followed the etcd upgrade procedure outlined here: https://github.com/etcd-io/etcd/blob/master/Documentation/upgrades/upgrading-etcd.md
|
||||
|
||||
## Explore Further
|
||||
- [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)
|
||||
|
||||
@@ -21,7 +21,7 @@ _init() {
|
||||
|
||||
## Minimum required versions for build dependencies
|
||||
GIT_VERSION="1.0"
|
||||
GO_VERSION="1.12"
|
||||
GO_VERSION="1.13"
|
||||
OSX_VERSION="10.8"
|
||||
KNAME=$(uname -s)
|
||||
ARCH=$(uname -m)
|
||||
|
||||
@@ -23,7 +23,6 @@ function _build() {
|
||||
export GOOS=$os
|
||||
export GOARCH=$arch
|
||||
export GO111MODULE=on
|
||||
export GOPROXY=https://proxy.golang.org
|
||||
go build -tags kqueue -o /dev/null
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
set -e
|
||||
|
||||
GOPROXY=https://proxy.golang.org GO111MODULE=on CGO_ENABLED=0 go test -v -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
GO111MODULE=on CGO_ENABLED=0 go test -v -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
@@ -33,7 +33,6 @@ 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" )
|
||||
|
||||
+347
-308
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2016, 2017, 2018, 2019 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2016-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.
|
||||
@@ -17,15 +17,17 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -33,13 +35,11 @@ import (
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/cpu"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
@@ -66,107 +66,139 @@ const (
|
||||
mgmtForceStop = "forceStop"
|
||||
)
|
||||
|
||||
var (
|
||||
// This struct literal represents the Admin API version that
|
||||
// the server uses.
|
||||
adminAPIVersionInfo = madmin.AdminAPIVersionInfo{
|
||||
Version: "1",
|
||||
func updateServer(updateURL, sha256Hex string, latestReleaseTime time.Time) (us madmin.ServerUpdateStatus, err error) {
|
||||
minioMode := getMinioMode()
|
||||
// No inputs provided we should try to update using the default URL.
|
||||
if updateURL == "" && sha256Hex == "" && latestReleaseTime.IsZero() {
|
||||
var updateMsg string
|
||||
updateMsg, sha256Hex, _, latestReleaseTime, err = getUpdateInfo(updateTimeout, minioMode)
|
||||
if err != nil {
|
||||
return us, err
|
||||
}
|
||||
if updateMsg == "" {
|
||||
us.CurrentVersion = Version
|
||||
us.UpdatedVersion = Version
|
||||
return us, nil
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
updateURL = minioReleaseURL + "minio.exe"
|
||||
} else {
|
||||
updateURL = minioReleaseURL + "minio"
|
||||
}
|
||||
}
|
||||
)
|
||||
if err = doUpdate(updateURL, sha256Hex, minioMode); err != nil {
|
||||
return us, err
|
||||
}
|
||||
us.CurrentVersion = Version
|
||||
us.UpdatedVersion = latestReleaseTime.Format(minioReleaseTagTimeLayout)
|
||||
return us, nil
|
||||
}
|
||||
|
||||
// VersionHandler - GET /minio/admin/version
|
||||
// -----------
|
||||
// Returns Administration API version
|
||||
func (a adminAPIHandlers) VersionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "Version")
|
||||
// ServerUpdateHandler - POST /minio/admin/v1/update?updateURL={updateURL}
|
||||
// ----------
|
||||
// updates all minio servers and restarts them gracefully.
|
||||
func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ServerUpdate")
|
||||
|
||||
objectAPI := validateAdminReq(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(adminAPIVersionInfo)
|
||||
if globalInplaceUpdateDisabled {
|
||||
// if MINIO_UPDATE=off - inplace update is disabled, mostly
|
||||
// in containers.
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
updateURL := vars[peerRESTUpdateURL]
|
||||
mode := getMinioMode()
|
||||
var sha256Hex string
|
||||
var latestReleaseTime time.Time
|
||||
if updateURL != "" {
|
||||
u, err := url.Parse(updateURL)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := downloadReleaseURL(updateURL, updateTimeout, mode)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
sha256Hex, latestReleaseTime, err = parseReleaseData(content)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
u.Path = path.Dir(u.Path) + "minio.exe"
|
||||
} else {
|
||||
u.Path = path.Dir(u.Path) + "minio"
|
||||
}
|
||||
|
||||
updateURL = u.String()
|
||||
}
|
||||
|
||||
for _, nerr := range globalNotificationSys.ServerUpdate(updateURL, sha256Hex, latestReleaseTime) {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus, err := updateServer(updateURL, sha256Hex, latestReleaseTime)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
}
|
||||
|
||||
// ServiceStatusHandler - GET /minio/admin/v1/service
|
||||
// ----------
|
||||
// Returns server version and uptime.
|
||||
func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ServiceStatus")
|
||||
|
||||
objectAPI := validateAdminReq(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch server version
|
||||
serverVersion := madmin.ServerVersion{
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
}
|
||||
|
||||
// Fetch uptimes from all peers and pick the latest.
|
||||
uptime := getPeerUptimes(globalNotificationSys.ServerInfo(ctx))
|
||||
|
||||
// Create API response
|
||||
serverStatus := madmin.ServiceStatus{
|
||||
ServerVersion: serverVersion,
|
||||
Uptime: uptime,
|
||||
}
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(serverStatus)
|
||||
jsonBytes, err := json.Marshal(updateStatus)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Reply with storage information (across nodes in a
|
||||
// distributed setup) as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
|
||||
if updateStatus.CurrentVersion != updateStatus.UpdatedVersion {
|
||||
// We did upgrade - restart all services.
|
||||
globalServiceSignalCh <- serviceRestart
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceStopNRestartHandler - POST /minio/admin/v1/service
|
||||
// Body: {"action": <restart-action>}
|
||||
// ServiceActionHandler - POST /minio/admin/v1/service?action={action}
|
||||
// ----------
|
||||
// Restarts/Stops minio server gracefully. In a distributed setup,
|
||||
// restarts all the servers in the cluster.
|
||||
func (a adminAPIHandlers) ServiceStopNRestartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ServiceStopNRestart")
|
||||
// restarts/stops minio server gracefully. In a distributed setup,
|
||||
func (a adminAPIHandlers) ServiceActionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ServiceAction")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
action := vars["action"]
|
||||
|
||||
objectAPI := validateAdminReq(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var sa madmin.ServiceAction
|
||||
err := json.NewDecoder(r.Body).Decode(&sa)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrRequestBodyParse), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var serviceSig serviceSignal
|
||||
switch sa.Action {
|
||||
case madmin.ServiceActionValueRestart:
|
||||
switch madmin.ServiceAction(action) {
|
||||
case madmin.ServiceActionRestart:
|
||||
serviceSig = serviceRestart
|
||||
case madmin.ServiceActionValueStop:
|
||||
case madmin.ServiceActionStop:
|
||||
serviceSig = serviceStop
|
||||
default:
|
||||
logger.LogIf(ctx, fmt.Errorf("Unrecognized service action %s requested", action))
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMalformedPOSTRequest), r.URL)
|
||||
logger.LogIf(ctx, errors.New("Invalid service action received"))
|
||||
return
|
||||
}
|
||||
|
||||
// Reply to the client before restarting minio server.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
|
||||
// Notify all other MinIO peers signal service.
|
||||
for _, nerr := range globalNotificationSys.SignalService(serviceSig) {
|
||||
if nerr.Err != nil {
|
||||
@@ -175,6 +207,9 @@ func (a adminAPIHandlers) ServiceStopNRestartHandler(w http.ResponseWriter, r *h
|
||||
}
|
||||
}
|
||||
|
||||
// Reply to the client before restarting, stopping MinIO server.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
|
||||
globalServiceSignalCh <- serviceSig
|
||||
}
|
||||
|
||||
@@ -277,15 +312,6 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
}
|
||||
|
||||
// ServerDrivesPerfInfo holds information about address, performance
|
||||
// of all drives on one server. It also reports any errors if encountered
|
||||
// while trying to reach this server.
|
||||
type ServerDrivesPerfInfo struct {
|
||||
Addr string `json:"addr"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Perf []disk.Performance `json:"perf"`
|
||||
}
|
||||
|
||||
// ServerCPULoadInfo holds informantion about cpu utilization
|
||||
// of one minio node. It also reports any errors if encountered
|
||||
// while trying to reach this server.
|
||||
@@ -337,8 +363,7 @@ func (a adminAPIHandlers) PerfInfoHandler(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
storage := objectAPI.StorageInfo(ctx)
|
||||
if !(storage.Backend.Type == BackendFS || storage.Backend.Type == BackendErasure) {
|
||||
if !globalIsDistXL {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -366,16 +391,25 @@ func (a adminAPIHandlers) PerfInfoHandler(w http.ResponseWriter, r *http.Request
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
|
||||
case "drive":
|
||||
info := objectAPI.StorageInfo(ctx)
|
||||
if !(info.Backend.Type == BackendFS || info.Backend.Type == BackendErasure) {
|
||||
// Drive Perf is only implemented for Erasure coded backends
|
||||
if !globalIsXL {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var size int64 = madmin.DefaultDrivePerfSize
|
||||
if sizeStr, found := vars["size"]; found {
|
||||
var err error
|
||||
if size, err = strconv.ParseInt(sizeStr, 10, 64); err != nil || size <= 0 {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Get drive performance details from local server's drive(s)
|
||||
dp := localEndpointsDrivePerf(globalEndpoints, r)
|
||||
dp := getLocalDrivesPerf(globalEndpoints, size, r)
|
||||
|
||||
// Notify all other MinIO peers to report drive performance numbers
|
||||
dps := globalNotificationSys.DrivePerfInfo()
|
||||
dps := globalNotificationSys.DrivePerfInfo(size)
|
||||
dps = append(dps, dp)
|
||||
|
||||
// Marshal API response
|
||||
@@ -390,7 +424,7 @@ func (a adminAPIHandlers) PerfInfoHandler(w http.ResponseWriter, r *http.Request
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
case "cpu":
|
||||
// Get CPU load details from local server's cpu(s)
|
||||
cpu := localEndpointsCPULoad(globalEndpoints, r)
|
||||
cpu := getLocalCPULoad(globalEndpoints, r)
|
||||
// Notify all other MinIO peers to report cpu load numbers
|
||||
cpus := globalNotificationSys.CPULoadInfo()
|
||||
cpus = append(cpus, cpu)
|
||||
@@ -407,7 +441,7 @@ func (a adminAPIHandlers) PerfInfoHandler(w http.ResponseWriter, r *http.Request
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
case "mem":
|
||||
// Get mem usage details from local server(s)
|
||||
m := localEndpointsMemUsage(globalEndpoints, r)
|
||||
m := getLocalMemUsage(globalEndpoints, r)
|
||||
// Notify all other MinIO peers to report mem usage numbers
|
||||
mems := globalNotificationSys.MemUsageInfo()
|
||||
mems = append(mems, m)
|
||||
@@ -438,7 +472,6 @@ func newLockEntry(l lockRequesterInfo, resource, server string) *madmin.LockEntr
|
||||
}
|
||||
|
||||
func topLockEntries(peerLocks []*PeerLocks) madmin.LockEntries {
|
||||
const listCount int = 10
|
||||
entryMap := make(map[string]*madmin.LockEntry)
|
||||
for _, peerLock := range peerLocks {
|
||||
if peerLock == nil {
|
||||
@@ -459,6 +492,7 @@ func topLockEntries(peerLocks []*PeerLocks) madmin.LockEntries {
|
||||
lockEntries = append(lockEntries, *v)
|
||||
}
|
||||
sort.Sort(lockEntries)
|
||||
const listCount int = 10
|
||||
if len(lockEntries) > listCount {
|
||||
lockEntries = lockEntries[:listCount]
|
||||
}
|
||||
@@ -616,45 +650,60 @@ func (a adminAPIHandlers) DownloadProfilingHandler(w http.ResponseWriter, r *htt
|
||||
}
|
||||
}
|
||||
|
||||
type healInitParams struct {
|
||||
bucket, objPrefix string
|
||||
hs madmin.HealOpts
|
||||
clientToken string
|
||||
forceStart, forceStop bool
|
||||
}
|
||||
|
||||
// extractHealInitParams - Validates params for heal init API.
|
||||
func extractHealInitParams(r *http.Request) (bucket, objPrefix string,
|
||||
hs madmin.HealOpts, clientToken string, forceStart bool, forceStop bool,
|
||||
err APIErrorCode) {
|
||||
func extractHealInitParams(vars map[string]string, qParms url.Values, r io.Reader) (hip healInitParams, err APIErrorCode) {
|
||||
hip.bucket = vars[string(mgmtBucket)]
|
||||
hip.objPrefix = vars[string(mgmtPrefix)]
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket = vars[string(mgmtBucket)]
|
||||
objPrefix = vars[string(mgmtPrefix)]
|
||||
|
||||
if bucket == "" {
|
||||
if objPrefix != "" {
|
||||
if hip.bucket == "" {
|
||||
if hip.objPrefix != "" {
|
||||
// Bucket is required if object-prefix is given
|
||||
err = ErrHealMissingBucket
|
||||
return
|
||||
}
|
||||
} else if isReservedOrInvalidBucket(bucket, false) {
|
||||
} else if isReservedOrInvalidBucket(hip.bucket, false) {
|
||||
err = ErrInvalidBucketName
|
||||
return
|
||||
}
|
||||
|
||||
// empty prefix is valid.
|
||||
if !IsValidObjectPrefix(objPrefix) {
|
||||
if !IsValidObjectPrefix(hip.objPrefix) {
|
||||
err = ErrInvalidObjectName
|
||||
return
|
||||
}
|
||||
|
||||
qParms := r.URL.Query()
|
||||
if len(qParms[string(mgmtClientToken)]) > 0 {
|
||||
clientToken = qParms[string(mgmtClientToken)][0]
|
||||
hip.clientToken = qParms[string(mgmtClientToken)][0]
|
||||
}
|
||||
if _, ok := qParms[string(mgmtForceStart)]; ok {
|
||||
forceStart = true
|
||||
hip.forceStart = true
|
||||
}
|
||||
if _, ok := qParms[string(mgmtForceStop)]; ok {
|
||||
forceStop = true
|
||||
hip.forceStop = true
|
||||
}
|
||||
|
||||
// Invalid request conditions:
|
||||
//
|
||||
// Cannot have both forceStart and forceStop in the same
|
||||
// request; If clientToken is provided, request can only be
|
||||
// to continue receiving logs, so it cannot be start or
|
||||
// stop;
|
||||
if (hip.forceStart && hip.forceStop) ||
|
||||
(hip.clientToken != "" && (hip.forceStart || hip.forceStop)) {
|
||||
err = ErrInvalidRequest
|
||||
return
|
||||
}
|
||||
|
||||
// ignore body if clientToken is provided
|
||||
if clientToken == "" {
|
||||
jerr := json.NewDecoder(r.Body).Decode(&hs)
|
||||
if hip.clientToken == "" {
|
||||
jerr := json.NewDecoder(r).Decode(&hip.hs)
|
||||
if jerr != nil {
|
||||
logger.LogIf(context.Background(), jerr)
|
||||
err = ErrRequestBodyParse
|
||||
@@ -693,7 +742,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
bucket, objPrefix, hs, clientToken, forceStart, forceStop, errCode := extractHealInitParams(r)
|
||||
hip, errCode := extractHealInitParams(mux.Vars(r), r.URL.Query(), r.Body)
|
||||
if errCode != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
|
||||
return
|
||||
@@ -767,8 +816,8 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
info := objectAPI.StorageInfo(ctx)
|
||||
numDisks := info.Backend.OfflineDisks + info.Backend.OnlineDisks
|
||||
|
||||
healPath := pathJoin(bucket, objPrefix)
|
||||
if clientToken == "" && !forceStart && !forceStop {
|
||||
healPath := pathJoin(hip.bucket, hip.objPrefix)
|
||||
if hip.clientToken == "" && !hip.forceStart && !hip.forceStop {
|
||||
nh, exists := globalAllHealState.getHealSequence(healPath)
|
||||
if exists && !nh.hasEnded() && len(nh.currentStatus.Items) > 0 {
|
||||
b, err := json.Marshal(madmin.HealStartSuccess{
|
||||
@@ -787,11 +836,11 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if clientToken != "" && !forceStart && !forceStop {
|
||||
if hip.clientToken != "" && !hip.forceStart && !hip.forceStop {
|
||||
// Since clientToken is given, fetch heal status from running
|
||||
// heal sequence.
|
||||
respBytes, errCode := globalAllHealState.PopHealStatusJSON(
|
||||
healPath, clientToken)
|
||||
healPath, hip.clientToken)
|
||||
if errCode != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(errCode), r.URL)
|
||||
} else {
|
||||
@@ -802,14 +851,14 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
respCh := make(chan healResp)
|
||||
switch {
|
||||
case forceStop:
|
||||
case hip.forceStop:
|
||||
go func() {
|
||||
respBytes, apiErr := globalAllHealState.stopHealSequence(healPath)
|
||||
hr := healResp{respBytes: respBytes, apiErr: apiErr}
|
||||
respCh <- hr
|
||||
}()
|
||||
case clientToken == "":
|
||||
nh := newHealSequence(bucket, objPrefix, handlers.GetSourceIP(r), numDisks, hs, forceStart)
|
||||
case hip.clientToken == "":
|
||||
nh := newHealSequence(hip.bucket, hip.objPrefix, handlers.GetSourceIP(r), numDisks, hip.hs, hip.forceStart)
|
||||
go func() {
|
||||
respBytes, apiErr, errMsg := globalAllHealState.LaunchNewHealSequence(nh)
|
||||
hr := healResp{respBytes, apiErr, errMsg}
|
||||
@@ -898,25 +947,6 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// Disable tidwall json array notation in JSON key path so
|
||||
// users can set json with a key as a number.
|
||||
// In tidwall json, notify.webhook.0 = val means { "notify" : { "webhook" : [val] }}
|
||||
// In MinIO, notify.webhook.0 = val means { "notify" : { "webhook" : {"0" : val}}}
|
||||
func normalizeJSONKey(input string) (key string) {
|
||||
subKeys := strings.Split(input, ".")
|
||||
for i, k := range subKeys {
|
||||
if i > 0 {
|
||||
key += "."
|
||||
}
|
||||
if _, err := strconv.Atoi(k); err == nil {
|
||||
key += ":" + k
|
||||
} else {
|
||||
key += k
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request) ObjectLayer {
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
@@ -935,60 +965,24 @@ func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
||||
return objectAPI
|
||||
}
|
||||
|
||||
// GetConfigHandler - GET /minio/admin/v1/config-keys
|
||||
// Get some keys in config.json of this minio setup.
|
||||
func (a adminAPIHandlers) GetConfigKeysHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetConfigKeysHandler")
|
||||
|
||||
objectAPI := validateAdminReq(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var keys []string
|
||||
queries := r.URL.Query()
|
||||
|
||||
for k := range queries {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
config, err := readServerConfig(ctx, objectAPI)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configStr := string(configData)
|
||||
newConfigStr := `{}`
|
||||
|
||||
for _, key := range keys {
|
||||
// sjson.Set does not return an error if key is empty
|
||||
// we should check by ourselves here
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
val := gjson.Get(configStr, key)
|
||||
if j, ierr := sjson.Set(newConfigStr, normalizeJSONKey(key), val.Value()); ierr == nil {
|
||||
newConfigStr = j
|
||||
}
|
||||
}
|
||||
|
||||
password := config.GetCredential().SecretKey
|
||||
econfigData, err := madmin.EncryptData(password, []byte(newConfigStr))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, []byte(econfigData))
|
||||
// AdminError - is a generic error for all admin APIs.
|
||||
type AdminError struct {
|
||||
Code string
|
||||
Message string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func (ae AdminError) Error() string {
|
||||
return ae.Message
|
||||
}
|
||||
|
||||
// Admin API errors
|
||||
const (
|
||||
AdminUpdateUnexpectedFailure = "XMinioAdminUpdateUnexpectedFailure"
|
||||
AdminUpdateURLNotReachable = "XMinioAdminUpdateURLNotReachable"
|
||||
AdminUpdateApplyFailure = "XMinioAdminUpdateApplyFailure"
|
||||
)
|
||||
|
||||
// toAdminAPIErrCode - converts errXLWriteQuorum error to admin API
|
||||
// specific error.
|
||||
func toAdminAPIErrCode(ctx context.Context, err error) APIErrorCode {
|
||||
@@ -1001,7 +995,21 @@ func toAdminAPIErrCode(ctx context.Context, err error) APIErrorCode {
|
||||
}
|
||||
|
||||
func toAdminAPIErr(ctx context.Context, err error) APIError {
|
||||
return errorCodes.ToAPIErr(toAdminAPIErrCode(ctx, err))
|
||||
if err == nil {
|
||||
return noError
|
||||
}
|
||||
apiErr := errorCodes.ToAPIErr(toAdminAPIErrCode(ctx, err))
|
||||
if apiErr.Code == "InternalError" {
|
||||
switch e := err.(type) {
|
||||
case AdminError:
|
||||
apiErr = APIError{
|
||||
Code: e.Code,
|
||||
Description: e.Message,
|
||||
HTTPStatusCode: e.StatusCode,
|
||||
}
|
||||
}
|
||||
}
|
||||
return apiErr
|
||||
}
|
||||
|
||||
// RemoveUser - DELETE /minio/admin/v1/remove-user?accessKey=<access_key>
|
||||
@@ -1173,7 +1181,12 @@ func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
groups := globalIAMSys.ListGroups()
|
||||
groups, err := globalIAMSys.ListGroups()
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := json.Marshal(groups)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
@@ -1533,127 +1546,6 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
func convertValueType(elem []byte, jsonType gjson.Type) (interface{}, error) {
|
||||
str := string(elem)
|
||||
switch jsonType {
|
||||
case gjson.False, gjson.True:
|
||||
return strconv.ParseBool(str)
|
||||
case gjson.JSON:
|
||||
return gjson.Parse(str).Value(), nil
|
||||
case gjson.String:
|
||||
return str, nil
|
||||
case gjson.Number:
|
||||
return strconv.ParseFloat(str, 64)
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfigKeysHandler - PUT /minio/admin/v1/config-keys
|
||||
func (a adminAPIHandlers) SetConfigKeysHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "SetConfigKeysHandler")
|
||||
|
||||
objectAPI := validateAdminReq(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Load config
|
||||
configStruct, err := readServerConfig(ctx, objectAPI)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert config to json bytes
|
||||
configBytes, err := json.Marshal(configStruct)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configStr := string(configBytes)
|
||||
|
||||
queries := r.URL.Query()
|
||||
password := globalServerConfig.GetCredential().SecretKey
|
||||
|
||||
// Set key values in the JSON config
|
||||
for k := range queries {
|
||||
// Decode encrypted data associated to the current key
|
||||
encryptedElem, dErr := base64.StdEncoding.DecodeString(queries.Get(k))
|
||||
if dErr != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("key", k)
|
||||
ctx = logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, dErr)
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), dErr.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
elem, dErr := madmin.DecryptData(password, bytes.NewBuffer([]byte(encryptedElem)))
|
||||
if dErr != nil {
|
||||
logger.LogIf(ctx, dErr)
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), dErr.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate the type of the current key from the
|
||||
// original config json
|
||||
jsonFieldType := gjson.Get(configStr, k).Type
|
||||
// Convert passed value to json filed type
|
||||
val, cErr := convertValueType(elem, jsonFieldType)
|
||||
if cErr != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), cErr.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
// Set the key/value in the new json document
|
||||
if s, sErr := sjson.Set(configStr, normalizeJSONKey(k), val); sErr == nil {
|
||||
configStr = s
|
||||
}
|
||||
}
|
||||
|
||||
configBytes = []byte(configStr)
|
||||
|
||||
// Validate config
|
||||
var config serverConfig
|
||||
if err = json.Unmarshal(configBytes, &config); err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = config.Validate(); err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = config.TestNotificationTargets(); err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// If credentials for the server are provided via environment,
|
||||
// then credentials in the provided configuration must match.
|
||||
if globalIsEnvCreds {
|
||||
if !globalServerConfig.GetCredential().Equal(config.Credential) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminCredentialsMismatch), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = saveServerConfig(ctx, objectAPI, &config); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Send success response
|
||||
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*
|
||||
@@ -1729,3 +1621,150 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The handler sends console logs to the connected HTTP client.
|
||||
func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ConsoleLog")
|
||||
|
||||
objectAPI := validateAdminReq(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
node := r.URL.Query().Get("node")
|
||||
// limit buffered console entries if client requested it.
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limitLines, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
limitLines = 10
|
||||
}
|
||||
// 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("Connection", "close")
|
||||
w.Header().Set(xhttp.ContentType, "text/event-stream")
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
defer close(doneCh)
|
||||
logCh := make(chan interface{}, 4000)
|
||||
|
||||
remoteHosts := getRemoteHosts(globalEndpoints)
|
||||
peers, err := getRestClients(remoteHosts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
globalConsoleSys.Subscribe(logCh, doneCh, node, limitLines, nil)
|
||||
|
||||
for _, peer := range peers {
|
||||
if node == "" || strings.EqualFold(peer.host.Name, node) {
|
||||
peer.ConsoleLog(logCh, doneCh)
|
||||
}
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
|
||||
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
|
||||
defer keepAliveTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case entry := <-logCh:
|
||||
log := entry.(madmin.LogInfo)
|
||||
if log.SendLog(node) {
|
||||
if err := enc.Encode(log); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
case <-keepAliveTicker.C:
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KMSKeyStatusHandler - GET /minio/admin/v1/kms/key/status?key-id=<master-key-id>
|
||||
func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "KMSKeyStatusHandler")
|
||||
|
||||
objectAPI := validateAdminReq(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if GlobalKMS == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
keyID := r.URL.Query().Get("key-id")
|
||||
if keyID == "" {
|
||||
keyID = globalKMSKeyID
|
||||
}
|
||||
var response = madmin.KMSKeyStatus{
|
||||
KeyID: keyID,
|
||||
}
|
||||
|
||||
kmsContext := crypto.Context{"MinIO admin API": "KMSKeyStatusHandler"} // Context for a test key operation
|
||||
// 1. Generate a new key using the KMS.
|
||||
key, sealedKey, err := GlobalKMS.GenerateKey(keyID, kmsContext)
|
||||
if err != nil {
|
||||
response.EncryptionErr = err.Error()
|
||||
resp, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
writeSuccessResponseJSON(w, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Check whether we can update / re-wrap the sealed key.
|
||||
sealedKey, err = GlobalKMS.UpdateKey(keyID, sealedKey, kmsContext)
|
||||
if err != nil {
|
||||
response.UpdateErr = err.Error()
|
||||
resp, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
writeSuccessResponseJSON(w, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Verify that we can indeed decrypt the (encrypted) key
|
||||
decryptedKey, err := GlobalKMS.UnsealKey(keyID, sealedKey, kmsContext)
|
||||
if err != nil {
|
||||
response.DecryptionErr = err.Error()
|
||||
resp, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
writeSuccessResponseJSON(w, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Compare generated key with decrypted key
|
||||
if subtle.ConstantTimeCompare(key[:], decryptedKey[:]) != 1 {
|
||||
response.DecryptionErr = "The generated and the decrypted data key do not match"
|
||||
resp, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
writeSuccessResponseJSON(w, resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
writeSuccessResponseJSON(w, resp)
|
||||
}
|
||||
|
||||
+111
-123
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2016, 2017, 2018 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2016-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.
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
@@ -344,117 +345,35 @@ func initTestXLObjLayer() (ObjectLayer, []string, error) {
|
||||
return objLayer, xlDirs, nil
|
||||
}
|
||||
|
||||
func TestAdminVersionHandler(t *testing.T) {
|
||||
adminTestBed, err := prepareAdminXLTestBed()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
||||
}
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
req, err := newTestRequest("GET", "/minio/admin/version", 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct request - %v", err)
|
||||
}
|
||||
cred := globalServerConfig.GetCredential()
|
||||
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to sign request - %v", err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.router.ServeHTTP(rec, req)
|
||||
if http.StatusOK != rec.Code {
|
||||
t.Errorf("Unexpected status code - got %d but expected %d",
|
||||
rec.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
var result madmin.AdminAPIVersionInfo
|
||||
err = json.NewDecoder(rec.Body).Decode(&result)
|
||||
if err != nil {
|
||||
t.Errorf("json parse err: %v", err)
|
||||
}
|
||||
|
||||
if result != adminAPIVersionInfo {
|
||||
t.Errorf("unexpected version: %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
// cmdType - Represents different service subcomands like status, stop
|
||||
// and restart.
|
||||
type cmdType int
|
||||
|
||||
const (
|
||||
statusCmd cmdType = iota
|
||||
restartCmd
|
||||
restartCmd cmdType = iota
|
||||
stopCmd
|
||||
setCreds
|
||||
)
|
||||
|
||||
// String - String representation for cmdType
|
||||
func (c cmdType) String() string {
|
||||
switch c {
|
||||
case statusCmd:
|
||||
return "status"
|
||||
case restartCmd:
|
||||
return "restart"
|
||||
case stopCmd:
|
||||
return "stop"
|
||||
case setCreds:
|
||||
return "set-credentials"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// apiMethod - Returns the HTTP method corresponding to the admin REST
|
||||
// API for a given cmdType value.
|
||||
func (c cmdType) apiMethod() string {
|
||||
switch c {
|
||||
case statusCmd:
|
||||
return "GET"
|
||||
case restartCmd:
|
||||
return "POST"
|
||||
case stopCmd:
|
||||
return "POST"
|
||||
case setCreds:
|
||||
return "PUT"
|
||||
}
|
||||
return "GET"
|
||||
}
|
||||
|
||||
// apiEndpoint - Return endpoint for each admin REST API mapped to a
|
||||
// command here.
|
||||
func (c cmdType) apiEndpoint() string {
|
||||
switch c {
|
||||
case statusCmd, restartCmd, stopCmd:
|
||||
return "/minio/admin/v1/service"
|
||||
case setCreds:
|
||||
return "/minio/admin/v1/config/credential"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// toServiceSignal - Helper function that translates a given cmdType
|
||||
// value to its corresponding serviceSignal value.
|
||||
func (c cmdType) toServiceSignal() serviceSignal {
|
||||
switch c {
|
||||
case statusCmd:
|
||||
return serviceStatus
|
||||
case restartCmd:
|
||||
return serviceRestart
|
||||
case stopCmd:
|
||||
return serviceStop
|
||||
}
|
||||
return serviceStatus
|
||||
return serviceRestart
|
||||
}
|
||||
|
||||
func (c cmdType) toServiceActionValue() madmin.ServiceActionValue {
|
||||
func (c cmdType) toServiceAction() madmin.ServiceAction {
|
||||
switch c {
|
||||
case restartCmd:
|
||||
return madmin.ServiceActionValueRestart
|
||||
return madmin.ServiceActionRestart
|
||||
case stopCmd:
|
||||
return madmin.ServiceActionValueStop
|
||||
return madmin.ServiceActionStop
|
||||
}
|
||||
return madmin.ServiceActionValueStop
|
||||
return madmin.ServiceActionRestart
|
||||
}
|
||||
|
||||
// testServiceSignalReceiver - Helper function that simulates a
|
||||
@@ -469,19 +388,15 @@ func testServiceSignalReceiver(cmd cmdType, t *testing.T) {
|
||||
|
||||
// getServiceCmdRequest - Constructs a management REST API request for service
|
||||
// subcommands for a given cmdType value.
|
||||
func getServiceCmdRequest(cmd cmdType, cred auth.Credentials, body []byte) (*http.Request, error) {
|
||||
req, err := newTestRequest(cmd.apiMethod(), cmd.apiEndpoint(), 0, nil)
|
||||
func getServiceCmdRequest(cmd cmdType, cred auth.Credentials) (*http.Request, error) {
|
||||
queryVal := url.Values{}
|
||||
queryVal.Set("action", string(cmd.toServiceAction()))
|
||||
resource := "/minio/admin/v1/service?" + queryVal.Encode()
|
||||
req, err := newTestRequest(http.MethodPost, resource, 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set body
|
||||
req.Body = ioutil.NopCloser(bytes.NewReader(body))
|
||||
req.ContentLength = int64(len(body))
|
||||
|
||||
// Set sha-sum header
|
||||
req.Header.Set("X-Amz-Content-Sha256", getSHA256Hash(body))
|
||||
|
||||
// management REST API uses signature V4 for authentication.
|
||||
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
||||
if err != nil {
|
||||
@@ -517,13 +432,7 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
||||
}
|
||||
credentials := globalServerConfig.GetCredential()
|
||||
|
||||
body, err := json.Marshal(madmin.ServiceAction{
|
||||
Action: cmd.toServiceActionValue()})
|
||||
if err != nil {
|
||||
t.Fatalf("JSONify error: %v", err)
|
||||
}
|
||||
|
||||
req, err := getServiceCmdRequest(cmd, credentials, body)
|
||||
req, err := getServiceCmdRequest(cmd, credentials)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build service status request %v", err)
|
||||
}
|
||||
@@ -531,19 +440,6 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.router.ServeHTTP(rec, req)
|
||||
|
||||
if cmd == statusCmd {
|
||||
expectedInfo := madmin.ServiceStatus{
|
||||
ServerVersion: madmin.ServerVersion{Version: Version, CommitID: CommitID},
|
||||
}
|
||||
receivedInfo := madmin.ServiceStatus{}
|
||||
if jsonErr := json.Unmarshal(rec.Body.Bytes(), &receivedInfo); jsonErr != nil {
|
||||
t.Errorf("Failed to unmarshal StorageInfo - %v", jsonErr)
|
||||
}
|
||||
if expectedInfo.ServerVersion != receivedInfo.ServerVersion {
|
||||
t.Errorf("Expected storage info and received storage info differ, %v %v", expectedInfo, receivedInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
resp, _ := ioutil.ReadAll(rec.Body)
|
||||
t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
|
||||
@@ -554,11 +450,6 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Test for service status management REST API.
|
||||
func TestServiceStatusHandler(t *testing.T) {
|
||||
testServicesCmdHandler(statusCmd, t)
|
||||
}
|
||||
|
||||
// Test for service restart management REST API.
|
||||
func TestServiceRestartHandler(t *testing.T) {
|
||||
testServicesCmdHandler(restartCmd, t)
|
||||
@@ -760,3 +651,100 @@ func TestToAdminAPIErrCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopLockEntries(t *testing.T) {
|
||||
t1 := UTCNow()
|
||||
t2 := UTCNow().Add(10 * time.Second)
|
||||
peerLocks := []*PeerLocks{
|
||||
{
|
||||
Addr: "1",
|
||||
Locks: map[string][]lockRequesterInfo{
|
||||
"1": {
|
||||
{false, "node2", "ep2", "2", t2, t2, ""},
|
||||
{true, "node1", "ep1", "1", t1, t1, ""},
|
||||
},
|
||||
"2": {
|
||||
{false, "node2", "ep2", "2", t2, t2, ""},
|
||||
{true, "node1", "ep1", "1", t1, t1, ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Addr: "2",
|
||||
Locks: map[string][]lockRequesterInfo{
|
||||
"1": {
|
||||
{false, "node2", "ep2", "2", t2, t2, ""},
|
||||
{true, "node1", "ep1", "1", t1, t1, ""},
|
||||
},
|
||||
"2": {
|
||||
{false, "node2", "ep2", "2", t2, t2, ""},
|
||||
{true, "node1", "ep1", "1", t1, t1, ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
les := topLockEntries(peerLocks)
|
||||
if len(les) != 2 {
|
||||
t.Fatalf("Did not get 2 results")
|
||||
}
|
||||
if les[0].Timestamp.After(les[1].Timestamp) {
|
||||
t.Fatalf("Got wrong sorted value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractHealInitParams(t *testing.T) {
|
||||
mkParams := func(clientToken string, forceStart, forceStop bool) url.Values {
|
||||
v := url.Values{}
|
||||
if clientToken != "" {
|
||||
v.Add(string(mgmtClientToken), clientToken)
|
||||
}
|
||||
if forceStart {
|
||||
v.Add(string(mgmtForceStart), "")
|
||||
}
|
||||
if forceStop {
|
||||
v.Add(string(mgmtForceStop), "")
|
||||
}
|
||||
return v
|
||||
}
|
||||
qParmsArr := []url.Values{
|
||||
// Invalid cases
|
||||
mkParams("", true, true),
|
||||
mkParams("111", true, true),
|
||||
mkParams("111", true, false),
|
||||
mkParams("111", false, true),
|
||||
// Valid cases follow
|
||||
mkParams("", true, false),
|
||||
mkParams("", false, true),
|
||||
mkParams("", false, false),
|
||||
mkParams("111", false, false),
|
||||
}
|
||||
varsArr := []map[string]string{
|
||||
// Invalid cases
|
||||
{string(mgmtPrefix): "objprefix"},
|
||||
// Valid cases
|
||||
{},
|
||||
{string(mgmtBucket): "bucket"},
|
||||
{string(mgmtBucket): "bucket", string(mgmtPrefix): "objprefix"},
|
||||
}
|
||||
|
||||
// Body is always valid - we do not test JSON decoding.
|
||||
body := `{"recursive": false, "dryRun": true, "remove": false, "scanMode": 0}`
|
||||
|
||||
// Test all combinations!
|
||||
for pIdx, parms := range qParmsArr {
|
||||
for vIdx, vars := range varsArr {
|
||||
_, err := extractHealInitParams(vars, parms, bytes.NewBuffer([]byte(body)))
|
||||
isErrCase := false
|
||||
if pIdx < 4 || vIdx < 1 {
|
||||
isErrCase = true
|
||||
}
|
||||
|
||||
if err != ErrNone && !isErrCase {
|
||||
t.Errorf("Got unexpected error: %v %v %v", pIdx, vIdx, err)
|
||||
} else if err == ErrNone && isErrCase {
|
||||
t.Errorf("Got no error but expected one: %v %v", pIdx, vIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
-12
@@ -38,17 +38,14 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
|
||||
adminRouter := router.PathPrefix(adminAPIPathPrefix).Subrouter()
|
||||
|
||||
// Version handler
|
||||
adminRouter.Methods(http.MethodGet).Path("/version").HandlerFunc(httpTraceAll(adminAPI.VersionHandler))
|
||||
|
||||
adminV1Router := adminRouter.PathPrefix("/v1").Subrouter()
|
||||
|
||||
/// Service operations
|
||||
|
||||
// Service status
|
||||
adminV1Router.Methods(http.MethodGet).Path("/service").HandlerFunc(httpTraceAll(adminAPI.ServiceStatusHandler))
|
||||
|
||||
// Service restart and stop - TODO
|
||||
adminV1Router.Methods(http.MethodPost).Path("/service").HandlerFunc(httpTraceAll(adminAPI.ServiceStopNRestartHandler))
|
||||
// Restart and stop MinIO service.
|
||||
adminV1Router.Methods(http.MethodPost).Path("/service").HandlerFunc(httpTraceAll(adminAPI.ServiceActionHandler)).Queries("action", "{action:.*}")
|
||||
// Update MinIO servers.
|
||||
adminV1Router.Methods(http.MethodPost).Path("/update").HandlerFunc(httpTraceAll(adminAPI.ServerUpdateHandler)).Queries("updateURL", "{updateURL:.*}")
|
||||
|
||||
// Info operations
|
||||
adminV1Router.Methods(http.MethodGet).Path("/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
|
||||
@@ -80,11 +77,6 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
|
||||
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
||||
// Set config
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
|
||||
|
||||
// Get config keys/values
|
||||
adminV1Router.Methods(http.MethodGet).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKeysHandler))
|
||||
// Set config keys/values
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKeysHandler))
|
||||
}
|
||||
|
||||
if enableIAMOps {
|
||||
@@ -138,6 +130,14 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
|
||||
|
||||
// HTTP Trace
|
||||
adminV1Router.Methods(http.MethodGet).Path("/trace").HandlerFunc(adminAPI.TraceHandler)
|
||||
|
||||
// Console Logs
|
||||
adminV1Router.Methods(http.MethodGet).Path("/log").HandlerFunc(httpTraceAll(adminAPI.ConsoleLogHandler))
|
||||
|
||||
// -- KMS APIs --
|
||||
//
|
||||
adminV1Router.Methods(http.MethodGet).Path("/kms/key/status").HandlerFunc(httpTraceAll(adminAPI.KMSKeyStatusHandler))
|
||||
|
||||
// If none of the routes match, return error.
|
||||
adminV1Router.NotFoundHandler = http.HandlerFunc(httpTraceHdrs(notFoundHandlerJSON))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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 (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/pkg/cpu"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/mem"
|
||||
)
|
||||
|
||||
// getLocalMemUsage - returns ServerMemUsageInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func getLocalMemUsage(endpoints EndpointList, r *http.Request) ServerMemUsageInfo {
|
||||
var memUsages []mem.Usage
|
||||
var historicUsages []mem.Usage
|
||||
seenHosts := set.NewStringSet()
|
||||
for _, endpoint := range endpoints {
|
||||
if seenHosts.Contains(endpoint.Host) {
|
||||
continue
|
||||
}
|
||||
seenHosts.Add(endpoint.Host)
|
||||
|
||||
// Only proceed for local endpoints
|
||||
if endpoint.IsLocal {
|
||||
memUsages = append(memUsages, mem.GetUsage())
|
||||
historicUsages = append(historicUsages, mem.GetHistoricUsage())
|
||||
}
|
||||
}
|
||||
addr := r.Host
|
||||
if globalIsDistXL {
|
||||
addr = GetLocalPeer(endpoints)
|
||||
}
|
||||
return ServerMemUsageInfo{
|
||||
Addr: addr,
|
||||
Usage: memUsages,
|
||||
HistoricUsage: historicUsages,
|
||||
}
|
||||
}
|
||||
|
||||
// getLocalCPULoad - returns ServerCPULoadInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func getLocalCPULoad(endpoints EndpointList, r *http.Request) ServerCPULoadInfo {
|
||||
var cpuLoads []cpu.Load
|
||||
var historicLoads []cpu.Load
|
||||
seenHosts := set.NewStringSet()
|
||||
for _, endpoint := range endpoints {
|
||||
if seenHosts.Contains(endpoint.Host) {
|
||||
continue
|
||||
}
|
||||
seenHosts.Add(endpoint.Host)
|
||||
|
||||
// Only proceed for local endpoints
|
||||
if endpoint.IsLocal {
|
||||
cpuLoads = append(cpuLoads, cpu.GetLoad())
|
||||
historicLoads = append(historicLoads, cpu.GetHistoricLoad())
|
||||
}
|
||||
}
|
||||
addr := r.Host
|
||||
if globalIsDistXL {
|
||||
addr = GetLocalPeer(endpoints)
|
||||
}
|
||||
return ServerCPULoadInfo{
|
||||
Addr: addr,
|
||||
Load: cpuLoads,
|
||||
HistoricLoad: historicLoads,
|
||||
}
|
||||
}
|
||||
|
||||
// getLocalDrivesPerf - returns ServerDrivesPerfInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func getLocalDrivesPerf(endpoints EndpointList, size int64, r *http.Request) madmin.ServerDrivesPerfInfo {
|
||||
var dps []disk.Performance
|
||||
for _, endpoint := range endpoints {
|
||||
// Only proceed for local endpoints
|
||||
if endpoint.IsLocal {
|
||||
if _, err := os.Stat(endpoint.Path); err != nil {
|
||||
// Since this drive is not available, add relevant details and proceed
|
||||
dps = append(dps, disk.Performance{Path: endpoint.Path, Error: err.Error()})
|
||||
continue
|
||||
}
|
||||
dp := disk.GetPerformance(pathJoin(endpoint.Path, minioMetaTmpBucket, mustGetUUID()), size)
|
||||
dp.Path = endpoint.Path
|
||||
dps = append(dps, dp)
|
||||
}
|
||||
}
|
||||
addr := r.Host
|
||||
if globalIsDistXL {
|
||||
addr = GetLocalPeer(endpoints)
|
||||
}
|
||||
return madmin.ServerDrivesPerfInfo{
|
||||
Addr: addr,
|
||||
Perf: dps,
|
||||
Size: size,
|
||||
}
|
||||
}
|
||||
@@ -203,6 +203,12 @@ func getClaimsFromToken(r *http.Request) (map[string]interface{}, error) {
|
||||
}
|
||||
|
||||
if globalPolicyOPA == nil {
|
||||
// If OPA is not set and if ldap claim key is set,
|
||||
// allow the claim.
|
||||
if _, ok := claims[ldapUser]; ok {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// If OPA is not set, session token should
|
||||
// have a policy and its mandatory, reject
|
||||
// requests without policy claim.
|
||||
|
||||
@@ -155,3 +155,11 @@ func bitrotWriterSum(w io.Writer) []byte {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the size of the file with bitrot protection
|
||||
func bitrotShardFileSize(size int64, shardSize int64, algo BitrotAlgorithm) int64 {
|
||||
if algo != HighwayHash256S {
|
||||
return size
|
||||
}
|
||||
return ceilFrac(size, shardSize)*int64(algo.New().Size()) + size
|
||||
}
|
||||
|
||||
@@ -288,6 +288,9 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
// Set delimiter value for "s3:delimiter" policy conditionals.
|
||||
r.Header.Set("delimiter", SlashSeparator)
|
||||
|
||||
// err will be nil here as we already called this function
|
||||
// earlier in this request.
|
||||
claims, _ := getClaimsFromToken(r)
|
||||
var newBucketsInfo []BucketInfo
|
||||
for _, bucketInfo := range bucketsInfo {
|
||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
@@ -297,6 +300,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
ConditionValues: getConditionValues(r, "", accessKey),
|
||||
IsOwner: owner,
|
||||
ObjectName: "",
|
||||
Claims: claims,
|
||||
}) {
|
||||
newBucketsInfo = append(newBucketsInfo, bucketInfo)
|
||||
}
|
||||
|
||||
+10
-3
@@ -31,7 +31,6 @@ import (
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/cmd/logger/target/console"
|
||||
"github.com/minio/minio/cmd/logger/target/http"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
@@ -67,7 +66,7 @@ func checkUpdate(mode string) {
|
||||
if globalInplaceUpdateDisabled {
|
||||
logger.StartupMessage(updateMsg)
|
||||
} else {
|
||||
logger.StartupMessage(prepareUpdateMessage("Run `minio update`", latestReleaseTime.Sub(currentReleaseTime)))
|
||||
logger.StartupMessage(prepareUpdateMessage("Run `mc admin update`", latestReleaseTime.Sub(currentReleaseTime)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +96,7 @@ func loadLoggers() {
|
||||
|
||||
if globalServerConfig.Logger.Console.Enabled {
|
||||
// Enable console logging
|
||||
logger.AddTarget(console.New())
|
||||
logger.AddTarget(globalConsoleSys.Console())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -359,6 +358,14 @@ func handleCommonEnvVars() {
|
||||
globalCacheMaxUse = maxUse
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if cacheEncKey := os.Getenv("MINIO_CACHE_ENCRYPTION_MASTER_KEY"); cacheEncKey != "" {
|
||||
globalCacheKMSKeyID, globalCacheKMS, err = parseKMSMasterKey(cacheEncKey)
|
||||
if err != nil {
|
||||
logger.Fatal(uiErrInvalidCacheEncryptionKey(err), "Invalid cache encryption master key")
|
||||
}
|
||||
}
|
||||
// In place update is true by default if the MINIO_UPDATE is not set
|
||||
// or is not set to 'off', if MINIO_UPDATE is set to 'off' then
|
||||
// in-place update is off.
|
||||
|
||||
+14
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2016, 2017, 2018 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2016-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.
|
||||
@@ -303,6 +303,12 @@ func (s *serverConfig) loadFromEnvs() {
|
||||
s.Policy.OPA.URL = opaArgs.URL
|
||||
s.Policy.OPA.AuthToken = opaArgs.AuthToken
|
||||
}
|
||||
|
||||
var err error
|
||||
s.LDAPServerConfig, err = newLDAPConfigFromEnv()
|
||||
if err != nil {
|
||||
logger.FatalIf(err, "Unable to parse LDAP configuration from env")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNotificationTargets tries to establish connections to all notification
|
||||
@@ -335,6 +341,9 @@ func (s *serverConfig) TestNotificationTargets() error {
|
||||
if !v.Enable {
|
||||
continue
|
||||
}
|
||||
if v.TLS.Enable {
|
||||
v.TLS.RootCAs = globalRootCAs
|
||||
}
|
||||
t, err := target.NewKafkaTarget(k, v, GlobalServiceDoneCh)
|
||||
if err != nil {
|
||||
return fmt.Errorf("kafka(%s): %s", k, err.Error())
|
||||
@@ -346,6 +355,7 @@ func (s *serverConfig) TestNotificationTargets() error {
|
||||
if !v.Enable {
|
||||
continue
|
||||
}
|
||||
v.RootCAs = globalRootCAs
|
||||
t, err := target.NewMQTTTarget(k, v, GlobalServiceDoneCh)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mqtt(%s): %s", k, err.Error())
|
||||
@@ -684,6 +694,9 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
|
||||
|
||||
for id, args := range config.Notify.Kafka {
|
||||
if args.Enable {
|
||||
if args.TLS.Enable {
|
||||
args.TLS.RootCAs = globalRootCAs
|
||||
}
|
||||
newTarget, err := target.NewKafkaTarget(id, args, GlobalServiceDoneCh)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
|
||||
@@ -926,4 +926,6 @@ type serverConfigV33 struct {
|
||||
|
||||
// Add new external policy enforcements here.
|
||||
} `json:"policy"`
|
||||
|
||||
LDAPServerConfig ldapServerConfig `json:"ldapserverconfig"`
|
||||
}
|
||||
|
||||
+15
-9
@@ -20,6 +20,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -136,18 +137,23 @@ func (sys *ConfigSys) Init(objAPI ObjectLayer) error {
|
||||
// of the object layer.
|
||||
// - Write quorum not met when upgrading configuration
|
||||
// version is needed.
|
||||
for range newRetryTimerSimple(doneCh) {
|
||||
if err := initConfig(objAPI); err != nil {
|
||||
if strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) ||
|
||||
strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) {
|
||||
logger.Info("Waiting for configuration to be initialized..")
|
||||
continue
|
||||
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||
for {
|
||||
select {
|
||||
case <-retryTimerCh:
|
||||
if err := initConfig(objAPI); err != nil {
|
||||
if strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) ||
|
||||
strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) {
|
||||
logger.Info("Waiting for configuration to be initialized..")
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
case <-globalOSSignalCh:
|
||||
return fmt.Errorf("Initializing config sub-system gracefully stopped")
|
||||
}
|
||||
break
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewConfigSys - creates new config system object.
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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 (
|
||||
ring "container/ring"
|
||||
"context"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/cmd/logger/message/log"
|
||||
"github.com/minio/minio/cmd/logger/target/console"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
"github.com/minio/minio/pkg/pubsub"
|
||||
)
|
||||
|
||||
// number of log messages to buffer
|
||||
const defaultLogBufferCount = 10000
|
||||
|
||||
//HTTPConsoleLoggerSys holds global console logger state
|
||||
type HTTPConsoleLoggerSys struct {
|
||||
pubsub *pubsub.PubSub
|
||||
console *console.Target
|
||||
nodeName string
|
||||
logBuf *ring.Ring
|
||||
}
|
||||
|
||||
// NewConsoleLogger - creates new HTTPConsoleLoggerSys with all nodes subscribed to
|
||||
// the console logging pub sub system
|
||||
func NewConsoleLogger(ctx context.Context, endpoints EndpointList) *HTTPConsoleLoggerSys {
|
||||
host, err := xnet.ParseHost(GetLocalPeer(globalEndpoints))
|
||||
if err != nil {
|
||||
logger.FatalIf(err, "Unable to start console logging subsystem")
|
||||
}
|
||||
var nodeName string
|
||||
if globalIsDistXL {
|
||||
nodeName = host.Name
|
||||
}
|
||||
ps := pubsub.New()
|
||||
return &HTTPConsoleLoggerSys{
|
||||
ps, nil, nodeName, ring.New(defaultLogBufferCount),
|
||||
}
|
||||
}
|
||||
|
||||
// HasLogListeners returns true if console log listeners are registered
|
||||
// for this node or peers
|
||||
func (sys *HTTPConsoleLoggerSys) HasLogListeners() bool {
|
||||
return sys != nil && sys.pubsub.HasSubscribers()
|
||||
}
|
||||
|
||||
// Subscribe starts console logging for this node.
|
||||
func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh chan struct{}, node string, last int, filter func(entry interface{}) bool) {
|
||||
// Enable console logging for remote client even if local console logging is disabled in the config.
|
||||
if !globalServerConfig.Logger.Console.Enabled && !sys.pubsub.HasSubscribers() {
|
||||
logger.AddTarget(globalConsoleSys.Console())
|
||||
}
|
||||
|
||||
cnt := 0
|
||||
// by default send all console logs in the ring buffer unless node or limit query parameters
|
||||
// are set.
|
||||
var lastN []madmin.LogInfo
|
||||
if last > defaultLogBufferCount || last <= 0 {
|
||||
last = defaultLogBufferCount
|
||||
}
|
||||
|
||||
lastN = make([]madmin.LogInfo, last)
|
||||
r := sys.logBuf
|
||||
r.Do(func(p interface{}) {
|
||||
if p != nil && (p.(madmin.LogInfo)).SendLog(node) {
|
||||
lastN[cnt%last] = p.(madmin.LogInfo)
|
||||
cnt++
|
||||
}
|
||||
})
|
||||
// send last n console log messages in order filtered by node
|
||||
if cnt > 0 {
|
||||
for i := 0; i < last; i++ {
|
||||
entry := lastN[(cnt+i)%last]
|
||||
if (entry == madmin.LogInfo{}) {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case subCh <- entry:
|
||||
case <-doneCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
sys.pubsub.Subscribe(subCh, doneCh, filter)
|
||||
}
|
||||
|
||||
// Console returns a console target
|
||||
func (sys *HTTPConsoleLoggerSys) Console() *HTTPConsoleLoggerSys {
|
||||
if sys.console == nil {
|
||||
sys.console = console.New()
|
||||
}
|
||||
return sys
|
||||
}
|
||||
|
||||
// Send log message 'e' to console and publish to console
|
||||
// log pubsub system
|
||||
func (sys *HTTPConsoleLoggerSys) Send(e interface{}) error {
|
||||
lg := madmin.LogInfo{}
|
||||
lg.Entry = e.(log.Entry)
|
||||
lg.NodeName = sys.nodeName
|
||||
sys.pubsub.Publish(lg)
|
||||
// add log to ring buffer
|
||||
sys.logBuf.Value = lg
|
||||
sys.logBuf = sys.logBuf.Next()
|
||||
|
||||
if globalServerConfig.Logger.Console.Enabled {
|
||||
return sys.console.Send(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -139,22 +139,24 @@ func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var objects []string
|
||||
for _, obj := range res.Objects {
|
||||
// Find the action that need to be executed
|
||||
action := l.ComputeAction(obj.Name, obj.ModTime)
|
||||
switch action {
|
||||
case lifecycle.DeleteAction:
|
||||
objAPI.DeleteObject(ctx, bucket.Name, obj.Name)
|
||||
objects = append(objects, obj.Name)
|
||||
default:
|
||||
// Nothing
|
||||
|
||||
// Do nothing, for now.
|
||||
}
|
||||
}
|
||||
// Deletes a list of objects.
|
||||
objAPI.DeleteObjects(ctx, bucket.Name, objects)
|
||||
if !res.IsTruncated {
|
||||
// We are done here, proceed to next bucket.
|
||||
break
|
||||
} else {
|
||||
marker = res.NextMarker
|
||||
}
|
||||
marker = res.NextMarker
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ func sweepRound(ctx context.Context, objAPI ObjectLayer) error {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, obj := range res.Objects {
|
||||
for _, l := range copyDailySweepListeners() {
|
||||
l <- pathJoin(bucket.Name, obj.Name)
|
||||
@@ -133,7 +134,6 @@ func dailySweeper() {
|
||||
// Start with random sleep time, so as to avoid "synchronous checks" between servers
|
||||
time.Sleep(time.Duration(rand.Float64() * float64(time.Hour)))
|
||||
|
||||
// Perform a sweep round each month
|
||||
for {
|
||||
if time.Since(lastSweepTime) < 30*24*time.Hour {
|
||||
time.Sleep(time.Hour)
|
||||
@@ -147,13 +147,12 @@ func dailySweeper() {
|
||||
// instance doing the sweep round
|
||||
case OperationTimedOut:
|
||||
lastSweepTime = time.Now()
|
||||
default:
|
||||
logger.LogIf(ctx, err)
|
||||
time.Sleep(time.Minute)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
lastSweepTime = time.Now()
|
||||
logger.LogIf(ctx, err)
|
||||
time.Sleep(time.Minute)
|
||||
continue
|
||||
}
|
||||
lastSweepTime = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
+99
-15
@@ -19,6 +19,7 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -33,8 +34,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/djherbis/atime"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
"github.com/minio/sio"
|
||||
"github.com/ncw/directio"
|
||||
)
|
||||
|
||||
@@ -45,6 +48,10 @@ const (
|
||||
cacheMetaVersion = "1.0.0"
|
||||
|
||||
cacheEnvDelimiter = ";"
|
||||
|
||||
// SSECacheEncrypted is the metadata key indicating that the object
|
||||
// is a cache entry encrypted with cache KMS master key in globalCacheKMS.
|
||||
SSECacheEncrypted = "X-Minio-Internal-Encrypted-Cache"
|
||||
)
|
||||
|
||||
// CacheChecksumInfoV1 - carries checksums of individual blocks on disk.
|
||||
@@ -223,6 +230,7 @@ func (c *diskCache) purge() {
|
||||
continue
|
||||
}
|
||||
cc := cacheControlOpts(objInfo)
|
||||
|
||||
if atime.Get(fi).Before(expiry) ||
|
||||
cc.isStale(objInfo.ModTime) {
|
||||
if err = removeAll(pathJoin(c.dir, obj.Name())); err != nil {
|
||||
@@ -273,6 +281,10 @@ func (c *diskCache) Stat(ctx context.Context, bucket, object string) (oi ObjectI
|
||||
}
|
||||
oi.Bucket = bucket
|
||||
oi.Name = object
|
||||
|
||||
if err = decryptCacheObjectETag(&oi); err != nil {
|
||||
return oi, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -323,7 +335,24 @@ func (c *diskCache) saveMetadata(ctx context.Context, bucket, object string, met
|
||||
|
||||
// Backend metadata could have changed through server side copy - reset cache metadata if that is the case
|
||||
func (c *diskCache) updateMetadataIfChanged(ctx context.Context, bucket, object string, bkObjectInfo, cacheObjInfo ObjectInfo) error {
|
||||
if !reflect.DeepEqual(bkObjectInfo.UserDefined, cacheObjInfo.UserDefined) ||
|
||||
|
||||
bkMeta := make(map[string]string)
|
||||
cacheMeta := make(map[string]string)
|
||||
for k, v := range bkObjectInfo.UserDefined {
|
||||
if hasPrefix(k, ReservedMetadataPrefix) {
|
||||
// Do not need to send any internal metadata
|
||||
continue
|
||||
}
|
||||
bkMeta[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
for k, v := range cacheObjInfo.UserDefined {
|
||||
if hasPrefix(k, ReservedMetadataPrefix) {
|
||||
// Do not need to send any internal metadata
|
||||
continue
|
||||
}
|
||||
cacheMeta[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
if !reflect.DeepEqual(bkMeta, cacheMeta) ||
|
||||
bkObjectInfo.ETag != cacheObjInfo.ETag ||
|
||||
bkObjectInfo.ContentType != cacheObjInfo.ContentType ||
|
||||
bkObjectInfo.Expires != cacheObjInfo.Expires {
|
||||
@@ -337,11 +366,11 @@ func getCacheSHADir(dir, bucket, object string) string {
|
||||
}
|
||||
|
||||
// Cache data to disk with bitrot checksum added for each block of 1MB
|
||||
func (c *diskCache) bitrotWriteToCache(ctx context.Context, cachePath string, reader io.Reader, size int64) (int64, error) {
|
||||
func (c *diskCache) bitrotWriteToCache(ctx context.Context, cachePath string, reader io.Reader, size uint64) (int64, error) {
|
||||
if err := os.MkdirAll(cachePath, 0777); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
bufSize := int64(readSizeV1)
|
||||
bufSize := uint64(readSizeV1)
|
||||
if size > 0 && bufSize > size {
|
||||
bufSize = size
|
||||
}
|
||||
@@ -367,28 +396,29 @@ func (c *diskCache) bitrotWriteToCache(ctx context.Context, cachePath string, re
|
||||
bufp := c.pool.Get().(*[]byte)
|
||||
defer c.pool.Put(bufp)
|
||||
|
||||
var n, n2 int
|
||||
for {
|
||||
n, err := io.ReadFull(reader, *bufp)
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF && err != io.ErrClosedPipe {
|
||||
n, err = io.ReadFull(reader, *bufp)
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
return 0, err
|
||||
}
|
||||
eof := err == io.EOF || err == io.ErrUnexpectedEOF || err == io.ErrClosedPipe
|
||||
eof := err == io.EOF || err == io.ErrUnexpectedEOF
|
||||
if n == 0 && size != 0 {
|
||||
// Reached EOF, nothing more to be done.
|
||||
break
|
||||
}
|
||||
h.Reset()
|
||||
if _, err := h.Write((*bufp)[:n]); err != nil {
|
||||
if _, err = h.Write((*bufp)[:n]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
hashBytes := h.Sum(nil)
|
||||
if _, err = f.Write(hashBytes); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err = f.Write((*bufp)[:n]); err != nil {
|
||||
if n2, err = f.Write((*bufp)[:n]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
bytesWritten += int64(n)
|
||||
bytesWritten += int64(n2)
|
||||
if eof {
|
||||
break
|
||||
}
|
||||
@@ -396,6 +426,39 @@ func (c *diskCache) bitrotWriteToCache(ctx context.Context, cachePath string, re
|
||||
return bytesWritten, nil
|
||||
}
|
||||
|
||||
func newCacheEncryptReader(content io.Reader, bucket, object string, metadata map[string]string) (r io.Reader, err error) {
|
||||
objectEncryptionKey, err := newCacheEncryptMetadata(bucket, object, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey[:], MinVersion: sio.Version20})
|
||||
if err != nil {
|
||||
return nil, crypto.ErrInvalidCustomerKey
|
||||
}
|
||||
return reader, nil
|
||||
}
|
||||
func newCacheEncryptMetadata(bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||
var sealedKey crypto.SealedKey
|
||||
if globalCacheKMS == nil {
|
||||
return nil, errKMSNotConfigured
|
||||
}
|
||||
key, encKey, err := globalCacheKMS.GenerateKey(globalCacheKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objectKey := crypto.GenerateKey(key, rand.Reader)
|
||||
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
|
||||
crypto.S3.CreateMetadata(metadata, globalCacheKMSKeyID, encKey, sealedKey)
|
||||
|
||||
if etag, ok := metadata["etag"]; ok {
|
||||
metadata["etag"] = hex.EncodeToString(objectKey.SealETag([]byte(etag)))
|
||||
}
|
||||
metadata[SSECacheEncrypted] = ""
|
||||
return objectKey[:], nil
|
||||
}
|
||||
|
||||
// Caches the object to disk
|
||||
func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Reader, size int64, opts ObjectOptions) error {
|
||||
if c.diskUsageHigh() {
|
||||
@@ -417,14 +480,33 @@ func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Read
|
||||
bufSize = size
|
||||
}
|
||||
|
||||
n, err := c.bitrotWriteToCache(ctx, cachePath, data, size)
|
||||
var metadata = make(map[string]string)
|
||||
for k, v := range opts.UserDefined {
|
||||
metadata[k] = v
|
||||
}
|
||||
var reader = data
|
||||
var actualSize = uint64(size)
|
||||
var err error
|
||||
if globalCacheKMS != nil {
|
||||
reader, err = newCacheEncryptReader(data, bucket, object, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actualSize, _ = sio.EncryptedSize(uint64(size))
|
||||
}
|
||||
n, err := c.bitrotWriteToCache(ctx, cachePath, reader, actualSize)
|
||||
if IsErr(err, baseErrs...) {
|
||||
c.setOnline(false)
|
||||
}
|
||||
if err != nil {
|
||||
removeAll(cachePath)
|
||||
return err
|
||||
}
|
||||
return c.saveMetadata(ctx, bucket, object, opts.UserDefined, n)
|
||||
if actualSize != uint64(n) {
|
||||
removeAll(cachePath)
|
||||
return IncompleteBody{}
|
||||
}
|
||||
return c.saveMetadata(ctx, bucket, object, metadata, n)
|
||||
}
|
||||
|
||||
// checks streaming bitrot checksum of cached object before returning data
|
||||
@@ -484,7 +566,6 @@ func (c *diskCache) bitrotReadFromCache(ctx context.Context, filePath string, of
|
||||
if _, err := io.ReadFull(rc, checksumHash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Reset()
|
||||
n, err := io.ReadFull(rc, *bufp)
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
@@ -527,7 +608,7 @@ func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRang
|
||||
var objInfo ObjectInfo
|
||||
cacheObjPath := getCacheSHADir(c.dir, bucket, object)
|
||||
|
||||
if objInfo, err = c.statCache(ctx, cacheObjPath); err != nil {
|
||||
if objInfo, err = c.Stat(ctx, bucket, object); err != nil {
|
||||
return nil, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -547,12 +628,15 @@ func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRang
|
||||
filePath := path.Join(cacheObjPath, cacheDataFile)
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
pw.CloseWithError(c.bitrotReadFromCache(ctx, filePath, off, length, pw))
|
||||
err := c.bitrotReadFromCache(ctx, filePath, off, length, pw)
|
||||
if err != nil {
|
||||
removeAll(cacheObjPath)
|
||||
}
|
||||
pw.CloseWithError(err)
|
||||
}()
|
||||
// Cleanup function to cause the go routine above to exit, in
|
||||
// case of incomplete read.
|
||||
pipeCloser := func() { pr.Close() }
|
||||
|
||||
return fn(pr, h, opts.CheckCopyPrecondFn, pipeCloser)
|
||||
|
||||
}
|
||||
|
||||
+51
-1
@@ -17,8 +17,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -124,7 +126,7 @@ func backendDownError(err error) bool {
|
||||
|
||||
// IsCacheable returns if the object should be saved in the cache.
|
||||
func (o ObjectInfo) IsCacheable() bool {
|
||||
return !crypto.IsEncrypted(o.UserDefined)
|
||||
return !crypto.IsEncrypted(o.UserDefined) || globalCacheKMS != nil
|
||||
}
|
||||
|
||||
// reads file cached on disk from offset upto length
|
||||
@@ -147,6 +149,10 @@ func readCacheFileStream(filePath string, offset, length int64) (io.ReadCloser,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = os.Chtimes(filePath, time.Now(), st.ModTime()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify if its not a regular file, since subsequent Seek is undefined.
|
||||
if !st.Mode().IsRegular() {
|
||||
return nil, errIsNotRegular
|
||||
@@ -168,3 +174,47 @@ func readCacheFileStream(filePath string, offset, length int64) (io.ReadCloser,
|
||||
io.Closer
|
||||
}{Reader: io.LimitReader(fr, length), Closer: fr}, nil
|
||||
}
|
||||
|
||||
func isCacheEncrypted(meta map[string]string) bool {
|
||||
_, ok := meta[SSECacheEncrypted]
|
||||
return ok
|
||||
}
|
||||
|
||||
// decryptCacheObjectETag tries to decrypt the ETag saved in encrypted format using the cache KMS
|
||||
func decryptCacheObjectETag(info *ObjectInfo) error {
|
||||
// Directories are never encrypted.
|
||||
if info.IsDir {
|
||||
return nil
|
||||
}
|
||||
encrypted := crypto.S3.IsEncrypted(info.UserDefined) && isCacheEncrypted(info.UserDefined)
|
||||
|
||||
switch {
|
||||
case encrypted:
|
||||
if globalCacheKMS == nil {
|
||||
return errKMSNotConfigured
|
||||
}
|
||||
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(info.UserDefined)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
extKey, err := globalCacheKMS.UnsealKey(keyID, kmsKey, crypto.Context{info.Bucket: path.Join(info.Bucket, info.Name)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var objectKey crypto.ObjectKey
|
||||
if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), info.Bucket, info.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
etagStr := tryDecryptETag(objectKey[:], info.ETag, false)
|
||||
// backend ETag was hex encoded before encrypting, so hex decode to get actual ETag
|
||||
etag, err := hex.DecodeString(etagStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.ETag = string(etag)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+58
-4
@@ -35,6 +35,7 @@ type CacheObjectLayer interface {
|
||||
GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
DeleteObject(ctx context.Context, bucket, object string) error
|
||||
DeleteObjects(ctx context.Context, bucket string, objects []string) ([]error, error)
|
||||
PutObject(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
// Storage operations.
|
||||
StorageInfo(ctx context.Context) CacheStorageInfo
|
||||
}
|
||||
@@ -58,6 +59,7 @@ type cacheObjects struct {
|
||||
GetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
DeleteObjectFn func(ctx context.Context, bucket, object string) error
|
||||
DeleteObjectsFn func(ctx context.Context, bucket string, objects []string) ([]error, error)
|
||||
PutObjectFn func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
}
|
||||
|
||||
func (c *cacheObjects) delete(ctx context.Context, dcache *diskCache, bucket, object string) (err error) {
|
||||
@@ -231,13 +233,13 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
teeReader := io.TeeReader(bkReader, pipeWriter)
|
||||
go func() {
|
||||
putErr := dcache.Put(ctx, bucket, object, io.LimitReader(pipeReader, bkReader.ObjInfo.Size), bkReader.ObjInfo.Size, ObjectOptions{UserDefined: getMetadata(bkReader.ObjInfo)})
|
||||
putErr := c.put(ctx, dcache, bucket, object, io.LimitReader(pipeReader, bkReader.ObjInfo.Size), bkReader.ObjInfo.Size, ObjectOptions{UserDefined: getMetadata(bkReader.ObjInfo)})
|
||||
// close the write end of the pipe, so the error gets
|
||||
// propagated to getObjReader
|
||||
pipeWriter.CloseWithError(putErr)
|
||||
}()
|
||||
cleanupBackend := func() { bkReader.Close() }
|
||||
cleanupPipe := func() { pipeReader.Close() }
|
||||
cleanupPipe := func() { pipeWriter.Close() }
|
||||
return NewGetObjectReaderFromReader(teeReader, bkReader.ObjInfo, opts.CheckCopyPrecondFn, cleanupBackend, cleanupPipe)
|
||||
}
|
||||
|
||||
@@ -263,7 +265,6 @@ func (c *cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string,
|
||||
return cachedObjInfo, nil
|
||||
}
|
||||
}
|
||||
|
||||
objInfo, err := getObjectInfoFn(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
if _, ok := err.(ObjectNotFound); ok {
|
||||
@@ -446,7 +447,8 @@ func checkAtimeSupport(dir string) (err error) {
|
||||
}
|
||||
func (c *cacheObjects) migrateCacheFromV1toV2(ctx context.Context) {
|
||||
logger.StartupMessage(colorBlue("Cache migration initiated ...."))
|
||||
var wg = &sync.WaitGroup{}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errs := make([]error, len(c.cache))
|
||||
for i, dc := range c.cache {
|
||||
if dc == nil {
|
||||
@@ -483,6 +485,55 @@ func (c *cacheObjects) migrateCacheFromV1toV2(ctx context.Context) {
|
||||
logger.StartupMessage(colorBlue("Cache migration completed successfully."))
|
||||
}
|
||||
|
||||
// PutObject - caches the uploaded object for single Put operations
|
||||
func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
putObjectFn := c.PutObjectFn
|
||||
dcache, err := c.getCacheToLoc(ctx, bucket, object)
|
||||
if err != nil {
|
||||
// disk cache could not be located,execute backend call.
|
||||
return putObjectFn(ctx, bucket, object, r, opts)
|
||||
}
|
||||
size := r.Size()
|
||||
if c.skipCache() {
|
||||
return putObjectFn(ctx, bucket, object, r, opts)
|
||||
}
|
||||
|
||||
// fetch from backend if there is no space on cache drive
|
||||
if !dcache.diskAvailable(size) {
|
||||
return putObjectFn(ctx, bucket, object, r, opts)
|
||||
}
|
||||
if opts.ServerSideEncryption != nil {
|
||||
return putObjectFn(ctx, bucket, object, r, opts)
|
||||
}
|
||||
// fetch from backend if cache exclude pattern or cache-control
|
||||
// directive set to exclude
|
||||
if c.isCacheExclude(bucket, object) {
|
||||
dcache.Delete(ctx, bucket, object)
|
||||
return putObjectFn(ctx, bucket, object, r, opts)
|
||||
}
|
||||
|
||||
objInfo, err = putObjectFn(ctx, bucket, object, r, opts)
|
||||
|
||||
if err == nil {
|
||||
go func() {
|
||||
// fill cache in the background
|
||||
bReader, bErr := c.GetObjectNInfoFn(ctx, bucket, object, nil, http.Header{}, readLock, ObjectOptions{})
|
||||
if bErr != nil {
|
||||
return
|
||||
}
|
||||
defer bReader.Close()
|
||||
oi, err := c.stat(ctx, dcache, bucket, object)
|
||||
// avoid cache overwrite if another background routine filled cache
|
||||
if err != nil || oi.ETag != bReader.ObjInfo.ETag {
|
||||
c.put(ctx, dcache, bucket, object, bReader, bReader.ObjInfo.Size, ObjectOptions{UserDefined: getMetadata(bReader.ObjInfo)})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return objInfo, err
|
||||
|
||||
}
|
||||
|
||||
// Returns cacheObjects for use by Server.
|
||||
func newServerCacheObjects(ctx context.Context, config CacheConfig) (CacheObjectLayer, error) {
|
||||
// list of disk caches for cache "drives" specified in config.json or MINIO_CACHE_DRIVES env var.
|
||||
@@ -513,6 +564,9 @@ func newServerCacheObjects(ctx context.Context, config CacheConfig) (CacheObject
|
||||
}
|
||||
return errs, nil
|
||||
},
|
||||
PutObjectFn: func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
return newObjectLayerFn().PutObject(ctx, bucket, object, data, opts)
|
||||
},
|
||||
}
|
||||
if migrateSw {
|
||||
go c.migrateCacheFromV1toV2(ctx)
|
||||
|
||||
@@ -279,6 +279,23 @@ func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]st
|
||||
switch {
|
||||
default:
|
||||
return nil, errObjectTampered
|
||||
case crypto.S3.IsEncrypted(metadata) && isCacheEncrypted(metadata):
|
||||
if globalCacheKMS == nil {
|
||||
return nil, errKMSNotConfigured
|
||||
}
|
||||
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extKey, err := globalCacheKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var objectKey crypto.ObjectKey
|
||||
if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), bucket, object); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return objectKey[:], nil
|
||||
case crypto.S3.IsEncrypted(metadata):
|
||||
if GlobalKMS == nil {
|
||||
return nil, errKMSNotConfigured
|
||||
|
||||
+1
-88
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2017 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2017-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.
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -34,9 +33,6 @@ import (
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/cpu"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
"github.com/minio/minio/pkg/mem"
|
||||
"github.com/minio/minio/pkg/mountinfo"
|
||||
)
|
||||
|
||||
@@ -287,89 +283,6 @@ func (endpoints EndpointList) UpdateIsLocal() error {
|
||||
|
||||
}
|
||||
|
||||
// localEndpointsMemUsage - returns ServerMemUsageInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func localEndpointsMemUsage(endpoints EndpointList, r *http.Request) ServerMemUsageInfo {
|
||||
var memUsages []mem.Usage
|
||||
var historicUsages []mem.Usage
|
||||
scratchSpace := map[string]bool{}
|
||||
for _, endpoint := range endpoints {
|
||||
// Only proceed for local endpoints
|
||||
if endpoint.IsLocal {
|
||||
if _, ok := scratchSpace[endpoint.Host]; ok {
|
||||
continue
|
||||
}
|
||||
memUsages = append(memUsages, mem.GetUsage())
|
||||
historicUsages = append(historicUsages, mem.GetHistoricUsage())
|
||||
scratchSpace[endpoint.Host] = true
|
||||
}
|
||||
}
|
||||
addr := r.Host
|
||||
if globalIsDistXL {
|
||||
addr = GetLocalPeer(endpoints)
|
||||
}
|
||||
return ServerMemUsageInfo{
|
||||
Addr: addr,
|
||||
Usage: memUsages,
|
||||
HistoricUsage: historicUsages,
|
||||
}
|
||||
}
|
||||
|
||||
// localEndpointsCPULoad - returns ServerCPULoadInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func localEndpointsCPULoad(endpoints EndpointList, r *http.Request) ServerCPULoadInfo {
|
||||
var cpuLoads []cpu.Load
|
||||
var historicLoads []cpu.Load
|
||||
scratchSpace := map[string]bool{}
|
||||
for _, endpoint := range endpoints {
|
||||
// Only proceed for local endpoints
|
||||
if endpoint.IsLocal {
|
||||
if _, ok := scratchSpace[endpoint.Host]; ok {
|
||||
continue
|
||||
}
|
||||
cpuLoads = append(cpuLoads, cpu.GetLoad())
|
||||
historicLoads = append(historicLoads, cpu.GetHistoricLoad())
|
||||
scratchSpace[endpoint.Host] = true
|
||||
}
|
||||
}
|
||||
addr := r.Host
|
||||
if globalIsDistXL {
|
||||
addr = GetLocalPeer(endpoints)
|
||||
}
|
||||
return ServerCPULoadInfo{
|
||||
Addr: addr,
|
||||
Load: cpuLoads,
|
||||
HistoricLoad: historicLoads,
|
||||
}
|
||||
}
|
||||
|
||||
// localEndpointsDrivePerf - returns ServerDrivesPerfInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func localEndpointsDrivePerf(endpoints EndpointList, r *http.Request) ServerDrivesPerfInfo {
|
||||
var dps []disk.Performance
|
||||
for _, endpoint := range endpoints {
|
||||
// Only proceed for local endpoints
|
||||
if endpoint.IsLocal {
|
||||
if _, err := os.Stat(endpoint.Path); err != nil {
|
||||
// Since this drive is not available, add relevant details and proceed
|
||||
dps = append(dps, disk.Performance{Path: endpoint.Path, Error: err.Error()})
|
||||
continue
|
||||
}
|
||||
dp := disk.GetPerformance(pathJoin(endpoint.Path, minioMetaTmpBucket, mustGetUUID()))
|
||||
dp.Path = endpoint.Path
|
||||
dps = append(dps, dp)
|
||||
}
|
||||
}
|
||||
addr := r.Host
|
||||
if globalIsDistXL {
|
||||
addr = GetLocalPeer(endpoints)
|
||||
}
|
||||
return ServerDrivesPerfInfo{
|
||||
Addr: addr,
|
||||
Perf: dps,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEndpointList - returns new endpoint list based on input args.
|
||||
func NewEndpointList(args ...string) (endpoints EndpointList, err error) {
|
||||
var endpointType EndpointType
|
||||
|
||||
+29
-15
@@ -30,6 +30,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/sio"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -362,7 +363,7 @@ func loadAndValidateCacheFormat(ctx context.Context, drives []string) (formats [
|
||||
|
||||
// reads cached object on disk and writes it back after adding bitrot
|
||||
// hashsum per block as per the new disk cache format.
|
||||
func migrateData(ctx context.Context, c *diskCache, oldfile, destDir string) error {
|
||||
func migrateCacheData(ctx context.Context, c *diskCache, bucket, object, oldfile, destDir string, metadata map[string]string) error {
|
||||
st, err := os.Stat(oldfile)
|
||||
if err != nil {
|
||||
err = osErrToFSFileErr(err)
|
||||
@@ -372,7 +373,18 @@ func migrateData(ctx context.Context, c *diskCache, oldfile, destDir string) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.bitrotWriteToCache(ctx, destDir, readCloser, st.Size())
|
||||
var reader io.Reader = readCloser
|
||||
|
||||
actualSize := uint64(st.Size())
|
||||
if globalCacheKMS != nil {
|
||||
reader, err = newCacheEncryptReader(readCloser, bucket, object, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actualSize, _ = sio.EncryptedSize(uint64(st.Size()))
|
||||
}
|
||||
|
||||
_, err = c.bitrotWriteToCache(ctx, destDir, reader, uint64(actualSize))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -401,6 +413,7 @@ func migrateOldCache(ctx context.Context, c *diskCache) error {
|
||||
}
|
||||
|
||||
for _, bucket := range buckets {
|
||||
bucket = strings.TrimSuffix(bucket, SlashSeparator)
|
||||
var objMetaPaths []string
|
||||
root := path.Join(oldCacheBucketsPath, bucket)
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
@@ -422,10 +435,23 @@ func migrateOldCache(ctx context.Context, c *diskCache) error {
|
||||
return err
|
||||
}
|
||||
prevCachedPath := path.Join(c.dir, bucket, object)
|
||||
|
||||
// get old cached metadata
|
||||
oldMetaPath := pathJoin(oldCacheBucketsPath, bucket, object, cacheMetaJSONFile)
|
||||
metaPath := pathJoin(destdir, cacheMetaJSONFile)
|
||||
metaBytes, err := ioutil.ReadFile(oldMetaPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// marshal cache metadata after adding version and stat info
|
||||
meta := &cacheMeta{}
|
||||
if err = json.Unmarshal(metaBytes, &meta); err != nil {
|
||||
return err
|
||||
}
|
||||
// move cached object to new cache directory path
|
||||
// migrate cache data and add bit-rot protection hash sum
|
||||
// at the start of each block
|
||||
if err := migrateData(ctx, c, prevCachedPath, destdir); err != nil {
|
||||
if err := migrateCacheData(ctx, c, bucket, object, prevCachedPath, destdir, meta.Meta); err != nil {
|
||||
continue
|
||||
}
|
||||
stat, err := os.Stat(prevCachedPath)
|
||||
@@ -440,19 +466,7 @@ func migrateOldCache(ctx context.Context, c *diskCache) error {
|
||||
if err := os.Remove(prevCachedPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// move cached metadata after changing cache metadata version
|
||||
oldMetaPath := pathJoin(oldCacheBucketsPath, bucket, object, cacheMetaJSONFile)
|
||||
metaPath := pathJoin(destdir, cacheMetaJSONFile)
|
||||
metaBytes, err := ioutil.ReadFile(oldMetaPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// marshal cache metadata after adding version and stat info
|
||||
meta := &cacheMeta{}
|
||||
if err = json.Unmarshal(metaBytes, &meta); err != nil {
|
||||
return err
|
||||
}
|
||||
meta.Checksum = CacheChecksumInfoV1{Algorithm: HighwayHash256S.String(), Blocksize: cacheBlkSize}
|
||||
meta.Version = cacheMetaVersion
|
||||
meta.Stat.Size = stat.Size()
|
||||
|
||||
+3
-3
@@ -325,7 +325,7 @@ func quorumUnformattedDisks(errs []error) bool {
|
||||
// loadFormatXLAll - load all format config from all input disks in parallel.
|
||||
func loadFormatXLAll(storageDisks []StorageAPI) ([]*formatXLV3, []error) {
|
||||
// Initialize sync waitgroup.
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Initialize list of errors.
|
||||
var sErrs = make([]error, len(storageDisks))
|
||||
@@ -652,7 +652,7 @@ func formatXLV3Check(reference *formatXLV3, format *formatXLV3) error {
|
||||
func saveFormatXLAll(ctx context.Context, storageDisks []StorageAPI, formats []*formatXLV3) error {
|
||||
var errs = make([]error, len(storageDisks))
|
||||
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Write `format.json` to all disks.
|
||||
for index, disk := range storageDisks {
|
||||
@@ -812,7 +812,7 @@ func initFormatXLMetaVolume(storageDisks []StorageAPI, formats []*formatXLV3) er
|
||||
// This happens for the first time, but keep this here since this
|
||||
// is the only place where it can be made expensive optimizing all
|
||||
// other calls. Create minio meta volume, if it doesn't exist yet.
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Initialize errs to collect errors inside go-routine.
|
||||
var errs = make([]error, len(storageDisks))
|
||||
|
||||
+3
-47
@@ -27,10 +27,10 @@ import (
|
||||
pathutil "path"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
"github.com/minio/minio/pkg/mimedb"
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// FS format, and object metadata.
|
||||
@@ -203,37 +203,6 @@ func (m *fsMetaV1) WriteTo(lk *lock.LockedFile) (n int64, err error) {
|
||||
return fi.Size(), nil
|
||||
}
|
||||
|
||||
func parseFSVersion(v *fastjson.Value) string {
|
||||
return string(v.GetStringBytes("version"))
|
||||
}
|
||||
|
||||
func parseFSMetaMap(v *fastjson.Value) map[string]string {
|
||||
metaMap := make(map[string]string)
|
||||
// Get fsMetaV1.Meta map.
|
||||
v.GetObject("meta").Visit(func(k []byte, kv *fastjson.Value) {
|
||||
metaMap[string(k)] = string(kv.GetStringBytes())
|
||||
})
|
||||
return metaMap
|
||||
}
|
||||
|
||||
func parseFSPartsArray(v *fastjson.Value) []ObjectPartInfo {
|
||||
// Get xlMetaV1.Parts array
|
||||
var partsArray []ObjectPartInfo
|
||||
for _, result := range v.GetArray("parts") {
|
||||
partsArray = append(partsArray, ObjectPartInfo{
|
||||
Number: result.GetInt("number"),
|
||||
Name: string(result.GetStringBytes("name")),
|
||||
ETag: string(result.GetStringBytes("etag")),
|
||||
Size: result.GetInt64("size"),
|
||||
ActualSize: result.GetInt64("actualSize"),
|
||||
})
|
||||
}
|
||||
return partsArray
|
||||
}
|
||||
|
||||
// fs.json parser pool
|
||||
var fsParserPool fastjson.ParserPool
|
||||
|
||||
func (m *fsMetaV1) ReadFrom(ctx context.Context, lk *lock.LockedFile) (n int64, err error) {
|
||||
var fsMetaBuf []byte
|
||||
fi, err := lk.Stat()
|
||||
@@ -253,18 +222,11 @@ func (m *fsMetaV1) ReadFrom(ctx context.Context, lk *lock.LockedFile) (n int64,
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
parser := fsParserPool.Get()
|
||||
defer fsParserPool.Put(parser)
|
||||
|
||||
var v *fastjson.Value
|
||||
v, err = parser.ParseBytes(fsMetaBuf)
|
||||
if err != nil {
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
if err = json.Unmarshal(fsMetaBuf, m); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// obtain version.
|
||||
m.Version = parseFSVersion(v)
|
||||
|
||||
// Verify if the format is valid, return corrupted format
|
||||
// for unrecognized formats.
|
||||
if !isFSMetaValid(m.Version) {
|
||||
@@ -273,12 +235,6 @@ func (m *fsMetaV1) ReadFrom(ctx context.Context, lk *lock.LockedFile) (n int64,
|
||||
return 0, errCorruptedFormat
|
||||
}
|
||||
|
||||
// obtain parts information
|
||||
m.Parts = parseFSPartsArray(v)
|
||||
|
||||
// obtain metadata.
|
||||
m.Meta = parseFSMetaMap(v)
|
||||
|
||||
// Success.
|
||||
return int64(len(fsMetaBuf)), nil
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
mioutil "github.com/minio/minio/pkg/ioutil"
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Returns EXPORT/.minio.sys/multipart/SHA256/UPLOADID
|
||||
@@ -472,16 +472,13 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
||||
return result, err
|
||||
}
|
||||
|
||||
parser := fsParserPool.Get()
|
||||
defer fsParserPool.Put(parser)
|
||||
|
||||
var v *fastjson.Value
|
||||
v, err = parser.ParseBytes(fsMetaBytes)
|
||||
if err != nil {
|
||||
var fsMeta fsMetaV1
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
if err = json.Unmarshal(fsMetaBytes, &fsMeta); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
result.UserDefined = parseFSMetaMap(v)
|
||||
result.UserDefined = fsMeta.Meta
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
+7
-10
@@ -30,6 +30,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/lifecycle"
|
||||
@@ -38,7 +39,6 @@ import (
|
||||
"github.com/minio/minio/pkg/mimedb"
|
||||
"github.com/minio/minio/pkg/mountinfo"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Default etag is used for pre-existing objects.
|
||||
@@ -1093,22 +1093,19 @@ func (fs *FSObjects) getObjectETag(ctx context.Context, bucket, entry string, lo
|
||||
return "", toObjectErr(err, bucket, entry)
|
||||
}
|
||||
|
||||
parser := fsParserPool.Get()
|
||||
defer fsParserPool.Put(parser)
|
||||
|
||||
var v *fastjson.Value
|
||||
v, err = parser.ParseBytes(fsMetaBuf)
|
||||
if err != nil {
|
||||
return "", toObjectErr(err, bucket, entry)
|
||||
var fsMeta fsMetaV1
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
if err = json.Unmarshal(fsMetaBuf, &fsMeta); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Check if FS metadata is valid, if not return error.
|
||||
if !isFSMetaValid(parseFSVersion(v)) {
|
||||
if !isFSMetaValid(fsMeta.Version) {
|
||||
logger.LogIf(ctx, errCorruptedFormat)
|
||||
return "", toObjectErr(errCorruptedFormat, bucket, entry)
|
||||
}
|
||||
|
||||
return extractETag(parseFSMetaMap(v)), nil
|
||||
return extractETag(fsMeta.Meta), nil
|
||||
}
|
||||
|
||||
// ListObjects - list all objects at prefix upto maxKeys., optionally delimited by '/'. Maintains the list pool
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
|
||||
minio "github.com/minio/minio-go/v6"
|
||||
)
|
||||
@@ -299,7 +300,7 @@ func ErrorRespToObjectError(err error, params ...string) error {
|
||||
object = params[1]
|
||||
}
|
||||
|
||||
if isNetworkOrHostDown(err) {
|
||||
if xnet.IsNetworkOrHostDown(err) {
|
||||
return BackendDown{}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,9 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
registerSTSRouter(router)
|
||||
}
|
||||
|
||||
// initialize globalConsoleSys system
|
||||
globalConsoleSys = NewConsoleLogger(context.Background(), globalEndpoints)
|
||||
|
||||
enableConfigOps := gatewayName == "nas"
|
||||
enableIAMOps := globalEtcdClient != nil
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"strings"
|
||||
@@ -460,13 +459,23 @@ func (l *b2Objects) GetObjectInfo(ctx context.Context, bucket string, object str
|
||||
if err != nil {
|
||||
return objInfo, err
|
||||
}
|
||||
f, err := bkt.DownloadFileByName(l.ctx, object, 0, 1)
|
||||
|
||||
f, _, err := bkt.ListFileNames(l.ctx, 1, object, "", "")
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
f.Close()
|
||||
fi, err := bkt.File(f.ID, object).GetFileInfo(l.ctx)
|
||||
|
||||
// B2's list will return the next item in the bucket if the object doesn't
|
||||
// exist so we need to perform a name check too
|
||||
if len(f) != 1 || (len(f) == 1 && f[0].Name != object) {
|
||||
return objInfo, minio.ObjectNotFound{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
}
|
||||
}
|
||||
|
||||
fi, err := bkt.File(f[0].ID, object).GetFileInfo(l.ctx)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, b2ToObjectError(err, bucket, object)
|
||||
@@ -474,7 +483,7 @@ func (l *b2Objects) GetObjectInfo(ctx context.Context, bucket string, object str
|
||||
return minio.ObjectInfo{
|
||||
Bucket: bucket,
|
||||
Name: object,
|
||||
ETag: minio.ToS3ETag(f.ID),
|
||||
ETag: minio.ToS3ETag(f[0].ID),
|
||||
Size: fi.Size,
|
||||
ModTime: fi.Timestamp,
|
||||
ContentType: fi.ContentType,
|
||||
@@ -595,14 +604,10 @@ func (l *b2Objects) DeleteObject(ctx context.Context, bucket string, object stri
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader, err := bkt.DownloadFileByName(l.ctx, object, 0, 1)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
io.Copy(ioutil.Discard, reader)
|
||||
reader.Close()
|
||||
err = bkt.File(reader.ID, object).DeleteFileVersion(l.ctx)
|
||||
|
||||
// If we hide the file we'll conform to B2's versioning policy, it also
|
||||
// saves an additional call to check if the file exists first
|
||||
_, err = bkt.HideFile(l.ctx, object)
|
||||
logger.LogIf(ctx, err)
|
||||
return b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package hdfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -280,7 +281,7 @@ func hdfsToObjectErr(ctx context.Context, err error, params ...string) error {
|
||||
return minio.PrefixAccessDenied{Bucket: bucket, Object: object}
|
||||
}
|
||||
return minio.BucketAlreadyOwnedByYou{Bucket: bucket}
|
||||
case isSysErrNotEmpty(err):
|
||||
case errors.Is(err, syscall.ENOTEMPTY):
|
||||
if object != "" {
|
||||
return minio.PrefixAccessDenied{Bucket: bucket, Object: object}
|
||||
}
|
||||
@@ -387,20 +388,6 @@ func (n *hdfsObjects) ListObjects(ctx context.Context, bucket, prefix, marker, d
|
||||
return minio.ListObjects(ctx, n, bucket, prefix, marker, delimiter, maxKeys, n.listPool, n.listDirFactory(), getObjectInfo, getObjectInfo)
|
||||
}
|
||||
|
||||
// Check if the given error corresponds to ENOTEMPTY for unix
|
||||
// and ERROR_DIR_NOT_EMPTY for windows (directory not empty).
|
||||
func isSysErrNotEmpty(err error) bool {
|
||||
if err == syscall.ENOTEMPTY {
|
||||
return true
|
||||
}
|
||||
if pathErr, ok := err.(*os.PathError); ok {
|
||||
if pathErr.Err == syscall.ENOTEMPTY {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// deleteObject deletes a file path if its empty. If it's successfully deleted,
|
||||
// it will recursively move up the tree, deleting empty parent directories
|
||||
// until it finds one with files in it. Returns nil for a non-empty directory.
|
||||
@@ -411,16 +398,13 @@ func (n *hdfsObjects) deleteObject(basePath, deletePath string) error {
|
||||
|
||||
// Attempt to remove path.
|
||||
if err := n.clnt.Remove(deletePath); err != nil {
|
||||
switch {
|
||||
case err == syscall.ENOTEMPTY:
|
||||
case isSysErrNotEmpty(err):
|
||||
if errors.Is(err, syscall.ENOTEMPTY) {
|
||||
// Ignore errors if the directory is not empty. The server relies on
|
||||
// this functionality, and sometimes uses recursion that should not
|
||||
// error on parent directories.
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Trailing slash is removed when found to ensure
|
||||
@@ -523,12 +507,34 @@ func (n *hdfsObjects) GetObject(ctx context.Context, bucket, key string, startOf
|
||||
return hdfsToObjectErr(ctx, err, bucket, key)
|
||||
}
|
||||
|
||||
func (n *hdfsObjects) isObjectDir(ctx context.Context, bucket, object string) bool {
|
||||
f, err := n.clnt.Open(minio.PathJoin(hdfsSeparator, bucket, object))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
logger.LogIf(ctx, err)
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
fis, err := f.Readdir(1)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return false
|
||||
}
|
||||
return len(fis) == 0
|
||||
}
|
||||
|
||||
// GetObjectInfo reads object info and replies back ObjectInfo.
|
||||
func (n *hdfsObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
_, err = n.clnt.Stat(minio.PathJoin(hdfsSeparator, bucket))
|
||||
if err != nil {
|
||||
return objInfo, hdfsToObjectErr(ctx, err, bucket)
|
||||
}
|
||||
if strings.HasSuffix(object, hdfsSeparator) && !n.isObjectDir(ctx, bucket, object) {
|
||||
return objInfo, hdfsToObjectErr(ctx, os.ErrNotExist, bucket, object)
|
||||
}
|
||||
|
||||
fi, err := n.clnt.Stat(minio.PathJoin(hdfsSeparator, bucket, object))
|
||||
if err != nil {
|
||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
||||
@@ -552,7 +558,7 @@ func (n *hdfsObjects) PutObject(ctx context.Context, bucket string, object strin
|
||||
name := minio.PathJoin(hdfsSeparator, bucket, object)
|
||||
|
||||
// If its a directory create a prefix {
|
||||
if strings.HasSuffix(object, hdfsSeparator) {
|
||||
if strings.HasSuffix(object, hdfsSeparator) && r.Size() == 0 {
|
||||
if err = n.clnt.MkdirAll(name, os.FileMode(0755)); err != nil {
|
||||
n.deleteObject(minio.PathJoin(hdfsSeparator, bucket), name)
|
||||
return objInfo, hdfsToObjectErr(ctx, err, bucket, object)
|
||||
|
||||
@@ -24,10 +24,10 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -137,101 +137,11 @@ func (m gwMetaV1) ObjectToPartOffset(ctx context.Context, offset int64) (partInd
|
||||
return 0, 0, minio.InvalidRange{}
|
||||
}
|
||||
|
||||
// parses gateway metadata stat info from metadata json
|
||||
func parseGWStat(v *fastjson.Value) (si minio.StatInfo, err error) {
|
||||
// obtain stat info.
|
||||
st := v.GetObject("stat")
|
||||
var mb []byte
|
||||
mb, err = st.Get("modTime").StringBytes()
|
||||
if err != nil {
|
||||
return si, err
|
||||
}
|
||||
// fetching modTime.
|
||||
si.ModTime, err = time.Parse(time.RFC3339, string(mb))
|
||||
if err != nil {
|
||||
return si, err
|
||||
}
|
||||
// obtain Stat.Size .
|
||||
si.Size, err = st.Get("size").Int64()
|
||||
if err != nil {
|
||||
return si, err
|
||||
}
|
||||
return si, nil
|
||||
}
|
||||
|
||||
// parses gateway metadata version from metadata json
|
||||
func parseGWVersion(v *fastjson.Value) string {
|
||||
return string(v.GetStringBytes("version"))
|
||||
}
|
||||
|
||||
// parses gateway ETag from metadata json
|
||||
func parseGWETag(v *fastjson.Value) string {
|
||||
return string(v.GetStringBytes("etag"))
|
||||
}
|
||||
|
||||
// parses gateway metadata format from metadata json
|
||||
func parseGWFormat(v *fastjson.Value) string {
|
||||
return string(v.GetStringBytes("format"))
|
||||
}
|
||||
|
||||
// parses gateway metadata json to get list of ObjectPartInfo
|
||||
func parseGWParts(v *fastjson.Value) []minio.ObjectPartInfo {
|
||||
// Parse the GW Parts.
|
||||
partsResult := v.GetArray("parts")
|
||||
partInfo := make([]minio.ObjectPartInfo, len(partsResult))
|
||||
for i, p := range partsResult {
|
||||
partInfo[i] = minio.ObjectPartInfo{
|
||||
Number: p.GetInt("number"),
|
||||
Name: string(p.GetStringBytes("name")),
|
||||
ETag: string(p.GetStringBytes("etag")),
|
||||
Size: p.GetInt64("size"),
|
||||
}
|
||||
}
|
||||
return partInfo
|
||||
}
|
||||
|
||||
// parses gateway metadata json to get the metadata map
|
||||
func parseGWMetaMap(v *fastjson.Value) map[string]string {
|
||||
metaMap := make(map[string]string)
|
||||
// Get gwMetaV1.Meta map.
|
||||
v.GetObject("meta").Visit(func(k []byte, kv *fastjson.Value) {
|
||||
metaMap[string(k)] = string(kv.GetStringBytes())
|
||||
})
|
||||
return metaMap
|
||||
}
|
||||
|
||||
var gwParserPool fastjson.ParserPool
|
||||
|
||||
// Constructs GWMetaV1 using `fastjson` lib to retrieve each field.
|
||||
// Constructs GWMetaV1 using `jsoniter` lib to retrieve each field.
|
||||
func gwMetaUnmarshalJSON(ctx context.Context, gwMetaBuf []byte) (gwMeta gwMetaV1, err error) {
|
||||
parser := gwParserPool.Get()
|
||||
defer gwParserPool.Put(parser)
|
||||
|
||||
var v *fastjson.Value
|
||||
v, err = parser.ParseBytes(gwMetaBuf)
|
||||
if err != nil {
|
||||
return gwMeta, err
|
||||
}
|
||||
|
||||
// obtain version.
|
||||
gwMeta.Version = parseGWVersion(v)
|
||||
// obtain format.
|
||||
gwMeta.Format = parseGWFormat(v)
|
||||
// Parse gwMetaV1.Stat .
|
||||
stat, err := parseGWStat(v)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return gwMeta, err
|
||||
}
|
||||
gwMeta.ETag = parseGWETag(v)
|
||||
gwMeta.Stat = stat
|
||||
|
||||
// Parse the GW Parts.
|
||||
gwMeta.Parts = parseGWParts(v)
|
||||
// parse gwMetaV1.
|
||||
gwMeta.Meta = parseGWMetaMap(v)
|
||||
|
||||
return gwMeta, nil
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
err = json.Unmarshal(gwMetaBuf, &gwMeta)
|
||||
return gwMeta, err
|
||||
}
|
||||
|
||||
// readGWMeta reads `dare.meta` and returns back GW metadata structure.
|
||||
|
||||
+27
-21
@@ -487,7 +487,6 @@ var notimplementedBucketResourceNames = map[string]bool{
|
||||
// List of not implemented object queries
|
||||
var notimplementedObjectResourceNames = map[string]bool{
|
||||
"acl": true,
|
||||
"policy": true,
|
||||
"restore": true,
|
||||
"tagging": true,
|
||||
"torrent": true,
|
||||
@@ -673,25 +672,29 @@ func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
|
||||
bucket, _ = urlPath2BucketObjectName(r.URL.Path)
|
||||
}
|
||||
}
|
||||
if bucket != "" {
|
||||
sr, err := globalDNSConfig.Get(bucket)
|
||||
if err != nil {
|
||||
if err == dns.ErrNoEntriesFound {
|
||||
writeErrorResponse(context.Background(), w, errorCodes.ToAPIErr(ErrNoSuchBucket), r.URL, guessIsBrowserReq(r))
|
||||
} else {
|
||||
writeErrorResponse(context.Background(), w, toAPIError(context.Background(), err), r.URL, guessIsBrowserReq(r))
|
||||
}
|
||||
return
|
||||
if bucket == "" {
|
||||
f.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
sr, err := globalDNSConfig.Get(bucket)
|
||||
if err != nil {
|
||||
if err == dns.ErrNoEntriesFound {
|
||||
writeErrorResponse(context.Background(), w, errorCodes.ToAPIErr(ErrNoSuchBucket),
|
||||
r.URL, guessIsBrowserReq(r))
|
||||
} else {
|
||||
writeErrorResponse(context.Background(), w, toAPIError(context.Background(), err),
|
||||
r.URL, guessIsBrowserReq(r))
|
||||
}
|
||||
if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
|
||||
r.URL.Scheme = "http"
|
||||
if globalIsSSL {
|
||||
r.URL.Scheme = "https"
|
||||
}
|
||||
r.URL.Host = getHostFromSrv(sr)
|
||||
f.fwd.ServeHTTP(w, r)
|
||||
return
|
||||
return
|
||||
}
|
||||
if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
|
||||
r.URL.Scheme = "http"
|
||||
if globalIsSSL {
|
||||
r.URL.Scheme = "https"
|
||||
}
|
||||
r.URL.Host = getHostFromSrv(sr)
|
||||
f.fwd.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
f.handler.ServeHTTP(w, r)
|
||||
return
|
||||
@@ -715,9 +718,12 @@ func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
|
||||
// CopyObject requests should be handled at current endpoint as path style
|
||||
// requests have target bucket and object in URI and source details are in
|
||||
// header fields
|
||||
if r.Method == http.MethodPut && r.Header.Get("X-Amz-Copy-Source") != "" {
|
||||
f.handler.ServeHTTP(w, r)
|
||||
return
|
||||
if r.Method == http.MethodPut && r.Header.Get(xhttp.AmzCopySource) != "" {
|
||||
bucket, object = urlPath2BucketObjectName(r.Header.Get(xhttp.AmzCopySource))
|
||||
if bucket == "" || object == "" {
|
||||
f.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
sr, err := globalDNSConfig.Get(bucket)
|
||||
if err != nil {
|
||||
|
||||
+8
-13
@@ -174,6 +174,10 @@ var (
|
||||
// registered listeners
|
||||
globalHTTPTrace = pubsub.New()
|
||||
|
||||
// global console system to send console logs to
|
||||
// registered listeners
|
||||
globalConsoleSys *HTTPConsoleLoggerSys
|
||||
|
||||
globalEndpoints EndpointList
|
||||
|
||||
// Global server's network statistics
|
||||
@@ -221,7 +225,10 @@ var (
|
||||
globalCacheExpiry = 90
|
||||
// Max allowed disk cache percentage
|
||||
globalCacheMaxUse = 80
|
||||
|
||||
// Disk cache KMS Key
|
||||
globalCacheKMSKeyID string
|
||||
// Initialized KMS configuration for disk cache
|
||||
globalCacheKMS crypto.KMS
|
||||
// Allocated etcd endpoint for config and bucket DNS.
|
||||
globalEtcdClient *etcd.Client
|
||||
|
||||
@@ -336,18 +343,6 @@ var (
|
||||
}
|
||||
return fmt.Sprintf
|
||||
}()
|
||||
colorGreenBold = func() func(format string, a ...interface{}) string {
|
||||
if isTerminal() {
|
||||
return color.New(color.FgGreen, color.Bold).SprintfFunc()
|
||||
}
|
||||
return fmt.Sprintf
|
||||
}()
|
||||
colorRedBold = func() func(format string, a ...interface{}) string {
|
||||
if isTerminal() {
|
||||
return color.New(color.FgRed, color.Bold).SprintfFunc()
|
||||
}
|
||||
return fmt.Sprintf
|
||||
}()
|
||||
)
|
||||
|
||||
// Returns minio global information, as a key value map.
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
@@ -54,7 +55,12 @@ func LivenessCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
objLayer := newObjectLayerFn()
|
||||
// Service not initialized yet
|
||||
if objLayer == nil {
|
||||
writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
|
||||
// Respond with 200 OK while server initializes to ensure a distributed cluster
|
||||
// is able to start on orchestration platforms like Docker Swarm.
|
||||
// Refer https://github.com/minio/minio/issues/8140 for more details.
|
||||
// Make sure to add server not initialized status in header
|
||||
w.Header().Set(xhttp.MinIOServerStatus, "Server-not-initialized")
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -30,6 +30,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
trace "github.com/minio/minio/pkg/trace"
|
||||
)
|
||||
|
||||
@@ -175,7 +176,7 @@ func Trace(f http.HandlerFunc, logBody bool, w http.ResponseWriter, r *http.Requ
|
||||
name := getOpName(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
|
||||
|
||||
// Setup a http request body recorder
|
||||
reqHeaders := cloneHeader(r.Header)
|
||||
reqHeaders := r.Header.Clone()
|
||||
reqHeaders.Set("Content-Length", strconv.Itoa(int(r.ContentLength)))
|
||||
reqHeaders.Set("Host", r.Host)
|
||||
for _, enc := range r.TransferEncoding {
|
||||
@@ -200,7 +201,7 @@ func Trace(f http.HandlerFunc, logBody bool, w http.ResponseWriter, r *http.Requ
|
||||
Method: r.Method,
|
||||
Path: r.URL.Path,
|
||||
RawQuery: r.URL.RawQuery,
|
||||
Client: r.RemoteAddr,
|
||||
Client: handlers.GetSourceIP(r),
|
||||
Headers: reqHeaders,
|
||||
Body: reqBodyRecorder.Data(),
|
||||
}
|
||||
@@ -211,7 +212,7 @@ func Trace(f http.HandlerFunc, logBody bool, w http.ResponseWriter, r *http.Requ
|
||||
|
||||
rs := trace.ResponseInfo{
|
||||
Time: time.Now().UTC(),
|
||||
Headers: cloneHeader(respBodyRecorder.Header()),
|
||||
Headers: respBodyRecorder.Header().Clone(),
|
||||
StatusCode: respBodyRecorder.statusCode,
|
||||
Body: respBodyRecorder.Body(),
|
||||
}
|
||||
|
||||
@@ -78,4 +78,7 @@ const (
|
||||
|
||||
// Deployment id.
|
||||
MinioDeploymentID = "x-minio-deployment-id"
|
||||
|
||||
// Server-Status
|
||||
MinIOServerStatus = "x-minio-server-status"
|
||||
)
|
||||
|
||||
+48
-29
@@ -213,7 +213,7 @@ func (ies *IAMEtcdStore) migrateToV1() error {
|
||||
case errConfigNotFound:
|
||||
// Need to migrate to V1.
|
||||
default:
|
||||
return errors.New("corrupt IAM format file")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if iamFmt.Version >= iamFormatVersion1 {
|
||||
@@ -415,23 +415,35 @@ func (ies *IAMEtcdStore) loadAll(sys *IAMSys, objectAPI ObjectLayer) error {
|
||||
iamUserPolicyMap := make(map[string]MappedPolicy)
|
||||
iamGroupPolicyMap := make(map[string]MappedPolicy)
|
||||
|
||||
isMinIOUsersSys := false
|
||||
sys.RLock()
|
||||
if sys.usersSysType == MinIOUsersSysType {
|
||||
isMinIOUsersSys = true
|
||||
}
|
||||
sys.RUnlock()
|
||||
|
||||
if err := ies.loadPolicyDocs(iamPolicyDocsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ies.loadUsers(false, iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
// load STS temp users into the same map
|
||||
|
||||
// load STS temp users
|
||||
if err := ies.loadUsers(true, iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ies.loadGroups(iamGroupsMap); err != nil {
|
||||
return err
|
||||
|
||||
if isMinIOUsersSys {
|
||||
// load long term users
|
||||
if err := ies.loadUsers(false, iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ies.loadGroups(iamGroupsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ies.loadMappedPolicies(false, false, iamUserPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := ies.loadMappedPolicies(false, false, iamUserPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
// load STS policy mappings into the same map
|
||||
if err := ies.loadMappedPolicies(true, false, iamUserPolicyMap); err != nil {
|
||||
return err
|
||||
@@ -491,28 +503,35 @@ func (ies *IAMEtcdStore) deleteGroupInfo(name string) error {
|
||||
|
||||
func (ies *IAMEtcdStore) watch(sys *IAMSys) {
|
||||
watchEtcd := func() {
|
||||
// Refresh IAMSys with etcd watch.
|
||||
for {
|
||||
outerLoop:
|
||||
// Refresh IAMSys with etcd watch.
|
||||
watchCh := ies.client.Watch(context.Background(),
|
||||
iamConfigPrefix, etcd.WithPrefix(), etcd.WithKeysOnly())
|
||||
select {
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case watchResp, ok := <-watchCh:
|
||||
if !ok {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
if err := watchResp.Err(); err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
// log and retry.
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
for _, event := range watchResp.Events {
|
||||
sys.Lock()
|
||||
ies.reloadFromEvent(sys, event)
|
||||
sys.Unlock()
|
||||
for {
|
||||
select {
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case watchResp, ok := <-watchCh:
|
||||
if !ok {
|
||||
time.Sleep(1 * time.Second)
|
||||
// Upon an error on watch channel
|
||||
// re-init the watch channel.
|
||||
goto outerLoop
|
||||
}
|
||||
if err := watchResp.Err(); err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
// log and retry.
|
||||
time.Sleep(1 * time.Second)
|
||||
// Upon an error on watch channel
|
||||
// re-init the watch channel.
|
||||
goto outerLoop
|
||||
}
|
||||
for _, event := range watchResp.Events {
|
||||
sys.Lock()
|
||||
ies.reloadFromEvent(sys, event)
|
||||
sys.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-13
@@ -127,8 +127,8 @@ func (iamOS *IAMObjectStore) migrateUsersConfigToV1(isSTS bool) error {
|
||||
identityPath := pathJoin(basePrefix, user, iamIdentityFile)
|
||||
var cred auth.Credentials
|
||||
if err := iamOS.loadIAMConfig(&cred, identityPath); err != nil {
|
||||
switch err.(type) {
|
||||
case ObjectNotFound:
|
||||
switch err {
|
||||
case errConfigNotFound:
|
||||
// This should not happen.
|
||||
default:
|
||||
// File may be corrupt or network error
|
||||
@@ -169,7 +169,7 @@ func (iamOS *IAMObjectStore) migrateToV1() error {
|
||||
case errConfigNotFound:
|
||||
// Need to migrate to V1.
|
||||
default:
|
||||
return errors.New("corrupt IAM format file")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if iamFmt.Version >= iamFormatVersion1 {
|
||||
@@ -429,24 +429,33 @@ func (iamOS *IAMObjectStore) loadAll(sys *IAMSys, objectAPI ObjectLayer) error {
|
||||
iamUserPolicyMap := make(map[string]MappedPolicy)
|
||||
iamGroupPolicyMap := make(map[string]MappedPolicy)
|
||||
|
||||
isMinIOUsersSys := false
|
||||
sys.RLock()
|
||||
if sys.usersSysType == MinIOUsersSysType {
|
||||
isMinIOUsersSys = true
|
||||
}
|
||||
sys.RUnlock()
|
||||
|
||||
if err := iamOS.loadPolicyDocs(iamPolicyDocsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := iamOS.loadUsers(false, iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
// load STS temp users into the same map
|
||||
// load STS temp users
|
||||
if err := iamOS.loadUsers(true, iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := iamOS.loadGroups(iamGroupsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
if isMinIOUsersSys {
|
||||
if err := iamOS.loadUsers(false, iamUsersMap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := iamOS.loadGroups(iamGroupsMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := iamOS.loadMappedPolicies(false, false, iamUserPolicyMap); err != nil {
|
||||
return err
|
||||
if err := iamOS.loadMappedPolicies(false, false, iamUserPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// load STS policy mappings into the same map
|
||||
// load STS policy mappings
|
||||
if err := iamOS.loadMappedPolicies(true, false, iamUserPolicyMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+153
-13
@@ -30,6 +30,20 @@ import (
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
)
|
||||
|
||||
// UsersSysType - defines the type of users and groups system that is
|
||||
// active on the server.
|
||||
type UsersSysType string
|
||||
|
||||
// Types of users configured in the server.
|
||||
const (
|
||||
// This mode uses the internal users system in MinIO.
|
||||
MinIOUsersSysType UsersSysType = "MinIOUsersSys"
|
||||
|
||||
// This mode uses users and groups from a configured LDAP
|
||||
// server.
|
||||
LDAPUsersSysType UsersSysType = "LDAPUsersSys"
|
||||
)
|
||||
|
||||
const (
|
||||
// IAM configuration directory.
|
||||
iamConfigPrefix = minioConfigPrefix + "/iam"
|
||||
@@ -145,6 +159,9 @@ func newMappedPolicy(policy string) MappedPolicy {
|
||||
// IAMSys - config system.
|
||||
type IAMSys struct {
|
||||
sync.RWMutex
|
||||
|
||||
usersSysType UsersSysType
|
||||
|
||||
// map of policy names to policy definitions
|
||||
iamPolicyDocsMap map[string]iampolicy.Policy
|
||||
// map of usernames to credentials
|
||||
@@ -463,6 +480,13 @@ func (sys *IAMSys) DeleteUser(accessKey string) error {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
if sys.usersSysType != MinIOUsersSysType {
|
||||
return errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
// It is ok to ignore deletion error on the mapped policy
|
||||
sys.store.deleteMappedPolicy(accessKey, false, false)
|
||||
err := sys.store.deleteUserIdentity(accessKey, false)
|
||||
@@ -472,9 +496,6 @@ func (sys *IAMSys) DeleteUser(accessKey string) error {
|
||||
err = nil
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
delete(sys.iamUsersMap, accessKey)
|
||||
delete(sys.iamUserPolicyMap, accessKey)
|
||||
|
||||
@@ -533,6 +554,10 @@ func (sys *IAMSys) ListUsers() (map[string]madmin.UserInfo, error) {
|
||||
sys.RLock()
|
||||
defer sys.RUnlock()
|
||||
|
||||
if sys.usersSysType != MinIOUsersSysType {
|
||||
return nil, errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
for k, v := range sys.iamUsersMap {
|
||||
users[k] = madmin.UserInfo{
|
||||
PolicyName: sys.iamUserPolicyMap[k].Policy,
|
||||
@@ -553,6 +578,12 @@ func (sys *IAMSys) GetUserInfo(name string) (u madmin.UserInfo, err error) {
|
||||
sys.RLock()
|
||||
defer sys.RUnlock()
|
||||
|
||||
if sys.usersSysType != MinIOUsersSysType {
|
||||
return madmin.UserInfo{
|
||||
PolicyName: sys.iamUserPolicyMap[name].Policy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
creds, found := sys.iamUsersMap[name]
|
||||
if !found {
|
||||
return u, errNoSuchUser
|
||||
@@ -580,6 +611,10 @@ func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus)
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
if sys.usersSysType != MinIOUsersSysType {
|
||||
return errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
cred, ok := sys.iamUsersMap[accessKey]
|
||||
if !ok {
|
||||
return errNoSuchUser
|
||||
@@ -614,6 +649,10 @@ func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error {
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
if sys.usersSysType != MinIOUsersSysType {
|
||||
return errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
if err := sys.store.saveUserIdentity(accessKey, false, u); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -636,6 +675,10 @@ func (sys *IAMSys) SetUserSecretKey(accessKey string, secretKey string) error {
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
if sys.usersSysType != MinIOUsersSysType {
|
||||
return errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
cred, ok := sys.iamUsersMap[accessKey]
|
||||
if !ok {
|
||||
return errNoSuchUser
|
||||
@@ -675,6 +718,10 @@ func (sys *IAMSys) AddUsersToGroup(group string, members []string) error {
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
if sys.usersSysType != MinIOUsersSysType {
|
||||
return errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
// Validate that all members exist.
|
||||
for _, member := range members {
|
||||
_, ok := sys.iamUsersMap[member]
|
||||
@@ -729,6 +776,10 @@ func (sys *IAMSys) RemoveUsersFromGroup(group string, members []string) error {
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
if sys.usersSysType != MinIOUsersSysType {
|
||||
return errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
// Validate that all members exist.
|
||||
for _, member := range members {
|
||||
_, ok := sys.iamUsersMap[member]
|
||||
@@ -802,6 +853,10 @@ func (sys *IAMSys) SetGroupStatus(group string, enabled bool) error {
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
if sys.usersSysType != MinIOUsersSysType {
|
||||
return errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
if group == "" {
|
||||
return errInvalidArgument
|
||||
}
|
||||
@@ -830,6 +885,7 @@ func (sys *IAMSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err e
|
||||
if err != nil {
|
||||
return gd, err
|
||||
}
|
||||
|
||||
// A group may be mapped to at most one policy.
|
||||
policy := ""
|
||||
if len(ps) > 0 {
|
||||
@@ -839,6 +895,13 @@ func (sys *IAMSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err e
|
||||
sys.RLock()
|
||||
defer sys.RUnlock()
|
||||
|
||||
if sys.usersSysType != MinIOUsersSysType {
|
||||
return madmin.GroupDesc{
|
||||
Name: group,
|
||||
Policy: policy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
gi, ok := sys.iamGroupsMap[group]
|
||||
if !ok {
|
||||
return gd, errNoSuchGroup
|
||||
@@ -852,15 +915,19 @@ func (sys *IAMSys) GetGroupDescription(group string) (gd madmin.GroupDesc, err e
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListGroups - lists groups
|
||||
func (sys *IAMSys) ListGroups() (r []string) {
|
||||
// ListGroups - lists groups.
|
||||
func (sys *IAMSys) ListGroups() (r []string, err error) {
|
||||
sys.RLock()
|
||||
defer sys.RUnlock()
|
||||
|
||||
if sys.usersSysType != MinIOUsersSysType {
|
||||
return nil, errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
for k := range sys.iamGroupsMap {
|
||||
r = append(r, k)
|
||||
}
|
||||
return r
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// PolicyDBSet - sets a policy for a user or group in the
|
||||
@@ -889,13 +956,16 @@ func (sys *IAMSys) policyDBSet(objectAPI ObjectLayer, name, policy string, isSTS
|
||||
if _, ok := sys.iamPolicyDocsMap[policy]; !ok {
|
||||
return errNoSuchPolicy
|
||||
}
|
||||
if !isGroup {
|
||||
if _, ok := sys.iamUsersMap[name]; !ok {
|
||||
return errNoSuchUser
|
||||
}
|
||||
} else {
|
||||
if _, ok := sys.iamGroupsMap[name]; !ok {
|
||||
return errNoSuchGroup
|
||||
|
||||
if sys.usersSysType == MinIOUsersSysType {
|
||||
if !isGroup {
|
||||
if _, ok := sys.iamUsersMap[name]; !ok {
|
||||
return errNoSuchUser
|
||||
}
|
||||
} else {
|
||||
if _, ok := sys.iamGroupsMap[name]; !ok {
|
||||
return errNoSuchGroup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -980,6 +1050,63 @@ func (sys *IAMSys) policyDBGet(name string, isGroup bool) ([]string, error) {
|
||||
// which implements claims validation and verification other than
|
||||
// applying policies.
|
||||
func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args) bool {
|
||||
// If it is an LDAP request, check that user and group
|
||||
// policies allow the request.
|
||||
if userIface, ok := args.Claims[ldapUser]; ok {
|
||||
var user string
|
||||
if u, ok := userIface.(string); ok {
|
||||
user = u
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
var groups []string
|
||||
groupsVal := args.Claims[ldapGroups]
|
||||
if g, ok := groupsVal.([]interface{}); ok {
|
||||
for _, eachG := range g {
|
||||
if eachGStr, ok := eachG.(string); ok {
|
||||
groups = append(groups, eachGStr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
sys.RLock()
|
||||
defer sys.RUnlock()
|
||||
|
||||
// We look up the policy mapping directly to bypass
|
||||
// users exists, group exists validations that do not
|
||||
// apply here.
|
||||
var policies []iampolicy.Policy
|
||||
if policy, ok := sys.iamUserPolicyMap[user]; ok {
|
||||
p, found := sys.iamPolicyDocsMap[policy.Policy]
|
||||
if found {
|
||||
policies = append(policies, p)
|
||||
}
|
||||
}
|
||||
for _, group := range groups {
|
||||
policy, ok := sys.iamGroupPolicyMap[group]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
p, found := sys.iamPolicyDocsMap[policy.Policy]
|
||||
if found {
|
||||
policies = append(policies, p)
|
||||
}
|
||||
}
|
||||
if len(policies) == 0 {
|
||||
return false
|
||||
}
|
||||
combinedPolicy := policies[0]
|
||||
for i := 1; i < len(policies); i++ {
|
||||
combinedPolicy.Statements =
|
||||
append(combinedPolicy.Statements,
|
||||
policies[i].Statements...)
|
||||
}
|
||||
return combinedPolicy.IsAllowed(args)
|
||||
}
|
||||
|
||||
pname, ok := args.Claims[iampolicy.PolicyName]
|
||||
if !ok {
|
||||
// When claims are set, it should have a "policy" field.
|
||||
@@ -1155,7 +1282,20 @@ func (sys *IAMSys) removeGroupFromMembershipsMap(group string) {
|
||||
|
||||
// NewIAMSys - creates new config system object.
|
||||
func NewIAMSys() *IAMSys {
|
||||
// Check global server configuration to determine the type of
|
||||
// users system configured.
|
||||
|
||||
// The default users system
|
||||
var utype UsersSysType
|
||||
switch {
|
||||
case globalServerConfig.LDAPServerConfig.ServerAddr != "":
|
||||
utype = LDAPUsersSysType
|
||||
default:
|
||||
utype = MinIOUsersSysType
|
||||
}
|
||||
|
||||
return &IAMSys{
|
||||
usersSysType: utype,
|
||||
iamUsersMap: make(map[string]auth.Credentials),
|
||||
iamPolicyDocsMap: make(map[string]iampolicy.Policy),
|
||||
iamUserPolicyMap: make(map[string]MappedPolicy),
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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 (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
ldap "gopkg.in/ldap.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultLDAPExpiry = time.Hour * 1
|
||||
)
|
||||
|
||||
// ldapServerConfig contains server connectivity information.
|
||||
type ldapServerConfig struct {
|
||||
IsEnabled bool `json:"enabled"`
|
||||
|
||||
// E.g. "ldap.minio.io:636"
|
||||
ServerAddr string `json:"serverAddr"`
|
||||
|
||||
// STS credentials expiry duration
|
||||
STSExpiryDuration string `json:"stsExpiryDuration"`
|
||||
stsExpiryDuration time.Duration // contains converted value
|
||||
|
||||
// Skips TLS verification (for testing, not
|
||||
// recommended in production).
|
||||
SkipTLSVerify bool `json:"skipTLSverify"`
|
||||
|
||||
// Format string for usernames
|
||||
UsernameFormat string `json:"usernameFormat"`
|
||||
|
||||
GroupSearchBaseDN string `json:"groupSearchBaseDN"`
|
||||
GroupSearchFilter string `json:"groupSearchFilter"`
|
||||
GroupNameAttribute string `json:"groupNameAttribute"`
|
||||
}
|
||||
|
||||
func (l *ldapServerConfig) Connect() (ldapConn *ldap.Conn, err error) {
|
||||
if l == nil {
|
||||
// Happens when LDAP is not configured.
|
||||
return
|
||||
}
|
||||
if l.SkipTLSVerify {
|
||||
ldapConn, err = ldap.DialTLS("tcp", l.ServerAddr, &tls.Config{InsecureSkipVerify: true})
|
||||
} else {
|
||||
ldapConn, err = ldap.DialTLS("tcp", l.ServerAddr, &tls.Config{})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// newLDAPConfigFromEnv loads configuration from the environment
|
||||
func newLDAPConfigFromEnv() (l ldapServerConfig, err error) {
|
||||
if ldapServer, ok := os.LookupEnv("MINIO_IDENTITY_LDAP_SERVER_ADDR"); ok {
|
||||
l.IsEnabled = true
|
||||
l.ServerAddr = ldapServer
|
||||
|
||||
if v := os.Getenv("MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY"); v == "true" {
|
||||
l.SkipTLSVerify = true
|
||||
}
|
||||
|
||||
if v := os.Getenv("MINIO_IDENTITY_LDAP_STS_EXPIRY"); v != "" {
|
||||
expDur, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return l, errors.New("LDAP expiry time err:" + err.Error())
|
||||
}
|
||||
if expDur <= 0 {
|
||||
return l, errors.New("LDAP expiry time has to be positive")
|
||||
}
|
||||
l.STSExpiryDuration = v
|
||||
l.stsExpiryDuration = expDur
|
||||
} else {
|
||||
l.stsExpiryDuration = defaultLDAPExpiry
|
||||
}
|
||||
|
||||
if v := os.Getenv("MINIO_IDENTITY_LDAP_USERNAME_FORMAT"); v != "" {
|
||||
subs := newSubstituter("username", "test")
|
||||
if _, err := subs.substitute(v); err != nil {
|
||||
return l, errors.New("Only username may be substituted in the username format")
|
||||
}
|
||||
l.UsernameFormat = v
|
||||
}
|
||||
|
||||
grpSearchFilter := os.Getenv("MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER")
|
||||
grpSearchNameAttr := os.Getenv("MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE")
|
||||
grpSearchBaseDN := os.Getenv("MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN")
|
||||
|
||||
// Either all group params must be set or none must be set.
|
||||
allNotSet := grpSearchFilter == "" && grpSearchNameAttr == "" && grpSearchBaseDN == ""
|
||||
allSet := grpSearchFilter != "" && grpSearchNameAttr != "" && grpSearchBaseDN != ""
|
||||
if !allNotSet && !allSet {
|
||||
return l, errors.New("All group related parameters must be set")
|
||||
}
|
||||
|
||||
if allSet {
|
||||
subs := newSubstituter("username", "test", "usernamedn", "test2")
|
||||
if _, err := subs.substitute(grpSearchFilter); err != nil {
|
||||
return l, errors.New("Only username and usernamedn may be substituted in the group search filter string")
|
||||
}
|
||||
l.GroupSearchFilter = grpSearchFilter
|
||||
|
||||
l.GroupNameAttribute = grpSearchNameAttr
|
||||
|
||||
subs = newSubstituter("username", "test", "usernamedn", "test2")
|
||||
if _, err := subs.substitute(grpSearchBaseDN); err != nil {
|
||||
return l, errors.New("Only username and usernamedn may be substituted in the base DN string")
|
||||
}
|
||||
l.GroupSearchBaseDN = grpSearchBaseDN
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// substituter - This type is to allow restricted runtime
|
||||
// substitutions of variables in LDAP configuration items during
|
||||
// runtime.
|
||||
type substituter struct {
|
||||
vals map[string]string
|
||||
}
|
||||
|
||||
// newSubstituter - sets up the substituter for usage, for e.g.:
|
||||
//
|
||||
// subber := newSubstituter("username", "john")
|
||||
func newSubstituter(v ...string) substituter {
|
||||
if len(v)%2 != 0 {
|
||||
log.Fatal("Need an even number of arguments")
|
||||
}
|
||||
vals := make(map[string]string)
|
||||
for i := 0; i < len(v); i += 2 {
|
||||
vals[v[i]] = v[i+1]
|
||||
}
|
||||
return substituter{vals: vals}
|
||||
}
|
||||
|
||||
// substitute - performs substitution on the given string `t`. Returns
|
||||
// an error if there are any variables in the input that do not have
|
||||
// values in the substituter. E.g.:
|
||||
//
|
||||
// subber.substitute("uid=${username},cn=users,dc=example,dc=com")
|
||||
//
|
||||
// returns "uid=john,cn=users,dc=example,dc=com"
|
||||
//
|
||||
// whereas:
|
||||
//
|
||||
// subber.substitute("uid=${usernamedn}")
|
||||
//
|
||||
// returns an error.
|
||||
func (s *substituter) substitute(t string) (string, error) {
|
||||
for k, v := range s.vals {
|
||||
re := regexp.MustCompile(fmt.Sprintf(`\$\{%s\}`, k))
|
||||
t = re.ReplaceAllLiteralString(t, v)
|
||||
}
|
||||
// Check if all requested substitutions have been made.
|
||||
re := regexp.MustCompile(`\$\{.*\}`)
|
||||
if re.MatchString(t) {
|
||||
return "", errors.New("unsupported substitution requested")
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
+17
-11
@@ -20,6 +20,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -134,20 +135,25 @@ func (sys *LifecycleSys) Init(objAPI ObjectLayer) error {
|
||||
// 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
|
||||
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||
for {
|
||||
select {
|
||||
case <-retryTimerCh:
|
||||
// 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
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
case <-globalOSSignalCh:
|
||||
return fmt.Errorf("Initializing Lifecycle sub-system gracefully stopped")
|
||||
}
|
||||
break
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh LifecycleSys.
|
||||
|
||||
+23
-4
@@ -43,6 +43,20 @@ type lockRESTClient struct {
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
func toLockError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch err.Error() {
|
||||
case errLockConflict.Error():
|
||||
return errLockConflict
|
||||
case errLockNotExpired.Error():
|
||||
return errLockNotExpired
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ServerAddr - dsync.NetLocker interface compatible method.
|
||||
func (client *lockRESTClient) ServerAddr() string {
|
||||
return client.serverURL.Host
|
||||
@@ -88,7 +102,6 @@ func (client *lockRESTClient) markHostDown() {
|
||||
// permanently. The only way to restore the connection is at the xl-sets layer by xlsets.monitorAndConnectEndpoints()
|
||||
// after verifying format.json
|
||||
func (client *lockRESTClient) call(method string, values url.Values, body io.Reader, length int64) (respBody io.ReadCloser, err error) {
|
||||
|
||||
if !client.isHostUp() {
|
||||
return nil, errors.New("Lock rest server node is down")
|
||||
}
|
||||
@@ -98,7 +111,6 @@ func (client *lockRESTClient) call(method string, values url.Values, body io.Rea
|
||||
}
|
||||
|
||||
respBody, err = client.restClient.Call(method, values, body, length)
|
||||
|
||||
if err == nil {
|
||||
return respBody, nil
|
||||
}
|
||||
@@ -107,7 +119,7 @@ func (client *lockRESTClient) call(method string, values url.Values, body io.Rea
|
||||
client.markHostDown()
|
||||
}
|
||||
|
||||
return nil, err
|
||||
return nil, toLockError(err)
|
||||
}
|
||||
|
||||
// Stringer provides a canonicalized representation of node.
|
||||
@@ -138,7 +150,14 @@ func (client *lockRESTClient) restCall(call string, args dsync.LockArgs) (reply
|
||||
|
||||
respBody, err := client.call(call, values, nil, -1)
|
||||
defer http.DrainBody(respBody)
|
||||
return err == nil, err
|
||||
switch err {
|
||||
case nil:
|
||||
return true, nil
|
||||
case errLockConflict, errLockNotExpired:
|
||||
return false, nil
|
||||
default:
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// RLock calls read lock REST API.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
@@ -53,6 +54,9 @@ type nameLockRequesterInfoPair struct {
|
||||
lri lockRequesterInfo
|
||||
}
|
||||
|
||||
var errLockConflict = errors.New("lock conflict")
|
||||
var errLockNotExpired = errors.New("lock not expired")
|
||||
|
||||
// Similar to removeEntry but only removes an entry only if the lock entry exists in map.
|
||||
func (l *localLocker) removeEntryIfExists(nlrip nameLockRequesterInfoPair) {
|
||||
// Check if entry is still in map (could have been removed altogether by 'concurrent' (R)Unlock of last entry)
|
||||
|
||||
@@ -35,7 +35,7 @@ func createLockTestServer(t *testing.T) (string, *lockRESTServer, string) {
|
||||
}
|
||||
|
||||
locker := &lockRESTServer{
|
||||
ll: localLocker{
|
||||
ll: &localLocker{
|
||||
mutex: sync.Mutex{},
|
||||
serviceEndpoint: "rpc-path",
|
||||
lockMap: make(map[string][]lockRequesterInfo),
|
||||
|
||||
+29
-13
@@ -42,7 +42,7 @@ const (
|
||||
|
||||
// To abstract a node over network.
|
||||
type lockRESTServer struct {
|
||||
ll localLocker
|
||||
ll *localLocker
|
||||
}
|
||||
|
||||
func (l *lockRESTServer) writeErrorResponse(w http.ResponseWriter, err error) {
|
||||
@@ -62,6 +62,7 @@ func (l *lockRESTServer) IsValid(w http.ResponseWriter, r *http.Request) bool {
|
||||
func getLockArgs(r *http.Request) dsync.LockArgs {
|
||||
return dsync.LockArgs{
|
||||
UID: r.URL.Query().Get(lockRESTUID),
|
||||
Source: r.URL.Query().Get(lockRESTSource),
|
||||
Resource: r.URL.Query().Get(lockRESTResource),
|
||||
ServerAddr: r.URL.Query().Get(lockRESTServerAddr),
|
||||
ServiceEndpoint: r.URL.Query().Get(lockRESTServerEndpoint),
|
||||
@@ -75,7 +76,11 @@ func (l *lockRESTServer) LockHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := l.ll.Lock(getLockArgs(r)); err != nil {
|
||||
success, err := l.ll.Lock(getLockArgs(r))
|
||||
if err == nil && !success {
|
||||
err = errLockConflict
|
||||
}
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
@@ -88,7 +93,10 @@ func (l *lockRESTServer) UnlockHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := l.ll.Unlock(getLockArgs(r)); err != nil {
|
||||
_, err := l.ll.Unlock(getLockArgs(r))
|
||||
// Ignore the Unlock() "reply" return value because if err == nil, "reply" is always true
|
||||
// Consequently, if err != nil, reply is always false
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
@@ -101,7 +109,11 @@ func (l *lockRESTServer) RLockHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := l.ll.RLock(getLockArgs(r)); err != nil {
|
||||
success, err := l.ll.RLock(getLockArgs(r))
|
||||
if err == nil && !success {
|
||||
err = errLockConflict
|
||||
}
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
@@ -114,7 +126,10 @@ func (l *lockRESTServer) RUnlockHandler(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := l.ll.RUnlock(getLockArgs(r)); err != nil {
|
||||
// Ignore the RUnlock() "reply" return value because if err == nil, "reply" is always true.
|
||||
// Consequently, if err != nil, reply is always false
|
||||
_, err := l.ll.RUnlock(getLockArgs(r))
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
@@ -127,6 +142,8 @@ func (l *lockRESTServer) ForceUnlockHandler(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore the ForceUnlock() "reply" return value because if err == nil, "reply" is always true
|
||||
// Consequently, if err != nil, reply is always false
|
||||
if _, err := l.ll.ForceUnlock(getLockArgs(r)); err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
@@ -142,7 +159,6 @@ func (l *lockRESTServer) ExpiredHandler(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
lockArgs := getLockArgs(r)
|
||||
|
||||
success := true
|
||||
l.ll.mutex.Lock()
|
||||
defer l.ll.mutex.Unlock()
|
||||
// Lock found, proceed to verify if belongs to given uid.
|
||||
@@ -150,15 +166,11 @@ func (l *lockRESTServer) ExpiredHandler(w http.ResponseWriter, r *http.Request)
|
||||
// Check whether uid is still active
|
||||
for _, entry := range lri {
|
||||
if entry.UID == lockArgs.UID {
|
||||
success = false // When uid found, lock is still active so return not expired.
|
||||
break
|
||||
l.writeErrorResponse(w, errLockNotExpired)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
l.writeErrorResponse(w, errors.New("lock already expired"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// lockMaintenance loops over locks that have been active for some time and checks back
|
||||
@@ -189,11 +201,15 @@ func (l *lockRESTServer) lockMaintenance(interval time.Duration) {
|
||||
}
|
||||
|
||||
// Call back to original server verify whether the lock is still active (based on name & uid)
|
||||
expired, _ := c.Expired(dsync.LockArgs{
|
||||
expired, err := c.Expired(dsync.LockArgs{
|
||||
UID: nlrip.lri.UID,
|
||||
Resource: nlrip.name,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// For successful response, verify if lock was indeed active or stale.
|
||||
if expired {
|
||||
// The lock is no longer active at server that originated
|
||||
|
||||
@@ -116,7 +116,6 @@ func newApp(name string) *cli.App {
|
||||
// Register all commands.
|
||||
registerCommand(serverCmd)
|
||||
registerCommand(gatewayCmd)
|
||||
registerCommand(updateCmd)
|
||||
registerCommand(versionCmd)
|
||||
|
||||
// Set up app.
|
||||
|
||||
@@ -67,7 +67,7 @@ func newDsyncNodes(endpoints EndpointList) (clnts []dsync.NetLocker, myNode int)
|
||||
myNode = len(clnts)
|
||||
|
||||
receiver := &lockRESTServer{
|
||||
ll: localLocker{
|
||||
ll: &localLocker{
|
||||
serverAddr: endpoint.Host,
|
||||
serviceEndpoint: lockServicePath,
|
||||
lockMap: make(map[string][]lockRequesterInfo),
|
||||
@@ -75,7 +75,7 @@ func newDsyncNodes(endpoints EndpointList) (clnts []dsync.NetLocker, myNode int)
|
||||
}
|
||||
|
||||
globalLockServer = receiver
|
||||
locker = &(receiver.ll)
|
||||
locker = receiver.ll
|
||||
} else {
|
||||
host, err := xnet.ParseHost(endpoint.Host)
|
||||
logger.FatalIf(err, "Unable to parse Lock RPC Host")
|
||||
|
||||
@@ -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, empty bool, algo BitrotAlgorithm, sum []byte, shardSize int64) error {
|
||||
func (d *naughtyDisk) VerifyFile(volume, path string, size int64, algo BitrotAlgorithm, sum []byte, shardSize int64) error {
|
||||
if err := d.calcError(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.disk.VerifyFile(volume, path, empty, algo, sum, shardSize)
|
||||
return d.disk.VerifyFile(volume, path, size, algo, sum, shardSize)
|
||||
}
|
||||
|
||||
+33
-13
@@ -405,6 +405,21 @@ func (sys *NotificationSys) DownloadProfilingData(ctx context.Context, writer io
|
||||
return profilingDataFound
|
||||
}
|
||||
|
||||
// ServerUpdate - updates remote peers.
|
||||
func (sys *NotificationSys) ServerUpdate(updateURL, sha256Hex string, latestReleaseTime time.Time) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
client := client
|
||||
ng.Go(context.Background(), func() error {
|
||||
return client.ServerUpdate(updateURL, sha256Hex, latestReleaseTime)
|
||||
}, idx, *client.host)
|
||||
}
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// SignalService - calls signal service RPC call on all peers.
|
||||
func (sys *NotificationSys) SignalService(sig serviceSignal) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
@@ -793,19 +808,24 @@ func (sys *NotificationSys) Init(objAPI ObjectLayer) error {
|
||||
// the following reasons:
|
||||
// - Read quorum is lost just after the initialization
|
||||
// of the object layer.
|
||||
for range newRetryTimerSimple(doneCh) {
|
||||
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 notification subsystem to be initialized..")
|
||||
continue
|
||||
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||
for {
|
||||
select {
|
||||
case <-retryTimerCh:
|
||||
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 notification subsystem to be initialized..")
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
case <-globalOSSignalCh:
|
||||
return fmt.Errorf("Initializing Notification sub-system gracefully stopped")
|
||||
}
|
||||
break
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddRulesMap - adds rules map for bucket name.
|
||||
@@ -957,8 +977,8 @@ func (sys *NotificationSys) CollectNetPerfInfo(size int64) map[string][]ServerNe
|
||||
}
|
||||
|
||||
// DrivePerfInfo - Drive speed (read and write) information
|
||||
func (sys *NotificationSys) DrivePerfInfo() []ServerDrivesPerfInfo {
|
||||
reply := make([]ServerDrivesPerfInfo, len(sys.peerClients))
|
||||
func (sys *NotificationSys) DrivePerfInfo(size int64) []madmin.ServerDrivesPerfInfo {
|
||||
reply := make([]madmin.ServerDrivesPerfInfo, len(sys.peerClients))
|
||||
var wg sync.WaitGroup
|
||||
for i, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
@@ -967,7 +987,7 @@ func (sys *NotificationSys) DrivePerfInfo() []ServerDrivesPerfInfo {
|
||||
wg.Add(1)
|
||||
go func(client *peerRESTClient, idx int) {
|
||||
defer wg.Done()
|
||||
di, err := client.DrivePerfInfo()
|
||||
di, err := client.DrivePerfInfo(size)
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", client.host.String())
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
|
||||
@@ -1063,7 +1063,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
r.Body = &detectDisconnect{r.Body, r.Context().Done()}
|
||||
|
||||
// X-Amz-Copy-Source shouldn't be set for this call.
|
||||
if _, ok := r.Header["X-Amz-Copy-Source"]; ok {
|
||||
if _, ok := r.Header[xhttp.AmzCopySource]; ok {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopySource), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -1086,7 +1086,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
size := r.ContentLength
|
||||
rAuthType := getRequestAuthType(r)
|
||||
if rAuthType == authTypeStreamingSigned {
|
||||
if sizeStr, ok := r.Header["X-Amz-Decoded-Content-Length"]; ok {
|
||||
if sizeStr, ok := r.Header[xhttp.AmzDecodedContentLength]; ok {
|
||||
if sizeStr[0] == "" {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1205,6 +1205,11 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
getObjectInfo := objectAPI.GetObjectInfo
|
||||
if api.CacheAPI() != nil {
|
||||
getObjectInfo = api.CacheAPI().GetObjectInfo
|
||||
putObject = api.CacheAPI().PutObject
|
||||
}
|
||||
|
||||
rawReader := hashReader
|
||||
pReader := NewPutObjReader(rawReader, nil, nil)
|
||||
@@ -1219,7 +1224,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
if _, err = objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil {
|
||||
if _, err = getObjectInfo(ctx, bucket, object, opts); err == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
+65
-3
@@ -173,8 +173,10 @@ func (client *peerRESTClient) CPULoadInfo() (info ServerCPULoadInfo, err error)
|
||||
}
|
||||
|
||||
// DrivePerfInfo - fetch Drive performance information for a remote node.
|
||||
func (client *peerRESTClient) DrivePerfInfo() (info ServerDrivesPerfInfo, err error) {
|
||||
respBody, err := client.call(peerRESTMethodDrivePerfInfo, nil, nil, -1)
|
||||
func (client *peerRESTClient) DrivePerfInfo(size int64) (info madmin.ServerDrivesPerfInfo, err error) {
|
||||
params := make(url.Values)
|
||||
params.Set(peerRESTDrivePerfSize, strconv.FormatInt(size, 10))
|
||||
respBody, err := client.call(peerRESTMethodDrivePerfInfo, params, nil, -1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -503,10 +505,28 @@ func (client *peerRESTClient) LoadGroup(group string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerUpdate - sends server update message to remote peers.
|
||||
func (client *peerRESTClient) ServerUpdate(updateURL, sha256Hex string, latestReleaseTime time.Time) error {
|
||||
values := make(url.Values)
|
||||
values.Set(peerRESTUpdateURL, updateURL)
|
||||
values.Set(peerRESTSha256Hex, sha256Hex)
|
||||
if !latestReleaseTime.IsZero() {
|
||||
values.Set(peerRESTLatestRelease, latestReleaseTime.Format(time.RFC3339))
|
||||
} else {
|
||||
values.Set(peerRESTLatestRelease, "")
|
||||
}
|
||||
respBody, err := client.call(peerRESTMethodServerUpdate, values, nil, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer http.DrainBody(respBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignalService - sends signal to peer nodes.
|
||||
func (client *peerRESTClient) SignalService(sig serviceSignal) error {
|
||||
values := make(url.Values)
|
||||
values.Set(peerRESTSignal, string(sig))
|
||||
values.Set(peerRESTSignal, strconv.Itoa(int(sig)))
|
||||
respBody, err := client.call(peerRESTMethodSignalService, values, nil, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -611,6 +631,48 @@ func (client *peerRESTClient) Trace(traceCh chan interface{}, doneCh chan struct
|
||||
}()
|
||||
}
|
||||
|
||||
// ConsoleLog - sends request to peer nodes to get console logs
|
||||
func (client *peerRESTClient) ConsoleLog(logCh chan interface{}, doneCh chan struct{}) {
|
||||
go func() {
|
||||
for {
|
||||
// get cancellation context to properly unsubscribe peers
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
respBody, err := client.callWithContext(ctx, peerRESTMethodLog, nil, nil, -1)
|
||||
if err != nil {
|
||||
// Retry the failed request.
|
||||
time.Sleep(5 * time.Second)
|
||||
} else {
|
||||
dec := gob.NewDecoder(respBody)
|
||||
|
||||
go func() {
|
||||
<-doneCh
|
||||
cancel()
|
||||
}()
|
||||
|
||||
for {
|
||||
var log madmin.LogInfo
|
||||
if err = dec.Decode(&log); err != nil {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case logCh <- log:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-doneCh:
|
||||
cancel()
|
||||
http.DrainBody(respBody)
|
||||
return
|
||||
default:
|
||||
// There was error in the REST request, retry.
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func getRemoteHosts(endpoints EndpointList) []*xnet.Host {
|
||||
var remoteHosts []*xnet.Host
|
||||
for _, hostStr := range GetRemotePeers(endpoints) {
|
||||
|
||||
+19
-13
@@ -27,6 +27,7 @@ const (
|
||||
peerRESTMethodMemUsageInfo = "memusageinfo"
|
||||
peerRESTMethodDrivePerfInfo = "driveperfinfo"
|
||||
peerRESTMethodDeleteBucket = "deletebucket"
|
||||
peerRESTMethodServerUpdate = "serverupdate"
|
||||
peerRESTMethodSignalService = "signalservice"
|
||||
peerRESTMethodBackgroundHealStatus = "backgroundhealstatus"
|
||||
peerRESTMethodBackgroundOpsStatus = "backgroundopsstatus"
|
||||
@@ -50,20 +51,25 @@ const (
|
||||
peerRESTMethodTrace = "trace"
|
||||
peerRESTMethodBucketLifecycleSet = "setbucketlifecycle"
|
||||
peerRESTMethodBucketLifecycleRemove = "removebucketlifecycle"
|
||||
peerRESTMethodLog = "log"
|
||||
)
|
||||
|
||||
const (
|
||||
peerRESTNetPerfSize = "netperfsize"
|
||||
peerRESTBucket = "bucket"
|
||||
peerRESTUser = "user"
|
||||
peerRESTGroup = "group"
|
||||
peerRESTUserTemp = "user-temp"
|
||||
peerRESTPolicy = "policy"
|
||||
peerRESTUserOrGroup = "user-or-group"
|
||||
peerRESTIsGroup = "is-group"
|
||||
peerRESTSignal = "signal"
|
||||
peerRESTProfiler = "profiler"
|
||||
peerRESTDryRun = "dry-run"
|
||||
peerRESTTraceAll = "all"
|
||||
peerRESTTraceErr = "err"
|
||||
peerRESTNetPerfSize = "netperfsize"
|
||||
peerRESTDrivePerfSize = "driveperfsize"
|
||||
peerRESTBucket = "bucket"
|
||||
peerRESTUser = "user"
|
||||
peerRESTGroup = "group"
|
||||
peerRESTUserTemp = "user-temp"
|
||||
peerRESTPolicy = "policy"
|
||||
peerRESTUserOrGroup = "user-or-group"
|
||||
peerRESTIsGroup = "is-group"
|
||||
peerRESTUpdateURL = "updateURL"
|
||||
peerRESTSha256Hex = "sha256Hex"
|
||||
peerRESTLatestRelease = "latestReleaseTime"
|
||||
peerRESTSignal = "signal"
|
||||
peerRESTProfiler = "profiler"
|
||||
peerRESTDryRun = "dry-run"
|
||||
peerRESTTraceAll = "all"
|
||||
peerRESTTraceErr = "err"
|
||||
)
|
||||
|
||||
+95
-50
@@ -24,7 +24,6 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -33,6 +32,7 @@ import (
|
||||
"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"
|
||||
trace "github.com/minio/minio/pkg/trace"
|
||||
@@ -67,46 +67,6 @@ func getServerInfo() (*ServerInfoData, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// uptimes - used to sort uptimes in chronological order.
|
||||
type uptimes []time.Duration
|
||||
|
||||
func (ts uptimes) Len() int {
|
||||
return len(ts)
|
||||
}
|
||||
|
||||
func (ts uptimes) Less(i, j int) bool {
|
||||
return ts[i] < ts[j]
|
||||
}
|
||||
|
||||
func (ts uptimes) Swap(i, j int) {
|
||||
ts[i], ts[j] = ts[j], ts[i]
|
||||
}
|
||||
|
||||
// getPeerUptimes - returns the uptime.
|
||||
func getPeerUptimes(serverInfo []ServerInfo) time.Duration {
|
||||
// In a single node Erasure or FS backend setup the uptime of
|
||||
// the setup is the uptime of the single minio server
|
||||
// instance.
|
||||
if !globalIsDistXL {
|
||||
return UTCNow().Sub(globalBootTime)
|
||||
}
|
||||
|
||||
var times []time.Duration
|
||||
|
||||
for _, info := range serverInfo {
|
||||
if info.Error != "" {
|
||||
continue
|
||||
}
|
||||
times = append(times, info.Data.Properties.Uptime)
|
||||
}
|
||||
|
||||
// Sort uptimes in chronological order.
|
||||
sort.Sort(uptimes(times))
|
||||
|
||||
// Return the latest time as the uptime.
|
||||
return times[0]
|
||||
}
|
||||
|
||||
// NetReadPerfInfoHandler - returns network read performance information.
|
||||
func (s *peerRESTServer) NetReadPerfInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
@@ -458,7 +418,7 @@ func (s *peerRESTServer) CPULoadInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
ctx := newContext(r, w, "CPULoadInfo")
|
||||
info := localEndpointsCPULoad(globalEndpoints, r)
|
||||
info := getLocalCPULoad(globalEndpoints, r)
|
||||
|
||||
defer w.(http.Flusher).Flush()
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(info))
|
||||
@@ -471,8 +431,23 @@ func (s *peerRESTServer) DrivePerfInfoHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
params := mux.Vars(r)
|
||||
|
||||
sizeStr, found := params[peerRESTDrivePerfSize]
|
||||
if !found {
|
||||
s.writeErrorResponse(w, errors.New("size is missing"))
|
||||
return
|
||||
}
|
||||
|
||||
size, err := strconv.ParseInt(sizeStr, 10, 64)
|
||||
if err != nil || size < 0 {
|
||||
s.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := newContext(r, w, "DrivePerfInfo")
|
||||
info := localEndpointsDrivePerf(globalEndpoints, r)
|
||||
|
||||
info := getLocalDrivesPerf(globalEndpoints, size, r)
|
||||
|
||||
defer w.(http.Flusher).Flush()
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(info))
|
||||
@@ -485,7 +460,7 @@ func (s *peerRESTServer) MemUsageInfoHandler(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
ctx := newContext(r, w, "MemUsageInfo")
|
||||
info := localEndpointsMemUsage(globalEndpoints, r)
|
||||
info := getLocalMemUsage(globalEndpoints, r)
|
||||
|
||||
defer w.(http.Flusher).Flush()
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(info))
|
||||
@@ -809,6 +784,35 @@ func (s *peerRESTServer) ListenBucketNotificationHandler(w http.ResponseWriter,
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// ServerUpdateHandler - updates the current server.
|
||||
func (s *peerRESTServer) ServerUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
s.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
updateURL := vars[peerRESTUpdateURL]
|
||||
sha256Hex := vars[peerRESTSha256Hex]
|
||||
var latestReleaseTime time.Time
|
||||
var err error
|
||||
if latestRelease := vars[peerRESTLatestRelease]; latestRelease != "" {
|
||||
latestReleaseTime, err = time.Parse(latestRelease, time.RFC3339)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
us, err := updateServer(updateURL, sha256Hex, latestReleaseTime)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
if us.CurrentVersion != us.UpdatedVersion {
|
||||
globalServiceSignalCh <- serviceRestart
|
||||
}
|
||||
}
|
||||
|
||||
var errUnsupportedSignal = fmt.Errorf("unsupported signal: only restart and stop signals are supported")
|
||||
|
||||
// SignalServiceHandler - signal service handler.
|
||||
@@ -824,10 +828,17 @@ func (s *peerRESTServer) SignalServiceHandler(w http.ResponseWriter, r *http.Req
|
||||
s.writeErrorResponse(w, errors.New("signal name is missing"))
|
||||
return
|
||||
}
|
||||
signal := serviceSignal(signalString)
|
||||
si, err := strconv.Atoi(signalString)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
signal := serviceSignal(si)
|
||||
defer w.(http.Flusher).Flush()
|
||||
switch signal {
|
||||
case serviceRestart, serviceStop:
|
||||
case serviceRestart:
|
||||
globalServiceSignalCh <- signal
|
||||
case serviceStop:
|
||||
globalServiceSignalCh <- signal
|
||||
default:
|
||||
s.writeErrorResponse(w, errUnsupportedSignal)
|
||||
@@ -908,6 +919,38 @@ func (s *peerRESTServer) BackgroundOpsStatusHandler(w http.ResponseWriter, r *ht
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(state))
|
||||
}
|
||||
|
||||
// ConsoleLogHandler sends console logs of this node back to peer rest client
|
||||
func (s *peerRESTServer) ConsoleLogHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
s.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Connection", "close")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.(http.Flusher).Flush()
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
defer close(doneCh)
|
||||
|
||||
ch := make(chan interface{}, 2000)
|
||||
globalConsoleSys.Subscribe(ch, doneCh, "", 0, nil)
|
||||
|
||||
enc := gob.NewEncoder(w)
|
||||
for {
|
||||
select {
|
||||
case entry := <-ch:
|
||||
log := entry.(madmin.LogInfo)
|
||||
if err := enc.Encode(log); err != nil {
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *peerRESTServer) writeErrorResponse(w http.ResponseWriter, err error) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte(err.Error()))
|
||||
@@ -926,20 +969,21 @@ func (s *peerRESTServer) IsValid(w http.ResponseWriter, r *http.Request) bool {
|
||||
func registerPeerRESTHandlers(router *mux.Router) {
|
||||
server := &peerRESTServer{}
|
||||
subrouter := router.PathPrefix(peerRESTPath).Subrouter()
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodNetReadPerfInfo).HandlerFunc(httpTraceHdrs(server.NetReadPerfInfoHandler))
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodCollectNetPerfInfo).HandlerFunc(httpTraceHdrs(server.CollectNetPerfInfoHandler))
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodNetReadPerfInfo).HandlerFunc(httpTraceHdrs(server.NetReadPerfInfoHandler)).Queries(restQueries(peerRESTNetPerfSize)...)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodCollectNetPerfInfo).HandlerFunc(httpTraceHdrs(server.CollectNetPerfInfoHandler)).Queries(restQueries(peerRESTNetPerfSize)...)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodGetLocks).HandlerFunc(httpTraceHdrs(server.GetLocksHandler))
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodServerInfo).HandlerFunc(httpTraceHdrs(server.ServerInfoHandler))
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodCPULoadInfo).HandlerFunc(httpTraceHdrs(server.CPULoadInfoHandler))
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodMemUsageInfo).HandlerFunc(httpTraceHdrs(server.MemUsageInfoHandler))
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodDrivePerfInfo).HandlerFunc(httpTraceHdrs(server.DrivePerfInfoHandler))
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodDrivePerfInfo).HandlerFunc(httpTraceHdrs(server.DrivePerfInfoHandler)).Queries(restQueries(peerRESTDrivePerfSize)...)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodDeleteBucket).HandlerFunc(httpTraceHdrs(server.DeleteBucketHandler)).Queries(restQueries(peerRESTBucket)...)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodSignalService).HandlerFunc(httpTraceHdrs(server.SignalServiceHandler)).Queries(restQueries(peerRESTSignal)...)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodServerUpdate).HandlerFunc(httpTraceHdrs(server.ServerUpdateHandler)).Queries(restQueries(peerRESTUpdateURL, peerRESTSha256Hex, peerRESTLatestRelease)...)
|
||||
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodBucketPolicyRemove).HandlerFunc(httpTraceAll(server.RemoveBucketPolicyHandler)).Queries(restQueries(peerRESTBucket)...)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodBucketPolicySet).HandlerFunc(httpTraceHdrs(server.SetBucketPolicyHandler)).Queries(restQueries(peerRESTBucket)...)
|
||||
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodDeletePolicy).HandlerFunc(httpTraceAll(server.LoadPolicyHandler)).Queries(restQueries(peerRESTPolicy)...)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodDeletePolicy).HandlerFunc(httpTraceAll(server.DeletePolicyHandler)).Queries(restQueries(peerRESTPolicy)...)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodLoadPolicy).HandlerFunc(httpTraceAll(server.LoadPolicyHandler)).Queries(restQueries(peerRESTPolicy)...)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodLoadPolicyMapping).HandlerFunc(httpTraceAll(server.LoadPolicyMappingHandler)).Queries(restQueries(peerRESTUserOrGroup)...)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodDeleteUser).HandlerFunc(httpTraceAll(server.LoadUserHandler)).Queries(restQueries(peerRESTUser)...)
|
||||
@@ -962,6 +1006,7 @@ func registerPeerRESTHandlers(router *mux.Router) {
|
||||
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodTrace).HandlerFunc(server.TraceHandler)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodBackgroundHealStatus).HandlerFunc(server.BackgroundHealStatusHandler)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + peerRESTMethodLog).HandlerFunc(server.ConsoleLogHandler)
|
||||
|
||||
router.NotFoundHandler = http.HandlerFunc(httpTraceAll(notFoundHandler))
|
||||
}
|
||||
|
||||
+16
-11
@@ -163,20 +163,25 @@ func (sys *PolicySys) Init(objAPI ObjectLayer) error {
|
||||
// the following reasons:
|
||||
// - Read quorum is lost just after the initialization
|
||||
// of the object layer.
|
||||
for range newRetryTimerSimple(doneCh) {
|
||||
// Load PolicySys 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 policy subsystem to be initialized..")
|
||||
continue
|
||||
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||
for {
|
||||
select {
|
||||
case <-retryTimerCh:
|
||||
// Load PolicySys 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 policy subsystem to be initialized..")
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
case <-globalOSSignalCh:
|
||||
return fmt.Errorf("Initializing Policy sub-system gracefully stopped")
|
||||
}
|
||||
break
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPolicySys - creates new policy system.
|
||||
|
||||
+30
-57
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
@@ -24,86 +25,54 @@ import (
|
||||
|
||||
// Function not implemented error
|
||||
func isSysErrNoSys(err error) bool {
|
||||
if err == syscall.ENOSYS {
|
||||
return true
|
||||
}
|
||||
pathErr, ok := err.(*os.PathError)
|
||||
return ok && pathErr.Err == syscall.ENOSYS
|
||||
|
||||
return errors.Is(err, syscall.ENOSYS)
|
||||
}
|
||||
|
||||
// Not supported error
|
||||
func isSysErrOpNotSupported(err error) bool {
|
||||
if err == syscall.EOPNOTSUPP {
|
||||
return true
|
||||
}
|
||||
pathErr, ok := err.(*os.PathError)
|
||||
return ok && pathErr.Err == syscall.EOPNOTSUPP
|
||||
|
||||
return errors.Is(err, syscall.EOPNOTSUPP)
|
||||
}
|
||||
|
||||
// No space left on device error
|
||||
func isSysErrNoSpace(err error) bool {
|
||||
if err == syscall.ENOSPC {
|
||||
return true
|
||||
}
|
||||
pathErr, ok := err.(*os.PathError)
|
||||
return ok && pathErr.Err == syscall.ENOSPC
|
||||
return errors.Is(err, syscall.ENOSPC)
|
||||
}
|
||||
|
||||
// Input/output error
|
||||
func isSysErrIO(err error) bool {
|
||||
if err == syscall.EIO {
|
||||
return true
|
||||
}
|
||||
pathErr, ok := err.(*os.PathError)
|
||||
return ok && pathErr.Err == syscall.EIO
|
||||
return errors.Is(err, syscall.EIO)
|
||||
}
|
||||
|
||||
// Check if the given error corresponds to EISDIR (is a directory).
|
||||
func isSysErrIsDir(err error) bool {
|
||||
if err == syscall.EISDIR {
|
||||
return true
|
||||
}
|
||||
pathErr, ok := err.(*os.PathError)
|
||||
return ok && pathErr.Err == syscall.EISDIR
|
||||
|
||||
return errors.Is(err, syscall.EISDIR)
|
||||
}
|
||||
|
||||
// Check if the given error corresponds to ENOTDIR (is not a directory).
|
||||
func isSysErrNotDir(err error) bool {
|
||||
if err == syscall.ENOTDIR {
|
||||
return true
|
||||
}
|
||||
pathErr, ok := err.(*os.PathError)
|
||||
return ok && pathErr.Err == syscall.ENOTDIR
|
||||
return errors.Is(err, syscall.ENOTDIR)
|
||||
}
|
||||
|
||||
// Check if the given error corresponds to the ENAMETOOLONG (name too long).
|
||||
func isSysErrTooLong(err error) bool {
|
||||
if err == syscall.ENAMETOOLONG {
|
||||
return true
|
||||
}
|
||||
pathErr, ok := err.(*os.PathError)
|
||||
return ok && pathErr.Err == syscall.ENAMETOOLONG
|
||||
return errors.Is(err, syscall.ENAMETOOLONG)
|
||||
}
|
||||
|
||||
// Check if the given error corresponds to ENOTEMPTY for unix
|
||||
// and ERROR_DIR_NOT_EMPTY for windows (directory not empty).
|
||||
func isSysErrNotEmpty(err error) bool {
|
||||
if err == syscall.ENOTEMPTY {
|
||||
if errors.Is(err, syscall.ENOTEMPTY) {
|
||||
return true
|
||||
}
|
||||
if pathErr, ok := err.(*os.PathError); ok {
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
if runtime.GOOS == globalWindowsOSName {
|
||||
if errno, _ok := pathErr.Err.(syscall.Errno); _ok && errno == 0x91 {
|
||||
var errno syscall.Errno
|
||||
if errors.As(pathErr.Err, &errno) {
|
||||
// ERROR_DIR_NOT_EMPTY
|
||||
return true
|
||||
return errno == 0x91
|
||||
}
|
||||
}
|
||||
if pathErr.Err == syscall.ENOTEMPTY {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -113,10 +82,12 @@ func isSysErrPathNotFound(err error) bool {
|
||||
if runtime.GOOS != globalWindowsOSName {
|
||||
return false
|
||||
}
|
||||
if pathErr, ok := err.(*os.PathError); ok {
|
||||
if errno, _ok := pathErr.Err.(syscall.Errno); _ok && errno == 0x03 {
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
var errno syscall.Errno
|
||||
if errors.As(pathErr.Err, &errno) {
|
||||
// ERROR_PATH_NOT_FOUND
|
||||
return true
|
||||
return errno == 0x03
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -128,20 +99,22 @@ func isSysErrHandleInvalid(err error) bool {
|
||||
return false
|
||||
}
|
||||
// Check if err contains ERROR_INVALID_HANDLE errno
|
||||
errno, ok := err.(syscall.Errno)
|
||||
return ok && errno == 0x6
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
var errno syscall.Errno
|
||||
if errors.As(pathErr.Err, &errno) {
|
||||
// ERROR_PATH_NOT_FOUND
|
||||
return errno == 0x6
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isSysErrCrossDevice(err error) bool {
|
||||
e, ok := err.(*os.LinkError)
|
||||
return ok && e.Err == syscall.EXDEV
|
||||
return errors.Is(err, syscall.EXDEV)
|
||||
}
|
||||
|
||||
// Check if given error corresponds to too many open files
|
||||
func isSysErrTooManyFiles(err error) bool {
|
||||
if err == syscall.ENFILE || err == syscall.EMFILE {
|
||||
return true
|
||||
}
|
||||
pathErr, ok := err.(*os.PathError)
|
||||
return ok && (pathErr.Err == syscall.ENFILE || pathErr.Err == syscall.EMFILE)
|
||||
return errors.Is(err, syscall.ENFILE) || errors.Is(err, syscall.EMFILE)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ func parseDirEnt(buf []byte) (consumed int, name string, typ os.FileMode, err er
|
||||
// to handle such files, MinIO is only interested in
|
||||
// files and directories.
|
||||
typ = unexpectedFileMode
|
||||
return
|
||||
}
|
||||
|
||||
nameBuf := (*[unsafe.Sizeof(dirent.Name)]byte)(unsafe.Pointer(&dirent.Name[0]))
|
||||
|
||||
+12
-15
@@ -19,6 +19,7 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@@ -47,7 +48,7 @@ const (
|
||||
diskMinFreeSpace = 900 * humanize.MiByte // Min 900MiB free space.
|
||||
diskMinTotalSpace = diskMinFreeSpace // Min 900MiB total space.
|
||||
maxAllowedIOError = 5
|
||||
readBlockSize = humanize.KiByte * 32 // Default read block size 32KiB.
|
||||
readBlockSize = 4 * humanize.MiByte // Default read block size 4MiB.
|
||||
)
|
||||
|
||||
// isValidVolname verifies a volname name in accordance with object
|
||||
@@ -845,17 +846,11 @@ func (s *posix) ReadAll(volume, path string) (buf []byte, err error) {
|
||||
return nil, errFileNotFound
|
||||
} else if os.IsPermission(err) {
|
||||
return nil, errFileAccessDenied
|
||||
} else if pathErr, ok := err.(*os.PathError); ok {
|
||||
switch pathErr.Err {
|
||||
case syscall.ENOTDIR, syscall.EISDIR:
|
||||
return nil, errFileNotFound
|
||||
default:
|
||||
if isSysErrHandleInvalid(pathErr.Err) {
|
||||
// This case is special and needs to be handled for windows.
|
||||
return nil, errFileNotFound
|
||||
}
|
||||
}
|
||||
return nil, pathErr
|
||||
} else if errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.EISDIR) {
|
||||
return nil, errFileNotFound
|
||||
} else if isSysErrHandleInvalid(err) {
|
||||
// This case is special and needs to be handled for windows.
|
||||
return nil, errFileNotFound
|
||||
} else if isSysErrIO(err) {
|
||||
return nil, errFaultyDisk
|
||||
}
|
||||
@@ -1230,7 +1225,7 @@ func (s *posix) CreateFile(volume, path string, fileSize int64, r io.Reader) (er
|
||||
bufp := s.pool.Get().(*[]byte)
|
||||
defer s.pool.Put(bufp)
|
||||
|
||||
written, err := xioutil.CopyAligned(w, r, *bufp)
|
||||
written, err := xioutil.CopyAligned(w, r, *bufp, fileSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1546,7 +1541,7 @@ func (s *posix) RenameFile(srcVolume, srcPath, dstVolume, dstPath string) (err e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *posix) VerifyFile(volume, path string, empty bool, algo BitrotAlgorithm, sum []byte, shardSize int64) (err error) {
|
||||
func (s *posix) VerifyFile(volume, path string, fileSize int64, algo BitrotAlgorithm, sum []byte, shardSize int64) (err error) {
|
||||
defer func() {
|
||||
if err == errFaultyDisk {
|
||||
atomic.AddInt32(&s.ioErrCount, 1)
|
||||
@@ -1622,7 +1617,9 @@ func (s *posix) VerifyFile(volume, path string, empty bool, algo BitrotAlgorithm
|
||||
return err
|
||||
}
|
||||
|
||||
if empty && fi.Size() != 0 || !empty && fi.Size() == 0 {
|
||||
// Calculate the size of the bitrot file and compare
|
||||
// it with the actual file size.
|
||||
if fi.Size() != bitrotShardFileSize(fileSize, shardSize, algo) {
|
||||
return errFileUnexpectedSize
|
||||
}
|
||||
|
||||
|
||||
+14
-4
@@ -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, false, algo, hashBytes, 0); err != nil {
|
||||
if err := posixStorage.VerifyFile(volName, fileName, size, algo, hashBytes, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1805,7 +1805,14 @@ func TestPosixVerifyFile(t *testing.T) {
|
||||
if err := posixStorage.AppendFile(volName, fileName, []byte("a")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := posixStorage.VerifyFile(volName, fileName, false, algo, hashBytes, 0); err == nil {
|
||||
|
||||
// Check if VerifyFile reports the incorrect file length (the correct length is `size+1`)
|
||||
if err := posixStorage.VerifyFile(volName, fileName, size, algo, hashBytes, 0); err == nil {
|
||||
t.Fatal("expected to fail bitrot check")
|
||||
}
|
||||
|
||||
// Check if bitrot fails
|
||||
if err := posixStorage.VerifyFile(volName, fileName, size+1, algo, hashBytes, 0); err == nil {
|
||||
t.Fatal("expected to fail bitrot check")
|
||||
}
|
||||
|
||||
@@ -1833,7 +1840,7 @@ func TestPosixVerifyFile(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Close()
|
||||
if err := posixStorage.VerifyFile(volName, fileName, false, algo, nil, shardSize); err != nil {
|
||||
if err := posixStorage.VerifyFile(volName, fileName, size, algo, nil, shardSize); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1847,7 +1854,10 @@ func TestPosixVerifyFile(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
if err := posixStorage.VerifyFile(volName, fileName, false, algo, nil, shardSize); err == nil {
|
||||
if err := posixStorage.VerifyFile(volName, fileName, size, algo, nil, shardSize); err == nil {
|
||||
t.Fatal("expected to fail bitrot check")
|
||||
}
|
||||
if err := posixStorage.VerifyFile(volName, fileName, size+1, algo, nil, shardSize); err == nil {
|
||||
t.Fatal("expected to fail bitrot check")
|
||||
}
|
||||
}
|
||||
|
||||
+6
-15
@@ -120,14 +120,7 @@ EXAMPLES:
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}miniostorage
|
||||
{{.Prompt}} {{.HelpName}} http://node{1...32}.example.com/mnt/export/{1...32}
|
||||
|
||||
6. Start minio server with edge caching enabled.
|
||||
{{.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}} /home/shared
|
||||
|
||||
7. Start minio server with KMS enabled.
|
||||
6. Start minio server with KMS enabled.
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SSE_VAULT_APPROLE_ID{{.AssignmentOperator}}9b56cc08-8258-45d5-24a3-679876769126
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SSE_VAULT_APPROLE_SECRET{{.AssignmentOperator}}4e30c52f-13e4-a6f5-0763-d50e8cb4321f
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SSE_VAULT_ENDPOINT{{.AssignmentOperator}}https://vault-endpoint-ip:8200
|
||||
@@ -289,6 +282,8 @@ func serverMain(ctx *cli.Context) {
|
||||
globalSweepHealState = initHealState()
|
||||
}
|
||||
|
||||
// initialize globalConsoleSys system
|
||||
globalConsoleSys = NewConsoleLogger(context.Background(), globalEndpoints)
|
||||
// Configure server.
|
||||
var handler http.Handler
|
||||
handler, err = configureServerHandler(globalEndpoints)
|
||||
@@ -337,13 +332,6 @@ func serverMain(ctx *cli.Context) {
|
||||
// Load logger subsystem
|
||||
loadLoggers()
|
||||
|
||||
var cacheConfig = globalServerConfig.GetCacheConfig()
|
||||
if len(cacheConfig.Drives) > 0 {
|
||||
// initialize the new disk cache objects.
|
||||
globalCacheObjectAPI, err = newServerCacheObjects(context.Background(), cacheConfig)
|
||||
logger.FatalIf(err, "Unable to initialize disk caching")
|
||||
}
|
||||
|
||||
// Create new IAM system.
|
||||
globalIAMSys = NewIAMSys()
|
||||
if err = globalIAMSys.Init(newObject); err != nil {
|
||||
@@ -358,6 +346,9 @@ func serverMain(ctx *cli.Context) {
|
||||
logger.Fatal(err, "Unable to initialize policy system")
|
||||
}
|
||||
|
||||
if globalIsDiskCacheEnabled {
|
||||
logger.StartupMessage(colorRed(colorBold("Disk caching is allowed only for gateway deployments")))
|
||||
}
|
||||
// Create new lifecycle system.
|
||||
globalLifecycleSys = NewLifecycleSys()
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@ func runAllTests(suite *TestSuiteCommon, c *check) {
|
||||
suite.TestObjectGetAnonymous(c)
|
||||
suite.TestObjectGet(c)
|
||||
suite.TestMultipleObjects(c)
|
||||
suite.TestNotImplemented(c)
|
||||
suite.TestHeader(c)
|
||||
suite.TestPutBucket(c)
|
||||
suite.TestCopyObject(c)
|
||||
@@ -996,20 +995,6 @@ func (s *TestSuiteCommon) TestMultipleObjects(c *check) {
|
||||
c.Assert(true, bytes.Equal(responseBody, []byte("hello three")))
|
||||
}
|
||||
|
||||
// TestNotImplemented - validates if object policy is implemented, should return 'NotImplemented'.
|
||||
func (s *TestSuiteCommon) TestNotImplemented(c *check) {
|
||||
// Generate a random bucket name.
|
||||
bucketName := getRandomBucketName()
|
||||
request, err := newTestSignedRequest("GET", s.endPoint+SlashSeparator+bucketName+"/object?policy",
|
||||
0, nil, s.accessKey, s.secretKey, s.signer)
|
||||
c.Assert(err, nil)
|
||||
|
||||
client := http.Client{Transport: s.transport}
|
||||
response, err := client.Do(request)
|
||||
c.Assert(err, nil)
|
||||
c.Assert(response.StatusCode, http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// TestHeader - Validates the error response for an attempt to fetch non-existent object.
|
||||
func (s *TestSuiteCommon) TestHeader(c *check) {
|
||||
// generate a random bucket name.
|
||||
|
||||
+3
-4
@@ -23,12 +23,11 @@ import (
|
||||
)
|
||||
|
||||
// Type of service signals currently supported.
|
||||
type serviceSignal string
|
||||
type serviceSignal int
|
||||
|
||||
const (
|
||||
serviceStatus serviceSignal = "serviceStatus" // Gets status about the service.
|
||||
serviceRestart = "serviceRestart" // Restarts the service.
|
||||
serviceStop = "serviceStop" // Stops the server.
|
||||
serviceRestart serviceSignal = iota // Restarts the server.
|
||||
serviceStop // Stops the server.
|
||||
// Add new service requests here.
|
||||
)
|
||||
|
||||
|
||||
@@ -78,8 +78,6 @@ func handleSignals() {
|
||||
exit(stopProcess())
|
||||
case signal := <-globalServiceSignalCh:
|
||||
switch signal {
|
||||
case serviceStatus:
|
||||
// Ignore this at the moment.
|
||||
case serviceRestart:
|
||||
logger.Info("Restarting on service signal")
|
||||
stop := stopProcess()
|
||||
|
||||
@@ -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, empty bool, algo BitrotAlgorithm, sum []byte, shardSize int64) error
|
||||
VerifyFile(volume, path string, size int64, 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)
|
||||
|
||||
@@ -45,7 +45,7 @@ func isNetworkError(err error) bool {
|
||||
return true
|
||||
}
|
||||
if nerr, ok := err.(*rest.NetworkError); ok {
|
||||
return isNetworkOrHostDown(nerr.Err)
|
||||
return xnet.IsNetworkOrHostDown(nerr.Err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -439,16 +439,15 @@ func (client *storageRESTClient) getInstanceID() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *storageRESTClient) VerifyFile(volume, path string, empty bool, algo BitrotAlgorithm, sum []byte, shardSize int64) error {
|
||||
func (client *storageRESTClient) VerifyFile(volume, path string, size int64, 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))
|
||||
}
|
||||
values.Set(storageRESTLength, strconv.FormatInt(size, 10))
|
||||
values.Set(storageRESTShardSize, strconv.Itoa(int(shardSize)))
|
||||
values.Set(storageRESTBitrotHash, hex.EncodeToString(sum))
|
||||
|
||||
respBody, err := client.call(storageRESTMethodVerifyFile, values, nil, -1)
|
||||
defer http.DrainBody(respBody)
|
||||
if err != nil {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package cmd
|
||||
|
||||
const storageRESTVersion = "v8"
|
||||
const storageRESTVersion = "v9"
|
||||
const storageRESTPath = minioReservedBucketPath + "/storage/" + storageRESTVersion + SlashSeparator
|
||||
|
||||
const (
|
||||
@@ -52,7 +52,7 @@ const (
|
||||
storageRESTDstPath = "destination-path"
|
||||
storageRESTOffset = "offset"
|
||||
storageRESTLength = "length"
|
||||
storageRESTEmpty = "empty"
|
||||
storageRESTShardSize = "shard-size"
|
||||
storageRESTCount = "count"
|
||||
storageRESTMarkerPath = "marker"
|
||||
storageRESTLeafFile = "leaf-file"
|
||||
|
||||
@@ -520,12 +520,12 @@ 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])
|
||||
size, err := strconv.ParseInt(vars[storageRESTLength], 10, 0)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
shardSize, err := strconv.Atoi(vars[storageRESTLength])
|
||||
shardSize, err := strconv.Atoi(vars[storageRESTShardSize])
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
@@ -547,7 +547,7 @@ func (s *storageRESTServer) VerifyFile(w http.ResponseWriter, r *http.Request) {
|
||||
algo := BitrotAlgorithmFromString(algoStr)
|
||||
w.Header().Set(xhttp.ContentType, "text/event-stream")
|
||||
doneCh := sendWhiteSpaceVerifyFile(w)
|
||||
err = s.storage.VerifyFile(volume, filePath, empty, algo, hash, int64(shardSize))
|
||||
err = s.storage.VerifyFile(volume, filePath, size, algo, hash, int64(shardSize))
|
||||
<-doneCh
|
||||
gob.NewEncoder(w).Encode(VerifyFileResp{err})
|
||||
}
|
||||
@@ -600,7 +600,7 @@ func registerStorageRESTHandlers(router *mux.Router, endpoints EndpointList) {
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + storageRESTMethodRenameFile).HandlerFunc(httpTraceHdrs(server.RenameFileHandler)).
|
||||
Queries(restQueries(storageRESTSrcVolume, storageRESTSrcPath, storageRESTDstVolume, storageRESTDstPath)...)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + storageRESTMethodVerifyFile).HandlerFunc(httpTraceHdrs(server.VerifyFile)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath, storageRESTBitrotAlgo, storageRESTLength)...)
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath, storageRESTBitrotAlgo, storageRESTBitrotHash, storageRESTLength, storageRESTShardSize)...)
|
||||
subrouter.Methods(http.MethodPost).Path(SlashSeparator + storageRESTMethodGetInstanceID).HandlerFunc(httpTraceAll(server.GetInstanceID))
|
||||
}
|
||||
|
||||
|
||||
@@ -174,3 +174,19 @@ type ClientGrantsResult struct {
|
||||
// provider as the token's sub (Subject) claim.
|
||||
SubjectFromToken string `xml:",omitempty"`
|
||||
}
|
||||
|
||||
// AssumeRoleWithLDAPResponse contains the result of successful
|
||||
// AssumeRoleWithLDAPIdentity request
|
||||
type AssumeRoleWithLDAPResponse struct {
|
||||
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithClientGrantsResponse" json:"-"`
|
||||
Result LDAPIdentityResult `xml:"AssumeRoleWithLDAPIdentity"`
|
||||
ResponseMetadata struct {
|
||||
RequestID string `xml:"RequestId,omitempty"`
|
||||
} `xml:"ResponseMetadata,omitempty"`
|
||||
}
|
||||
|
||||
// LDAPIdentityResult - contains credentials for a successful
|
||||
// AssumeRoleWithLDAPIdentity request.
|
||||
type LDAPIdentityResult struct {
|
||||
Credentials auth.Credentials `xml:",omitempty"`
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/iam/validator"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
ldap "gopkg.in/ldap.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,9 +40,14 @@ const (
|
||||
// STS API action constants
|
||||
clientGrants = "AssumeRoleWithClientGrants"
|
||||
webIdentity = "AssumeRoleWithWebIdentity"
|
||||
ldapIdentity = "AssumeRoleWithLDAPIdentity"
|
||||
assumeRole = "AssumeRole"
|
||||
|
||||
stsRequestBodyLimit = 10 * (1 << 20) // 10 MiB
|
||||
|
||||
// LDAP claim keys
|
||||
ldapUser = "ldapUser"
|
||||
ldapGroups = "ldapGroups"
|
||||
)
|
||||
|
||||
// stsAPIHandlers implements and provides http handlers for AWS STS API.
|
||||
@@ -82,6 +88,12 @@ func registerSTSRouter(router *mux.Router) {
|
||||
Queries("Version", stsAPIVersion).
|
||||
Queries("WebIdentityToken", "{Token:.*}")
|
||||
|
||||
// AssumeRoleWithLDAPIdentity
|
||||
stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithLDAPIdentity)).
|
||||
Queries("Action", ldapIdentity).
|
||||
Queries("Version", stsAPIVersion).
|
||||
Queries("LDAPUsername", "{LDAPUsername:.*}").
|
||||
Queries("LDAPPassword", "{LDAPPassword:.*}")
|
||||
}
|
||||
|
||||
func checkAssumeRoleAuth(ctx context.Context, r *http.Request) (user auth.Credentials, stsErr STSErrorCode) {
|
||||
@@ -411,3 +423,138 @@ func (sts *stsAPIHandlers) AssumeRoleWithWebIdentity(w http.ResponseWriter, r *h
|
||||
func (sts *stsAPIHandlers) AssumeRoleWithClientGrants(w http.ResponseWriter, r *http.Request) {
|
||||
sts.AssumeRoleWithJWT(w, r)
|
||||
}
|
||||
|
||||
// AssumeRoleWithLDAPIdentity - implements user auth against LDAP server
|
||||
func (sts *stsAPIHandlers) AssumeRoleWithLDAPIdentity(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AssumeRoleWithLDAPIdentity")
|
||||
|
||||
// Parse the incoming form data.
|
||||
if err := r.ParseForm(); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
|
||||
return
|
||||
}
|
||||
|
||||
if r.Form.Get("Version") != stsAPIVersion {
|
||||
logger.LogIf(ctx, fmt.Errorf("Invalid STS API version %s, expecting %s", r.Form.Get("Version"), stsAPIVersion))
|
||||
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSMissingParameter))
|
||||
return
|
||||
}
|
||||
|
||||
action := r.Form.Get("Action")
|
||||
switch action {
|
||||
case ldapIdentity:
|
||||
default:
|
||||
logger.LogIf(ctx, fmt.Errorf("Unsupported action %s", action))
|
||||
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
|
||||
return
|
||||
}
|
||||
|
||||
ctx = newContext(r, w, action)
|
||||
defer logger.AuditLog(w, r, action, nil)
|
||||
|
||||
ldapUsername := r.Form.Get("LDAPUsername")
|
||||
ldapPassword := r.Form.Get("LDAPPassword")
|
||||
|
||||
if ldapUsername == "" || ldapPassword == "" {
|
||||
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSMissingParameter))
|
||||
return
|
||||
}
|
||||
|
||||
ldapConn, err := globalServerConfig.LDAPServerConfig.Connect()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("LDAP server connection failure: %v", err))
|
||||
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
|
||||
return
|
||||
}
|
||||
if ldapConn == nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("LDAP server not configured: %v", err))
|
||||
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
|
||||
return
|
||||
}
|
||||
|
||||
usernameSubs := newSubstituter("username", ldapUsername)
|
||||
// We ignore error below as we already validated the username
|
||||
// format string at startup.
|
||||
usernameDN, _ := usernameSubs.substitute(globalServerConfig.LDAPServerConfig.UsernameFormat)
|
||||
// Bind with user credentials to validate the password
|
||||
err = ldapConn.Bind(usernameDN, ldapPassword)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("LDAP authentication failure: %v", err))
|
||||
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
|
||||
return
|
||||
}
|
||||
|
||||
groups := []string{}
|
||||
if globalServerConfig.LDAPServerConfig.GroupSearchFilter != "" {
|
||||
// Verified user credentials. Now we find the groups they are
|
||||
// a member of.
|
||||
searchSubs := newSubstituter(
|
||||
"username", ldapUsername,
|
||||
"usernamedn", usernameDN,
|
||||
)
|
||||
// We ignore error below as we already validated the search string
|
||||
// at startup.
|
||||
groupSearchFilter, _ := searchSubs.substitute(globalServerConfig.LDAPServerConfig.GroupSearchFilter)
|
||||
baseDN, _ := searchSubs.substitute(globalServerConfig.LDAPServerConfig.GroupSearchBaseDN)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
baseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
groupSearchFilter,
|
||||
[]string{globalServerConfig.LDAPServerConfig.GroupNameAttribute},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := ldapConn.Search(searchRequest)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("LDAP search failure: %v", err))
|
||||
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInvalidParameterValue))
|
||||
return
|
||||
}
|
||||
for _, entry := range sr.Entries {
|
||||
// We only queried one attribute, so we only look up
|
||||
// the first one.
|
||||
groups = append(groups, entry.Attributes[0].Values...)
|
||||
}
|
||||
}
|
||||
expiryDur := globalServerConfig.LDAPServerConfig.stsExpiryDuration
|
||||
m := map[string]interface{}{
|
||||
"exp": UTCNow().Add(expiryDur).Unix(),
|
||||
"ldapUser": ldapUsername,
|
||||
"ldapGroups": groups,
|
||||
}
|
||||
|
||||
secret := globalServerConfig.GetCredential().SecretKey
|
||||
cred, err := auth.GetNewCredentialsWithMetadata(m, secret)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInternalError))
|
||||
return
|
||||
}
|
||||
|
||||
policyName := ""
|
||||
// Set the newly generated credentials.
|
||||
if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, stsErrCodes.ToSTSErr(ErrSTSInternalError))
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other MinIO peers to reload temp users
|
||||
for _, nerr := range globalNotificationSys.LoadUser(cred.AccessKey, true) {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
|
||||
ldapIdentityResponse := &AssumeRoleWithLDAPResponse{
|
||||
Result: LDAPIdentityResult{
|
||||
Credentials: cred,
|
||||
},
|
||||
}
|
||||
ldapIdentityResponse.ResponseMetadata.RequestID = w.Header().Get(xhttp.AmzRequestID)
|
||||
encodedSuccessResponse := encodeResponse(ldapIdentityResponse)
|
||||
|
||||
writeSuccessResponseXML(w, encodedSuccessResponse)
|
||||
}
|
||||
|
||||
@@ -1932,18 +1932,18 @@ func ExecObjectLayerAPITest(t *testing.T, objAPITest objAPITestType, endpoints [
|
||||
t.Fatalf("Initialization of API handler tests failed: <ERROR> %s", err)
|
||||
}
|
||||
|
||||
globalIAMSys = NewIAMSys()
|
||||
globalIAMSys.Init(objLayer)
|
||||
|
||||
globalPolicySys = NewPolicySys()
|
||||
globalPolicySys.Init(objLayer)
|
||||
|
||||
// initialize the server and obtain the credentials and root.
|
||||
// credentials are necessary to sign the HTTP request.
|
||||
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||
t.Fatalf("Unable to initialize server config. %s", err)
|
||||
}
|
||||
|
||||
globalIAMSys = NewIAMSys()
|
||||
globalIAMSys.Init(objLayer)
|
||||
|
||||
globalPolicySys = NewPolicySys()
|
||||
globalPolicySys.Init(objLayer)
|
||||
|
||||
credentials := globalServerConfig.GetCredential()
|
||||
|
||||
// Executing the object layer tests for single node setup.
|
||||
|
||||
@@ -90,5 +90,8 @@ var errGroupNotEmpty = errors.New("Specified group is not empty - cannot remove
|
||||
// error returned in IAM subsystem when policy doesn't exist.
|
||||
var errNoSuchPolicy = errors.New("Specified canned policy does not exist")
|
||||
|
||||
// error returned in IAM subsystem when an external users systems is configured.
|
||||
var errIAMActionNotAllowed = errors.New("Specified IAM action is not allowed under the current configuration")
|
||||
|
||||
// error returned when access is denied.
|
||||
var errAccessDenied = errors.New("Do not have enough permissions to access this resource")
|
||||
|
||||
+9
-21
@@ -17,9 +17,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
@@ -80,27 +80,15 @@ func errorToUIErr(err error) uiErr {
|
||||
}
|
||||
|
||||
// Show a generic message for known golang errors
|
||||
switch e := err.(type) {
|
||||
case *net.OpError:
|
||||
if e.Op == "listen" {
|
||||
if oe, ok := e.Err.(*os.SyscallError); ok {
|
||||
if oe.Err == syscall.EADDRINUSE {
|
||||
return uiErrPortAlreadyInUse(e).Msg("Specified port '" + e.Addr.String() + "' is already in use")
|
||||
} else if oe.Err == syscall.EACCES {
|
||||
return uiErrPortAccess(e).Msg("Insufficient permissions to use specified port '" + e.Addr.String() + "'")
|
||||
}
|
||||
}
|
||||
}
|
||||
case *os.PathError:
|
||||
if os.IsPermission(e) {
|
||||
return uiErrNoPermissionsToAccessDirFiles(e).Msg("Insufficient permissions to access path, `" + e.Path + "`")
|
||||
}
|
||||
}
|
||||
|
||||
switch err {
|
||||
case io.ErrUnexpectedEOF:
|
||||
if errors.Is(err, syscall.EADDRINUSE) {
|
||||
return uiErrPortAlreadyInUse(err).Msg("Specified port is already in use")
|
||||
} else if errors.Is(err, syscall.EACCES) {
|
||||
return uiErrPortAccess(err).Msg("Insufficient permissions to use specified port")
|
||||
} else if os.IsPermission(err) {
|
||||
return uiErrNoPermissionsToAccessDirFiles(err).Msg("Insufficient permissions to access path")
|
||||
} else if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return uiErrUnexpectedDataContent(err)
|
||||
default:
|
||||
} else {
|
||||
// Failed to identify what type of error this, return a simple UI error
|
||||
return uiErr{msg: err.Error()}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,12 @@ var (
|
||||
"MINIO_CACHE_MAXUSE: Valid cache max-use value between 0-100",
|
||||
)
|
||||
|
||||
uiErrInvalidCacheEncryptionKey = newUIErrFn(
|
||||
"Invalid cache encryption master key value",
|
||||
"Please check the passed value",
|
||||
"MINIO_CACHE_ENCRYPTION_MASTER_KEY: For more information, please refer to https://docs.min.io/docs/minio-disk-cache-guide",
|
||||
)
|
||||
|
||||
uiErrInvalidCredentials = newUIErrFn(
|
||||
"Invalid credentials",
|
||||
"Please provide correct credentials",
|
||||
|
||||
+136
-133
@@ -21,8 +21,10 @@ import (
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -30,45 +32,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/inconshreveable/go-update"
|
||||
"github.com/minio/cli"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
_ "github.com/minio/sha256-simd" // Needed for sha256 hash verifier.
|
||||
)
|
||||
|
||||
// Check for new software updates.
|
||||
var updateCmd = cli.Command{
|
||||
Name: "update",
|
||||
Usage: "update minio to latest release",
|
||||
Action: mainUpdate,
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Usage: "disable any update prompt message",
|
||||
},
|
||||
},
|
||||
CustomHelpTemplate: `Name:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.HelpName}}{{if .VisibleFlags}} [FLAGS]{{end}}
|
||||
{{if .VisibleFlags}}
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
EXIT STATUS:
|
||||
0 - You are already running the most recent version.
|
||||
1 - New update was applied successfully.
|
||||
-1 - Error in getting update information.
|
||||
|
||||
EXAMPLES:
|
||||
1. Check and update minio:
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
`,
|
||||
}
|
||||
|
||||
const (
|
||||
minioReleaseTagTimeLayout = "2006-01-02T15-04-05Z"
|
||||
minioOSARCH = runtime.GOOS + "-" + runtime.GOARCH
|
||||
@@ -109,11 +79,14 @@ func releaseTimeToReleaseTag(releaseTime time.Time) string {
|
||||
|
||||
// releaseTagToReleaseTime - reverse of `releaseTimeToReleaseTag()`
|
||||
func releaseTagToReleaseTime(releaseTag string) (releaseTime time.Time, err error) {
|
||||
tagTimePart := strings.TrimPrefix(releaseTag, "RELEASE.")
|
||||
if tagTimePart == releaseTag {
|
||||
fields := strings.Split(releaseTag, ".")
|
||||
if len(fields) < 2 || len(fields) > 3 {
|
||||
return releaseTime, fmt.Errorf("%s is not a valid release tag", releaseTag)
|
||||
}
|
||||
return time.Parse(minioReleaseTagTimeLayout, tagTimePart)
|
||||
if fields[0] != "RELEASE" {
|
||||
return releaseTime, fmt.Errorf("%s is not a valid release tag", releaseTag)
|
||||
}
|
||||
return time.Parse(minioReleaseTagTimeLayout, fields[1])
|
||||
}
|
||||
|
||||
// getModTime - get the file modification time of `path`
|
||||
@@ -298,38 +271,58 @@ func getUserAgent(mode string) string {
|
||||
}
|
||||
|
||||
func downloadReleaseURL(releaseChecksumURL string, timeout time.Duration, mode string) (content string, err error) {
|
||||
req, err := http.NewRequest("GET", releaseChecksumURL, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, releaseChecksumURL, nil)
|
||||
if err != nil {
|
||||
return content, err
|
||||
return content, AdminError{
|
||||
Code: AdminUpdateUnexpectedFailure,
|
||||
Message: err.Error(),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
req.Header.Set("User-Agent", getUserAgent(mode))
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
// need to close connection after usage.
|
||||
DisableKeepAlives: true,
|
||||
},
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: getUpdateTransport(timeout)}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return content, err
|
||||
if xnet.IsNetworkOrHostDown(err) {
|
||||
return content, AdminError{
|
||||
Code: AdminUpdateURLNotReachable,
|
||||
Message: err.Error(),
|
||||
StatusCode: http.StatusServiceUnavailable,
|
||||
}
|
||||
}
|
||||
return content, AdminError{
|
||||
Code: AdminUpdateUnexpectedFailure,
|
||||
Message: err.Error(),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
if resp == nil {
|
||||
return content, fmt.Errorf("No response from server to download URL %s", releaseChecksumURL)
|
||||
return content, AdminError{
|
||||
Code: AdminUpdateUnexpectedFailure,
|
||||
Message: fmt.Sprintf("No response from server to download URL %s", releaseChecksumURL),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
defer xhttp.DrainBody(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return content, fmt.Errorf("Error downloading URL %s. Response: %v", releaseChecksumURL, resp.Status)
|
||||
return content, AdminError{
|
||||
Code: AdminUpdateUnexpectedFailure,
|
||||
Message: fmt.Sprintf("Error downloading URL %s. Response: %v", releaseChecksumURL, resp.Status),
|
||||
StatusCode: resp.StatusCode,
|
||||
}
|
||||
}
|
||||
contentBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return content, fmt.Errorf("Error reading response. %s", err)
|
||||
return content, AdminError{
|
||||
Code: AdminUpdateUnexpectedFailure,
|
||||
Message: fmt.Sprintf("Error reading response. %s", err),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
|
||||
return string(contentBytes), err
|
||||
return string(contentBytes), nil
|
||||
}
|
||||
|
||||
// DownloadReleaseData - downloads release data from minio official server.
|
||||
@@ -338,6 +331,7 @@ func DownloadReleaseData(timeout time.Duration, mode string) (data string, err e
|
||||
if runtime.GOOS == globalWindowsOSName {
|
||||
releaseURLs = minioReleaseWindowsInfoURLs
|
||||
}
|
||||
|
||||
return func() (data string, err error) {
|
||||
for _, url := range releaseURLs {
|
||||
data, err = downloadReleaseURL(url, timeout, mode)
|
||||
@@ -345,7 +339,7 @@ func DownloadReleaseData(timeout time.Duration, mode string) (data string, err e
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
return data, fmt.Errorf("Failed to fetch release URL - last error: %s", err)
|
||||
return data, err
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -354,10 +348,20 @@ func DownloadReleaseData(timeout time.Duration, mode string) (data string, err e
|
||||
//
|
||||
// The expected format is a single line with two words like:
|
||||
//
|
||||
// fbe246edbd382902db9a4035df7dce8cb441357d minio.RELEASE.2016-10-07T01-16-39Z
|
||||
// fbe246edbd382902db9a4035df7dce8cb441357d minio.RELEASE.2016-10-07T01-16-39Z.<hotfix_optional>
|
||||
//
|
||||
// The second word must be `minio.` appended to a standard release tag.
|
||||
func parseReleaseData(data string) (sha256Hex string, releaseTime time.Time, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = AdminError{
|
||||
Code: AdminUpdateUnexpectedFailure,
|
||||
Message: err.Error(),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
fields := strings.Fields(data)
|
||||
if len(fields) != 2 {
|
||||
err = fmt.Errorf("Unknown release data `%s`", data)
|
||||
@@ -367,17 +371,18 @@ func parseReleaseData(data string) (sha256Hex string, releaseTime time.Time, err
|
||||
sha256Hex = fields[0]
|
||||
releaseInfo := fields[1]
|
||||
|
||||
fields = strings.SplitN(releaseInfo, ".", 2)
|
||||
if len(fields) != 2 {
|
||||
// Split release of style minio.RELEASE.2019-08-21T19-40-07Z.<hotfix>
|
||||
nfields := strings.SplitN(releaseInfo, ".", 2)
|
||||
if len(nfields) != 2 {
|
||||
err = fmt.Errorf("Unknown release information `%s`", releaseInfo)
|
||||
return sha256Hex, releaseTime, err
|
||||
}
|
||||
if fields[0] != "minio" {
|
||||
if nfields[0] != "minio" {
|
||||
err = fmt.Errorf("Unknown release `%s`", releaseInfo)
|
||||
return sha256Hex, releaseTime, err
|
||||
}
|
||||
|
||||
releaseTime, err = releaseTagToReleaseTime(fields[1])
|
||||
releaseTime, err = releaseTagToReleaseTime(nfields[1])
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Unknown release tag format. %s", err)
|
||||
}
|
||||
@@ -385,6 +390,27 @@ func parseReleaseData(data string) (sha256Hex string, releaseTime time.Time, err
|
||||
return sha256Hex, releaseTime, err
|
||||
}
|
||||
|
||||
const updateTimeout = 10 * time.Second
|
||||
|
||||
func getUpdateTransport(timeout time.Duration) http.RoundTripper {
|
||||
var updateTransport http.RoundTripper = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: timeout,
|
||||
KeepAlive: timeout,
|
||||
DualStack: true,
|
||||
}
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
},
|
||||
IdleConnTimeout: timeout,
|
||||
TLSHandshakeTimeout: timeout,
|
||||
ExpectContinueTimeout: timeout,
|
||||
DisableCompression: true,
|
||||
}
|
||||
return updateTransport
|
||||
}
|
||||
|
||||
func getLatestReleaseTime(timeout time.Duration, mode string) (sha256Hex string, releaseTime time.Time, err error) {
|
||||
data, err := DownloadReleaseData(timeout, mode)
|
||||
if err != nil {
|
||||
@@ -450,21 +476,43 @@ func getUpdateInfo(timeout time.Duration, mode string) (updateMsg string, sha256
|
||||
return prepareUpdateMessage(downloadURL, older), sha256Hex, currentReleaseTime, latestReleaseTime, nil
|
||||
}
|
||||
|
||||
func doUpdate(sha256Hex string, latestReleaseTime time.Time, ok bool) (updateStatusMsg string, err error) {
|
||||
if !ok {
|
||||
updateStatusMsg = colorRedBold("MinIO update to version RELEASE.%s canceled.",
|
||||
latestReleaseTime.Format(minioReleaseTagTimeLayout))
|
||||
return updateStatusMsg, nil
|
||||
}
|
||||
func doUpdate(updateURL, sha256Hex, mode string) (err error) {
|
||||
var sha256Sum []byte
|
||||
sha256Sum, err = hex.DecodeString(sha256Hex)
|
||||
if err != nil {
|
||||
return updateStatusMsg, err
|
||||
return AdminError{
|
||||
Code: AdminUpdateUnexpectedFailure,
|
||||
Message: err.Error(),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.Get(getDownloadURL(releaseTimeToReleaseTag(latestReleaseTime)))
|
||||
clnt := &http.Client{Transport: getUpdateTransport(30 * time.Second)}
|
||||
req, err := http.NewRequest(http.MethodGet, updateURL, nil)
|
||||
if err != nil {
|
||||
return updateStatusMsg, err
|
||||
return AdminError{
|
||||
Code: AdminUpdateUnexpectedFailure,
|
||||
Message: err.Error(),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", getUserAgent(mode))
|
||||
|
||||
resp, err := clnt.Do(req)
|
||||
if err != nil {
|
||||
if xnet.IsNetworkOrHostDown(err) {
|
||||
return AdminError{
|
||||
Code: AdminUpdateURLNotReachable,
|
||||
Message: err.Error(),
|
||||
StatusCode: http.StatusServiceUnavailable,
|
||||
}
|
||||
}
|
||||
return AdminError{
|
||||
Code: AdminUpdateUnexpectedFailure,
|
||||
Message: err.Error(),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
defer xhttp.DrainBody(resp.Body)
|
||||
|
||||
@@ -475,73 +523,28 @@ func doUpdate(sha256Hex string, latestReleaseTime time.Time, ok bool) (updateSta
|
||||
Checksum: sha256Sum,
|
||||
},
|
||||
); err != nil {
|
||||
return updateStatusMsg, err
|
||||
}
|
||||
|
||||
return colorGreenBold("MinIO updated to version RELEASE.%s successfully.",
|
||||
latestReleaseTime.Format(minioReleaseTagTimeLayout)), nil
|
||||
}
|
||||
|
||||
// Confirm continues prompting until the input is boolean-ish.
|
||||
func confirm(prompt string, args ...interface{}) bool {
|
||||
for {
|
||||
var s string
|
||||
fmt.Fprintf(color.Output, prompt+": ", args...)
|
||||
fmt.Scanln(&s)
|
||||
switch s {
|
||||
case "Yes", "yes", "y", "Y":
|
||||
return true
|
||||
case "No", "no", "n", "N":
|
||||
return false
|
||||
if rerr := update.RollbackError(err); rerr != nil {
|
||||
return AdminError{
|
||||
Code: AdminUpdateApplyFailure,
|
||||
Message: fmt.Sprintf("Failed to rollback from bad update: %v", rerr),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
return AdminError{
|
||||
Code: AdminUpdateApplyFailure,
|
||||
Message: fmt.Sprintf("Unable to update the binary at %s: %v",
|
||||
filepath.Dir(pathErr.Path), pathErr.Err),
|
||||
StatusCode: http.StatusForbidden,
|
||||
}
|
||||
}
|
||||
return AdminError{
|
||||
Code: AdminUpdateApplyFailure,
|
||||
Message: err.Error(),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func shouldUpdate(quiet bool, sha256Hex string, latestReleaseTime time.Time) (ok bool) {
|
||||
ok = true
|
||||
if !quiet {
|
||||
ok = confirm(colorGreenBold("Update to RELEASE.%s ? [%s]", latestReleaseTime.Format(minioReleaseTagTimeLayout), "y/n"))
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func mainUpdate(ctx *cli.Context) {
|
||||
if len(ctx.Args()) != 0 {
|
||||
cli.ShowCommandHelpAndExit(ctx, "update", -1)
|
||||
}
|
||||
|
||||
handleCommonEnvVars()
|
||||
|
||||
quiet := ctx.Bool("quiet") || ctx.GlobalBool("quiet")
|
||||
if quiet {
|
||||
logger.EnableQuiet()
|
||||
}
|
||||
|
||||
minioMode := ""
|
||||
updateMsg, sha256Hex, _, latestReleaseTime, err := getUpdateInfo(10*time.Second, minioMode)
|
||||
if err != nil {
|
||||
logger.Info(err.Error())
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
// Nothing to update running the latest release.
|
||||
if updateMsg == "" {
|
||||
logger.Info(colorGreenBold("You are already running the most recent version of ‘minio’."))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
logger.Info(updateMsg)
|
||||
// if the in-place update is disabled then we shouldn't ask the
|
||||
// user to update the binaries.
|
||||
if strings.Contains(updateMsg, minioReleaseURL) && !globalInplaceUpdateDisabled {
|
||||
var updateStatusMsg string
|
||||
updateStatusMsg, err = doUpdate(sha256Hex, latestReleaseTime, shouldUpdate(quiet, sha256Hex, latestReleaseTime))
|
||||
if err != nil {
|
||||
logger.Info(colorRedBold("Unable to update ‘minio’."))
|
||||
logger.Info(err.Error())
|
||||
os.Exit(-1)
|
||||
}
|
||||
logger.Info(updateStatusMsg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -285,6 +285,7 @@ func TestParseReleaseData(t *testing.T) {
|
||||
{"more minio.RELEASE.fields", time.Time{}, "", fmt.Errorf(`Unknown release tag format. parsing time "fields" as "2006-01-02T15-04-05Z": cannot parse "fields" as "2006"`)},
|
||||
{"more minio.RELEASE.2016-10-07T01-16-39Z", releaseTime, "more", nil},
|
||||
{"fbe246edbd382902db9a4035df7dce8cb441357d minio.RELEASE.2016-10-07T01-16-39Z\n", releaseTime, "fbe246edbd382902db9a4035df7dce8cb441357d", nil},
|
||||
{"fbe246edbd382902db9a4035df7dce8cb441357d minio.RELEASE.2016-10-07T01-16-39Z.customer-hotfix\n", releaseTime, "fbe246edbd382902db9a4035df7dce8cb441357d", nil},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
+2
-36
@@ -52,25 +52,13 @@ func IsErrIgnored(err error, ignoredErrs ...error) bool {
|
||||
// IsErr returns whether given error is exact error.
|
||||
func IsErr(err error, errs ...error) bool {
|
||||
for _, exactErr := range errs {
|
||||
if err == exactErr {
|
||||
if errors.Is(err, exactErr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// make a copy of http.Header
|
||||
func cloneHeader(h http.Header) http.Header {
|
||||
h2 := make(http.Header, len(h))
|
||||
for k, vv := range h {
|
||||
vv2 := make([]string, len(vv))
|
||||
copy(vv2, vv)
|
||||
h2[k] = vv2
|
||||
|
||||
}
|
||||
return h2
|
||||
}
|
||||
|
||||
func request2BucketObjectName(r *http.Request) (bucketName, objectName string) {
|
||||
path, err := getResource(r.URL.Path, r.Host, globalDomainNames)
|
||||
if err != nil {
|
||||
@@ -288,7 +276,7 @@ var globalProfiler minioProfiler
|
||||
|
||||
// dump the request into a string in JSON format.
|
||||
func dumpRequest(r *http.Request) string {
|
||||
header := cloneHeader(r.Header)
|
||||
header := r.Header.Clone()
|
||||
header.Set("Host", r.Host)
|
||||
// Replace all '%' to '%%' so that printer format parser
|
||||
// to ignore URL encoded values.
|
||||
@@ -437,28 +425,6 @@ func newContext(r *http.Request, w http.ResponseWriter, api string) context.Cont
|
||||
return logger.SetReqInfo(r.Context(), reqInfo)
|
||||
}
|
||||
|
||||
// isNetworkOrHostDown - if there was a network error or if the host is down.
|
||||
func isNetworkOrHostDown(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
// 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()
|
||||
}
|
||||
// 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)
|
||||
// If it is passed ["aaaa", "bbbb"], it returns ["aaaa", "{aaaa:.*}", "bbbb", "{bbbb:.*}"]
|
||||
func restQueries(keys ...string) []string {
|
||||
|
||||
@@ -30,30 +30,6 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests http.Header clone.
|
||||
func TestCloneHeader(t *testing.T) {
|
||||
headers := []http.Header{
|
||||
{
|
||||
"Content-Type": {"text/html; charset=UTF-8"},
|
||||
"Content-Length": {"0"},
|
||||
},
|
||||
{
|
||||
"Content-Length": {"0", "1", "2"},
|
||||
},
|
||||
{
|
||||
"Expires": {"-1"},
|
||||
"Content-Length": {"0"},
|
||||
"Content-Encoding": {"gzip"},
|
||||
},
|
||||
}
|
||||
for i, header := range headers {
|
||||
clonedHeader := cloneHeader(header)
|
||||
if !reflect.DeepEqual(header, clonedHeader) {
|
||||
t.Errorf("Test %d failed", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests maximum object size.
|
||||
func TestMaxObjectSize(t *testing.T) {
|
||||
sizes := []struct {
|
||||
|
||||
+6
-28
@@ -1053,6 +1053,9 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
putObject := objectAPI.PutObject
|
||||
if web.CacheAPI() != nil {
|
||||
putObject = web.CacheAPI().PutObject
|
||||
}
|
||||
|
||||
objInfo, err := putObject(context.Background(), bucket, object, pReader, opts)
|
||||
if err != nil {
|
||||
@@ -1316,17 +1319,6 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
info := gr.ObjInfo
|
||||
|
||||
length = info.Size
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if _, err = DecryptObjectInfo(&info, r.Header); err != nil {
|
||||
writeWebErrorResponse(w, err)
|
||||
return err
|
||||
}
|
||||
if crypto.IsEncrypted(info.UserDefined) {
|
||||
length, _ = info.DecryptedSize()
|
||||
}
|
||||
}
|
||||
length = info.Size
|
||||
var actualSize int64
|
||||
if info.IsCompressed() {
|
||||
// Read the decompressed size from the meta.json.
|
||||
@@ -1345,21 +1337,16 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
writeWebErrorResponse(w, errUnexpected)
|
||||
return err
|
||||
}
|
||||
var startOffset int64
|
||||
var writer io.Writer
|
||||
|
||||
if info.IsCompressed() {
|
||||
// The decompress metrics are set.
|
||||
snappyStartOffset := 0
|
||||
snappyLength := actualSize
|
||||
|
||||
// Open a pipe for compression
|
||||
// Where compressWriter is actually passed to the getObject
|
||||
decompressReader, compressWriter := io.Pipe()
|
||||
snappyReader := snappy.NewReader(decompressReader)
|
||||
|
||||
// The limit is set to the actual size.
|
||||
responseWriter := ioutil.LimitedWriter(zipWriter, int64(snappyStartOffset), snappyLength)
|
||||
responseWriter := ioutil.LimitedWriter(zipWriter, 0, actualSize)
|
||||
wg.Add(1) //For closures.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -1374,17 +1361,6 @@ func (web *webAPIHandlers) DownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
writer = zipWriter
|
||||
}
|
||||
if objectAPI.IsEncryptionSupported() && crypto.S3.IsEncrypted(info.UserDefined) {
|
||||
// Response writer should be limited early on for decryption upto required length,
|
||||
// additionally also skipping mod(offset)64KiB boundaries.
|
||||
writer = ioutil.LimitedWriter(writer, startOffset%(64*1024), length)
|
||||
writer, _, length, err = DecryptBlocksRequest(writer, r,
|
||||
args.BucketName, objectName, startOffset, length, info, false)
|
||||
if err != nil {
|
||||
writeWebErrorResponse(w, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
httpWriter := ioutil.WriteOnClose(writer)
|
||||
|
||||
// Write object content to response body
|
||||
@@ -1972,6 +1948,8 @@ func toWebAPIError(ctx context.Context, err error) APIError {
|
||||
return getAPIError(ErrStorageFull)
|
||||
case BucketNotFound:
|
||||
return getAPIError(ErrNoSuchBucket)
|
||||
case BucketNotEmpty:
|
||||
return getAPIError(ErrBucketNotEmpty)
|
||||
case BucketExists:
|
||||
return getAPIError(ErrBucketAlreadyOwnedByYou)
|
||||
case BucketNameInvalid:
|
||||
|
||||
@@ -338,7 +338,7 @@ func testDeleteBucketWebHandler(obj ObjectLayer, instanceType string, t TestErrH
|
||||
{"minio", false, "false token", "Authentication failed"},
|
||||
{"minio", false, token, "The specified bucket is not valid"},
|
||||
{bucketName, false, token, ""},
|
||||
{bucketName, true, token, "Bucket not empty"},
|
||||
{bucketName, true, token, "The bucket you tried to delete is not empty"},
|
||||
{bucketName, false, "", "JWT token missing"},
|
||||
}
|
||||
|
||||
|
||||
+111
-71
@@ -304,14 +304,26 @@ func newXLSets(endpoints EndpointList, format *formatXLV3, setCount int, drivesP
|
||||
// StorageInfo - combines output of StorageInfo across all erasure coded object sets.
|
||||
func (s *xlSets) StorageInfo(ctx context.Context) StorageInfo {
|
||||
var storageInfo StorageInfo
|
||||
var wg sync.WaitGroup
|
||||
|
||||
storageInfos := make([]StorageInfo, len(s.sets))
|
||||
storageInfo.Backend.Type = BackendErasure
|
||||
for _, set := range s.sets {
|
||||
lstorageInfo := set.StorageInfo(ctx)
|
||||
storageInfo.Used = storageInfo.Used + lstorageInfo.Used
|
||||
storageInfo.Total = storageInfo.Total + lstorageInfo.Total
|
||||
storageInfo.Available = storageInfo.Available + lstorageInfo.Available
|
||||
storageInfo.Backend.OnlineDisks = storageInfo.Backend.OnlineDisks + lstorageInfo.Backend.OnlineDisks
|
||||
storageInfo.Backend.OfflineDisks = storageInfo.Backend.OfflineDisks + lstorageInfo.Backend.OfflineDisks
|
||||
for index, set := range s.sets {
|
||||
wg.Add(1)
|
||||
go func(id int, set *xlObjects) {
|
||||
defer wg.Done()
|
||||
storageInfos[id] = set.StorageInfo(ctx)
|
||||
}(index, set)
|
||||
}
|
||||
// Wait for the go routines.
|
||||
wg.Wait()
|
||||
|
||||
for _, lstorageInfo := range storageInfos {
|
||||
storageInfo.Used += lstorageInfo.Used
|
||||
storageInfo.Total += lstorageInfo.Total
|
||||
storageInfo.Available += lstorageInfo.Available
|
||||
storageInfo.Backend.OnlineDisks += lstorageInfo.Backend.OnlineDisks
|
||||
storageInfo.Backend.OfflineDisks += lstorageInfo.Backend.OfflineDisks
|
||||
}
|
||||
|
||||
scData, scParity := getRedundancyCount(standardStorageClass, s.drivesPerSet)
|
||||
@@ -809,13 +821,16 @@ func (f *FileInfoCh) Push(fi FileInfo) {
|
||||
f.Valid = true
|
||||
}
|
||||
|
||||
// Calculate least entry across multiple FileInfo channels, additionally
|
||||
// returns a boolean to indicate if the caller needs to call again.
|
||||
func leastEntry(entriesCh []FileInfoCh, readQuorum int) (FileInfo, bool) {
|
||||
var entriesValid = make([]bool, len(entriesCh))
|
||||
var entries = make([]FileInfo, len(entriesCh))
|
||||
for i := range entriesCh {
|
||||
entries[i], entriesValid[i] = entriesCh[i].Pop()
|
||||
// Calculate least entry across multiple FileInfo channels,
|
||||
// returns the least common entry and the total number of times
|
||||
// we found this entry. Additionally also returns a boolean
|
||||
// to indicate if the caller needs to call this function
|
||||
// again to list the next entry. It is callers responsibility
|
||||
// if the caller wishes to list N entries to call leastEntry
|
||||
// N times until this boolean is 'false'.
|
||||
func leastEntry(entryChs []FileInfoCh, entries []FileInfo, entriesValid []bool) (FileInfo, int, bool) {
|
||||
for i := range entryChs {
|
||||
entries[i], entriesValid[i] = entryChs[i].Pop()
|
||||
}
|
||||
|
||||
var isTruncated = false
|
||||
@@ -844,9 +859,9 @@ func leastEntry(entriesCh []FileInfoCh, readQuorum int) (FileInfo, bool) {
|
||||
}
|
||||
|
||||
// We haven't been able to find any least entry,
|
||||
// this would mean that we don't have valid.
|
||||
// this would mean that we don't have valid entry.
|
||||
if !found {
|
||||
return lentry, isTruncated
|
||||
return lentry, 0, isTruncated
|
||||
}
|
||||
|
||||
leastEntryCount := 0
|
||||
@@ -864,50 +879,76 @@ func leastEntry(entriesCh []FileInfoCh, readQuorum int) (FileInfo, bool) {
|
||||
|
||||
// Push all entries which are lexically higher
|
||||
// and will be returned later in Pop()
|
||||
entriesCh[i].Push(entries[i])
|
||||
entryChs[i].Push(entries[i])
|
||||
}
|
||||
|
||||
if readQuorum < 0 {
|
||||
return lentry, isTruncated
|
||||
}
|
||||
|
||||
quorum := lentry.Quorum
|
||||
if quorum == 0 {
|
||||
quorum = readQuorum
|
||||
}
|
||||
|
||||
if leastEntryCount >= quorum {
|
||||
return lentry, isTruncated
|
||||
}
|
||||
|
||||
return leastEntry(entriesCh, readQuorum)
|
||||
return lentry, leastEntryCount, isTruncated
|
||||
}
|
||||
|
||||
// mergeEntriesCh - merges FileInfo channel to entries upto maxKeys.
|
||||
func mergeEntriesCh(entriesCh []FileInfoCh, maxKeys int, readQuorum int) (entries FilesInfo) {
|
||||
func mergeEntriesCh(entryChs []FileInfoCh, maxKeys int, totalDrives int, heal bool) (entries FilesInfo) {
|
||||
var i = 0
|
||||
entriesInfos := make([]FileInfo, len(entryChs))
|
||||
entriesValid := make([]bool, len(entryChs))
|
||||
for {
|
||||
fi, valid := leastEntry(entriesCh, readQuorum)
|
||||
fi, quorumCount, valid := leastEntry(entryChs, entriesInfos, entriesValid)
|
||||
if !valid {
|
||||
// We have reached EOF across all entryChs, break the loop.
|
||||
break
|
||||
}
|
||||
if i == maxKeys {
|
||||
entries.IsTruncated = true
|
||||
// Re-insert the last entry so it can be
|
||||
// listed in the next listing iteration.
|
||||
for j := range entriesCh {
|
||||
if !entriesCh[j].Valid {
|
||||
entriesCh[j].Push(fi)
|
||||
}
|
||||
|
||||
rquorum := fi.Quorum
|
||||
// Quorum is zero for all directories.
|
||||
if rquorum == 0 {
|
||||
// Choose N/2 quoroum for directory entries.
|
||||
rquorum = totalDrives / 2
|
||||
}
|
||||
|
||||
if heal {
|
||||
// When healing is enabled, we should
|
||||
// list only objects which need healing.
|
||||
if quorumCount == totalDrives {
|
||||
// Skip good entries.
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Regular listing, we skip entries not in quorum.
|
||||
if quorumCount < rquorum {
|
||||
// Skip entries which do not have quorum.
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
entries.Files = append(entries.Files, fi)
|
||||
i++
|
||||
if i == maxKeys {
|
||||
entries.IsTruncated = isTruncated(entryChs, entriesInfos, entriesValid)
|
||||
break
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func isTruncated(entryChs []FileInfoCh, entries []FileInfo, entriesValid []bool) bool {
|
||||
for i := range entryChs {
|
||||
entries[i], entriesValid[i] = entryChs[i].Pop()
|
||||
}
|
||||
|
||||
var isTruncated = false
|
||||
for _, valid := range entriesValid {
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
isTruncated = true
|
||||
break
|
||||
}
|
||||
for i := range entryChs {
|
||||
if entriesValid[i] {
|
||||
entryChs[i].Push(entries[i])
|
||||
}
|
||||
}
|
||||
return isTruncated
|
||||
}
|
||||
|
||||
// Starts a walk channel across all disks and returns a slice.
|
||||
func (s *xlSets) startMergeWalks(ctx context.Context, bucket, prefix, marker string, recursive bool, endWalkCh chan struct{}) []FileInfoCh {
|
||||
var entryChs []FileInfoCh
|
||||
@@ -936,20 +977,30 @@ func (s *xlSets) listObjectsNonSlash(ctx context.Context, bucket, prefix, marker
|
||||
recursive := true
|
||||
entryChs := s.startMergeWalks(context.Background(), bucket, prefix, "", recursive, endWalkCh)
|
||||
|
||||
readQuorum := s.drivesPerSet / 2
|
||||
var objInfos []ObjectInfo
|
||||
var eof bool
|
||||
var prevPrefix string
|
||||
|
||||
entriesValid := make([]bool, len(entryChs))
|
||||
entries := make([]FileInfo, len(entryChs))
|
||||
for {
|
||||
if len(objInfos) == maxKeys {
|
||||
break
|
||||
}
|
||||
result, ok := leastEntry(entryChs, readQuorum)
|
||||
result, quorumCount, ok := leastEntry(entryChs, entries, entriesValid)
|
||||
if !ok {
|
||||
eof = true
|
||||
break
|
||||
}
|
||||
rquorum := result.Quorum
|
||||
// Quorum is zero for all directories.
|
||||
if rquorum == 0 {
|
||||
// Choose N/2 quorum for directory entries.
|
||||
rquorum = s.drivesPerSet / 2
|
||||
}
|
||||
if quorumCount < rquorum {
|
||||
continue
|
||||
}
|
||||
|
||||
var objInfo ObjectInfo
|
||||
|
||||
@@ -1075,12 +1126,7 @@ func (s *xlSets) listObjects(ctx context.Context, bucket, prefix, marker, delimi
|
||||
entryChs = s.startMergeWalks(context.Background(), bucket, prefix, marker, recursive, endWalkCh)
|
||||
}
|
||||
|
||||
readQuorum := s.drivesPerSet / 2
|
||||
if heal {
|
||||
readQuorum = -1
|
||||
}
|
||||
|
||||
entries := mergeEntriesCh(entryChs, maxKeys, readQuorum)
|
||||
entries := mergeEntriesCh(entryChs, maxKeys, s.drivesPerSet, heal)
|
||||
if len(entries.Files) == 0 {
|
||||
return loi, nil
|
||||
}
|
||||
@@ -1239,35 +1285,29 @@ fi
|
||||
*/
|
||||
|
||||
func formatsToDrivesInfo(endpoints EndpointList, formats []*formatXLV3, sErrs []error) (beforeDrives []madmin.DriveInfo) {
|
||||
beforeDrives = make([]madmin.DriveInfo, len(endpoints))
|
||||
// Existing formats are available (i.e. ok), so save it in
|
||||
// result, also populate disks to be healed.
|
||||
for i, format := range formats {
|
||||
drive := endpoints.GetString(i)
|
||||
var state = madmin.DriveStateCorrupt
|
||||
switch {
|
||||
case format != nil:
|
||||
beforeDrives = append(beforeDrives, madmin.DriveInfo{
|
||||
UUID: format.XL.This,
|
||||
Endpoint: drive,
|
||||
State: madmin.DriveStateOk,
|
||||
})
|
||||
state = madmin.DriveStateOk
|
||||
case sErrs[i] == errUnformattedDisk:
|
||||
beforeDrives = append(beforeDrives, madmin.DriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: drive,
|
||||
State: madmin.DriveStateMissing,
|
||||
})
|
||||
state = madmin.DriveStateMissing
|
||||
case sErrs[i] == errDiskNotFound:
|
||||
beforeDrives = append(beforeDrives, madmin.DriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: drive,
|
||||
State: madmin.DriveStateOffline,
|
||||
})
|
||||
default:
|
||||
beforeDrives = append(beforeDrives, madmin.DriveInfo{
|
||||
UUID: "",
|
||||
Endpoint: drive,
|
||||
State: madmin.DriveStateCorrupt,
|
||||
})
|
||||
state = madmin.DriveStateOffline
|
||||
}
|
||||
beforeDrives[i] = madmin.DriveInfo{
|
||||
UUID: func() string {
|
||||
if format != nil {
|
||||
return format.XL.This
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
Endpoint: drive,
|
||||
State: state,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -43,7 +43,7 @@ func (xl xlObjects) MakeBucketWithLocation(ctx context.Context, bucket, location
|
||||
}
|
||||
|
||||
// Initialize sync waitgroup.
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Initialize list of errors.
|
||||
var dErrs = make([]error, len(xl.getDisks()))
|
||||
@@ -82,7 +82,7 @@ func (xl xlObjects) MakeBucketWithLocation(ctx context.Context, bucket, location
|
||||
|
||||
func (xl xlObjects) undoDeleteBucket(bucket string) {
|
||||
// Initialize sync waitgroup.
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
// Undo previous make bucket entry on all underlying storage disks.
|
||||
for index, disk := range xl.getDisks() {
|
||||
if disk == nil {
|
||||
@@ -103,7 +103,7 @@ func (xl xlObjects) undoDeleteBucket(bucket string) {
|
||||
// undo make bucket operation upon quorum failure.
|
||||
func undoMakeBucket(storageDisks []StorageAPI, bucket string) {
|
||||
// Initialize sync waitgroup.
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
// Undo previous make bucket entry on all underlying storage disks.
|
||||
for index, disk := range storageDisks {
|
||||
if disk == nil {
|
||||
@@ -245,7 +245,7 @@ func (xl xlObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
// Collect if all disks report volume not found.
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
var dErrs = make([]error, len(xl.getDisks()))
|
||||
|
||||
// Remove a volume entry on all underlying storage disks.
|
||||
|
||||
@@ -183,7 +183,7 @@ 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), part.Size == 0, checksumInfo.Algorithm, checksumInfo.Hash, erasure.ShardSize())
|
||||
err = onlineDisk.VerifyFile(bucket, pathJoin(object, part.Name), erasure.ShardFileSize(part.Size), checksumInfo.Algorithm, checksumInfo.Hash, erasure.ShardSize())
|
||||
if err != nil {
|
||||
isCorrupt := strings.HasPrefix(err.Error(), "Bitrot verification mismatch - expected ")
|
||||
if !isCorrupt && err != errFileNotFound && err != errVolumeNotFound && err != errFileUnexpectedSize {
|
||||
|
||||
@@ -57,7 +57,7 @@ func healBucket(ctx context.Context, storageDisks []StorageAPI, bucket string, w
|
||||
dryRun bool) (res madmin.HealResultItem, err error) {
|
||||
|
||||
// Initialize sync waitgroup.
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Initialize list of errors.
|
||||
var dErrs = make([]error, len(storageDisks))
|
||||
|
||||
+10
-47
@@ -19,7 +19,6 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
@@ -28,6 +27,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
@@ -75,8 +76,6 @@ func (c ChecksumInfo) MarshalJSON() ([]byte, error) {
|
||||
|
||||
// UnmarshalJSON - should never be called, instead xlMetaV1UnmarshalJSON() should be used.
|
||||
func (c *ChecksumInfo) UnmarshalJSON(data []byte) error {
|
||||
logger.LogIf(context.Background(), errUnexpected)
|
||||
|
||||
var info checksumInfoJSON
|
||||
if err := json.Unmarshal(data, &info); err != nil {
|
||||
return err
|
||||
@@ -453,7 +452,7 @@ func renameXLMetadata(ctx context.Context, disks []StorageAPI, srcBucket, srcEnt
|
||||
|
||||
// writeUniqueXLMetadata - writes unique `xl.json` content for each disk in order.
|
||||
func writeUniqueXLMetadata(ctx context.Context, disks []StorageAPI, bucket, prefix string, xlMetas []xlMetaV1, quorum int) ([]StorageAPI, error) {
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
var mErrs = make([]error, len(disks))
|
||||
|
||||
// Start writing `xl.json` to all disks in parallel.
|
||||
@@ -463,19 +462,17 @@ func writeUniqueXLMetadata(ctx context.Context, disks []StorageAPI, bucket, pref
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
|
||||
// Pick one xlMeta for a disk at index.
|
||||
xlMetas[index].Erasure.Index = index + 1
|
||||
|
||||
// Write `xl.json` in a routine.
|
||||
go func(index int, disk StorageAPI) {
|
||||
go func(index int, disk StorageAPI, xlMeta xlMetaV1) {
|
||||
defer wg.Done()
|
||||
|
||||
// Pick one xlMeta for a disk at index.
|
||||
xlMetas[index].Erasure.Index = index + 1
|
||||
|
||||
// Write unique `xl.json` for a disk at index.
|
||||
err := writeXLMetadata(ctx, disk, bucket, prefix, xlMetas[index])
|
||||
if err != nil {
|
||||
mErrs[index] = err
|
||||
}
|
||||
}(index, disk)
|
||||
mErrs[index] = writeXLMetadata(ctx, disk, bucket, prefix, xlMeta)
|
||||
}(index, disk, xlMetas[index])
|
||||
}
|
||||
|
||||
// Wait for all the routines.
|
||||
@@ -484,37 +481,3 @@ func writeUniqueXLMetadata(ctx context.Context, disks []StorageAPI, bucket, pref
|
||||
err := reduceWriteQuorumErrs(ctx, mErrs, objectOpIgnoredErrs, quorum)
|
||||
return evalDisks(disks, mErrs), err
|
||||
}
|
||||
|
||||
// writeSameXLMetadata - write `xl.json` on all disks in order.
|
||||
func writeSameXLMetadata(ctx context.Context, disks []StorageAPI, bucket, prefix string, xlMeta xlMetaV1, writeQuorum int) ([]StorageAPI, error) {
|
||||
var wg = &sync.WaitGroup{}
|
||||
var mErrs = make([]error, len(disks))
|
||||
|
||||
// Start writing `xl.json` to all disks in parallel.
|
||||
for index, disk := range disks {
|
||||
if disk == nil {
|
||||
mErrs[index] = errDiskNotFound
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
// Write `xl.json` in a routine.
|
||||
go func(index int, disk StorageAPI, metadata xlMetaV1) {
|
||||
defer wg.Done()
|
||||
|
||||
// Save the disk order index.
|
||||
metadata.Erasure.Index = index + 1
|
||||
|
||||
// Write xl metadata.
|
||||
err := writeXLMetadata(ctx, disk, bucket, prefix, metadata)
|
||||
if err != nil {
|
||||
mErrs[index] = err
|
||||
}
|
||||
}(index, disk, xlMeta)
|
||||
}
|
||||
|
||||
// Wait for all the routines.
|
||||
wg.Wait()
|
||||
|
||||
err := reduceWriteQuorumErrs(ctx, mErrs, objectOpIgnoredErrs, writeQuorum)
|
||||
return evalDisks(disks, mErrs), err
|
||||
}
|
||||
|
||||
+17
-15
@@ -56,7 +56,7 @@ func (xl xlObjects) checkUploadIDExists(ctx context.Context, bucket, object, upl
|
||||
// Removes part given by partName belonging to a mulitpart upload from minioMetaBucket
|
||||
func (xl xlObjects) removeObjectPart(bucket, object, uploadID, partName string) {
|
||||
curpartPath := path.Join(bucket, object, uploadID, partName)
|
||||
wg := sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
for i, disk := range xl.getDisks() {
|
||||
if disk == nil {
|
||||
continue
|
||||
@@ -103,7 +103,7 @@ func (xl xlObjects) statPart(ctx context.Context, bucket, object, uploadID, part
|
||||
|
||||
// commitXLMetadata - commit `xl.json` from source prefix to destination prefix in the given slice of disks.
|
||||
func commitXLMetadata(ctx context.Context, disks []StorageAPI, srcBucket, srcPrefix, dstBucket, dstPrefix string, quorum int) ([]StorageAPI, error) {
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
var mErrs = make([]error, len(disks))
|
||||
|
||||
srcJSONFile := path.Join(srcPrefix, xlMetaJSONFile)
|
||||
@@ -123,13 +123,7 @@ func commitXLMetadata(ctx context.Context, disks []StorageAPI, srcBucket, srcPre
|
||||
defer disk.DeleteFile(srcBucket, srcPrefix)
|
||||
|
||||
// Renames `xl.json` from source prefix to destination prefix.
|
||||
rErr := disk.RenameFile(srcBucket, srcJSONFile, dstBucket, dstJSONFile)
|
||||
if rErr != nil {
|
||||
logger.LogIf(ctx, rErr)
|
||||
mErrs[index] = rErr
|
||||
return
|
||||
}
|
||||
mErrs[index] = nil
|
||||
mErrs[index] = disk.RenameFile(srcBucket, srcJSONFile, dstBucket, dstJSONFile)
|
||||
}(index, disk)
|
||||
}
|
||||
// Wait for all the routines.
|
||||
@@ -218,16 +212,23 @@ func (xl xlObjects) newMultipartUpload(ctx context.Context, bucket string, objec
|
||||
// success.
|
||||
defer xl.deleteObject(ctx, minioMetaTmpBucket, tempUploadIDPath, writeQuorum, false)
|
||||
|
||||
onlineDisks := xl.getDisks()
|
||||
var partsMetadata = make([]xlMetaV1, len(onlineDisks))
|
||||
for i := range onlineDisks {
|
||||
partsMetadata[i] = xlMeta
|
||||
}
|
||||
|
||||
var err error
|
||||
// Write updated `xl.json` to all disks.
|
||||
disks, err := writeSameXLMetadata(ctx, xl.getDisks(), minioMetaTmpBucket, tempUploadIDPath, xlMeta, writeQuorum)
|
||||
onlineDisks, err = writeUniqueXLMetadata(ctx, onlineDisks, minioMetaTmpBucket, tempUploadIDPath, partsMetadata, writeQuorum)
|
||||
if err != nil {
|
||||
return "", toObjectErr(err, minioMetaTmpBucket, tempUploadIDPath)
|
||||
}
|
||||
|
||||
// Attempt to rename temp upload object to actual upload path object
|
||||
_, rErr := rename(ctx, disks, minioMetaTmpBucket, tempUploadIDPath, minioMetaMultipartBucket, uploadIDPath, true, writeQuorum, nil)
|
||||
if rErr != nil {
|
||||
return "", toObjectErr(rErr, minioMetaMultipartBucket, uploadIDPath)
|
||||
_, err = rename(ctx, onlineDisks, minioMetaTmpBucket, tempUploadIDPath, minioMetaMultipartBucket, uploadIDPath, true, writeQuorum, nil)
|
||||
if err != nil {
|
||||
return "", toObjectErr(err, minioMetaMultipartBucket, uploadIDPath)
|
||||
}
|
||||
|
||||
// Return success.
|
||||
@@ -456,7 +457,8 @@ func (xl xlObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
||||
defer xl.deleteObject(ctx, minioMetaTmpBucket, tempXLMetaPath, writeQuorum, false)
|
||||
|
||||
// Writes a unique `xl.json` each disk carrying new checksum related information.
|
||||
if onlineDisks, err = writeUniqueXLMetadata(ctx, onlineDisks, minioMetaTmpBucket, tempXLMetaPath, partsMetadata, writeQuorum); err != nil {
|
||||
onlineDisks, err = writeUniqueXLMetadata(ctx, onlineDisks, minioMetaTmpBucket, tempXLMetaPath, partsMetadata, writeQuorum)
|
||||
if err != nil {
|
||||
return pi, toObjectErr(err, minioMetaTmpBucket, tempXLMetaPath)
|
||||
}
|
||||
|
||||
@@ -518,7 +520,7 @@ func (xl xlObjects) ListObjectParts(ctx context.Context, bucket, object, uploadI
|
||||
|
||||
reducedErr := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||
if reducedErr == errXLWriteQuorum {
|
||||
return result, toObjectErr(err, minioMetaMultipartBucket, uploadIDPath)
|
||||
return result, toObjectErr(reducedErr, minioMetaMultipartBucket, uploadIDPath)
|
||||
}
|
||||
|
||||
_, modTime := listOnlineDisks(storageDisks, partsMetadata, errs)
|
||||
|
||||
+5
-5
@@ -33,7 +33,7 @@ var objectOpIgnoredErrs = append(baseIgnoredErrs, errDiskAccessDenied)
|
||||
|
||||
// putObjectDir hints the bottom layer to create a new directory.
|
||||
func (xl xlObjects) putObjectDir(ctx context.Context, bucket, object string, writeQuorum int) error {
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
errs := make([]error, len(xl.getDisks()))
|
||||
// Prepare object creation in all disks
|
||||
@@ -335,7 +335,7 @@ func (xl xlObjects) getObject(ctx context.Context, bucket, object string, startO
|
||||
|
||||
// getObjectInfoDir - This getObjectInfo is specific to object directory lookup.
|
||||
func (xl xlObjects) getObjectInfoDir(ctx context.Context, bucket, object string) (oi ObjectInfo, err error) {
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
errs := make([]error, len(xl.getDisks()))
|
||||
// Prepare object creation in a all disks
|
||||
@@ -423,7 +423,7 @@ func (xl xlObjects) getObjectInfo(ctx context.Context, bucket, object string) (o
|
||||
}
|
||||
|
||||
func undoRename(disks []StorageAPI, srcBucket, srcEntry, dstBucket, dstEntry string, isDir bool, errs []error) {
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
// Undo rename object on disks where RenameFile succeeded.
|
||||
|
||||
// If srcEntry/dstEntry are objects then add a trailing slash to copy
|
||||
@@ -453,7 +453,7 @@ func undoRename(disks []StorageAPI, srcBucket, srcEntry, dstBucket, dstEntry str
|
||||
// the respective underlying storage layer representations.
|
||||
func rename(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, dstBucket, dstEntry string, isDir bool, writeQuorum int, ignoredErr []error) ([]StorageAPI, error) {
|
||||
// Initialize sync waitgroup.
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Initialize list of errors.
|
||||
var errs = make([]error, len(disks))
|
||||
@@ -737,7 +737,7 @@ func (xl xlObjects) deleteObject(ctx context.Context, bucket, object string, wri
|
||||
}
|
||||
|
||||
// Initialize sync waitgroup.
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Initialize list of errors.
|
||||
var dErrs = make([]error, len(disks))
|
||||
|
||||
+10
-179
@@ -18,15 +18,13 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"hash/crc32"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Returns number of errors that occurred the most (incl. nil) and the
|
||||
@@ -117,167 +115,11 @@ func hashOrder(key string, cardinality int) []int {
|
||||
return nums
|
||||
}
|
||||
|
||||
func parseXLStat(v *fastjson.Value) (si statInfo, err error) {
|
||||
// obtain stat info.
|
||||
st := v.GetObject("stat")
|
||||
var mb []byte
|
||||
mb, err = st.Get("modTime").StringBytes()
|
||||
if err != nil {
|
||||
return si, err
|
||||
}
|
||||
// fetching modTime.
|
||||
si.ModTime, err = time.Parse(time.RFC3339, string(mb))
|
||||
if err != nil {
|
||||
return si, err
|
||||
}
|
||||
// obtain Stat.Size .
|
||||
si.Size, err = st.Get("size").Int64()
|
||||
if err != nil {
|
||||
return si, err
|
||||
}
|
||||
return si, nil
|
||||
}
|
||||
|
||||
func parseXLVersion(v *fastjson.Value) string {
|
||||
return string(v.GetStringBytes("version"))
|
||||
}
|
||||
|
||||
func parseXLFormat(v *fastjson.Value) string {
|
||||
return string(v.GetStringBytes("format"))
|
||||
}
|
||||
|
||||
func parseXLRelease(v *fastjson.Value) string {
|
||||
return string(v.GetStringBytes("minio", "release"))
|
||||
}
|
||||
|
||||
func parseXLErasureInfo(ctx context.Context, v *fastjson.Value) (ErasureInfo, error) {
|
||||
erasure := ErasureInfo{}
|
||||
// parse the xlV1Meta.Erasure.Distribution.
|
||||
er := v.GetObject("erasure")
|
||||
disResult := er.Get("distribution").GetArray()
|
||||
distribution := make([]int, len(disResult))
|
||||
var err error
|
||||
for i, dis := range disResult {
|
||||
distribution[i], err = dis.Int()
|
||||
if err != nil {
|
||||
return erasure, err
|
||||
}
|
||||
}
|
||||
erasure.Distribution = distribution
|
||||
|
||||
erasure.Algorithm = string(er.Get("algorithm").GetStringBytes())
|
||||
erasure.DataBlocks = er.Get("data").GetInt()
|
||||
erasure.ParityBlocks = er.Get("parity").GetInt()
|
||||
erasure.BlockSize = er.Get("blockSize").GetInt64()
|
||||
erasure.Index = er.Get("index").GetInt()
|
||||
checkSumsResult := er.Get("checksum").GetArray()
|
||||
|
||||
// Parse xlMetaV1.Erasure.Checksum array.
|
||||
checkSums := make([]ChecksumInfo, len(checkSumsResult))
|
||||
for i, ck := range checkSumsResult {
|
||||
algorithm := BitrotAlgorithmFromString(string(ck.GetStringBytes("algorithm")))
|
||||
if !algorithm.Available() {
|
||||
logger.LogIf(ctx, errBitrotHashAlgoInvalid)
|
||||
return erasure, errBitrotHashAlgoInvalid
|
||||
}
|
||||
srcHash := ck.GetStringBytes("hash")
|
||||
n, err := hex.Decode(srcHash, srcHash)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return erasure, err
|
||||
}
|
||||
nmb := ck.GetStringBytes("name")
|
||||
if nmb == nil {
|
||||
return erasure, errCorruptedFormat
|
||||
}
|
||||
checkSums[i] = ChecksumInfo{
|
||||
Name: string(nmb),
|
||||
Algorithm: algorithm,
|
||||
Hash: srcHash[:n],
|
||||
}
|
||||
}
|
||||
erasure.Checksums = checkSums
|
||||
return erasure, nil
|
||||
}
|
||||
|
||||
func parseXLParts(partsResult []*fastjson.Value) []ObjectPartInfo {
|
||||
// Parse the XL Parts.
|
||||
partInfo := make([]ObjectPartInfo, len(partsResult))
|
||||
for i, p := range partsResult {
|
||||
partInfo[i] = ObjectPartInfo{
|
||||
Number: p.GetInt("number"),
|
||||
Name: string(p.GetStringBytes("name")),
|
||||
ETag: string(p.GetStringBytes("etag")),
|
||||
Size: p.GetInt64("size"),
|
||||
ActualSize: p.GetInt64("actualSize"),
|
||||
}
|
||||
}
|
||||
return partInfo
|
||||
}
|
||||
|
||||
func parseXLMetaMap(v *fastjson.Value) map[string]string {
|
||||
metaMap := make(map[string]string)
|
||||
// Get xlMetaV1.Meta map.
|
||||
v.GetObject("meta").Visit(func(k []byte, kv *fastjson.Value) {
|
||||
metaMap[string(k)] = string(kv.GetStringBytes())
|
||||
})
|
||||
return metaMap
|
||||
}
|
||||
|
||||
// xl.json Parser pool
|
||||
var xlParserPool fastjson.ParserPool
|
||||
|
||||
// Constructs XLMetaV1 using `fastjson` lib to retrieve each field.
|
||||
// Constructs xlMetaV1 using `jsoniter` lib.
|
||||
func xlMetaV1UnmarshalJSON(ctx context.Context, xlMetaBuf []byte) (xlMeta xlMetaV1, err error) {
|
||||
parser := xlParserPool.Get()
|
||||
defer xlParserPool.Put(parser)
|
||||
|
||||
var v *fastjson.Value
|
||||
v, err = parser.ParseBytes(xlMetaBuf)
|
||||
if err != nil {
|
||||
return xlMeta, err
|
||||
}
|
||||
|
||||
// obtain version.
|
||||
xlMeta.Version = parseXLVersion(v)
|
||||
// obtain format.
|
||||
xlMeta.Format = parseXLFormat(v)
|
||||
|
||||
// Validate if the xl.json we read is sane, return corrupted format.
|
||||
if !isXLMetaFormatValid(xlMeta.Version, xlMeta.Format) {
|
||||
// For version mismatchs and unrecognized format, return corrupted format.
|
||||
logger.LogIf(ctx, errCorruptedFormat)
|
||||
return xlMeta, errCorruptedFormat
|
||||
}
|
||||
|
||||
// Parse xlMetaV1.Stat .
|
||||
stat, err := parseXLStat(v)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return xlMeta, err
|
||||
}
|
||||
|
||||
xlMeta.Stat = stat
|
||||
// parse the xlV1Meta.Erasure fields.
|
||||
xlMeta.Erasure, err = parseXLErasureInfo(ctx, v)
|
||||
if err != nil {
|
||||
return xlMeta, err
|
||||
}
|
||||
|
||||
// Check for scenario where checksum information missing for some parts.
|
||||
partsResult := v.Get("parts").GetArray()
|
||||
if len(xlMeta.Erasure.Checksums) != len(partsResult) {
|
||||
return xlMeta, errCorruptedFormat
|
||||
}
|
||||
|
||||
// Parse the XL Parts.
|
||||
xlMeta.Parts = parseXLParts(partsResult)
|
||||
// Get the xlMetaV1.Realse field.
|
||||
xlMeta.Minio.Release = parseXLRelease(v)
|
||||
// parse xlMetaV1.
|
||||
xlMeta.Meta = parseXLMetaMap(v)
|
||||
|
||||
return xlMeta, nil
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
err = json.Unmarshal(xlMetaBuf, &xlMeta)
|
||||
return xlMeta, err
|
||||
}
|
||||
|
||||
// read xl.json from the given disk, parse and return xlV1MetaV1.Parts.
|
||||
@@ -298,7 +140,7 @@ func readXLMetaParts(ctx context.Context, disk StorageAPI, bucket string, object
|
||||
return xlMeta.Parts, xlMeta.Meta, nil
|
||||
}
|
||||
|
||||
// read xl.json from the given disk and parse xlV1Meta.Stat and xlV1Meta.Meta using fastjson.
|
||||
// read xl.json from the given disk and parse xlV1Meta.Stat and xlV1Meta.Meta using jsoniter.
|
||||
func readXLMetaStat(ctx context.Context, disk StorageAPI, bucket string, object string) (si statInfo,
|
||||
mp map[string]string, e error) {
|
||||
// Reads entire `xl.json`.
|
||||
@@ -332,14 +174,8 @@ func readXLMeta(ctx context.Context, disk StorageAPI, bucket string, object stri
|
||||
if len(xlMetaBuf) == 0 {
|
||||
return xlMetaV1{}, errFileNotFound
|
||||
}
|
||||
xlMeta, err = xlMetaV1UnmarshalJSON(ctx, xlMetaBuf)
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).AppendTags("disk", disk.String())
|
||||
logger.LogIf(ctx, err)
|
||||
return xlMetaV1{}, err
|
||||
}
|
||||
// Return structured `xl.json`.
|
||||
return xlMeta, nil
|
||||
logger.GetReqInfo(ctx).AppendTags("disk", disk.String())
|
||||
return xlMetaV1UnmarshalJSON(ctx, xlMetaBuf)
|
||||
}
|
||||
|
||||
// Reads all `xl.json` metadata as a xlMetaV1 slice.
|
||||
@@ -347,7 +183,7 @@ func readXLMeta(ctx context.Context, disk StorageAPI, bucket string, object stri
|
||||
func readAllXLMetadata(ctx context.Context, disks []StorageAPI, bucket, object string) ([]xlMetaV1, []error) {
|
||||
errs := make([]error, len(disks))
|
||||
metadataArray := make([]xlMetaV1, len(disks))
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wg sync.WaitGroup
|
||||
// Read `xl.json` parallelly across disks.
|
||||
for index, disk := range disks {
|
||||
if disk == nil {
|
||||
@@ -358,12 +194,7 @@ func readAllXLMetadata(ctx context.Context, disks []StorageAPI, bucket, object s
|
||||
// Read `xl.json` in routine.
|
||||
go func(index int, disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
var err error
|
||||
metadataArray[index], err = readXLMeta(ctx, disk, bucket, object)
|
||||
if err != nil {
|
||||
errs[index] = err
|
||||
return
|
||||
}
|
||||
metadataArray[index], errs[index] = readXLMeta(ctx, disk, bucket, object)
|
||||
}(index, disk)
|
||||
}
|
||||
|
||||
|
||||
+58
-58
@@ -212,99 +212,99 @@ func getSampleXLMeta(totalParts int) xlMetaV1 {
|
||||
return xlMeta
|
||||
}
|
||||
|
||||
// Compare the unmarshaled XLMetaV1 with the one obtained from fastjson parsing.
|
||||
func compareXLMetaV1(t *testing.T, unMarshalXLMeta, fastjsonXLMeta xlMetaV1) {
|
||||
// Start comparing the fields of xlMetaV1 obtained from fastjson parsing with one parsed using json unmarshaling.
|
||||
if unMarshalXLMeta.Version != fastjsonXLMeta.Version {
|
||||
t.Errorf("Expected the Version to be \"%s\", but got \"%s\".", unMarshalXLMeta.Version, fastjsonXLMeta.Version)
|
||||
// Compare the unmarshaled XLMetaV1 with the one obtained from jsoniter parsing.
|
||||
func compareXLMetaV1(t *testing.T, unMarshalXLMeta, jsoniterXLMeta xlMetaV1) {
|
||||
// Start comparing the fields of xlMetaV1 obtained from jsoniter parsing with one parsed using json unmarshaling.
|
||||
if unMarshalXLMeta.Version != jsoniterXLMeta.Version {
|
||||
t.Errorf("Expected the Version to be \"%s\", but got \"%s\".", unMarshalXLMeta.Version, jsoniterXLMeta.Version)
|
||||
}
|
||||
if unMarshalXLMeta.Format != fastjsonXLMeta.Format {
|
||||
t.Errorf("Expected the format to be \"%s\", but got \"%s\".", unMarshalXLMeta.Format, fastjsonXLMeta.Format)
|
||||
if unMarshalXLMeta.Format != jsoniterXLMeta.Format {
|
||||
t.Errorf("Expected the format to be \"%s\", but got \"%s\".", unMarshalXLMeta.Format, jsoniterXLMeta.Format)
|
||||
}
|
||||
if unMarshalXLMeta.Stat.Size != fastjsonXLMeta.Stat.Size {
|
||||
t.Errorf("Expected the stat size to be %v, but got %v.", unMarshalXLMeta.Stat.Size, fastjsonXLMeta.Stat.Size)
|
||||
if unMarshalXLMeta.Stat.Size != jsoniterXLMeta.Stat.Size {
|
||||
t.Errorf("Expected the stat size to be %v, but got %v.", unMarshalXLMeta.Stat.Size, jsoniterXLMeta.Stat.Size)
|
||||
}
|
||||
if !unMarshalXLMeta.Stat.ModTime.Equal(fastjsonXLMeta.Stat.ModTime) {
|
||||
t.Errorf("Expected the modTime to be \"%v\", but got \"%v\".", unMarshalXLMeta.Stat.ModTime, fastjsonXLMeta.Stat.ModTime)
|
||||
if !unMarshalXLMeta.Stat.ModTime.Equal(jsoniterXLMeta.Stat.ModTime) {
|
||||
t.Errorf("Expected the modTime to be \"%v\", but got \"%v\".", unMarshalXLMeta.Stat.ModTime, jsoniterXLMeta.Stat.ModTime)
|
||||
}
|
||||
if unMarshalXLMeta.Erasure.Algorithm != fastjsonXLMeta.Erasure.Algorithm {
|
||||
t.Errorf("Expected the erasure algorithm to be \"%v\", but got \"%v\".", unMarshalXLMeta.Erasure.Algorithm, fastjsonXLMeta.Erasure.Algorithm)
|
||||
if unMarshalXLMeta.Erasure.Algorithm != jsoniterXLMeta.Erasure.Algorithm {
|
||||
t.Errorf("Expected the erasure algorithm to be \"%v\", but got \"%v\".", unMarshalXLMeta.Erasure.Algorithm, jsoniterXLMeta.Erasure.Algorithm)
|
||||
}
|
||||
if unMarshalXLMeta.Erasure.DataBlocks != fastjsonXLMeta.Erasure.DataBlocks {
|
||||
t.Errorf("Expected the erasure data blocks to be %v, but got %v.", unMarshalXLMeta.Erasure.DataBlocks, fastjsonXLMeta.Erasure.DataBlocks)
|
||||
if unMarshalXLMeta.Erasure.DataBlocks != jsoniterXLMeta.Erasure.DataBlocks {
|
||||
t.Errorf("Expected the erasure data blocks to be %v, but got %v.", unMarshalXLMeta.Erasure.DataBlocks, jsoniterXLMeta.Erasure.DataBlocks)
|
||||
}
|
||||
if unMarshalXLMeta.Erasure.ParityBlocks != fastjsonXLMeta.Erasure.ParityBlocks {
|
||||
t.Errorf("Expected the erasure parity blocks to be %v, but got %v.", unMarshalXLMeta.Erasure.ParityBlocks, fastjsonXLMeta.Erasure.ParityBlocks)
|
||||
if unMarshalXLMeta.Erasure.ParityBlocks != jsoniterXLMeta.Erasure.ParityBlocks {
|
||||
t.Errorf("Expected the erasure parity blocks to be %v, but got %v.", unMarshalXLMeta.Erasure.ParityBlocks, jsoniterXLMeta.Erasure.ParityBlocks)
|
||||
}
|
||||
if unMarshalXLMeta.Erasure.BlockSize != fastjsonXLMeta.Erasure.BlockSize {
|
||||
t.Errorf("Expected the erasure block size to be %v, but got %v.", unMarshalXLMeta.Erasure.BlockSize, fastjsonXLMeta.Erasure.BlockSize)
|
||||
if unMarshalXLMeta.Erasure.BlockSize != jsoniterXLMeta.Erasure.BlockSize {
|
||||
t.Errorf("Expected the erasure block size to be %v, but got %v.", unMarshalXLMeta.Erasure.BlockSize, jsoniterXLMeta.Erasure.BlockSize)
|
||||
}
|
||||
if unMarshalXLMeta.Erasure.Index != fastjsonXLMeta.Erasure.Index {
|
||||
t.Errorf("Expected the erasure index to be %v, but got %v.", unMarshalXLMeta.Erasure.Index, fastjsonXLMeta.Erasure.Index)
|
||||
if unMarshalXLMeta.Erasure.Index != jsoniterXLMeta.Erasure.Index {
|
||||
t.Errorf("Expected the erasure index to be %v, but got %v.", unMarshalXLMeta.Erasure.Index, jsoniterXLMeta.Erasure.Index)
|
||||
}
|
||||
if len(unMarshalXLMeta.Erasure.Distribution) != len(fastjsonXLMeta.Erasure.Distribution) {
|
||||
t.Errorf("Expected the size of Erasure Distribution to be %d, but got %d.", len(unMarshalXLMeta.Erasure.Distribution), len(fastjsonXLMeta.Erasure.Distribution))
|
||||
if len(unMarshalXLMeta.Erasure.Distribution) != len(jsoniterXLMeta.Erasure.Distribution) {
|
||||
t.Errorf("Expected the size of Erasure Distribution to be %d, but got %d.", len(unMarshalXLMeta.Erasure.Distribution), len(jsoniterXLMeta.Erasure.Distribution))
|
||||
} else {
|
||||
for i := 0; i < len(unMarshalXLMeta.Erasure.Distribution); i++ {
|
||||
if unMarshalXLMeta.Erasure.Distribution[i] != fastjsonXLMeta.Erasure.Distribution[i] {
|
||||
t.Errorf("Expected the Erasure Distribution to be %d, got %d.", unMarshalXLMeta.Erasure.Distribution[i], fastjsonXLMeta.Erasure.Distribution[i])
|
||||
if unMarshalXLMeta.Erasure.Distribution[i] != jsoniterXLMeta.Erasure.Distribution[i] {
|
||||
t.Errorf("Expected the Erasure Distribution to be %d, got %d.", unMarshalXLMeta.Erasure.Distribution[i], jsoniterXLMeta.Erasure.Distribution[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(unMarshalXLMeta.Erasure.Checksums) != len(fastjsonXLMeta.Erasure.Checksums) {
|
||||
t.Errorf("Expected the size of Erasure Checksums to be %d, but got %d.", len(unMarshalXLMeta.Erasure.Checksums), len(fastjsonXLMeta.Erasure.Checksums))
|
||||
if len(unMarshalXLMeta.Erasure.Checksums) != len(jsoniterXLMeta.Erasure.Checksums) {
|
||||
t.Errorf("Expected the size of Erasure Checksums to be %d, but got %d.", len(unMarshalXLMeta.Erasure.Checksums), len(jsoniterXLMeta.Erasure.Checksums))
|
||||
} else {
|
||||
for i := 0; i < len(unMarshalXLMeta.Erasure.Checksums); i++ {
|
||||
if unMarshalXLMeta.Erasure.Checksums[i].Name != fastjsonXLMeta.Erasure.Checksums[i].Name {
|
||||
t.Errorf("Expected the Erasure Checksum Name to be \"%s\", got \"%s\".", unMarshalXLMeta.Erasure.Checksums[i].Name, fastjsonXLMeta.Erasure.Checksums[i].Name)
|
||||
if unMarshalXLMeta.Erasure.Checksums[i].Name != jsoniterXLMeta.Erasure.Checksums[i].Name {
|
||||
t.Errorf("Expected the Erasure Checksum Name to be \"%s\", got \"%s\".", unMarshalXLMeta.Erasure.Checksums[i].Name, jsoniterXLMeta.Erasure.Checksums[i].Name)
|
||||
}
|
||||
if unMarshalXLMeta.Erasure.Checksums[i].Algorithm != fastjsonXLMeta.Erasure.Checksums[i].Algorithm {
|
||||
t.Errorf("Expected the Erasure Checksum Algorithm to be \"%s\", got \"%s\".", unMarshalXLMeta.Erasure.Checksums[i].Algorithm, fastjsonXLMeta.Erasure.Checksums[i].Algorithm)
|
||||
if unMarshalXLMeta.Erasure.Checksums[i].Algorithm != jsoniterXLMeta.Erasure.Checksums[i].Algorithm {
|
||||
t.Errorf("Expected the Erasure Checksum Algorithm to be \"%s\", got \"%s\".", unMarshalXLMeta.Erasure.Checksums[i].Algorithm, jsoniterXLMeta.Erasure.Checksums[i].Algorithm)
|
||||
}
|
||||
if !bytes.Equal(unMarshalXLMeta.Erasure.Checksums[i].Hash, fastjsonXLMeta.Erasure.Checksums[i].Hash) {
|
||||
t.Errorf("Expected the Erasure Checksum Hash to be \"%s\", got \"%s\".", unMarshalXLMeta.Erasure.Checksums[i].Hash, fastjsonXLMeta.Erasure.Checksums[i].Hash)
|
||||
if !bytes.Equal(unMarshalXLMeta.Erasure.Checksums[i].Hash, jsoniterXLMeta.Erasure.Checksums[i].Hash) {
|
||||
t.Errorf("Expected the Erasure Checksum Hash to be \"%s\", got \"%s\".", unMarshalXLMeta.Erasure.Checksums[i].Hash, jsoniterXLMeta.Erasure.Checksums[i].Hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if unMarshalXLMeta.Minio.Release != fastjsonXLMeta.Minio.Release {
|
||||
t.Errorf("Expected the Release string to be \"%s\", but got \"%s\".", unMarshalXLMeta.Minio.Release, fastjsonXLMeta.Minio.Release)
|
||||
if unMarshalXLMeta.Minio.Release != jsoniterXLMeta.Minio.Release {
|
||||
t.Errorf("Expected the Release string to be \"%s\", but got \"%s\".", unMarshalXLMeta.Minio.Release, jsoniterXLMeta.Minio.Release)
|
||||
}
|
||||
if len(unMarshalXLMeta.Parts) != len(fastjsonXLMeta.Parts) {
|
||||
t.Errorf("Expected info of %d parts to be present, but got %d instead.", len(unMarshalXLMeta.Parts), len(fastjsonXLMeta.Parts))
|
||||
if len(unMarshalXLMeta.Parts) != len(jsoniterXLMeta.Parts) {
|
||||
t.Errorf("Expected info of %d parts to be present, but got %d instead.", len(unMarshalXLMeta.Parts), len(jsoniterXLMeta.Parts))
|
||||
} else {
|
||||
for i := 0; i < len(unMarshalXLMeta.Parts); i++ {
|
||||
if unMarshalXLMeta.Parts[i].Name != fastjsonXLMeta.Parts[i].Name {
|
||||
t.Errorf("Expected the name of part %d to be \"%s\", got \"%s\".", i+1, unMarshalXLMeta.Parts[i].Name, fastjsonXLMeta.Parts[i].Name)
|
||||
if unMarshalXLMeta.Parts[i].Name != jsoniterXLMeta.Parts[i].Name {
|
||||
t.Errorf("Expected the name of part %d to be \"%s\", got \"%s\".", i+1, unMarshalXLMeta.Parts[i].Name, jsoniterXLMeta.Parts[i].Name)
|
||||
}
|
||||
if unMarshalXLMeta.Parts[i].ETag != fastjsonXLMeta.Parts[i].ETag {
|
||||
t.Errorf("Expected the ETag of part %d to be \"%s\", got \"%s\".", i+1, unMarshalXLMeta.Parts[i].ETag, fastjsonXLMeta.Parts[i].ETag)
|
||||
if unMarshalXLMeta.Parts[i].ETag != jsoniterXLMeta.Parts[i].ETag {
|
||||
t.Errorf("Expected the ETag of part %d to be \"%s\", got \"%s\".", i+1, unMarshalXLMeta.Parts[i].ETag, jsoniterXLMeta.Parts[i].ETag)
|
||||
}
|
||||
if unMarshalXLMeta.Parts[i].Number != fastjsonXLMeta.Parts[i].Number {
|
||||
t.Errorf("Expected the number of part %d to be \"%d\", got \"%d\".", i+1, unMarshalXLMeta.Parts[i].Number, fastjsonXLMeta.Parts[i].Number)
|
||||
if unMarshalXLMeta.Parts[i].Number != jsoniterXLMeta.Parts[i].Number {
|
||||
t.Errorf("Expected the number of part %d to be \"%d\", got \"%d\".", i+1, unMarshalXLMeta.Parts[i].Number, jsoniterXLMeta.Parts[i].Number)
|
||||
}
|
||||
if unMarshalXLMeta.Parts[i].Size != fastjsonXLMeta.Parts[i].Size {
|
||||
t.Errorf("Expected the size of part %d to be %v, got %v.", i+1, unMarshalXLMeta.Parts[i].Size, fastjsonXLMeta.Parts[i].Size)
|
||||
if unMarshalXLMeta.Parts[i].Size != jsoniterXLMeta.Parts[i].Size {
|
||||
t.Errorf("Expected the size of part %d to be %v, got %v.", i+1, unMarshalXLMeta.Parts[i].Size, jsoniterXLMeta.Parts[i].Size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, val := range unMarshalXLMeta.Meta {
|
||||
fastjsonVal, exists := fastjsonXLMeta.Meta[key]
|
||||
jsoniterVal, exists := jsoniterXLMeta.Meta[key]
|
||||
if !exists {
|
||||
t.Errorf("No meta data entry for Key \"%s\" exists.", key)
|
||||
}
|
||||
if val != fastjsonVal {
|
||||
t.Errorf("Expected the value for Meta data key \"%s\" to be \"%s\", but got \"%s\".", key, val, fastjsonVal)
|
||||
if val != jsoniterVal {
|
||||
t.Errorf("Expected the value for Meta data key \"%s\" to be \"%s\", but got \"%s\".", key, val, jsoniterVal)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the correctness of constructing XLMetaV1 using fastjson lib.
|
||||
// Tests the correctness of constructing XLMetaV1 using jsoniter lib.
|
||||
// The result will be compared with the result obtained from json.unMarshal of the byte data.
|
||||
func TestGetXLMetaV1Fastjson1(t *testing.T) {
|
||||
func TestGetXLMetaV1Jsoniter1(t *testing.T) {
|
||||
xlMetaJSON := getXLMetaBytes(1)
|
||||
|
||||
var unMarshalXLMeta xlMetaV1
|
||||
@@ -312,16 +312,16 @@ func TestGetXLMetaV1Fastjson1(t *testing.T) {
|
||||
t.Errorf("Unmarshalling failed: %v", err)
|
||||
}
|
||||
|
||||
fastjsonXLMeta, err := xlMetaV1UnmarshalJSON(context.Background(), xlMetaJSON)
|
||||
jsoniterXLMeta, err := xlMetaV1UnmarshalJSON(context.Background(), xlMetaJSON)
|
||||
if err != nil {
|
||||
t.Errorf("fastjson parsing of XLMeta failed: %v", err)
|
||||
t.Errorf("jsoniter parsing of XLMeta failed: %v", err)
|
||||
}
|
||||
compareXLMetaV1(t, unMarshalXLMeta, fastjsonXLMeta)
|
||||
compareXLMetaV1(t, unMarshalXLMeta, jsoniterXLMeta)
|
||||
}
|
||||
|
||||
// Tests the correctness of constructing XLMetaV1 using fastjson lib for XLMetaV1 of size 10 parts.
|
||||
// Tests the correctness of constructing XLMetaV1 using jsoniter lib for XLMetaV1 of size 10 parts.
|
||||
// The result will be compared with the result obtained from json.unMarshal of the byte data.
|
||||
func TestGetXLMetaV1Fastjson10(t *testing.T) {
|
||||
func TestGetXLMetaV1Jsoniter10(t *testing.T) {
|
||||
|
||||
xlMetaJSON := getXLMetaBytes(10)
|
||||
|
||||
@@ -329,11 +329,11 @@ func TestGetXLMetaV1Fastjson10(t *testing.T) {
|
||||
if err := json.Unmarshal(xlMetaJSON, &unMarshalXLMeta); err != nil {
|
||||
t.Errorf("Unmarshalling failed: %v", err)
|
||||
}
|
||||
fastjsonXLMeta, err := xlMetaV1UnmarshalJSON(context.Background(), xlMetaJSON)
|
||||
jsoniterXLMeta, err := xlMetaV1UnmarshalJSON(context.Background(), xlMetaJSON)
|
||||
if err != nil {
|
||||
t.Errorf("fastjson parsing of XLMeta failed: %v", err)
|
||||
t.Errorf("jsoniter parsing of XLMeta failed: %v", err)
|
||||
}
|
||||
compareXLMetaV1(t, unMarshalXLMeta, fastjsonXLMeta)
|
||||
compareXLMetaV1(t, unMarshalXLMeta, jsoniterXLMeta)
|
||||
}
|
||||
|
||||
// Test the predicted part size from the part index
|
||||
|
||||
+27
-11
@@ -19,6 +19,7 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/bpool"
|
||||
@@ -70,24 +71,39 @@ func (d byDiskTotal) Less(i, j int) bool {
|
||||
// getDisksInfo - fetch disks info across all other storage API.
|
||||
func getDisksInfo(disks []StorageAPI) (disksInfo []DiskInfo, onlineDisks int, offlineDisks int) {
|
||||
disksInfo = make([]DiskInfo, len(disks))
|
||||
errs := make([]error, len(disks))
|
||||
var wg sync.WaitGroup
|
||||
for i, storageDisk := range disks {
|
||||
if storageDisk == nil {
|
||||
// Storage disk is empty, perhaps ignored disk or not available.
|
||||
errs[i] = errDiskNotFound
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(id int, sDisk StorageAPI) {
|
||||
defer wg.Done()
|
||||
info, err := sDisk.DiskInfo()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("disk", sDisk.String())
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
if IsErr(err, baseErrs...) {
|
||||
errs[id] = err
|
||||
return
|
||||
}
|
||||
}
|
||||
disksInfo[id] = info
|
||||
}(i, storageDisk)
|
||||
}
|
||||
// Wait for the routines.
|
||||
wg.Wait()
|
||||
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
offlineDisks++
|
||||
continue
|
||||
}
|
||||
info, err := storageDisk.DiskInfo()
|
||||
if err != nil {
|
||||
ctx := context.Background()
|
||||
logger.GetReqInfo(ctx).AppendTags("disk", storageDisk.String())
|
||||
logger.LogIf(ctx, err)
|
||||
if IsErr(err, baseErrs...) {
|
||||
offlineDisks++
|
||||
continue
|
||||
}
|
||||
}
|
||||
onlineDisks++
|
||||
disksInfo[i] = info
|
||||
}
|
||||
|
||||
// Success.
|
||||
|
||||
@@ -1047,7 +1047,59 @@ mc cp myphoto.jpg myminio/images
|
||||
|
||||
```
|
||||
kafkacat -b localhost:9092 -t bucketevents
|
||||
{"EventType":"s3:ObjectCreated:Put","Key":"images/myphoto.jpg","Records":[{"eventVersion":"2.0","eventSource":"aws:s3","awsRegion":"","eventTime":"2017-01-31T10:01:51Z","eventName":"s3:ObjectCreated:Put","userIdentity":{"principalId":"88QR09S7IOT4X1IBAQ9B"},"requestParameters":{"sourceIPAddress":"192.173.5.2:57904"},"responseElements":{"x-amz-request-id":"149ED2FD25589220","x-minio-origin-endpoint":"http://192.173.5.2:9000"},"s3":{"s3SchemaVersion":"1.0","configurationId":"Config","bucket":{"name":"images","ownerIdentity":{"principalId":"88QR09S7IOT4X1IBAQ9B"},"arn":"arn:aws:s3:::images"},"object":{"key":"myphoto.jpg","size":541596,"eTag":"04451d05b4faf4d62f3d538156115e2a","sequencer":"149ED2FD25589220"}}}],"level":"info","msg":"","time":"2017-01-31T15:31:51+05:30"}
|
||||
{
|
||||
"EventName": "s3:ObjectCreated:Put",
|
||||
"Key": "images/myphoto.jpg",
|
||||
"Records": [
|
||||
{
|
||||
"eventVersion": "2.0",
|
||||
"eventSource": "minio:s3",
|
||||
"awsRegion": "",
|
||||
"eventTime": "2019-09-10T17:41:54Z",
|
||||
"eventName": "s3:ObjectCreated:Put",
|
||||
"userIdentity": {
|
||||
"principalId": "AKIAIOSFODNN7EXAMPLE"
|
||||
},
|
||||
"requestParameters": {
|
||||
"accessKey": "AKIAIOSFODNN7EXAMPLE",
|
||||
"region": "",
|
||||
"sourceIPAddress": "192.168.56.192"
|
||||
},
|
||||
"responseElements": {
|
||||
"x-amz-request-id": "15C3249451E12784",
|
||||
"x-minio-deployment-id": "751a8ba6-acb2-42f6-a297-4cdf1cf1fa4f",
|
||||
"x-minio-origin-endpoint": "http://192.168.97.83:9000"
|
||||
},
|
||||
"s3": {
|
||||
"s3SchemaVersion": "1.0",
|
||||
"configurationId": "Config",
|
||||
"bucket": {
|
||||
"name": "images",
|
||||
"ownerIdentity": {
|
||||
"principalId": "AKIAIOSFODNN7EXAMPLE"
|
||||
},
|
||||
"arn": "arn:aws:s3:::images"
|
||||
},
|
||||
"object": {
|
||||
"key": "myphoto.jpg",
|
||||
"size": 6474,
|
||||
"eTag": "430f89010c77aa34fc8760696da62d08-1",
|
||||
"contentType": "image/jpeg",
|
||||
"userMetadata": {
|
||||
"content-type": "image/jpeg"
|
||||
},
|
||||
"versionId": "1",
|
||||
"sequencer": "15C32494527B46C5"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"host": "192.168.56.192",
|
||||
"port": "",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<a name="webhooks"></a>
|
||||
|
||||
@@ -5,7 +5,7 @@ This document explains some basic assumptions and design approach, limits of the
|
||||
## Command-line
|
||||
|
||||
```
|
||||
minio server -h
|
||||
minio gateway <name> -h
|
||||
...
|
||||
...
|
||||
CACHE:
|
||||
@@ -16,17 +16,18 @@ minio server -h
|
||||
...
|
||||
...
|
||||
|
||||
7. Start minio server with edge caching enabled on '/mnt/drive1', '/mnt/drive2' and '/mnt/export1 ... /mnt/export24',
|
||||
7. Start MinIO gateway to s3 with edge caching enabled on '/mnt/drive1', '/mnt/drive2' and '/mnt/export1 ... /mnt/export24',
|
||||
exclude all objects under 'mybucket', exclude all objects with '.pdf' as extension
|
||||
with expiry up to 40 days.
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/export{1..24}"
|
||||
$ export MINIO_CACHE_EXCLUDE="mybucket/*;*.pdf"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ minio server /home/shared
|
||||
$ minio gateway s3
|
||||
```
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Disk cache size defaults to 80% of your drive capacity.
|
||||
- The cache drives are required to be a filesystem mount point with [`atime`](http://kerolasa.github.io/filetimes.html) support to be enabled on the drive. Alternatively writable directories with atime support can be specified in MINIO_CACHE_DRIVES
|
||||
- Expiration of each cached entry takes user provided expiry as a hint, and defaults to 90 days if not provided.
|
||||
@@ -34,22 +35,27 @@ minio server -h
|
||||
- An object is only cached when drive has sufficient disk space.
|
||||
|
||||
## Behavior
|
||||
|
||||
Disk caching caches objects for **downloaded** objects i.e
|
||||
|
||||
- Caches new objects for entries not found in cache while downloading. Otherwise serves from the cache.
|
||||
- Bitrot protection is added to cached content and verified when object is served from cache.
|
||||
- When an object is deleted, corresponding entry in cache if any is deleted as well.
|
||||
- Cache continues to work for read-only operations such as GET, HEAD when backend is offline.
|
||||
- Cache-Control and Expires headers can be used to control how long objects stay in the cache
|
||||
- Cache-Control and Expires headers can be used to control how long objects stay in the cache. ETag of cached objects are not validated with backend until expiry time as per the Cache-Control or Expires header is met.
|
||||
- To ensure security guarantees, encrypted objects are normally not cached. However, if you wish to encrypt cached content on disk, you can set MINIO_CACHE_ENCRYPTION_MASTER_KEY environment variable to set a cache KMS
|
||||
master key to automatically encrypt all cached content.
|
||||
|
||||
Note that cache KMS master key is not recommended for use in production deployments. If the MinIO server/gateway machine is ever compromised, the cache KMS master key must also be treated as compromised.
|
||||
Support for external KMS to manage cache KMS keys is on the roadmap,and would be ideal for production use cases.
|
||||
|
||||
> NOTE: Expiration happens automatically based on the configured interval as explained above, frequently accessed objects stay alive in cache for a significantly longer time.
|
||||
|
||||
### Crash Recovery
|
||||
Upon restart of minio server after a running minio process is killed or crashes, disk caching resumes automatically. The garbage collection cycle resumes and any previously cached entries are served from cache.
|
||||
|
||||
Upon restart of minio gateway after a running minio process is killed or crashes, disk caching resumes automatically. The garbage collection cycle resumes and any previously cached entries are served from cache.
|
||||
|
||||
## Limits
|
||||
|
||||
- Bucket policies are not cached, so anonymous operations are not supported when backend is offline.
|
||||
- Objects are distributed using deterministic hashing among the list of configured cache drives. If one or more drives go offline, or cache drive configuration is altered in any way, performance may degrade to a linear lookup time depending on the number of disks in cache.
|
||||
|
||||
## TODO
|
||||
- Encrypt cached objects automatically with a cache encryption master key
|
||||
@@ -8,42 +8,29 @@ Disk caching feature here refers to the use of caching disks to store content cl
|
||||
## Get started
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
Install MinIO - [MinIO Quickstart Guide](https://docs.min.io/docs/minio-quickstart-guide).
|
||||
|
||||
### 2. Run MinIO with cache
|
||||
Disk caching can be enabled by updating the `cache` config settings for MinIO server. Config `cache` settings takes the mounted drive(s) or directory paths, cache expiry duration (in days) and any wildcard patterns to exclude from being cached.
|
||||
### 2. Run MinIO gateway with cache
|
||||
|
||||
```json
|
||||
"cache": {
|
||||
"drives": ["/mnt/drive1", "/mnt/drive2", "/mnt/drive3"],
|
||||
"expiry": 90,
|
||||
"exclude": ["*.pdf","mybucket/*"],
|
||||
"maxuse" : 70,
|
||||
},
|
||||
```
|
||||
Disk caching can be enabled by setting the `cache` environment variables for MinIO gateway . `cache` environment variables takes the mounted drive(s) or directory paths, cache expiry duration (in days) and any wildcard patterns to exclude from being cached.
|
||||
|
||||
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio cluster in json format, and save it locally.
|
||||
```sh
|
||||
$ mc admin config get myminio/ > /tmp/myconfig
|
||||
```
|
||||
After updating the cache configuration in /tmp/myconfig , use `mc admin config set` command to update the configuration for the cluster.Restart the MinIO server to put the changes into effect.
|
||||
```sh
|
||||
$ mc admin config set myminio < /tmp/myconfig
|
||||
```
|
||||
The cache settings may also be set through environment variables. When set, environment variables override any `cache` config settings for MinIO server. Following example uses `/mnt/drive1`, `/mnt/drive2` ,`/mnt/cache1` ... `/mnt/cache3` for caching, with expiry up to 90 days while excluding all objects under bucket `mybucket` and all objects with '.pdf' as extension while starting a standalone erasure coded setup. Cache max usage is restricted to 80% of disk capacity in this example.
|
||||
Following example uses `/mnt/drive1`, `/mnt/drive2` ,`/mnt/cache1` ... `/mnt/cache3` for caching, with expiry up to 90 days while excluding all objects under bucket `mybucket` and all objects with '.pdf' as extension while starting a s3 gateway setup. Cache max usage is restricted to 80% of disk capacity in this example.
|
||||
|
||||
```bash
|
||||
export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/cache{1...3}"
|
||||
export MINIO_CACHE_EXPIRY=90
|
||||
export MINIO_CACHE_EXCLUDE="*.pdf;mybucket/*"
|
||||
export MINIO_CACHE_MAXUSE=80
|
||||
minio server /export{1...24}
|
||||
minio gateway s3
|
||||
```
|
||||
|
||||
### 3. Test your setup
|
||||
To test this setup, access the MinIO server via browser or [`mc`](https://docs.min.io/docs/minio-client-quickstart-guide). You’ll see the uploaded files are accessible from the all the MinIO endpoints.
|
||||
|
||||
To test this setup, access the MinIO gateway via browser or [`mc`](https://docs.min.io/docs/minio-client-quickstart-guide). You’ll see the uploaded files are accessible from all the MinIO endpoints.
|
||||
|
||||
# Explore Further
|
||||
|
||||
- [Disk cache design](https://github.com/minio/minio/blob/master/docs/disk-caching/DESIGN.md)
|
||||
- [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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user