mirror of
https://github.com/pgsty/minio.git
synced 2026-07-31 07:45:19 +03:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8766c5eb22 | |||
| e0d22359e7 | |||
| 6dd13e68c2 | |||
| 633001c8ba | |||
| e23a42305c | |||
| 63d2583e91 | |||
| a2f66abbe8 | |||
| b28661b673 | |||
| e8791ae274 | |||
| 309975d477 | |||
| 6571641735 | |||
| 8757c963ba | |||
| 9a71f2fdfa | |||
| 9c26fe47b0 | |||
| f3f47d8cd3 | |||
| de1d39e436 | |||
| ed1275a063 | |||
| a7d407fa42 | |||
| 4e6e05f8e0 | |||
| b0deea27df | |||
| e98d89274f | |||
| c59206bcd3 | |||
| 7f2d439baa | |||
| 5a80cbec2a | |||
| 2d19011a1d | |||
| e82dcd195c | |||
| fcb56d864c | |||
| 75cd4201b0 | |||
| f24c017e9a | |||
| b5280ba243 | |||
| 2a0e4b6f58 | |||
| 1898961ce3 |
+6
-4
@@ -16,22 +16,24 @@ matrix:
|
||||
sudo: required
|
||||
env:
|
||||
- ARCH=x86_64
|
||||
go: 1.10.4
|
||||
- CGO_ENABLED=0
|
||||
go: 1.11.4
|
||||
script:
|
||||
- make
|
||||
- diff -au <(gofmt -s -d cmd) <(printf "")
|
||||
- diff -au <(gofmt -s -d pkg) <(printf "")
|
||||
- make test GOFLAGS="-timeout 15m -race -v"
|
||||
- for d in $(go list ./... | grep -v browser); do CGO_ENABLED=1 go test -v -race --timeout 15m "$d"; done
|
||||
- make verify
|
||||
- make coverage
|
||||
- cd browser && yarn && yarn test && cd ..
|
||||
- os: windows
|
||||
env:
|
||||
- ARCH=x86_64
|
||||
go: 1.10.4
|
||||
- CGO_ENABLED=0
|
||||
go: 1.11.4
|
||||
script:
|
||||
- go build --ldflags="$(go run buildscripts/gen-ldflags.go)" -o %GOPATH%\bin\minio.exe
|
||||
- for d in $(go list ./... | grep -v browser); do go test -v -race --timeout 20m "$d"; done
|
||||
- for d in $(go list ./... | grep -v browser); do CGO_ENABLED=1 go test -v -race --timeout 20m "$d"; done
|
||||
- bash buildscripts/go-coverage.sh
|
||||
|
||||
before_script:
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.10.4-alpine3.7
|
||||
FROM golang:1.11.4-alpine3.7
|
||||
|
||||
LABEL maintainer="Minio Inc <dev@minio.io>"
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ spelling:
|
||||
check: test
|
||||
test: verifiers build
|
||||
@echo "Running unit tests"
|
||||
@go test $(GOFLAGS) -tags kqueue ./...
|
||||
@CGO_ENABLED=0 go test -tags kqueue ./...
|
||||
|
||||
verify: build
|
||||
@echo "Verifying build"
|
||||
@@ -73,7 +73,7 @@ coverage: build
|
||||
# Builds minio locally.
|
||||
build: checks
|
||||
@echo "Building minio binary to './minio'"
|
||||
@CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
||||
@GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
||||
|
||||
docker: build
|
||||
@docker build -t $(TAG) . -f Dockerfile.dev
|
||||
|
||||
@@ -53,6 +53,15 @@ chmod +x minio
|
||||
./minio server /data
|
||||
```
|
||||
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|GNU/Linux|ppc64le|https://dl.minio.io/server/minio/release/linux-ppc64le/minio |
|
||||
```sh
|
||||
wget https://dl.minio.io/server/minio/release/linux-ppc64le/minio
|
||||
chmod +x minio
|
||||
./minio server /data
|
||||
```
|
||||
|
||||
## Microsoft Windows
|
||||
### Binary Download
|
||||
| Platform| Architecture | URL|
|
||||
|
||||
+10
-9
@@ -13,8 +13,7 @@
|
||||
"setupTestFrameworkScriptFile": "./app/js/jest/setup.js",
|
||||
"testURL": "https://localhost:8080",
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
||||
"<rootDir>/app/js/jest/__mocks__/fileMock.js",
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/app/js/jest/__mocks__/fileMock.js",
|
||||
"\\.(css|scss)$": "identity-obj-proxy"
|
||||
}
|
||||
},
|
||||
@@ -40,29 +39,31 @@
|
||||
"babel-preset-es2015": "^6.14.0",
|
||||
"babel-preset-react": "^6.11.1",
|
||||
"babel-register": "^6.14.0",
|
||||
"copy-webpack-plugin": "^0.3.3",
|
||||
"copy-webpack-plugin": "^4.6.0",
|
||||
"css-loader": "^0.23.1",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"esformatter": "^0.10.0",
|
||||
"esformatter-jsx": "^7.4.1",
|
||||
"esformatter-jsx-ignore": "^1.0.6",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"jest": "^22.1.4",
|
||||
"jest-enzyme": "^4.0.2",
|
||||
"json-loader": "^0.5.4",
|
||||
"less": "^2.7.1",
|
||||
"less-loader": "^2.2.3",
|
||||
"purifycss-webpack-plugin": "^2.0.3",
|
||||
"less": "^3.9.0",
|
||||
"less-loader": "^4.1.0",
|
||||
"purgecss-webpack-plugin": "^1.4.0",
|
||||
"style-loader": "^0.13.1",
|
||||
"url-loader": "^0.5.7",
|
||||
"webpack-dev-server": "^2.11.1"
|
||||
"webpack-cli": "^3.2.0",
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^3.3.6",
|
||||
"classnames": "^2.2.3",
|
||||
"expect": "^1.20.2",
|
||||
"font-awesome": "^4.7.0",
|
||||
"glob-all": "^3.1.0",
|
||||
"history": "^4.7.2",
|
||||
"humanize": "0.0.9",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
@@ -89,6 +90,6 @@
|
||||
"reselect": "^3.0.1",
|
||||
"superagent": "^3.8.2",
|
||||
"superagent-es6-promise": "^1.0.0",
|
||||
"webpack": "^3.10.0"
|
||||
"webpack": "^4.28.3"
|
||||
}
|
||||
}
|
||||
|
||||
+42
-34
File diff suppressed because one or more lines are too long
@@ -16,11 +16,13 @@
|
||||
|
||||
var webpack = require('webpack')
|
||||
var path = require('path')
|
||||
var glob = require('glob-all')
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
var purify = require("purifycss-webpack-plugin")
|
||||
var PurgecssPlugin = require('purgecss-webpack-plugin')
|
||||
|
||||
var exports = {
|
||||
context: __dirname,
|
||||
mode: 'development',
|
||||
entry: [
|
||||
path.resolve(__dirname, 'app/index.js')
|
||||
],
|
||||
@@ -99,12 +101,11 @@ var exports = {
|
||||
{from: 'app/index.html'}
|
||||
]),
|
||||
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
|
||||
new purify({
|
||||
basePath: __dirname,
|
||||
paths: [
|
||||
"app/index.html",
|
||||
"app/js/*.js"
|
||||
]
|
||||
new PurgecssPlugin({
|
||||
paths: glob.sync([
|
||||
path.join(__dirname, 'app/index.html'),
|
||||
path.join(__dirname, 'app/js/*.js')
|
||||
])
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
|
||||
var webpack = require('webpack')
|
||||
var path = require('path')
|
||||
var glob = require('glob-all')
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
var purify = require("purifycss-webpack-plugin")
|
||||
var PurgecssPlugin = require('purgecss-webpack-plugin')
|
||||
|
||||
var exports = {
|
||||
context: __dirname,
|
||||
mode: 'production',
|
||||
entry: [
|
||||
path.resolve(__dirname, 'app/index.js')
|
||||
],
|
||||
@@ -74,16 +76,12 @@ var exports = {
|
||||
{from: 'app/img/logo.svg'},
|
||||
{from: 'app/index.html'}
|
||||
]),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
}),
|
||||
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
|
||||
new purify({
|
||||
basePath: __dirname,
|
||||
paths: [
|
||||
"app/index.html",
|
||||
"app/js/*.js"
|
||||
]
|
||||
new PurgecssPlugin({
|
||||
paths: glob.sync([
|
||||
path.join(__dirname, 'app/index.html'),
|
||||
path.join(__dirname, 'app/js/*.js')
|
||||
])
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
+2532
-1059
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ set -e
|
||||
echo "" > coverage.txt
|
||||
|
||||
for d in $(go list ./... | grep -v browser); do
|
||||
go test -v -coverprofile=profile.out -covermode=atomic "$d"
|
||||
CGO_ENABLED=0 go test -v -coverprofile=profile.out -covermode=atomic "$d"
|
||||
if [ -f profile.out ]; then
|
||||
cat profile.out >> coverage.txt
|
||||
rm profile.out
|
||||
|
||||
+261
-131
@@ -17,30 +17,32 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/cpu"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/mem"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -94,6 +96,11 @@ func (a adminAPIHandlers) VersionHandler(w http.ResponseWriter, r *http.Request)
|
||||
func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ServiceStatus")
|
||||
|
||||
if globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
@@ -106,13 +113,8 @@ func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Re
|
||||
CommitID: CommitID,
|
||||
}
|
||||
|
||||
// Fetch uptimes from all peers. This may fail to due to lack
|
||||
// of read-quorum availability.
|
||||
uptime, err := getPeerUptimes(globalAdminPeers)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
// Fetch uptimes from all peers and pick the latest.
|
||||
uptime := getPeerUptimes(globalNotificationSys.ServerInfo(ctx))
|
||||
|
||||
// Create API response
|
||||
serverStatus := madmin.ServiceStatus{
|
||||
@@ -126,6 +128,7 @@ func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Reply with storage information (across nodes in a
|
||||
// distributed setup) as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
@@ -139,6 +142,11 @@ func (a adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *http.Re
|
||||
func (a adminAPIHandlers) ServiceStopNRestartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ServiceStopNRestart")
|
||||
|
||||
if globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
@@ -148,7 +156,6 @@ func (a adminAPIHandlers) ServiceStopNRestartHandler(w http.ResponseWriter, r *h
|
||||
var sa madmin.ServiceAction
|
||||
err := json.NewDecoder(r.Body).Decode(&sa)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, ErrRequestBodyParse, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -168,7 +175,15 @@ func (a adminAPIHandlers) ServiceStopNRestartHandler(w http.ResponseWriter, r *h
|
||||
// Reply to the client before restarting minio server.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
|
||||
sendServiceCmd(globalAdminPeers, serviceSig)
|
||||
// Notify all other Minio peers signal service.
|
||||
for _, nerr := range globalNotificationSys.SignalService(serviceSig) {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
|
||||
globalServiceSignalCh <- serviceSig
|
||||
}
|
||||
|
||||
// ServerProperties holds some server information such as, version, region
|
||||
@@ -232,6 +247,12 @@ type ServerInfo struct {
|
||||
func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ServerInfo")
|
||||
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate request
|
||||
|
||||
// Setting the region as empty so as the mc server info command is irrespective to the region.
|
||||
@@ -241,39 +262,33 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
// Web service response
|
||||
reply := make([]ServerInfo, len(globalAdminPeers))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Gather server information for all nodes
|
||||
for i, p := range globalAdminPeers {
|
||||
wg.Add(1)
|
||||
|
||||
// Gather information from a peer in a goroutine
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
|
||||
// Initialize server info at index
|
||||
reply[idx] = ServerInfo{Addr: peer.addr}
|
||||
|
||||
serverInfoData, err := peer.cmdRunner.ServerInfo()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", peer.addr)
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
reply[idx].Error = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
reply[idx].Data = &serverInfoData
|
||||
}(i, p)
|
||||
thisAddr, err := xnet.ParseHost(GetLocalPeer(globalEndpoints))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
serverInfo := globalNotificationSys.ServerInfo(ctx)
|
||||
// Once we have received all the ServerInfo from peers
|
||||
// add the local peer server info as well.
|
||||
serverInfo = append(serverInfo, ServerInfo{
|
||||
Addr: thisAddr.String(),
|
||||
Data: &ServerInfoData{
|
||||
StorageInfo: objectAPI.StorageInfo(ctx),
|
||||
ConnStats: globalConnStats.toServerConnStats(),
|
||||
HTTPStats: globalHTTPStats.toServerHTTPStats(),
|
||||
Properties: ServerProperties{
|
||||
Uptime: UTCNow().Sub(globalBootTime),
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
SQSARN: globalNotificationSys.GetARNList(),
|
||||
Region: globalServerConfig.GetRegion(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(reply)
|
||||
jsonBytes, err := json.Marshal(serverInfo)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
@@ -284,6 +299,122 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
}
|
||||
|
||||
// ServerDrivesPerfInfo holds informantion 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.
|
||||
type ServerCPULoadInfo struct {
|
||||
Addr string `json:"addr"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Load []cpu.Load `json:"load"`
|
||||
}
|
||||
|
||||
// ServerMemUsageInfo holds informantion about memory utilization
|
||||
// of one minio node. It also reports any errors if encountered
|
||||
// while trying to reach this server.
|
||||
type ServerMemUsageInfo struct {
|
||||
Addr string `json:"addr"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Usage []mem.Usage `json:"usage"`
|
||||
}
|
||||
|
||||
// PerfInfoHandler - GET /minio/admin/v1/performance?perfType={perfType}
|
||||
// ----------
|
||||
// Get all performance information based on input type
|
||||
// Supported types = drive
|
||||
func (a adminAPIHandlers) PerfInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PerfInfo")
|
||||
|
||||
// Get object layer instance.
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate request
|
||||
// Setting the region as empty so as the mc server info command is irrespective to the region.
|
||||
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
perfType := vars["perfType"]
|
||||
|
||||
if perfType == "drive" {
|
||||
info := objLayer.StorageInfo(ctx)
|
||||
if !(info.Backend.Type == BackendFS || info.Backend.Type == BackendErasure) {
|
||||
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
// Get drive performance details from local server's drive(s)
|
||||
dp := localEndpointsDrivePerf(globalEndpoints)
|
||||
|
||||
// Notify all other Minio peers to report drive performance numbers
|
||||
dps := globalNotificationSys.DrivePerfInfo()
|
||||
dps = append(dps, dp)
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(dps)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Reply with performance information (across nodes in a
|
||||
// distributed setup) as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
} else if perfType == "cpu" {
|
||||
// Get CPU load details from local server's cpu(s)
|
||||
cpu := localEndpointsCPULoad(globalEndpoints)
|
||||
// Notify all other Minio peers to report cpu load numbers
|
||||
cpus := globalNotificationSys.CPULoadInfo()
|
||||
cpus = append(cpus, cpu)
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(cpus)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Reply with cpu load information (across nodes in a
|
||||
// distributed setup) as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
} else if perfType == "mem" {
|
||||
// Get mem usage details from local server(s)
|
||||
m := localEndpointsMemUsage(globalEndpoints)
|
||||
// Notify all other Minio peers to report mem usage numbers
|
||||
mems := globalNotificationSys.MemUsageInfo()
|
||||
mems = append(mems, m)
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(mems)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Reply with mem usage information (across nodes in a
|
||||
// distributed setup) as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
} else {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// StartProfilingResult contains the status of the starting
|
||||
// profiling action in a given server
|
||||
type StartProfilingResult struct {
|
||||
@@ -298,6 +429,11 @@ type StartProfilingResult struct {
|
||||
func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "StartProfiling")
|
||||
|
||||
if globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
@@ -307,31 +443,53 @@ func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.R
|
||||
vars := mux.Vars(r)
|
||||
profiler := vars["profilerType"]
|
||||
|
||||
startProfilingResult := make([]StartProfilingResult, len(globalAdminPeers))
|
||||
|
||||
// Call StartProfiling function on all nodes and save results
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range globalAdminPeers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
result := StartProfilingResult{NodeName: peer.addr}
|
||||
if err := peer.cmdRunner.StartProfiling(profiler); err != nil {
|
||||
result.Error = err.Error()
|
||||
return
|
||||
}
|
||||
result.Success = true
|
||||
startProfilingResult[idx] = result
|
||||
}(i, peer)
|
||||
thisAddr, err := xnet.ParseHost(GetLocalPeer(globalEndpoints))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Start profiling on remote servers.
|
||||
hostErrs := globalNotificationSys.StartProfiling(profiler)
|
||||
|
||||
// Start profiling locally as well.
|
||||
{
|
||||
if globalProfiler != nil {
|
||||
globalProfiler.Stop()
|
||||
}
|
||||
prof, err := startProfiler(profiler, "")
|
||||
if err != nil {
|
||||
hostErrs = append(hostErrs, NotificationPeerErr{
|
||||
Host: *thisAddr,
|
||||
Err: err,
|
||||
})
|
||||
} else {
|
||||
globalProfiler = prof
|
||||
hostErrs = append(hostErrs, NotificationPeerErr{
|
||||
Host: *thisAddr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var startProfilingResult []StartProfilingResult
|
||||
|
||||
for _, nerr := range hostErrs {
|
||||
result := StartProfilingResult{NodeName: nerr.Host.String()}
|
||||
if nerr.Err != nil {
|
||||
result.Error = nerr.Err.Error()
|
||||
} else {
|
||||
result.Success = true
|
||||
}
|
||||
startProfilingResult = append(startProfilingResult, result)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Create JSON result and send it to the client
|
||||
startProfilingResultInBytes, err := json.Marshal(startProfilingResult)
|
||||
if err != nil {
|
||||
writeCustomErrorResponseJSON(w, http.StatusInternalServerError, err.Error(), r.URL)
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, []byte(startProfilingResultInBytes))
|
||||
}
|
||||
|
||||
@@ -359,51 +517,18 @@ func (f dummyFileInfo) Sys() interface{} { return f.sys }
|
||||
func (a adminAPIHandlers) DownloadProfilingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DownloadProfiling")
|
||||
|
||||
if globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
adminAPIErr := checkAdminRequestAuthType(ctx, r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
profilingDataFound := false
|
||||
|
||||
// Initialize a zip writer which will provide a zipped content
|
||||
// of profiling data of all nodes
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for i, peer := range globalAdminPeers {
|
||||
// Get profiling data from a node
|
||||
data, err := peer.cmdRunner.DownloadProfilingData()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to download profiling data from node `%s`, reason: %s", peer.addr, err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
profilingDataFound = true
|
||||
|
||||
// Send profiling data to zip as file
|
||||
header, err := zip.FileInfoHeader(dummyFileInfo{
|
||||
name: fmt.Sprintf("profiling-%d", i),
|
||||
size: int64(len(data)),
|
||||
mode: 0600,
|
||||
modTime: time.Now().UTC(),
|
||||
isDir: false,
|
||||
sys: nil,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, err = io.Copy(writer, bytes.NewBuffer(data)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !profilingDataFound {
|
||||
if !globalNotificationSys.DownloadProfilingData(ctx, w) {
|
||||
writeErrorResponseJSON(w, ErrAdminProfilerNotEnabled, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -833,6 +958,11 @@ func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "SetUserStatus")
|
||||
|
||||
if globalNotificationSys == nil || globalIAMSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
@@ -869,10 +999,10 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -883,7 +1013,7 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -937,10 +1067,10 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -951,7 +1081,7 @@ func (a adminAPIHandlers) ListCannedPolicies(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalIAMSys == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -983,7 +1113,7 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalIAMSys == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1010,10 +1140,10 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1024,7 +1154,7 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalIAMSys == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1075,10 +1205,10 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1089,7 +1219,7 @@ func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalIAMSys == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1122,10 +1252,10 @@ func (a adminAPIHandlers) SetUserPolicy(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload users
|
||||
for host, err := range globalNotificationSys.LoadUsers() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1136,7 +1266,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1353,7 +1483,7 @@ func (a adminAPIHandlers) UpdateAdminCredentialsHandler(w http.ResponseWriter,
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1416,10 +1546,10 @@ func (a adminAPIHandlers) UpdateAdminCredentialsHandler(w http.ResponseWriter,
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to update credentials
|
||||
for host, err := range globalNotificationSys.LoadCredentials() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
for _, nerr := range globalNotificationSys.LoadCredentials() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,8 @@ var (
|
||||
"username": "",
|
||||
"password": "",
|
||||
"reconnectInterval": 0,
|
||||
"keepAliveInterval": 0
|
||||
"keepAliveInterval": 0,
|
||||
"queueDir": ""
|
||||
}
|
||||
},
|
||||
"mysql": {
|
||||
@@ -533,7 +534,6 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
||||
// single node setup, this degenerates to a simple function
|
||||
// call under the hood.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
@@ -607,7 +607,6 @@ func TestServiceSetCreds(t *testing.T) {
|
||||
// single node setup, this degenerates to a simple function
|
||||
// call under the hood.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
credentials := globalServerConfig.GetCredential()
|
||||
|
||||
@@ -706,7 +705,6 @@ func TestGetConfigHandler(t *testing.T) {
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
// Prepare query params for get-config mgmt REST API.
|
||||
queryVal := url.Values{}
|
||||
@@ -735,7 +733,6 @@ func TestSetConfigHandler(t *testing.T) {
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
// Prepare query params for set-config mgmt REST API.
|
||||
queryVal := url.Values{}
|
||||
@@ -807,7 +804,6 @@ func TestAdminServerInfo(t *testing.T) {
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
// Prepare query params for set-config mgmt REST API.
|
||||
queryVal := url.Values{}
|
||||
|
||||
@@ -133,7 +133,7 @@ func (ahs *allHealState) periodicHealSeqsClean() {
|
||||
}
|
||||
}
|
||||
ahs.Unlock()
|
||||
case <-globalServiceDoneCh:
|
||||
case <-GlobalServiceDoneCh:
|
||||
// server could be restarting - need
|
||||
// to exit immediately
|
||||
return
|
||||
@@ -641,10 +641,10 @@ func (h *healSequence) healDiskFormat() error {
|
||||
// Healing succeeded notify the peers to reload format and re-initialize disks.
|
||||
// We will not notify peers only if healing succeeded.
|
||||
if err == nil {
|
||||
for host, rerr := range globalNotificationSys.ReloadFormat(h.settings.DryRun) {
|
||||
if rerr != nil {
|
||||
logger.GetReqInfo(h.ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(h.ctx, rerr)
|
||||
for _, nerr := range globalNotificationSys.ReloadFormat(h.settings.DryRun) {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(h.ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(h.ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,12 @@ func registerAdminRouter(router *mux.Router, enableIAM bool) {
|
||||
adminV1Router.Methods(http.MethodPost).Path("/heal/").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||
adminV1Router.Methods(http.MethodPost).Path("/heal/{bucket}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||
adminV1Router.Methods(http.MethodPost).Path("/heal/{bucket}/{prefix:.*}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||
|
||||
/// Health operations
|
||||
|
||||
}
|
||||
// Performance command - return performance details based on input type
|
||||
adminV1Router.Methods(http.MethodGet).Path("/performance").HandlerFunc(httpTraceAll(adminAPI.PerfInfoHandler)).Queries("perfType", "{perfType:.*}")
|
||||
|
||||
// Profiling operations
|
||||
adminV1Router.Methods(http.MethodPost).Path("/profiling/start").HandlerFunc(httpTraceAll(adminAPI.StartProfilingHandler)).
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2014, 2015, 2016, 2017, 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
var errUnsupportedSignal = fmt.Errorf("unsupported signal: only restart and stop signals are supported")
|
||||
|
||||
// AdminRPCClient - admin RPC client talks to admin RPC server.
|
||||
type AdminRPCClient struct {
|
||||
*RPCClient
|
||||
}
|
||||
|
||||
// SignalService - calls SignalService RPC.
|
||||
func (rpcClient *AdminRPCClient) SignalService(signal serviceSignal) (err error) {
|
||||
args := SignalServiceArgs{Sig: signal}
|
||||
reply := VoidReply{}
|
||||
|
||||
return rpcClient.Call(adminServiceName+".SignalService", &args, &reply)
|
||||
}
|
||||
|
||||
// ServerInfo - returns the server info of the server to which the RPC call is made.
|
||||
func (rpcClient *AdminRPCClient) ServerInfo() (sid ServerInfoData, err error) {
|
||||
err = rpcClient.Call(adminServiceName+".ServerInfo", &AuthArgs{}, &sid)
|
||||
return sid, err
|
||||
}
|
||||
|
||||
// StartProfiling - starts profiling in the remote server.
|
||||
func (rpcClient *AdminRPCClient) StartProfiling(profiler string) error {
|
||||
args := StartProfilingArgs{Profiler: profiler}
|
||||
reply := VoidReply{}
|
||||
return rpcClient.Call(adminServiceName+".StartProfiling", &args, &reply)
|
||||
}
|
||||
|
||||
// DownloadProfilingData - returns profiling data of the remote server.
|
||||
func (rpcClient *AdminRPCClient) DownloadProfilingData() ([]byte, error) {
|
||||
args := AuthArgs{}
|
||||
var reply []byte
|
||||
|
||||
err := rpcClient.Call(adminServiceName+".DownloadProfilingData", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// NewAdminRPCClient - returns new admin RPC client.
|
||||
func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
|
||||
scheme := "http"
|
||||
if globalIsSSL {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
serviceURL := &xnet.URL{
|
||||
Scheme: scheme,
|
||||
Host: host.String(),
|
||||
Path: adminServicePath,
|
||||
}
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if globalIsSSL {
|
||||
tlsConfig = &tls.Config{
|
||||
ServerName: host.Name,
|
||||
RootCAs: globalRootCAs,
|
||||
}
|
||||
}
|
||||
|
||||
rpcClient, err := NewRPCClient(
|
||||
RPCClientArgs{
|
||||
NewAuthTokenFunc: newAuthToken,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceName: adminServiceName,
|
||||
ServiceURL: serviceURL,
|
||||
TLSConfig: tlsConfig,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AdminRPCClient{rpcClient}, nil
|
||||
}
|
||||
|
||||
// adminCmdRunner - abstracts local and remote execution of admin
|
||||
// commands like service stop and service restart.
|
||||
type adminCmdRunner interface {
|
||||
SignalService(s serviceSignal) error
|
||||
ServerInfo() (ServerInfoData, error)
|
||||
StartProfiling(string) error
|
||||
DownloadProfilingData() ([]byte, error)
|
||||
}
|
||||
|
||||
// adminPeer - represents an entity that implements admin API RPCs.
|
||||
type adminPeer struct {
|
||||
addr string
|
||||
cmdRunner adminCmdRunner
|
||||
isLocal bool
|
||||
}
|
||||
|
||||
// type alias for a collection of adminPeer.
|
||||
type adminPeers []adminPeer
|
||||
|
||||
// makeAdminPeers - helper function to construct a collection of adminPeer.
|
||||
func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
|
||||
localAddr := GetLocalPeer(endpoints)
|
||||
if strings.HasPrefix(localAddr, "127.0.0.1:") {
|
||||
// Use first IPv4 instead of loopback address.
|
||||
localAddr = net.JoinHostPort(sortIPs(localIP4.ToSlice())[0], globalMinioPort)
|
||||
}
|
||||
if strings.HasPrefix(localAddr, "[::1]:") {
|
||||
// Use first IPv4 instead of loopback address.
|
||||
localAddr = net.JoinHostPort(localIP6.ToSlice()[0], globalMinioPort)
|
||||
}
|
||||
|
||||
adminPeerList = append(adminPeerList, adminPeer{
|
||||
addr: localAddr,
|
||||
cmdRunner: localAdminClient{},
|
||||
isLocal: true,
|
||||
})
|
||||
|
||||
for _, hostStr := range GetRemotePeers(endpoints) {
|
||||
host, err := xnet.ParseHost(hostStr)
|
||||
logger.FatalIf(err, "Unable to parse Admin RPC Host")
|
||||
rpcClient, err := NewAdminRPCClient(host)
|
||||
logger.FatalIf(err, "Unable to initialize Admin RPC Client")
|
||||
adminPeerList = append(adminPeerList, adminPeer{
|
||||
addr: hostStr,
|
||||
cmdRunner: rpcClient,
|
||||
})
|
||||
}
|
||||
|
||||
return adminPeerList
|
||||
}
|
||||
|
||||
// Initialize global adminPeer collection.
|
||||
func initGlobalAdminPeers(endpoints EndpointList) {
|
||||
globalAdminPeers = makeAdminPeers(endpoints)
|
||||
}
|
||||
|
||||
// invokeServiceCmd - Invoke Restart/Stop command.
|
||||
func invokeServiceCmd(cp adminPeer, cmd serviceSignal) (err error) {
|
||||
switch cmd {
|
||||
case serviceRestart, serviceStop:
|
||||
err = cp.cmdRunner.SignalService(cmd)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// sendServiceCmd - Invoke Restart command on remote peers
|
||||
// adminPeer followed by on the local peer.
|
||||
func sendServiceCmd(cps adminPeers, cmd serviceSignal) {
|
||||
// Send service command like stop or restart to all remote nodes and finally run on local node.
|
||||
errs := make([]error, len(cps))
|
||||
var wg sync.WaitGroup
|
||||
remotePeers := cps[1:]
|
||||
for i := range remotePeers {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
// we use idx+1 because remotePeers slice is 1 position shifted w.r.t cps
|
||||
errs[idx+1] = invokeServiceCmd(remotePeers[idx], cmd)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
errs[0] = invokeServiceCmd(cps[0], cmd)
|
||||
}
|
||||
|
||||
// uptimeSlice - used to sort uptimes in chronological order.
|
||||
type uptimeSlice []struct {
|
||||
err error
|
||||
uptime time.Duration
|
||||
}
|
||||
|
||||
func (ts uptimeSlice) Len() int {
|
||||
return len(ts)
|
||||
}
|
||||
|
||||
func (ts uptimeSlice) Less(i, j int) bool {
|
||||
return ts[i].uptime < ts[j].uptime
|
||||
}
|
||||
|
||||
func (ts uptimeSlice) Swap(i, j int) {
|
||||
ts[i], ts[j] = ts[j], ts[i]
|
||||
}
|
||||
|
||||
// getPeerUptimes - returns the uptime since the last time read quorum
|
||||
// was established on success. Otherwise returns errXLReadQuorum.
|
||||
func getPeerUptimes(peers adminPeers) (time.Duration, error) {
|
||||
// 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), nil
|
||||
}
|
||||
|
||||
uptimes := make(uptimeSlice, len(peers))
|
||||
|
||||
// Get up time of all servers.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
serverInfoData, rpcErr := peer.cmdRunner.ServerInfo()
|
||||
uptimes[idx].uptime, uptimes[idx].err = serverInfoData.Properties.Uptime, rpcErr
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Sort uptimes in chronological order.
|
||||
sort.Sort(uptimes)
|
||||
|
||||
// Pick the readQuorum'th uptime in chronological order. i.e,
|
||||
// the time at which read quorum was (re-)established.
|
||||
readQuorum := len(uptimes) / 2
|
||||
validCount := 0
|
||||
latestUptime := time.Duration(0)
|
||||
for _, uptime := range uptimes {
|
||||
if uptime.err != nil {
|
||||
logger.LogIf(context.Background(), uptime.err)
|
||||
continue
|
||||
}
|
||||
|
||||
validCount++
|
||||
if validCount >= readQuorum {
|
||||
latestUptime = uptime.uptime
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Less than readQuorum "Admin.Uptime" RPC call returned
|
||||
// successfully, so read-quorum unavailable.
|
||||
if validCount < readQuorum {
|
||||
return time.Duration(0), InsufficientReadQuorum{}
|
||||
}
|
||||
|
||||
return latestUptime, nil
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
xrpc "github.com/minio/minio/cmd/rpc"
|
||||
)
|
||||
|
||||
const adminServiceName = "Admin"
|
||||
const adminServiceSubPath = "/admin"
|
||||
|
||||
var adminServicePath = path.Join(minioReservedBucketPath, adminServiceSubPath)
|
||||
|
||||
// adminRPCReceiver - Admin RPC receiver for admin RPC server.
|
||||
type adminRPCReceiver struct {
|
||||
local *localAdminClient
|
||||
}
|
||||
|
||||
// SignalServiceArgs - provides the signal argument to SignalService RPC
|
||||
type SignalServiceArgs struct {
|
||||
AuthArgs
|
||||
Sig serviceSignal
|
||||
}
|
||||
|
||||
// SignalService - Send a restart or stop signal to the service
|
||||
func (receiver *adminRPCReceiver) SignalService(args *SignalServiceArgs, reply *VoidReply) error {
|
||||
return receiver.local.SignalService(args.Sig)
|
||||
}
|
||||
|
||||
// ServerInfo - returns the server info when object layer was initialized on this server.
|
||||
func (receiver *adminRPCReceiver) ServerInfo(args *AuthArgs, reply *ServerInfoData) (err error) {
|
||||
*reply, err = receiver.local.ServerInfo()
|
||||
return err
|
||||
}
|
||||
|
||||
// StartProfilingArgs - holds the RPC argument for StartingProfiling RPC call
|
||||
type StartProfilingArgs struct {
|
||||
AuthArgs
|
||||
Profiler string
|
||||
}
|
||||
|
||||
// StartProfiling - starts profiling of this server
|
||||
func (receiver *adminRPCReceiver) StartProfiling(args *StartProfilingArgs, reply *VoidReply) error {
|
||||
return receiver.local.StartProfiling(args.Profiler)
|
||||
}
|
||||
|
||||
// DownloadProfilingData - stops and returns profiling data of this server
|
||||
func (receiver *adminRPCReceiver) DownloadProfilingData(args *AuthArgs, reply *[]byte) (err error) {
|
||||
*reply, err = receiver.local.DownloadProfilingData()
|
||||
return
|
||||
}
|
||||
|
||||
// NewAdminRPCServer - returns new admin RPC server.
|
||||
func NewAdminRPCServer() (*xrpc.Server, error) {
|
||||
rpcServer := xrpc.NewServer()
|
||||
if err := rpcServer.RegisterName(adminServiceName, &adminRPCReceiver{&localAdminClient{}}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rpcServer, nil
|
||||
}
|
||||
|
||||
// registerAdminRPCRouter - creates and registers Admin RPC server and its router.
|
||||
func registerAdminRPCRouter(router *mux.Router) {
|
||||
rpcServer, err := NewAdminRPCServer()
|
||||
logger.FatalIf(err, "Unable to initialize Lock RPC Server")
|
||||
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
|
||||
subrouter.Path(adminServiceSubPath).HandlerFunc(httpTraceHdrs(rpcServer.ServeHTTP))
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// localAdminClient and AdminRPCClient are adminCmdRunner interface compatible,
|
||||
// hence below test functions are available for both clients.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Admin RPC server, adminRPCReceiver and AdminRPCClient are
|
||||
// inter-dependent, below test functions are sufficient to test all of them.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func testAdminCmdRunnerSignalService(t *testing.T, client adminCmdRunner) {
|
||||
defer func(sigChan chan serviceSignal) { globalServiceSignalCh = sigChan }(globalServiceSignalCh)
|
||||
globalServiceSignalCh = make(chan serviceSignal, 10)
|
||||
|
||||
testCases := []struct {
|
||||
signal serviceSignal
|
||||
expectErr bool
|
||||
}{
|
||||
{serviceRestart, false},
|
||||
{serviceStop, false},
|
||||
{serviceStatus, true},
|
||||
{serviceSignal(100), true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := client.SignalService(testCase.signal)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
|
||||
tmpGlobalBootTime := globalBootTime
|
||||
tmpGlobalObjectAPI := globalObjectAPI
|
||||
tmpGlobalConnStats := globalConnStats
|
||||
tmpGlobalHTTPStats := globalHTTPStats
|
||||
tmpGlobalNotificationSys := globalNotificationSys
|
||||
defer func() {
|
||||
globalBootTime = tmpGlobalBootTime
|
||||
globalObjectAPI = tmpGlobalObjectAPI
|
||||
globalConnStats = tmpGlobalConnStats
|
||||
globalHTTPStats = tmpGlobalHTTPStats
|
||||
globalNotificationSys = tmpGlobalNotificationSys
|
||||
}()
|
||||
|
||||
endpoints := new(EndpointList)
|
||||
|
||||
notificationSys := NewNotificationSys(globalServerConfig, *endpoints)
|
||||
|
||||
testCases := []struct {
|
||||
bootTime time.Time
|
||||
objectAPI ObjectLayer
|
||||
connStats *ConnStats
|
||||
httpStats *HTTPStats
|
||||
notificationSys *NotificationSys
|
||||
expectErr bool
|
||||
}{
|
||||
{UTCNow(), &DummyObjectLayer{}, newConnStats(), newHTTPStats(), notificationSys, false},
|
||||
{time.Time{}, nil, nil, nil, nil, true},
|
||||
{UTCNow(), nil, nil, nil, nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
globalBootTime = testCase.bootTime
|
||||
globalObjectAPI = testCase.objectAPI
|
||||
globalConnStats = testCase.connStats
|
||||
globalHTTPStats = testCase.httpStats
|
||||
globalNotificationSys = testCase.notificationSys
|
||||
_, err := client.ServerInfo()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newAdminRPCHTTPServerClient(t *testing.T) (*httptest.Server, *AdminRPCClient, *serverConfig) {
|
||||
rpcServer, err := NewAdminRPCServer()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rpcServer.ServeHTTP(w, r)
|
||||
}))
|
||||
|
||||
url, err := xnet.ParseURL(httpServer.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
host, err := xnet.ParseHost(url.Host)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
prevGlobalServerConfig := globalServerConfig
|
||||
globalServerConfig = newServerConfig()
|
||||
|
||||
rpcClient, err := NewAdminRPCClient(host)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
return httpServer, rpcClient, prevGlobalServerConfig
|
||||
}
|
||||
|
||||
func TestAdminRPCClientSignalService(t *testing.T) {
|
||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
defer func() {
|
||||
globalServerConfig = prevGlobalServerConfig
|
||||
}()
|
||||
|
||||
testAdminCmdRunnerSignalService(t, rpcClient)
|
||||
}
|
||||
|
||||
func TestAdminRPCClientServerInfo(t *testing.T) {
|
||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
defer func() {
|
||||
globalServerConfig = prevGlobalServerConfig
|
||||
}()
|
||||
|
||||
testAdminCmdRunnerServerInfo(t, rpcClient)
|
||||
}
|
||||
@@ -28,8 +28,6 @@ import (
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/s3select"
|
||||
"github.com/minio/minio/pkg/s3select/format"
|
||||
)
|
||||
|
||||
// APIError structure
|
||||
@@ -1512,168 +1510,6 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
case errOperationTimedOut, context.Canceled, context.DeadlineExceeded:
|
||||
apiErr = ErrOperationTimedOut
|
||||
}
|
||||
switch err {
|
||||
case s3select.ErrBusy:
|
||||
apiErr = ErrBusy
|
||||
case s3select.ErrUnauthorizedAccess:
|
||||
apiErr = ErrUnauthorizedAccess
|
||||
case s3select.ErrExpressionTooLong:
|
||||
apiErr = ErrExpressionTooLong
|
||||
case s3select.ErrIllegalSQLFunctionArgument:
|
||||
apiErr = ErrIllegalSQLFunctionArgument
|
||||
case s3select.ErrInvalidKeyPath:
|
||||
apiErr = ErrInvalidKeyPath
|
||||
case s3select.ErrInvalidCompressionFormat:
|
||||
apiErr = ErrInvalidCompressionFormat
|
||||
case s3select.ErrInvalidFileHeaderInfo:
|
||||
apiErr = ErrInvalidFileHeaderInfo
|
||||
case s3select.ErrInvalidJSONType:
|
||||
apiErr = ErrInvalidJSONType
|
||||
case s3select.ErrInvalidQuoteFields:
|
||||
apiErr = ErrInvalidQuoteFields
|
||||
case s3select.ErrInvalidRequestParameter:
|
||||
apiErr = ErrInvalidRequestParameter
|
||||
case s3select.ErrInvalidDataType:
|
||||
apiErr = ErrInvalidDataType
|
||||
case s3select.ErrInvalidTextEncoding:
|
||||
apiErr = ErrInvalidTextEncoding
|
||||
case s3select.ErrInvalidTableAlias:
|
||||
apiErr = ErrInvalidTableAlias
|
||||
case s3select.ErrMissingRequiredParameter:
|
||||
apiErr = ErrMissingRequiredParameter
|
||||
case s3select.ErrObjectSerializationConflict:
|
||||
apiErr = ErrObjectSerializationConflict
|
||||
case s3select.ErrUnsupportedSQLOperation:
|
||||
apiErr = ErrUnsupportedSQLOperation
|
||||
case s3select.ErrUnsupportedSQLStructure:
|
||||
apiErr = ErrUnsupportedSQLStructure
|
||||
case s3select.ErrUnsupportedSyntax:
|
||||
apiErr = ErrUnsupportedSyntax
|
||||
case s3select.ErrUnsupportedRangeHeader:
|
||||
apiErr = ErrUnsupportedRangeHeader
|
||||
case s3select.ErrLexerInvalidChar:
|
||||
apiErr = ErrLexerInvalidChar
|
||||
case s3select.ErrLexerInvalidOperator:
|
||||
apiErr = ErrLexerInvalidOperator
|
||||
case s3select.ErrLexerInvalidLiteral:
|
||||
apiErr = ErrLexerInvalidLiteral
|
||||
case s3select.ErrLexerInvalidIONLiteral:
|
||||
apiErr = ErrLexerInvalidIONLiteral
|
||||
case s3select.ErrParseExpectedDatePart:
|
||||
apiErr = ErrParseExpectedDatePart
|
||||
case s3select.ErrParseExpectedKeyword:
|
||||
apiErr = ErrParseExpectedKeyword
|
||||
case s3select.ErrParseExpectedTokenType:
|
||||
apiErr = ErrParseExpectedTokenType
|
||||
case s3select.ErrParseExpected2TokenTypes:
|
||||
apiErr = ErrParseExpected2TokenTypes
|
||||
case s3select.ErrParseExpectedNumber:
|
||||
apiErr = ErrParseExpectedNumber
|
||||
case s3select.ErrParseExpectedRightParenBuiltinFunctionCall:
|
||||
apiErr = ErrParseExpectedRightParenBuiltinFunctionCall
|
||||
case s3select.ErrParseExpectedTypeName:
|
||||
apiErr = ErrParseExpectedTypeName
|
||||
case s3select.ErrParseExpectedWhenClause:
|
||||
apiErr = ErrParseExpectedWhenClause
|
||||
case s3select.ErrParseUnsupportedToken:
|
||||
apiErr = ErrParseUnsupportedToken
|
||||
case s3select.ErrParseUnsupportedLiteralsGroupBy:
|
||||
apiErr = ErrParseUnsupportedLiteralsGroupBy
|
||||
case s3select.ErrParseExpectedMember:
|
||||
apiErr = ErrParseExpectedMember
|
||||
case s3select.ErrParseUnsupportedSelect:
|
||||
apiErr = ErrParseUnsupportedSelect
|
||||
case s3select.ErrParseUnsupportedCase:
|
||||
apiErr = ErrParseUnsupportedCase
|
||||
case s3select.ErrParseUnsupportedCaseClause:
|
||||
apiErr = ErrParseUnsupportedCaseClause
|
||||
case s3select.ErrParseUnsupportedAlias:
|
||||
apiErr = ErrParseUnsupportedAlias
|
||||
case s3select.ErrParseUnsupportedSyntax:
|
||||
apiErr = ErrParseUnsupportedSyntax
|
||||
case s3select.ErrParseUnknownOperator:
|
||||
apiErr = ErrParseUnknownOperator
|
||||
case s3select.ErrParseMissingIdentAfterAt:
|
||||
apiErr = ErrParseMissingIdentAfterAt
|
||||
case s3select.ErrParseUnexpectedOperator:
|
||||
apiErr = ErrParseUnexpectedOperator
|
||||
case s3select.ErrParseUnexpectedTerm:
|
||||
apiErr = ErrParseUnexpectedTerm
|
||||
case s3select.ErrParseUnexpectedToken:
|
||||
apiErr = ErrParseUnexpectedToken
|
||||
case s3select.ErrParseUnexpectedKeyword:
|
||||
apiErr = ErrParseUnexpectedKeyword
|
||||
case s3select.ErrParseExpectedExpression:
|
||||
apiErr = ErrParseExpectedExpression
|
||||
case s3select.ErrParseExpectedLeftParenAfterCast:
|
||||
apiErr = ErrParseExpectedLeftParenAfterCast
|
||||
case s3select.ErrParseExpectedLeftParenValueConstructor:
|
||||
apiErr = ErrParseExpectedLeftParenValueConstructor
|
||||
case s3select.ErrParseExpectedLeftParenBuiltinFunctionCall:
|
||||
apiErr = ErrParseExpectedLeftParenBuiltinFunctionCall
|
||||
case s3select.ErrParseExpectedArgumentDelimiter:
|
||||
apiErr = ErrParseExpectedArgumentDelimiter
|
||||
case s3select.ErrParseCastArity:
|
||||
apiErr = ErrParseCastArity
|
||||
case s3select.ErrParseInvalidTypeParam:
|
||||
apiErr = ErrParseInvalidTypeParam
|
||||
case s3select.ErrParseEmptySelect:
|
||||
apiErr = ErrParseEmptySelect
|
||||
case s3select.ErrParseSelectMissingFrom:
|
||||
apiErr = ErrParseSelectMissingFrom
|
||||
case s3select.ErrParseExpectedIdentForGroupName:
|
||||
apiErr = ErrParseExpectedIdentForGroupName
|
||||
case s3select.ErrParseExpectedIdentForAlias:
|
||||
apiErr = ErrParseExpectedIdentForAlias
|
||||
case s3select.ErrParseUnsupportedCallWithStar:
|
||||
apiErr = ErrParseUnsupportedCallWithStar
|
||||
case s3select.ErrParseNonUnaryAgregateFunctionCall:
|
||||
apiErr = ErrParseNonUnaryAgregateFunctionCall
|
||||
case s3select.ErrParseMalformedJoin:
|
||||
apiErr = ErrParseMalformedJoin
|
||||
case s3select.ErrParseExpectedIdentForAt:
|
||||
apiErr = ErrParseExpectedIdentForAt
|
||||
case s3select.ErrParseAsteriskIsNotAloneInSelectList:
|
||||
apiErr = ErrParseAsteriskIsNotAloneInSelectList
|
||||
case s3select.ErrParseCannotMixSqbAndWildcardInSelectList:
|
||||
apiErr = ErrParseCannotMixSqbAndWildcardInSelectList
|
||||
case s3select.ErrParseInvalidContextForWildcardInSelectList:
|
||||
apiErr = ErrParseInvalidContextForWildcardInSelectList
|
||||
case s3select.ErrIncorrectSQLFunctionArgumentType:
|
||||
apiErr = ErrIncorrectSQLFunctionArgumentType
|
||||
case s3select.ErrValueParseFailure:
|
||||
apiErr = ErrValueParseFailure
|
||||
case s3select.ErrIntegerOverflow:
|
||||
apiErr = ErrIntegerOverflow
|
||||
case s3select.ErrLikeInvalidInputs:
|
||||
apiErr = ErrLikeInvalidInputs
|
||||
case s3select.ErrCastFailed:
|
||||
apiErr = ErrCastFailed
|
||||
case s3select.ErrInvalidCast:
|
||||
apiErr = ErrInvalidCast
|
||||
case s3select.ErrEvaluatorInvalidTimestampFormatPattern:
|
||||
apiErr = ErrEvaluatorInvalidTimestampFormatPattern
|
||||
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing:
|
||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing
|
||||
case s3select.ErrEvaluatorTimestampFormatPatternDuplicateFields:
|
||||
apiErr = ErrEvaluatorTimestampFormatPatternDuplicateFields
|
||||
case s3select.ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch:
|
||||
apiErr = ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch
|
||||
case s3select.ErrEvaluatorUnterminatedTimestampFormatPatternToken:
|
||||
apiErr = ErrEvaluatorUnterminatedTimestampFormatPatternToken
|
||||
case s3select.ErrEvaluatorInvalidTimestampFormatPatternToken:
|
||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternToken
|
||||
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbol:
|
||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
||||
case s3select.ErrEvaluatorBindingDoesNotExist:
|
||||
apiErr = ErrEvaluatorBindingDoesNotExist
|
||||
case s3select.ErrMissingHeaders:
|
||||
apiErr = ErrMissingHeaders
|
||||
case format.ErrParseInvalidPathComponent:
|
||||
apiErr = ErrMissingHeaders
|
||||
case format.ErrInvalidColumnIndex:
|
||||
apiErr = ErrInvalidColumnIndex
|
||||
}
|
||||
|
||||
// Compression errors
|
||||
switch err {
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ func mustGetRequestID(t time.Time) string {
|
||||
|
||||
// Write http common headers
|
||||
func setCommonHeaders(w http.ResponseWriter) {
|
||||
w.Header().Set("Server", globalServerUserAgent)
|
||||
w.Header().Set("Server", "Minio/"+ReleaseTag)
|
||||
// Set `x-amz-bucket-region` only if region is set on the server
|
||||
// by default minio uses an empty region.
|
||||
if region := globalServerConfig.GetRegion(); region != "" {
|
||||
|
||||
+6
-1
@@ -26,14 +26,19 @@ import (
|
||||
type objectAPIHandlers struct {
|
||||
ObjectAPI func() ObjectLayer
|
||||
CacheAPI func() CacheObjectLayer
|
||||
// Returns true of handlers should interpret encryption.
|
||||
EncryptionEnabled func() bool
|
||||
}
|
||||
|
||||
// registerAPIRouter - registers S3 compatible APIs.
|
||||
func registerAPIRouter(router *mux.Router) {
|
||||
func registerAPIRouter(router *mux.Router, encryptionEnabled bool) {
|
||||
// Initialize API.
|
||||
api := objectAPIHandlers{
|
||||
ObjectAPI: newObjectLayerFn,
|
||||
CacheAPI: newCacheObjectsFn,
|
||||
EncryptionEnabled: func() bool {
|
||||
return encryptionEnabled
|
||||
},
|
||||
}
|
||||
|
||||
// API Router
|
||||
|
||||
+10
-2
@@ -486,7 +486,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -627,9 +627,17 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
pReader := NewPutObjReader(rawReader, nil, nil)
|
||||
var objectEncryptionKey []byte
|
||||
|
||||
// This request header needs to be set prior to setting ObjectOptions
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
}
|
||||
// get gateway encryption options
|
||||
var opts ObjectOptions
|
||||
opts, err = putEncryptionOpts(ctx, r, bucket, object, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if hasServerSideEncryptionHeader(formValues) && !hasSuffix(object, slashSeparator) { // handle SSE-C and SSE-S3 requests
|
||||
var reader io.Reader
|
||||
@@ -656,7 +664,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
}
|
||||
}
|
||||
|
||||
objInfo, err := objectAPI.PutObject(ctx, bucket, object, pReader, metadata, ObjectOptions{})
|
||||
objInfo, err := objectAPI.PutObject(ctx, bucket, object, pReader, metadata, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
|
||||
+52
-26
@@ -78,6 +78,47 @@ func loadLoggers() {
|
||||
|
||||
}
|
||||
|
||||
func newConfigDirFromCtx(ctx *cli.Context, option string, getDefaultDir func() string) (*ConfigDir, bool) {
|
||||
var dir string
|
||||
var dirSet bool
|
||||
|
||||
switch {
|
||||
case ctx.IsSet(option):
|
||||
dir = ctx.String(option)
|
||||
dirSet = true
|
||||
case ctx.GlobalIsSet(option):
|
||||
dir = ctx.GlobalString(option)
|
||||
dirSet = true
|
||||
// cli package does not expose parent's option option. Below code is workaround.
|
||||
if dir == "" || dir == getDefaultDir() {
|
||||
dirSet = false // Unset to false since GlobalIsSet() true is a false positive.
|
||||
if ctx.Parent().GlobalIsSet(option) {
|
||||
dir = ctx.Parent().GlobalString(option)
|
||||
dirSet = true
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Neither local nor global option is provided. In this case, try to use
|
||||
// default directory.
|
||||
dir = getDefaultDir()
|
||||
if dir == "" {
|
||||
logger.FatalIf(errInvalidArgument, "%s option must be provided", option)
|
||||
}
|
||||
}
|
||||
|
||||
if dir == "" {
|
||||
logger.FatalIf(errors.New("empty directory"), "%s directory cannot be empty", option)
|
||||
}
|
||||
|
||||
// Disallow relative paths, figure out absolute paths.
|
||||
dirAbs, err := filepath.Abs(dir)
|
||||
logger.FatalIf(err, "Unable to fetch absolute path for %s=%s", option, dir)
|
||||
|
||||
logger.FatalIf(mkdirAllIgnorePerm(dirAbs), "Unable to create directory specified %s=%s", option, dir)
|
||||
|
||||
return &ConfigDir{path: dirAbs}, dirSet
|
||||
}
|
||||
|
||||
func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
|
||||
// Get "json" flag from command line argument and
|
||||
@@ -105,36 +146,21 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
globalCLIContext.Addr = ctx.String("address")
|
||||
}
|
||||
|
||||
var configDir string
|
||||
// Set all config, certs and CAs directories.
|
||||
var configSet, certsSet bool
|
||||
globalConfigDir, configSet = newConfigDirFromCtx(ctx, "config-dir", defaultConfigDir.Get)
|
||||
globalCertsDir, certsSet = newConfigDirFromCtx(ctx, "certs-dir", defaultCertsDir.Get)
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("config-dir"):
|
||||
configDir = ctx.String("config-dir")
|
||||
case ctx.GlobalIsSet("config-dir"):
|
||||
configDir = ctx.GlobalString("config-dir")
|
||||
// cli package does not expose parent's "config-dir" option. Below code is workaround.
|
||||
if configDir == "" || configDir == getConfigDir() {
|
||||
if ctx.Parent().GlobalIsSet("config-dir") {
|
||||
configDir = ctx.Parent().GlobalString("config-dir")
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Neither local nor global config-dir option is provided. In this case, try to use
|
||||
// default config directory.
|
||||
configDir = getConfigDir()
|
||||
if configDir == "" {
|
||||
logger.FatalIf(errors.New("missing option"), "config-dir option must be provided")
|
||||
}
|
||||
// Remove this code when we deprecate and remove config-dir.
|
||||
// This code is to make sure we inherit from the config-dir
|
||||
// option if certs-dir is not provided.
|
||||
if !certsSet && configSet {
|
||||
globalCertsDir = &ConfigDir{path: filepath.Join(globalConfigDir.Get(), certsDir)}
|
||||
}
|
||||
|
||||
if configDir == "" {
|
||||
logger.FatalIf(errors.New("empty directory"), "Configuration directory cannot be empty")
|
||||
}
|
||||
globalCertsCADir = &ConfigDir{path: filepath.Join(globalCertsDir.Get(), certsCADir)}
|
||||
|
||||
// Disallow relative paths, figure out absolute paths.
|
||||
configDirAbs, err := filepath.Abs(configDir)
|
||||
logger.FatalIf(err, "Unable to fetch absolute path for config directory %s", configDir)
|
||||
setConfigDir(configDirAbs)
|
||||
logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get())
|
||||
}
|
||||
|
||||
// Parses the given compression exclude list `extensions` or `content-types`.
|
||||
|
||||
@@ -233,7 +233,7 @@ func TestValidateConfig(t *testing.T) {
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
|
||||
|
||||
// Test 27 - Test MQTT
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "clientId": "", "username": "", "password": ""}}}}`, false},
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "clientId": "", "username": "", "password": "", "queueDir": ""}}}}`, false},
|
||||
|
||||
// Test 28 - Test NSQ
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nsq": { "1": { "enable": true, "nsqdAddress": "", "topic": ""} }}}`, false},
|
||||
|
||||
+37
-70
@@ -19,7 +19,6 @@ package cmd
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
)
|
||||
@@ -41,63 +40,9 @@ const (
|
||||
privateKeyFile = "private.key"
|
||||
)
|
||||
|
||||
// ConfigDir - configuration directory with locking.
|
||||
// ConfigDir - points to a user set directory.
|
||||
type ConfigDir struct {
|
||||
sync.Mutex
|
||||
dir string
|
||||
}
|
||||
|
||||
// Set - saves given directory as configuration directory.
|
||||
func (config *ConfigDir) Set(dir string) {
|
||||
config.Lock()
|
||||
defer config.Unlock()
|
||||
|
||||
config.dir = dir
|
||||
}
|
||||
|
||||
// Get - returns current configuration directory.
|
||||
func (config *ConfigDir) Get() string {
|
||||
config.Lock()
|
||||
defer config.Unlock()
|
||||
|
||||
return config.dir
|
||||
}
|
||||
|
||||
func (config *ConfigDir) getCertsDir() string {
|
||||
return filepath.Join(config.Get(), certsDir)
|
||||
}
|
||||
|
||||
// GetCADir - returns certificate CA directory.
|
||||
func (config *ConfigDir) GetCADir() string {
|
||||
return filepath.Join(config.getCertsDir(), certsCADir)
|
||||
}
|
||||
|
||||
// Create - creates configuration directory tree.
|
||||
func (config *ConfigDir) Create() error {
|
||||
err := os.MkdirAll(config.GetCADir(), 0700)
|
||||
// It is possible in kubernetes like deployments this directory
|
||||
// is already mounted and is not writable, ignore any write errors.
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetMinioConfigFile - returns absolute path of config.json file.
|
||||
func (config *ConfigDir) GetMinioConfigFile() string {
|
||||
return filepath.Join(config.Get(), minioConfigFile)
|
||||
}
|
||||
|
||||
// GetPublicCertFile - returns absolute path of public.crt file.
|
||||
func (config *ConfigDir) GetPublicCertFile() string {
|
||||
return filepath.Join(config.getCertsDir(), publicCertFile)
|
||||
}
|
||||
|
||||
// GetPrivateKeyFile - returns absolute path of private.key file.
|
||||
func (config *ConfigDir) GetPrivateKeyFile() string {
|
||||
return filepath.Join(config.getCertsDir(), privateKeyFile)
|
||||
path string
|
||||
}
|
||||
|
||||
func getDefaultConfigDir() string {
|
||||
@@ -109,32 +54,54 @@ func getDefaultConfigDir() string {
|
||||
return filepath.Join(homeDir, defaultMinioConfigDir)
|
||||
}
|
||||
|
||||
var configDir = &ConfigDir{dir: getDefaultConfigDir()}
|
||||
|
||||
func setConfigDir(dir string) {
|
||||
configDir.Set(dir)
|
||||
func getDefaultCertsDir() string {
|
||||
return filepath.Join(getDefaultConfigDir(), certsDir)
|
||||
}
|
||||
|
||||
func getConfigDir() string {
|
||||
return configDir.Get()
|
||||
func getDefaultCertsCADir() string {
|
||||
return filepath.Join(getDefaultCertsDir(), certsCADir)
|
||||
}
|
||||
|
||||
func getCADir() string {
|
||||
return configDir.GetCADir()
|
||||
var (
|
||||
// Default config, certs and CA directories.
|
||||
defaultConfigDir = &ConfigDir{path: getDefaultConfigDir()}
|
||||
defaultCertsDir = &ConfigDir{path: getDefaultCertsDir()}
|
||||
defaultCertsCADir = &ConfigDir{path: getDefaultCertsCADir()}
|
||||
|
||||
// Points to current configuration directory -- deprecated, to be removed in future.
|
||||
globalConfigDir = defaultConfigDir
|
||||
// Points to current certs directory set by user with --certs-dir
|
||||
globalCertsDir = defaultCertsDir
|
||||
// Points to relative path to certs directory and is <value-of-certs-dir>/CAs
|
||||
globalCertsCADir = defaultCertsCADir
|
||||
)
|
||||
|
||||
// Get - returns current directory.
|
||||
func (dir *ConfigDir) Get() string {
|
||||
return dir.path
|
||||
}
|
||||
|
||||
func createConfigDir() error {
|
||||
return configDir.Create()
|
||||
// Attempts to create all directories, ignores any permission denied errors.
|
||||
func mkdirAllIgnorePerm(path string) error {
|
||||
err := os.MkdirAll(path, 0700)
|
||||
if err != nil {
|
||||
// It is possible in kubernetes like deployments this directory
|
||||
// is already mounted and is not writable, ignore any write errors.
|
||||
if os.IsPermission(err) {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func getConfigFile() string {
|
||||
return configDir.GetMinioConfigFile()
|
||||
return filepath.Join(globalConfigDir.Get(), minioConfigFile)
|
||||
}
|
||||
|
||||
func getPublicCertFile() string {
|
||||
return configDir.GetPublicCertFile()
|
||||
return filepath.Join(globalCertsDir.Get(), publicCertFile)
|
||||
}
|
||||
|
||||
func getPrivateKeyFile() string {
|
||||
return configDir.GetPrivateKeyFile()
|
||||
return filepath.Join(globalCertsDir.Get(), privateKeyFile)
|
||||
}
|
||||
|
||||
+19
-4
@@ -231,7 +231,7 @@ func migrateConfig() error {
|
||||
|
||||
// Version '1' is not supported anymore and deprecated, safe to delete.
|
||||
func purgeV1() error {
|
||||
configFile := filepath.Join(getConfigDir(), "fsUsers.json")
|
||||
configFile := filepath.Join(globalConfigDir.Get(), "fsUsers.json")
|
||||
|
||||
cv1 := &configV1{}
|
||||
_, err := Load(configFile, cv1)
|
||||
@@ -2415,6 +2415,14 @@ func migrateV27ToV28() error {
|
||||
|
||||
// Migrates ${HOME}/.minio/config.json to '<export_path>/.minio.sys/config/config.json'
|
||||
func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
||||
// Construct path to config.json for the given bucket.
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
|
||||
// Verify if config was already available in .minio.sys in which case, nothing more to be done.
|
||||
if err = checkConfig(context.Background(), objAPI, configFile); err != errConfigNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// Rename config.json to config.json.deprecated only upon
|
||||
// success of this function.
|
||||
@@ -2423,8 +2431,6 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
// Construct path to config.json for the given bucket.
|
||||
transactionConfigFile := configFile + ".transaction"
|
||||
|
||||
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
|
||||
@@ -2436,7 +2442,7 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
||||
}
|
||||
defer objLock.Unlock()
|
||||
|
||||
// Verify if backend already has the file.
|
||||
// Verify if backend already has the file (after holding lock)
|
||||
if err = checkConfig(context.Background(), objAPI, configFile); err != errConfigNotFound {
|
||||
return err
|
||||
} // if errConfigNotFound proceed to migrate..
|
||||
@@ -2459,6 +2465,15 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
||||
func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
|
||||
// Check if the config version is latest, if not migrate.
|
||||
ok, _, err := checkConfigVersion(objAPI, configFile, serverConfigVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Construct path to config.json for the given bucket.
|
||||
transactionConfigFile := configFile + ".transaction"
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestServerConfigMigrateV1(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
setConfigDir(rootPath)
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
|
||||
globalObjLayerMutex.Lock()
|
||||
globalObjectAPI = objLayer
|
||||
@@ -77,7 +77,7 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
setConfigDir(rootPath)
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
|
||||
if err := migrateV2ToV3(); err != nil {
|
||||
t.Fatal("migrate v2 to v3 should succeed when no config file is found")
|
||||
@@ -166,7 +166,8 @@ func TestServerConfigMigrateV2toV33(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
setConfigDir(rootPath)
|
||||
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
|
||||
objLayer, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
@@ -235,7 +236,8 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
setConfigDir(rootPath)
|
||||
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
configPath := rootPath + "/" + minioConfigFile
|
||||
|
||||
// Create a corrupted config file
|
||||
@@ -331,7 +333,8 @@ func TestServerConfigMigrateCorruptedConfig(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
setConfigDir(rootPath)
|
||||
|
||||
globalConfigDir = &ConfigDir{path: rootPath}
|
||||
configPath := rootPath + "/" + minioConfigFile
|
||||
|
||||
for i := 3; i <= 17; i++ {
|
||||
|
||||
@@ -893,7 +893,7 @@ type serverConfigV32 struct {
|
||||
} `json:"policy"`
|
||||
}
|
||||
|
||||
// serverConfigV33 is just like version '32', removes clientID from NATS.
|
||||
// serverConfigV33 is just like version '32', removes clientID from NATS and adds queueDir with MQTT.
|
||||
type serverConfigV33 struct {
|
||||
quick.Config `json:"-"` // ignore interfaces
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ var ssecIsRequestedTests = []struct {
|
||||
Header http.Header
|
||||
Expected bool
|
||||
}{
|
||||
{Header: http.Header{}, Expected: false}, // 0
|
||||
{Header: http.Header{}, Expected: false}, // 0
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}}, Expected: true}, // 1
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}}, Expected: true}, // 2
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3
|
||||
@@ -137,7 +137,7 @@ var ssecCopyIsRequestedTests = []struct {
|
||||
Header http.Header
|
||||
Expected bool
|
||||
}{
|
||||
{Header: http.Header{}, Expected: false}, // 0
|
||||
{Header: http.Header{}, Expected: false}, // 0
|
||||
{Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}}, Expected: true}, // 1
|
||||
{Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}}, Expected: true}, // 2
|
||||
{Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3
|
||||
|
||||
@@ -40,6 +40,14 @@ func RemoveSensitiveEntries(metadata map[string]string) { // The functions is te
|
||||
delete(metadata, SSECopyKey)
|
||||
}
|
||||
|
||||
// RemoveSSEHeaders removes all crypto-specific SSE
|
||||
// header entries from the metadata map.
|
||||
func RemoveSSEHeaders(metadata map[string]string) {
|
||||
delete(metadata, SSEHeader)
|
||||
delete(metadata, SSECKeyMD5)
|
||||
delete(metadata, SSECAlgorithm)
|
||||
}
|
||||
|
||||
// RemoveInternalEntries removes all crypto-specific internal
|
||||
// metadata entries from the metadata map.
|
||||
func RemoveInternalEntries(metadata map[string]string) {
|
||||
|
||||
@@ -370,9 +370,9 @@ var isETagSealedTests = []struct {
|
||||
ETag string
|
||||
IsSealed bool
|
||||
}{
|
||||
{ETag: "", IsSealed: false}, // 0
|
||||
{ETag: "90682b8e8cc7609c4671e1d64c73fc30", IsSealed: false}, // 1
|
||||
{ETag: "f201040c9dc593e39ea004dc1323699bcd", IsSealed: true}, // 2 not valid ciphertext but looks like sealed ETag
|
||||
{ETag: "", IsSealed: false}, // 0
|
||||
{ETag: "90682b8e8cc7609c4671e1d64c73fc30", IsSealed: false}, // 1
|
||||
{ETag: "f201040c9dc593e39ea004dc1323699bcd", IsSealed: true}, // 2 not valid ciphertext but looks like sealed ETag
|
||||
{ETag: "20000f00fba2ee2ae4845f725964eeb9e092edfabc7ab9f9239e8344341f769a51ce99b4801b0699b92b16a72fa94972", IsSealed: true}, // 3
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ func newCacheFSObjects(dir string, expiry int, maxDiskUsagePct int) (*cacheFSObj
|
||||
appendFileMap: make(map[string]*fsAppendFile),
|
||||
}
|
||||
|
||||
go fsObjects.cleanupStaleMultipartUploads(context.Background(), globalMultipartCleanupInterval, globalMultipartExpiry, globalServiceDoneCh)
|
||||
go fsObjects.cleanupStaleMultipartUploads(context.Background(), GlobalMultipartCleanupInterval, GlobalMultipartExpiry, GlobalServiceDoneCh)
|
||||
|
||||
cacheFS := cacheFSObjects{
|
||||
FSObjects: fsObjects,
|
||||
@@ -158,7 +158,7 @@ func (cfs *cacheFSObjects) purgeTrash() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-globalServiceDoneCh:
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
trashPath := path.Join(cfs.fsPath, minioMetaBucket, cacheTrashDir)
|
||||
|
||||
@@ -134,7 +134,7 @@ func TestCacheExclusion(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cobj := cobjects.(*cacheObjects)
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
objectName string
|
||||
|
||||
@@ -99,7 +99,7 @@ func (api *DummyObjectLayer) PutObjectPart(ctx context.Context, bucket, object,
|
||||
return
|
||||
}
|
||||
|
||||
func (api *DummyObjectLayer) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int) (result ListPartsInfo, err error) {
|
||||
func (api *DummyObjectLayer) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+137
-26
@@ -29,6 +29,7 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/ioutil"
|
||||
@@ -55,11 +56,11 @@ const (
|
||||
// SSEIVSize is the size of the IV data
|
||||
SSEIVSize = 32 // 32 bytes
|
||||
|
||||
// SSE dare package block size.
|
||||
sseDAREPackageBlockSize = 64 * 1024 // 64KiB bytes
|
||||
// SSEDAREPackageBlockSize - SSE dare package block size.
|
||||
SSEDAREPackageBlockSize = 64 * 1024 // 64KiB bytes
|
||||
|
||||
// SSE dare package meta padding bytes.
|
||||
sseDAREPackageMetaSize = 32 // 32 bytes
|
||||
// SSEDAREPackageMetaSize - SSE dare package meta padding bytes.
|
||||
SSEDAREPackageMetaSize = 32 // 32 bytes
|
||||
|
||||
)
|
||||
|
||||
@@ -158,14 +159,14 @@ func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map
|
||||
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
||||
return nil
|
||||
case crypto.S3.IsEncrypted(metadata):
|
||||
if globalKMS == nil {
|
||||
if GlobalKMS == nil {
|
||||
return errKMSNotConfigured
|
||||
}
|
||||
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldKey, err := globalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
oldKey, err := GlobalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -174,7 +175,7 @@ func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map
|
||||
return err
|
||||
}
|
||||
|
||||
newKey, encKey, err := globalKMS.GenerateKey(globalKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
newKey, encKey, err := GlobalKMS.GenerateKey(globalKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -187,10 +188,10 @@ func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map
|
||||
func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string, sseS3 bool) ([]byte, error) {
|
||||
var sealedKey crypto.SealedKey
|
||||
if sseS3 {
|
||||
if globalKMS == nil {
|
||||
if GlobalKMS == nil {
|
||||
return nil, errKMSNotConfigured
|
||||
}
|
||||
key, encKey, err := globalKMS.GenerateKey(globalKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
key, encKey, err := GlobalKMS.GenerateKey(globalKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -279,7 +280,7 @@ func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]st
|
||||
default:
|
||||
return nil, errObjectTampered
|
||||
case crypto.S3.IsEncrypted(metadata):
|
||||
if globalKMS == nil {
|
||||
if GlobalKMS == nil {
|
||||
return nil, errKMSNotConfigured
|
||||
}
|
||||
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
|
||||
@@ -287,7 +288,7 @@ func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extKey, err := globalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
extKey, err := GlobalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -395,7 +396,6 @@ func DecryptBlocksRequestR(inputReader io.Reader, h http.Header, offset,
|
||||
io.Reader, error) {
|
||||
|
||||
bucket, object := oi.Bucket, oi.Name
|
||||
|
||||
// Single part case
|
||||
if !isEncryptedMultipart(oi) {
|
||||
var reader io.Reader
|
||||
@@ -411,8 +411,8 @@ func DecryptBlocksRequestR(inputReader io.Reader, h http.Header, offset,
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
partDecRelOffset := int64(seqNumber) * sseDAREPackageBlockSize
|
||||
partEncRelOffset := int64(seqNumber) * (sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||
partDecRelOffset := int64(seqNumber) * SSEDAREPackageBlockSize
|
||||
partEncRelOffset := int64(seqNumber) * (SSEDAREPackageBlockSize + SSEDAREPackageMetaSize)
|
||||
|
||||
w := &DecryptBlocksReader{
|
||||
reader: inputReader,
|
||||
@@ -477,7 +477,7 @@ type DecryptBlocksReader struct {
|
||||
// Current part index
|
||||
partIndex int
|
||||
// Parts information
|
||||
parts []objectPartInfo
|
||||
parts []ObjectPartInfo
|
||||
header http.Header
|
||||
bucket, object string
|
||||
metadata map[string]string
|
||||
@@ -592,7 +592,7 @@ type DecryptBlocksWriter struct {
|
||||
// Current part index
|
||||
partIndex int
|
||||
// Parts information
|
||||
parts []objectPartInfo
|
||||
parts []ObjectPartInfo
|
||||
req *http.Request
|
||||
bucket, object string
|
||||
metadata map[string]string
|
||||
@@ -743,6 +743,7 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object stri
|
||||
}
|
||||
|
||||
seqNumber, encStartOffset, encLength = getEncryptedMultipartsOffsetLength(startOffset, length, objInfo)
|
||||
|
||||
var partStartIndex int
|
||||
var partStartOffset = startOffset
|
||||
// Skip parts until final offset maps to a particular part offset.
|
||||
@@ -765,8 +766,8 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object stri
|
||||
partStartOffset -= int64(decryptedSize)
|
||||
}
|
||||
|
||||
startSeqNum := partStartOffset / sseDAREPackageBlockSize
|
||||
partEncRelOffset := int64(startSeqNum) * (sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||
startSeqNum := partStartOffset / SSEDAREPackageBlockSize
|
||||
partEncRelOffset := int64(startSeqNum) * (SSEDAREPackageBlockSize + SSEDAREPackageMetaSize)
|
||||
|
||||
w := &DecryptBlocksWriter{
|
||||
writer: client,
|
||||
@@ -857,9 +858,9 @@ func getEncryptedMultipartsOffsetLength(offset, length int64, obj ObjectInfo) (u
|
||||
|
||||
// getEncryptedSinglePartOffsetLength - fetch sequence number, encrypted start offset and encrypted length.
|
||||
func getEncryptedSinglePartOffsetLength(offset, length int64, objInfo ObjectInfo) (seqNumber uint32, encOffset int64, encLength int64) {
|
||||
onePkgSize := int64(sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||
onePkgSize := int64(SSEDAREPackageBlockSize + SSEDAREPackageMetaSize)
|
||||
|
||||
seqNumber = uint32(offset / sseDAREPackageBlockSize)
|
||||
seqNumber = uint32(offset / SSEDAREPackageBlockSize)
|
||||
encOffset = int64(seqNumber) * onePkgSize
|
||||
// The math to compute the encrypted length is always
|
||||
// originalLength i.e (offset+length-1) to be divided under
|
||||
@@ -867,10 +868,10 @@ func getEncryptedSinglePartOffsetLength(offset, length int64, objInfo ObjectInfo
|
||||
// block. This is then multiplied by final package size which
|
||||
// is basically 64KiB + 32. Finally negate the encrypted offset
|
||||
// to get the final encrypted length on disk.
|
||||
encLength = ((offset+length)/sseDAREPackageBlockSize)*onePkgSize - encOffset
|
||||
encLength = ((offset+length)/SSEDAREPackageBlockSize)*onePkgSize - encOffset
|
||||
|
||||
// Check for the remainder, to figure if we need one extract package to read from.
|
||||
if (offset+length)%sseDAREPackageBlockSize > 0 {
|
||||
if (offset+length)%SSEDAREPackageBlockSize > 0 {
|
||||
encLength += onePkgSize
|
||||
}
|
||||
|
||||
@@ -1041,11 +1042,11 @@ func (o *ObjectInfo) GetDecryptedRange(rs *HTTPRangeSpec) (encOff, encLength, sk
|
||||
// partStart is always found in the loop above,
|
||||
// because off is validated.
|
||||
|
||||
sseDAREEncPackageBlockSize := int64(sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||
startPkgNum := (off - cumulativeSum) / sseDAREPackageBlockSize
|
||||
sseDAREEncPackageBlockSize := int64(SSEDAREPackageBlockSize + SSEDAREPackageMetaSize)
|
||||
startPkgNum := (off - cumulativeSum) / SSEDAREPackageBlockSize
|
||||
|
||||
// Now we can calculate the number of bytes to skip
|
||||
skipLen = (off - cumulativeSum) % sseDAREPackageBlockSize
|
||||
skipLen = (off - cumulativeSum) % SSEDAREPackageBlockSize
|
||||
|
||||
encOff = encCumulativeSum + startPkgNum*sseDAREEncPackageBlockSize
|
||||
// Locate the part containing the end of the required range
|
||||
@@ -1062,7 +1063,7 @@ func (o *ObjectInfo) GetDecryptedRange(rs *HTTPRangeSpec) (encOff, encLength, sk
|
||||
}
|
||||
// partEnd is always found in the loop above, because off and
|
||||
// length are validated.
|
||||
endPkgNum := (endOffset - cumulativeSum) / sseDAREPackageBlockSize
|
||||
endPkgNum := (endOffset - cumulativeSum) / SSEDAREPackageBlockSize
|
||||
// Compute endEncOffset with one additional DARE package (so
|
||||
// we read the package containing the last desired byte).
|
||||
endEncOffset := encCumulativeSum + (endPkgNum+1)*sseDAREEncPackageBlockSize
|
||||
@@ -1157,3 +1158,113 @@ func DecryptObjectInfo(info *ObjectInfo, headers http.Header) (encrypted bool, e
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// The customer key in the header is used by the gateway for encryption in the case of
|
||||
// s3 gateway double encryption. A new client key is derived from the customer provided
|
||||
// key to be sent to the s3 backend for encryption at the backend.
|
||||
func deriveClientKey(clientKey [32]byte, bucket, object string) [32]byte {
|
||||
var key [32]byte
|
||||
mac := hmac.New(sha256.New, clientKey[:])
|
||||
mac.Write([]byte(crypto.SSEC.String()))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
mac.Sum(key[:0])
|
||||
return key
|
||||
}
|
||||
|
||||
// extract encryption options for pass through to backend in the case of gateway
|
||||
func extractEncryptionOption(header http.Header, copySource bool, metadata map[string]string) (opts ObjectOptions, err error) {
|
||||
var clientKey [32]byte
|
||||
var sse encrypt.ServerSide
|
||||
|
||||
if copySource {
|
||||
if crypto.SSECopy.IsRequested(header) {
|
||||
clientKey, err = crypto.SSECopy.ParseHTTP(header)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if sse, err = encrypt.NewSSEC(clientKey[:]); err != nil {
|
||||
return
|
||||
}
|
||||
return ObjectOptions{ServerSideEncryption: encrypt.SSECopy(sse)}, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if crypto.SSEC.IsRequested(header) {
|
||||
clientKey, err = crypto.SSEC.ParseHTTP(header)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if sse, err = encrypt.NewSSEC(clientKey[:]); err != nil {
|
||||
return
|
||||
}
|
||||
return ObjectOptions{ServerSideEncryption: sse}, nil
|
||||
}
|
||||
if crypto.S3.IsRequested(header) || (metadata != nil && crypto.S3.IsEncrypted(metadata)) {
|
||||
return ObjectOptions{ServerSideEncryption: encrypt.NewSSE()}, nil
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// get ObjectOptions for GET calls from encryption headers
|
||||
func getEncryptionOpts(ctx context.Context, r *http.Request, bucket, object string) (ObjectOptions, error) {
|
||||
var (
|
||||
encryption encrypt.ServerSide
|
||||
opts ObjectOptions
|
||||
)
|
||||
if GlobalGatewaySSE.SSEC() && crypto.SSEC.IsRequested(r.Header) {
|
||||
key, err := crypto.SSEC.ParseHTTP(r.Header)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
derivedKey := deriveClientKey(key, bucket, object)
|
||||
encryption, err = encrypt.NewSSEC(derivedKey[:])
|
||||
logger.CriticalIf(ctx, err)
|
||||
return ObjectOptions{ServerSideEncryption: encryption}, nil
|
||||
}
|
||||
// default case of passing encryption headers to backend
|
||||
return extractEncryptionOption(r.Header, false, nil)
|
||||
}
|
||||
|
||||
// get ObjectOptions for PUT calls from encryption headers
|
||||
func putEncryptionOpts(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) {
|
||||
// In the case of multipart custom format, the metadata needs to be checked in addition to header to see if it
|
||||
// is SSE-S3 encrypted, primarily because S3 protocol does not require SSE-S3 headers in PutObjectPart calls
|
||||
if GlobalGatewaySSE.SSES3() && (crypto.S3.IsRequested(r.Header) || crypto.S3.IsEncrypted(metadata)) {
|
||||
return ObjectOptions{ServerSideEncryption: encrypt.NewSSE()}, nil
|
||||
}
|
||||
if GlobalGatewaySSE.SSEC() && crypto.SSEC.IsRequested(r.Header) {
|
||||
return getEncryptionOpts(ctx, r, bucket, object)
|
||||
}
|
||||
// default case of passing encryption headers to backend
|
||||
return extractEncryptionOption(r.Header, false, metadata)
|
||||
}
|
||||
|
||||
// get ObjectOptions for Copy calls for encryption headers provided on the target side
|
||||
func copyDstEncryptionOpts(ctx context.Context, r *http.Request, bucket, object string, metadata map[string]string) (opts ObjectOptions, err error) {
|
||||
return putEncryptionOpts(ctx, r, bucket, object, metadata)
|
||||
}
|
||||
|
||||
// get ObjectOptions for Copy calls for encryption headers provided on the source side
|
||||
func copySrcEncryptionOpts(ctx context.Context, r *http.Request, bucket, object string) (ObjectOptions, error) {
|
||||
var (
|
||||
ssec encrypt.ServerSide
|
||||
opts ObjectOptions
|
||||
)
|
||||
|
||||
if GlobalGatewaySSE.SSEC() && crypto.SSECopy.IsRequested(r.Header) {
|
||||
key, err := crypto.SSECopy.ParseHTTP(r.Header)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
derivedKey := deriveClientKey(key, bucket, object)
|
||||
ssec, err = encrypt.NewSSEC(derivedKey[:])
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
return ObjectOptions{ServerSideEncryption: encrypt.SSECopy(ssec)}, nil
|
||||
}
|
||||
|
||||
// default case of passing encryption headers to backend
|
||||
return extractEncryptionOption(r.Header, true, nil)
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/sio"
|
||||
)
|
||||
@@ -355,7 +357,7 @@ func TestGetDecryptedRange_Issue50(t *testing.T) {
|
||||
"content-type": "application/octet-stream",
|
||||
"etag": "166b1545b4c1535294ee0686678bea8c-2",
|
||||
},
|
||||
Parts: []objectPartInfo{
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Number: 1,
|
||||
Name: "part.1",
|
||||
@@ -503,7 +505,7 @@ func TestGetDecryptedRange(t *testing.T) {
|
||||
var (
|
||||
// make a multipart object-info given part sizes
|
||||
mkMPObj = func(sizes []int64) ObjectInfo {
|
||||
r := make([]objectPartInfo, len(sizes))
|
||||
r := make([]ObjectPartInfo, len(sizes))
|
||||
sum := int64(0)
|
||||
for i, s := range sizes {
|
||||
r[i].Number = i
|
||||
@@ -675,3 +677,84 @@ func TestGetDecryptedRange(t *testing.T) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var extractEncryptionOptionTests = []struct {
|
||||
headers http.Header
|
||||
copySource bool
|
||||
metadata map[string]string
|
||||
encryptionType encrypt.Type
|
||||
err error
|
||||
}{
|
||||
{headers: http.Header{crypto.SSECAlgorithm: []string{"AES256"},
|
||||
crypto.SSECKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||
crypto.SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="}},
|
||||
copySource: false,
|
||||
metadata: nil,
|
||||
encryptionType: encrypt.SSEC,
|
||||
err: nil}, // 0
|
||||
{headers: http.Header{crypto.SSECAlgorithm: []string{"AES256"},
|
||||
crypto.SSECKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||
crypto.SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="}},
|
||||
copySource: true,
|
||||
metadata: nil,
|
||||
encryptionType: "",
|
||||
err: nil}, // 1
|
||||
{headers: http.Header{crypto.SSECAlgorithm: []string{"AES256"},
|
||||
crypto.SSECKey: []string{"Mz"},
|
||||
crypto.SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="}},
|
||||
copySource: false,
|
||||
metadata: nil,
|
||||
encryptionType: "",
|
||||
err: crypto.ErrInvalidCustomerKey}, // 2
|
||||
{headers: http.Header{crypto.SSEHeader: []string{"AES256"}},
|
||||
copySource: false,
|
||||
metadata: nil,
|
||||
encryptionType: encrypt.S3,
|
||||
err: nil}, // 3
|
||||
{headers: http.Header{},
|
||||
copySource: false,
|
||||
metadata: map[string]string{crypto.S3SealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)),
|
||||
crypto.S3KMSKeyID: "kms-key",
|
||||
crypto.S3KMSSealedKey: "m-key"},
|
||||
encryptionType: encrypt.S3,
|
||||
err: nil}, // 4
|
||||
{headers: http.Header{},
|
||||
copySource: true,
|
||||
metadata: map[string]string{crypto.S3SealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)),
|
||||
crypto.S3KMSKeyID: "kms-key",
|
||||
crypto.S3KMSSealedKey: "m-key"},
|
||||
encryptionType: "",
|
||||
err: nil}, // 5
|
||||
{headers: http.Header{crypto.SSECopyAlgorithm: []string{"AES256"},
|
||||
crypto.SSECopyKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||
crypto.SSECopyKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="}},
|
||||
copySource: true,
|
||||
metadata: nil,
|
||||
encryptionType: encrypt.SSEC,
|
||||
err: nil}, // 6
|
||||
{headers: http.Header{crypto.SSECopyAlgorithm: []string{"AES256"},
|
||||
crypto.SSECopyKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||
crypto.SSECopyKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="}},
|
||||
copySource: false,
|
||||
metadata: nil,
|
||||
encryptionType: "",
|
||||
err: nil}, // 7
|
||||
}
|
||||
|
||||
func TestExtractEncryptionOptions(t *testing.T) {
|
||||
for i, test := range extractEncryptionOptionTests {
|
||||
opts, err := extractEncryptionOption(test.headers, test.copySource, test.metadata)
|
||||
if test.err != err {
|
||||
t.Errorf("Case %d: expected err: %v , actual err: %v", i, test.err, err)
|
||||
}
|
||||
if err == nil {
|
||||
if opts.ServerSideEncryption == nil && test.encryptionType != "" {
|
||||
t.Errorf("Case %d: expected opts to be of %v encryption type", i, test.encryptionType)
|
||||
|
||||
}
|
||||
if opts.ServerSideEncryption != nil && test.encryptionType != opts.ServerSideEncryption.Type() {
|
||||
t.Errorf("Case %d: expected opts to have encryption type %v but was %v ", i, test.encryptionType, opts.ServerSideEncryption.Type())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ import (
|
||||
|
||||
"github.com/minio/minio-go/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"
|
||||
)
|
||||
|
||||
@@ -197,6 +200,82 @@ func (endpoints EndpointList) GetString(i int) string {
|
||||
return endpoints[i].String()
|
||||
}
|
||||
|
||||
// localEndpointsMemUsage - returns ServerMemUsageInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func localEndpointsMemUsage(endpoints EndpointList) ServerMemUsageInfo {
|
||||
var memUsages []mem.Usage
|
||||
var addr string
|
||||
scratchSpace := map[string]bool{}
|
||||
for _, endpoint := range endpoints {
|
||||
// Only proceed for local endpoints
|
||||
if endpoint.IsLocal {
|
||||
if _, ok := scratchSpace[endpoint.Host]; ok {
|
||||
continue
|
||||
}
|
||||
addr = GetLocalPeer(endpoints)
|
||||
memUsage := mem.GetUsage()
|
||||
memUsages = append(memUsages, memUsage)
|
||||
scratchSpace[endpoint.Host] = true
|
||||
}
|
||||
}
|
||||
return ServerMemUsageInfo{
|
||||
Addr: addr,
|
||||
Usage: memUsages,
|
||||
}
|
||||
}
|
||||
|
||||
// localEndpointsCPULoad - returns ServerCPULoadInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func localEndpointsCPULoad(endpoints EndpointList) ServerCPULoadInfo {
|
||||
var cpuLoads []cpu.Load
|
||||
var addr string
|
||||
scratchSpace := map[string]bool{}
|
||||
for _, endpoint := range endpoints {
|
||||
// Only proceed for local endpoints
|
||||
if endpoint.IsLocal {
|
||||
if _, ok := scratchSpace[endpoint.Host]; ok {
|
||||
continue
|
||||
}
|
||||
addr = GetLocalPeer(endpoints)
|
||||
cpuLoad := cpu.GetLoad()
|
||||
cpuLoads = append(cpuLoads, cpuLoad)
|
||||
scratchSpace[endpoint.Host] = true
|
||||
}
|
||||
}
|
||||
return ServerCPULoadInfo{
|
||||
Addr: addr,
|
||||
Load: cpuLoads,
|
||||
}
|
||||
}
|
||||
|
||||
// localEndpointsDrivePerf - returns ServerDrivesPerfInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func localEndpointsDrivePerf(endpoints EndpointList) ServerDrivesPerfInfo {
|
||||
var dps []disk.Performance
|
||||
var addr string
|
||||
for _, endpoint := range endpoints {
|
||||
// Only proceed for local endpoints
|
||||
if endpoint.IsLocal {
|
||||
addr = GetLocalPeer(endpoints)
|
||||
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
|
||||
}
|
||||
tempObj := mustGetUUID()
|
||||
fsPath := pathJoin(endpoint.Path, minioMetaTmpBucket, tempObj)
|
||||
dp := disk.GetPerformance(fsPath)
|
||||
dp.Path = endpoint.Path
|
||||
dps = append(dps, dp)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
+3
-3
@@ -136,13 +136,13 @@ func (env environment) LookupKMSConfig(config crypto.KMSConfig) (err error) {
|
||||
if !config.Vault.IsEmpty() { // Vault and KMS master key provided
|
||||
return errors.New("Ambiguous KMS configuration: vault configuration and a master key are provided at the same time")
|
||||
}
|
||||
globalKMSKeyID, globalKMS, err = parseKMSMasterKey(masterKey)
|
||||
globalKMSKeyID, GlobalKMS, err = parseKMSMasterKey(masterKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !config.Vault.IsEmpty() {
|
||||
globalKMS, err = crypto.NewVault(config.Vault)
|
||||
GlobalKMS, err = crypto.NewVault(config.Vault)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -154,7 +154,7 @@ func (env environment) LookupKMSConfig(config crypto.KMSConfig) (err error) {
|
||||
return err
|
||||
}
|
||||
globalAutoEncryption = bool(autoEncryption)
|
||||
if globalAutoEncryption && globalKMS == nil { // auto-encryption enabled but no KMS
|
||||
if globalAutoEncryption && GlobalKMS == nil { // auto-encryption enabled but no KMS
|
||||
return errors.New("Invalid KMS configuration: auto-encryption is enabled but no valid KMS configuration is present")
|
||||
}
|
||||
return nil
|
||||
|
||||
+34
-19
@@ -24,6 +24,7 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
)
|
||||
|
||||
@@ -332,29 +333,43 @@ func formatFSFixDeploymentID(fsFormatPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
formatStartTime := time.Now().Round(time.Second)
|
||||
getElapsedTime := func() string {
|
||||
return time.Now().Round(time.Second).Sub(formatStartTime).String()
|
||||
}
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
defer close(doneCh)
|
||||
|
||||
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||
for {
|
||||
wlk, err := lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR, 0)
|
||||
if err == lock.ErrAlreadyLocked {
|
||||
// Lock already present, sleep and attempt again.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wlk.Close()
|
||||
select {
|
||||
case <-retryTimerCh:
|
||||
|
||||
err = jsonLoad(wlk, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wlk, err := lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR, 0)
|
||||
if err == lock.ErrAlreadyLocked {
|
||||
// Lock already present, sleep and attempt again
|
||||
|
||||
// Check if it needs to be updated
|
||||
if format.ID != "" {
|
||||
return nil
|
||||
logger.Info("Another minio process(es) might be holding a lock to the file %s. Please kill that minio process(es) (elapsed %s)\n", fsFormatPath, getElapsedTime())
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wlk.Close()
|
||||
|
||||
err = jsonLoad(wlk, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if it needs to be updated
|
||||
if format.ID != "" {
|
||||
return nil
|
||||
}
|
||||
format.ID = mustGetUUID()
|
||||
return jsonSave(wlk, format)
|
||||
}
|
||||
format.ID = mustGetUUID()
|
||||
return jsonSave(wlk, format)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ type fsMetaV1 struct {
|
||||
// Metadata map for current object.
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
// parts info for current object - used in encryption.
|
||||
Parts []objectPartInfo `json:"parts,omitempty"`
|
||||
Parts []ObjectPartInfo `json:"parts,omitempty"`
|
||||
}
|
||||
|
||||
// IsValid - tells if the format is sane by validating the version
|
||||
@@ -207,9 +207,9 @@ func parseFSMetaMap(fsMetaBuf []byte) map[string]string {
|
||||
return metaMap
|
||||
}
|
||||
|
||||
func parseFSPartsArray(fsMetaBuf []byte) []objectPartInfo {
|
||||
func parseFSPartsArray(fsMetaBuf []byte) []ObjectPartInfo {
|
||||
// Get xlMetaV1.Parts array
|
||||
var partsArray []objectPartInfo
|
||||
var partsArray []ObjectPartInfo
|
||||
|
||||
partsArrayResult := gjson.GetBytes(fsMetaBuf, "parts")
|
||||
partsArrayResult.ForEach(func(key, part gjson.Result) bool {
|
||||
@@ -219,7 +219,7 @@ func parseFSPartsArray(fsMetaBuf []byte) []objectPartInfo {
|
||||
etag := gjson.Get(partJSON, "etag").String()
|
||||
size := gjson.Get(partJSON, "size").Int()
|
||||
actualSize := gjson.Get(partJSON, "actualSize").Int()
|
||||
partsArray = append(partsArray, objectPartInfo{
|
||||
partsArray = append(partsArray, ObjectPartInfo{
|
||||
Number: int(number),
|
||||
Name: name,
|
||||
ETag: etag,
|
||||
|
||||
@@ -353,7 +353,7 @@ func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
||||
// Implements S3 compatible ListObjectParts API. The resulting
|
||||
// ListPartsInfo structure is unmarshalled directly into XML and
|
||||
// replied back to the client.
|
||||
func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int) (result ListPartsInfo, e error) {
|
||||
func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int, opts ObjectOptions) (result ListPartsInfo, e error) {
|
||||
if err := checkListPartsArgs(ctx, bucket, object, fs); err != nil {
|
||||
return result, toObjectErr(err)
|
||||
}
|
||||
@@ -515,7 +515,7 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
fsMeta := fsMetaV1{}
|
||||
|
||||
// Allocate parts similar to incoming slice.
|
||||
fsMeta.Parts = make([]objectPartInfo, len(parts))
|
||||
fsMeta.Parts = make([]ObjectPartInfo, len(parts))
|
||||
|
||||
entries, err := readDir(uploadIDDir)
|
||||
if err != nil {
|
||||
@@ -560,7 +560,7 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
partSize = actualSize
|
||||
}
|
||||
|
||||
fsMeta.Parts[i] = objectPartInfo{
|
||||
fsMeta.Parts[i] = ObjectPartInfo{
|
||||
Number: part.PartNumber,
|
||||
ETag: part.ETag,
|
||||
Size: fi.Size(),
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
|
||||
|
||||
// Close the go-routine, we are going to
|
||||
// manually start it and test in this test case.
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
@@ -47,14 +47,14 @@ func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
|
||||
go fs.cleanupStaleMultipartUploads(context.Background(), 20*time.Millisecond, 0, globalServiceDoneCh)
|
||||
go fs.cleanupStaleMultipartUploads(context.Background(), 20*time.Millisecond, 0, GlobalServiceDoneCh)
|
||||
|
||||
// Wait for 40ms such that - we have given enough time for
|
||||
// cleanup routine to kick in.
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
|
||||
// Close the routine we do not need it anymore.
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
|
||||
// Check if upload id was already purged.
|
||||
if err = obj.AbortMultipartUpload(context.Background(), bucketName, objectName, uploadID); err != nil {
|
||||
|
||||
+3
-3
@@ -152,10 +152,10 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||
fs.fsFormatRlk = rlk
|
||||
|
||||
if !fs.diskMount {
|
||||
go fs.diskUsage(globalServiceDoneCh)
|
||||
go fs.diskUsage(GlobalServiceDoneCh)
|
||||
}
|
||||
|
||||
go fs.cleanupStaleMultipartUploads(ctx, globalMultipartCleanupInterval, globalMultipartExpiry, globalServiceDoneCh)
|
||||
go fs.cleanupStaleMultipartUploads(ctx, GlobalMultipartCleanupInterval, GlobalMultipartExpiry, GlobalServiceDoneCh)
|
||||
|
||||
// Return successfully initialized object layer.
|
||||
return fs, nil
|
||||
@@ -1317,7 +1317,7 @@ func (fs *FSObjects) IsListenBucketSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsEncryptionSupported returns whether server side encryption is applicable for this layer.
|
||||
// IsEncryptionSupported returns whether server side encryption is implemented for this layer.
|
||||
func (fs *FSObjects) IsEncryptionSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
+1
-1
@@ -354,7 +354,7 @@ func TestFSListBuckets(t *testing.T) {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
|
||||
// Create a bucket with invalid name
|
||||
if err := os.MkdirAll(pathJoin(fs.fsPath, "vo^"), 0777); err != nil {
|
||||
|
||||
@@ -18,7 +18,10 @@ package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
|
||||
minio "github.com/minio/minio-go"
|
||||
@@ -30,8 +33,27 @@ var (
|
||||
|
||||
// MustGetUUID function alias.
|
||||
MustGetUUID = mustGetUUID
|
||||
|
||||
// IsMinAllowedPartSize function alias.
|
||||
IsMinAllowedPartSize = isMinAllowedPartSize
|
||||
|
||||
// GetCompleteMultipartMD5 functon alias.
|
||||
GetCompleteMultipartMD5 = getCompleteMultipartMD5
|
||||
|
||||
// Contains function alias.
|
||||
Contains = contains
|
||||
|
||||
// ExtractETag provides extractETag function alias.
|
||||
ExtractETag = extractETag
|
||||
// CleanMetadataKeys provides cleanMetadataKeys function alias.
|
||||
CleanMetadataKeys = cleanMetadataKeys
|
||||
)
|
||||
|
||||
// StatInfo - alias for statInfo
|
||||
type StatInfo struct {
|
||||
statInfo
|
||||
}
|
||||
|
||||
// AnonErrToObjectErr - converts standard http codes into meaningful object layer errors.
|
||||
func AnonErrToObjectErr(statusCode int, params ...string) error {
|
||||
bucket := ""
|
||||
@@ -321,3 +343,30 @@ func ErrorRespToObjectError(err error, params ...string) error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// parse gateway sse env variable
|
||||
func parseGatewaySSE(s string) (gatewaySSE, error) {
|
||||
l := strings.Split(s, ";")
|
||||
var gwSlice = make([]string, 0)
|
||||
for _, val := range l {
|
||||
v := strings.ToUpper(val)
|
||||
if v == gatewaySSES3 || v == gatewaySSEC {
|
||||
gwSlice = append(gwSlice, v)
|
||||
continue
|
||||
}
|
||||
return nil, uiErrInvalidGWSSEValue(nil).Msg("gateway SSE cannot be (%s) ", v)
|
||||
}
|
||||
return gatewaySSE(gwSlice), nil
|
||||
}
|
||||
|
||||
// handle gateway env vars
|
||||
func handleGatewayEnvVars() {
|
||||
gwsseVal, ok := os.LookupEnv("MINIO_GATEWAY_SSE")
|
||||
if ok {
|
||||
var err error
|
||||
GlobalGatewaySSE, err = parseGatewaySSE(gwsseVal)
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to parse MINIO_GATEWAY_SSE value (`%s`)", gwsseVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests cache exclude parsing.
|
||||
func TestParseGatewaySSE(t *testing.T) {
|
||||
testCases := []struct {
|
||||
gwSSEStr string
|
||||
expected gatewaySSE
|
||||
success bool
|
||||
}{
|
||||
// valid input
|
||||
{"c;S3", []string{"C", "S3"}, true},
|
||||
{"S3", []string{"S3"}, true},
|
||||
{"c,S3", []string{}, false},
|
||||
{"c;S3;KMS", []string{}, false},
|
||||
{"C;s3", []string{"C", "S3"}, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
gwSSE, err := parseGatewaySSE(testCase.gwSSEStr)
|
||||
if err != nil && testCase.success {
|
||||
t.Errorf("Test %d: Expected success but failed instead %s", i+1, err)
|
||||
}
|
||||
if err == nil && !testCase.success {
|
||||
t.Errorf("Test %d: Expected failure but passed instead", i+1)
|
||||
}
|
||||
if err == nil {
|
||||
if !reflect.DeepEqual(gwSSE, testCase.expected) {
|
||||
t.Errorf("Test %d: Expected %v, got %v", i+1, testCase.expected, gwSSE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,37 +14,35 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package format
|
||||
package cmd
|
||||
|
||||
import "strconv"
|
||||
type gatewaySSE []string
|
||||
|
||||
// IsInt - returns a true or false, whether a string can
|
||||
// be represented as an int.
|
||||
func IsInt(s string) bool {
|
||||
_, err := strconv.Atoi(s)
|
||||
return err == nil
|
||||
}
|
||||
const (
|
||||
// GatewaySSES3 is set when SSE-S3 encryption needed on both gateway and backend
|
||||
gatewaySSES3 = "S3"
|
||||
// GatewaySSEC is set when SSE-C encryption needed on both gateway and backend
|
||||
gatewaySSEC = "C"
|
||||
)
|
||||
|
||||
// StringInSlice - this function finds whether a string is in a list
|
||||
func StringInSlice(x string, list []string) bool {
|
||||
for _, y := range list {
|
||||
if x == y {
|
||||
func (sse gatewaySSE) SSES3() bool {
|
||||
for _, v := range sse {
|
||||
if v == gatewaySSES3 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ProcessSize - this function processes size so that we can calculate bytes BytesProcessed.
|
||||
func ProcessSize(myrecord []string) int64 {
|
||||
if len(myrecord) > 0 {
|
||||
var size int64
|
||||
size = int64(len(myrecord)-1) + 1
|
||||
for i := range myrecord {
|
||||
size += int64(len(myrecord[i]))
|
||||
func (sse gatewaySSE) SSEC() bool {
|
||||
for _, v := range sse {
|
||||
if v == gatewaySSEC {
|
||||
return true
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
return 0
|
||||
return false
|
||||
}
|
||||
|
||||
func (sse gatewaySSE) IsSet() bool {
|
||||
return sse.SSES3() || sse.SSEC()
|
||||
}
|
||||
+21
-17
@@ -18,7 +18,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -124,21 +123,21 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
// To avoid this error situation we check for port availability.
|
||||
logger.FatalIf(checkPortAvailability(globalMinioPort), "Unable to start the gateway")
|
||||
|
||||
// Create certs path.
|
||||
logger.FatalIf(createConfigDir(), "Unable to create configuration directories")
|
||||
|
||||
// Check and load TLS certificates.
|
||||
var err error
|
||||
globalPublicCerts, globalTLSCerts, globalIsSSL, err = getTLSConfig()
|
||||
logger.FatalIf(err, "Invalid TLS certificate file")
|
||||
|
||||
// Check and load Root CAs.
|
||||
globalRootCAs, err = getRootCAs(getCADir())
|
||||
globalRootCAs, err = getRootCAs(globalCertsCADir.Get())
|
||||
logger.FatalIf(err, "Failed to read root CAs (%v)", err)
|
||||
|
||||
// Handle common env vars.
|
||||
handleCommonEnvVars()
|
||||
|
||||
// Handle gateway specific env
|
||||
handleGatewayEnvVars()
|
||||
|
||||
// Validate if we have access, secret set through environment.
|
||||
if !globalIsEnvCreds {
|
||||
logger.Fatal(uiErrEnvCredentialsMissingGateway(nil), "Unable to start gateway")
|
||||
@@ -171,17 +170,11 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
logger.FatalIf(registerWebRouter(router), "Unable to configure web browser")
|
||||
}
|
||||
|
||||
// Currently only NAS and S3 gateway support encryption headers.
|
||||
encryptionEnabled := gatewayName == "s3" || gatewayName == "nas"
|
||||
|
||||
// Add API router.
|
||||
registerAPIRouter(router)
|
||||
|
||||
// Dummy endpoint representing gateway instance.
|
||||
globalEndpoints = []Endpoint{{
|
||||
URL: &url.URL{Path: "/minio/gateway"},
|
||||
IsLocal: true,
|
||||
}}
|
||||
|
||||
// Initialize Admin Peers.
|
||||
initGlobalAdminPeers(globalEndpoints)
|
||||
registerAPIRouter(router, encryptionEnabled)
|
||||
|
||||
var getCert certs.GetCertificateFunc
|
||||
if globalTLSCerts != nil {
|
||||
@@ -229,6 +222,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
// Load globalServerConfig from etcd
|
||||
_ = globalConfigSys.Init(newObject)
|
||||
}
|
||||
|
||||
// Load logger subsystem
|
||||
loadLoggers()
|
||||
|
||||
@@ -265,8 +259,18 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
_ = globalNotificationSys.Init(newObject)
|
||||
}
|
||||
|
||||
if globalAutoEncryption && !newObject.IsEncryptionSupported() {
|
||||
logger.Fatal(errors.New("Invalid KMS configuration"), "auto-encryption is enabled but gateway does not support encryption")
|
||||
// Encryption support checks in gateway mode.
|
||||
{
|
||||
|
||||
if (globalAutoEncryption || GlobalKMS != nil) && !newObject.IsEncryptionSupported() {
|
||||
logger.Fatal(errInvalidArgument,
|
||||
"Encryption support is requested but (%s) gateway does not support encryption", gw.Name())
|
||||
}
|
||||
|
||||
if GlobalGatewaySSE.IsSet() && GlobalKMS == nil {
|
||||
logger.Fatal(uiErrInvalidGWSSEEnvValue(nil).Msg("MINIO_GATEWAY_SSE set but KMS is not configured"),
|
||||
"Unable to start gateway with SSE")
|
||||
}
|
||||
}
|
||||
|
||||
// Once endpoints are finalized, initialize the new object api.
|
||||
|
||||
@@ -49,7 +49,7 @@ func (a GatewayUnsupported) PutObjectPart(ctx context.Context, bucket string, ob
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||
func (a GatewayUnsupported) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (lpi ListPartsInfo, err error) {
|
||||
func (a GatewayUnsupported) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (lpi ListPartsInfo, err error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return lpi, NotImplemented{}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func (a GatewayUnsupported) IsListenBucketSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEncryptionSupported returns whether server side encryption is applicable for this layer.
|
||||
// IsEncryptionSupported returns whether server side encryption is implemented for this layer.
|
||||
func (a GatewayUnsupported) IsEncryptionSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -981,7 +981,7 @@ func (a *azureObjects) PutObjectPart(ctx context.Context, bucket, object, upload
|
||||
}
|
||||
|
||||
// ListObjectParts - Use Azure equivalent GetBlockList.
|
||||
func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int) (result minio.ListPartsInfo, err error) {
|
||||
func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (result minio.ListPartsInfo, err error) {
|
||||
if err = a.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -684,7 +684,7 @@ func (l *b2Objects) PutObjectPart(ctx context.Context, bucket string, object str
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket, uses B2's LargeFile upload API.
|
||||
func (l *b2Objects) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (lpi minio.ListPartsInfo, err error) {
|
||||
func (l *b2Objects) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (lpi minio.ListPartsInfo, err error) {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return lpi, err
|
||||
|
||||
@@ -1033,8 +1033,7 @@ func (l *gcsGateway) ListMultipartUploads(ctx context.Context, bucket string, pr
|
||||
UploadID: components[3],
|
||||
Initiated: attrs.Created,
|
||||
}
|
||||
uploads = []minio.MultipartInfo{upload}
|
||||
break
|
||||
uploads = append(uploads, upload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1121,7 +1120,7 @@ func gcsGetPartInfo(ctx context.Context, attrs *storage.ObjectAttrs) (minio.Part
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||
func (l *gcsGateway) ListObjectParts(ctx context.Context, bucket string, key string, uploadID string, partNumberMarker int, maxParts int) (minio.ListPartsInfo, error) {
|
||||
func (l *gcsGateway) ListObjectParts(ctx context.Context, bucket string, key string, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (minio.ListPartsInfo, error) {
|
||||
it := l.client.Bucket(bucket).Objects(ctx, &storage.Query{
|
||||
Prefix: path.Join(gcsMinioMultipartPathV1, uploadID),
|
||||
})
|
||||
|
||||
@@ -886,7 +886,7 @@ func (l *ossObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, d
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||
func (l *ossObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int) (lpi minio.ListPartsInfo, err error) {
|
||||
func (l *ossObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int, opts minio.ObjectOptions) (lpi minio.ListPartsInfo, err error) {
|
||||
lupr, err := ossListObjectParts(l.Client, bucket, object, uploadID, partNumberMarker, maxParts)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package s3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
minio "github.com/minio/minio/cmd"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var (
|
||||
errGWMetaNotFound = errors.New("dare.meta file not found")
|
||||
errGWMetaInvalidFormat = errors.New("dare.meta format is invalid")
|
||||
)
|
||||
|
||||
// A gwMetaV1 represents `gw.json` metadata header.
|
||||
type gwMetaV1 struct {
|
||||
Version string `json:"version"` // Version of the current `gw.json`.
|
||||
Format string `json:"format"` // Format of the current `gw.json`.
|
||||
Stat minio.StatInfo `json:"stat"` // Stat of the current object `gw.json`.
|
||||
ETag string `json:"etag"` // ETag of the current object
|
||||
|
||||
// Metadata map for current object `gw.json`.
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
// Captures all the individual object `gw.json`.
|
||||
Parts []minio.ObjectPartInfo `json:"parts,omitempty"`
|
||||
}
|
||||
|
||||
// Gateway metadata constants.
|
||||
const (
|
||||
// Gateway meta version.
|
||||
gwMetaVersion = "1.0.0"
|
||||
|
||||
// Gateway meta version.
|
||||
gwMetaVersion100 = "1.0.0"
|
||||
|
||||
// Gateway meta format string.
|
||||
gwMetaFormat = "gw"
|
||||
|
||||
// Add new constants here.
|
||||
)
|
||||
|
||||
// newGWMetaV1 - initializes new gwMetaV1, adds version.
|
||||
func newGWMetaV1() (gwMeta gwMetaV1) {
|
||||
gwMeta = gwMetaV1{}
|
||||
gwMeta.Version = gwMetaVersion
|
||||
gwMeta.Format = gwMetaFormat
|
||||
return gwMeta
|
||||
}
|
||||
|
||||
// IsValid - tells if the format is sane by validating the version
|
||||
// string, format fields.
|
||||
func (m gwMetaV1) IsValid() bool {
|
||||
return ((m.Version == gwMetaVersion || m.Version == gwMetaVersion100) &&
|
||||
m.Format == gwMetaFormat)
|
||||
}
|
||||
|
||||
// Converts metadata to object info.
|
||||
func (m gwMetaV1) ToObjectInfo(bucket, object string) minio.ObjectInfo {
|
||||
filterKeys := append([]string{
|
||||
"ETag",
|
||||
"Content-Length",
|
||||
"Last-Modified",
|
||||
"Content-Type",
|
||||
}, defaultFilterKeys...)
|
||||
objInfo := minio.ObjectInfo{
|
||||
IsDir: false,
|
||||
Bucket: bucket,
|
||||
Name: object,
|
||||
Size: m.Stat.Size,
|
||||
ModTime: m.Stat.ModTime,
|
||||
ContentType: m.Meta["content-type"],
|
||||
ContentEncoding: m.Meta["content-encoding"],
|
||||
ETag: minio.CanonicalizeETag(m.ETag),
|
||||
UserDefined: minio.CleanMetadataKeys(m.Meta, filterKeys...),
|
||||
Parts: m.Parts,
|
||||
}
|
||||
|
||||
if sc, ok := m.Meta["x-amz-storage-class"]; ok {
|
||||
objInfo.StorageClass = sc
|
||||
}
|
||||
// Success.
|
||||
return objInfo
|
||||
}
|
||||
|
||||
// ObjectToPartOffset - translate offset of an object to offset of its individual part.
|
||||
func (m gwMetaV1) ObjectToPartOffset(ctx context.Context, offset int64) (partIndex int, partOffset int64, err error) {
|
||||
if offset == 0 {
|
||||
// Special case - if offset is 0, then partIndex and partOffset are always 0.
|
||||
return 0, 0, nil
|
||||
}
|
||||
partOffset = offset
|
||||
// Seek until object offset maps to a particular part offset.
|
||||
for i, part := range m.Parts {
|
||||
partIndex = i
|
||||
// Offset is smaller than size we have reached the proper part offset.
|
||||
if partOffset < part.Size {
|
||||
return partIndex, partOffset, nil
|
||||
}
|
||||
// Continue to towards the next part.
|
||||
partOffset -= part.Size
|
||||
}
|
||||
logger.LogIf(ctx, minio.InvalidRange{})
|
||||
// Offset beyond the size of the object return InvalidRange.
|
||||
return 0, 0, minio.InvalidRange{}
|
||||
}
|
||||
|
||||
// parses gateway metadata stat info from metadata json
|
||||
func parseGWStat(gwMetaBuf []byte) (si minio.StatInfo, e error) {
|
||||
// obtain stat info.
|
||||
stat := minio.StatInfo{}
|
||||
// fetching modTime.
|
||||
modTime, err := time.Parse(time.RFC3339, gjson.GetBytes(gwMetaBuf, "stat.modTime").String())
|
||||
if err != nil {
|
||||
return si, err
|
||||
}
|
||||
stat.ModTime = modTime
|
||||
// obtain Stat.Size .
|
||||
stat.Size = gjson.GetBytes(gwMetaBuf, "stat.size").Int()
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
// parses gateway metadata version from metadata json
|
||||
func parseGWVersion(gwMetaBuf []byte) string {
|
||||
return gjson.GetBytes(gwMetaBuf, "version").String()
|
||||
}
|
||||
|
||||
// parses gateway ETag from metadata json
|
||||
func parseGWETag(gwMetaBuf []byte) string {
|
||||
return gjson.GetBytes(gwMetaBuf, "etag").String()
|
||||
}
|
||||
|
||||
// parses gateway metadata format from metadata json
|
||||
func parseGWFormat(gwMetaBuf []byte) string {
|
||||
return gjson.GetBytes(gwMetaBuf, "format").String()
|
||||
}
|
||||
|
||||
// parses gateway metadata json to get list of ObjectPartInfo
|
||||
func parseGWParts(gwMetaBuf []byte) []minio.ObjectPartInfo {
|
||||
// Parse the GW Parts.
|
||||
partsResult := gjson.GetBytes(gwMetaBuf, "parts").Array()
|
||||
partInfo := make([]minio.ObjectPartInfo, len(partsResult))
|
||||
for i, p := range partsResult {
|
||||
info := minio.ObjectPartInfo{}
|
||||
info.Number = int(p.Get("number").Int())
|
||||
info.Name = p.Get("name").String()
|
||||
info.ETag = p.Get("etag").String()
|
||||
info.Size = p.Get("size").Int()
|
||||
partInfo[i] = info
|
||||
}
|
||||
return partInfo
|
||||
}
|
||||
|
||||
// parses gateway metadata json to get the metadata map
|
||||
func parseGWMetaMap(gwMetaBuf []byte) map[string]string {
|
||||
// Get gwMetaV1.Meta map.
|
||||
metaMapResult := gjson.GetBytes(gwMetaBuf, "meta").Map()
|
||||
metaMap := make(map[string]string)
|
||||
for key, valResult := range metaMapResult {
|
||||
metaMap[key] = valResult.String()
|
||||
}
|
||||
return metaMap
|
||||
}
|
||||
|
||||
// Constructs GWMetaV1 using `gjson` lib to retrieve each field.
|
||||
func gwMetaUnmarshalJSON(ctx context.Context, gwMetaBuf []byte) (gwMeta gwMetaV1, e error) {
|
||||
// obtain version.
|
||||
gwMeta.Version = parseGWVersion(gwMetaBuf)
|
||||
// obtain format.
|
||||
gwMeta.Format = parseGWFormat(gwMetaBuf)
|
||||
// Parse gwMetaV1.Stat .
|
||||
stat, err := parseGWStat(gwMetaBuf)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return gwMeta, err
|
||||
}
|
||||
gwMeta.ETag = parseGWETag(gwMetaBuf)
|
||||
gwMeta.Stat = stat
|
||||
|
||||
// Parse the GW Parts.
|
||||
gwMeta.Parts = parseGWParts(gwMetaBuf)
|
||||
// parse gwMetaV1.
|
||||
gwMeta.Meta = parseGWMetaMap(gwMetaBuf)
|
||||
|
||||
return gwMeta, nil
|
||||
}
|
||||
|
||||
// readGWMeta reads `dare.meta` and returns back GW metadata structure.
|
||||
func readGWMetadata(ctx context.Context, buf bytes.Buffer) (gwMeta gwMetaV1, err error) {
|
||||
if buf.Len() == 0 {
|
||||
return gwMetaV1{}, errGWMetaNotFound
|
||||
}
|
||||
gwMeta, err = gwMetaUnmarshalJSON(ctx, buf.Bytes())
|
||||
if err != nil {
|
||||
return gwMetaV1{}, err
|
||||
}
|
||||
if !gwMeta.IsValid() {
|
||||
return gwMetaV1{}, errGWMetaInvalidFormat
|
||||
}
|
||||
// Return structured `dare.meta`.
|
||||
return gwMeta, nil
|
||||
}
|
||||
|
||||
// getGWMetadata - unmarshals dare.meta into a *minio.PutObjReader
|
||||
func getGWMetadata(ctx context.Context, bucket, prefix string, gwMeta gwMetaV1) (*minio.PutObjReader, error) {
|
||||
// Marshal json.
|
||||
metadataBytes, err := json.Marshal(&gwMeta)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
hashReader, err := hash.NewReader(bytes.NewReader(metadataBytes), int64(len(metadataBytes)), "", "", int64(len(metadataBytes)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return minio.NewPutObjReader(hashReader, nil, nil), nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package s3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests for GW metadata format validity.
|
||||
func TestGWMetaFormatValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name int
|
||||
version string
|
||||
format string
|
||||
want bool
|
||||
}{
|
||||
{1, "123", "fs", false},
|
||||
{2, "123", gwMetaFormat, false},
|
||||
{3, gwMetaVersion, "test", false},
|
||||
{4, gwMetaVersion100, "hello", false},
|
||||
{5, gwMetaVersion, gwMetaFormat, true},
|
||||
{6, gwMetaVersion100, gwMetaFormat, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
m := newGWMetaV1()
|
||||
m.Version = tt.version
|
||||
m.Format = tt.format
|
||||
if got := m.IsValid(); got != tt.want {
|
||||
t.Errorf("Test %d: Expected %v but received %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for reading GW metadata info.
|
||||
func TestReadGWMetadata(t *testing.T) {
|
||||
tests := []struct {
|
||||
metaStr string
|
||||
pass bool
|
||||
}{
|
||||
{`{"version": "` + gwMetaVersion + `", "format":"` + gwMetaFormat + `", {"stat": {"size": "132", "modTime": "2018-08-31T22:25:39.23626461Z" }}}`, true},
|
||||
{`{"version": "` + gwMetaVersion + `", "format":"` + gwMetaFormat + `", {"stat": {"size": "132", "modTime": "0000-00-00T00:00:00.00000000Z" }}}`, false},
|
||||
{`{"version": "` + gwMetaVersion + `", "format":"` + gwMetaFormat + `", {"stat": {"size": "5242880", "modTime": "2018-08-31T22:25:39.23626461Z" }},"meta":{"content-type":"application/octet-stream","etag":"57c743902b2fc8eea6ba3bb4fc58c8e8"},"parts":[{"number":1,"name":"part.1","etag":"","size":5242880}]}}`, true},
|
||||
{`{"version": "` + gwMetaVersion + `", "format":"` + gwMetaFormat + `", {"stat": {"size": "68190720", "modTime": "2018-08-31T22:25:39.23626461Z" }},"meta":{"X-Minio-Internal-Encrypted-Multipart":"","X-Minio-Internal-Server-Side-Encryption-Iv":"kdbOcKdXD3Sew8tOiHe5eI9xkX1oQ2W9JURz0oslCZA=","X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm":"DAREv2-HMAC-SHA256","X-Minio-Internal-Server-Side-Encryption-Sealed-Key":"IAAfAMfqKrxMXC9LuiI7ENP+p0xArepzAiIeB/MftFp7Xmq2OzDkKlmNbj5RKI89RrjiAbOVLSSEMvqQsrIrTQ==","content-type":"text/plain; charset=utf-8","etag":"2b137fa4ab80126af54623b010c98de6-2"},"parts":[{"number":1,"name":"part.1","etag":"c5cac075eefdab801a5198812f51b36e","size":67141632},{"number":2,"name":"part.2","etag":"ccdf4b774bc3be8eef9a8987309e8171","size":1049088}]}`, true},
|
||||
{`{"version": "` + gwMetaVersion + `", "format":"` + gwMetaFormat + `", {"stat": {"size": "68190720", "modTime": "2018-08-31T22:25:39.23626461Z" }},"meta":{"X-Minio-Internal-Encrypted-Multipart":"","X-Minio-Internal-Server-Side-Encryption-Iv":"kdbOcKdXD3Sew8tOiHe5eI9xkX1oQ2W9JURz0oslCZA=","X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm":"DAREv2-HMAC-SHA256","X-Minio-Internal-Server-Side-Encryption-Sealed-Key":"IAAfAMfqKrxMXC9LuiI7ENP+p0xArepzAiIeB/MftFp7Xmq2OzDkKlmNbj5RKI89RrjiAbOVLSSEMvqQsrIrTQ==","content-type":"text/plain; charset=utf-8","etag":"2b137fa4ab80126af54623b010c98de6-2"},"parts":"123"}`, true},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
buf := bytes.NewBufferString(tt.metaStr)
|
||||
m, err := readGWMetadata(context.Background(), *buf)
|
||||
if err != nil && tt.pass {
|
||||
t.Errorf("Test %d: Expected parse gw metadata to succeed, but failed", i)
|
||||
}
|
||||
if err == nil && !tt.pass {
|
||||
t.Errorf("Test %d: Expected parse gw metadata to succeed, but failed", i)
|
||||
}
|
||||
if err == nil {
|
||||
if m.Version != gwMetaVersion {
|
||||
t.Errorf("Test %d: Expected version %s, but failed with %s", i, gwMetaVersion, m.Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,778 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package s3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// name of custom multipart metadata file for s3 backend.
|
||||
gwdareMetaJSON string = "dare.meta"
|
||||
|
||||
// name of temporary per part metadata file
|
||||
gwpartMetaJSON string = "part.meta"
|
||||
// custom multipart files are stored under the defaultMinioGWPrefix
|
||||
defaultMinioGWPrefix = ".minio"
|
||||
defaultGWContentFileName = "data"
|
||||
slashSeparator = "/"
|
||||
)
|
||||
|
||||
// s3EncObjects is a wrapper around s3Objects and implements gateway calls for
|
||||
// custom large objects encrypted at the gateway
|
||||
type s3EncObjects struct {
|
||||
s3Objects
|
||||
}
|
||||
|
||||
/*
|
||||
NOTE:
|
||||
Custom gateway encrypted objects are stored on backend as follows:
|
||||
obj/.minio/data <= encrypted content
|
||||
obj/.minio/dare.meta <= metadata
|
||||
|
||||
When a multipart upload operation is in progress, the metadata set during
|
||||
NewMultipartUpload is stored in obj/.minio/uploadID/dare.meta and each
|
||||
UploadPart operation saves additional state of the part's encrypted ETag and
|
||||
encrypted size in obj/.minio/uploadID/part1/part.meta
|
||||
|
||||
All the part metadata and temp dare.meta are cleaned up when upload completes
|
||||
*/
|
||||
|
||||
// ListObjects lists all blobs in S3 bucket filtered by prefix
|
||||
func (l *s3EncObjects) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi minio.ListObjectsInfo, e error) {
|
||||
var continuationToken, startAfter string
|
||||
res, err := l.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, maxKeys, false, startAfter)
|
||||
if err != nil {
|
||||
return loi, err
|
||||
}
|
||||
loi.IsTruncated = res.IsTruncated
|
||||
loi.NextMarker = res.NextContinuationToken
|
||||
loi.Objects = res.Objects
|
||||
loi.Prefixes = res.Prefixes
|
||||
return loi, nil
|
||||
|
||||
}
|
||||
|
||||
// ListObjectsV2 lists all blobs in S3 bucket filtered by prefix
|
||||
func (l *s3EncObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, e error) {
|
||||
|
||||
var objects []minio.ObjectInfo
|
||||
var prefixes []string
|
||||
var isTruncated bool
|
||||
|
||||
// filter out objects that contain a .minio prefix, but is not a dare.meta metadata file.
|
||||
for {
|
||||
loi, e = l.s3Objects.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, 1000, fetchOwner, startAfter)
|
||||
if e != nil {
|
||||
return loi, minio.ErrorRespToObjectError(e, bucket)
|
||||
}
|
||||
for _, obj := range loi.Objects {
|
||||
startAfter = obj.Name
|
||||
continuationToken = loi.NextContinuationToken
|
||||
isTruncated = loi.IsTruncated
|
||||
|
||||
if !isGWObject(obj.Name) {
|
||||
continue
|
||||
}
|
||||
// get objectname and ObjectInfo from the custom metadata file
|
||||
if strings.HasSuffix(obj.Name, gwdareMetaJSON) {
|
||||
objSlice := strings.Split(obj.Name, slashSeparator+defaultMinioGWPrefix)
|
||||
gwMeta, e := l.getGWMetadata(ctx, bucket, getDareMetaPath(objSlice[0]))
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
oInfo := gwMeta.ToObjectInfo(bucket, objSlice[0])
|
||||
objects = append(objects, oInfo)
|
||||
} else {
|
||||
objects = append(objects, obj)
|
||||
}
|
||||
if len(objects) > maxKeys {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, p := range loi.Prefixes {
|
||||
objName := strings.TrimSuffix(p, slashSeparator)
|
||||
gm, err := l.getGWMetadata(ctx, bucket, getDareMetaPath(objName))
|
||||
// if prefix is actually a custom multi-part object, append it to objects
|
||||
if err == nil {
|
||||
objects = append(objects, gm.ToObjectInfo(bucket, objName))
|
||||
continue
|
||||
}
|
||||
isPrefix := l.isPrefix(ctx, bucket, p, fetchOwner, startAfter)
|
||||
if isPrefix {
|
||||
prefixes = append(prefixes, p)
|
||||
}
|
||||
}
|
||||
if (len(objects) > maxKeys) || !loi.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
loi.IsTruncated = isTruncated
|
||||
loi.ContinuationToken = continuationToken
|
||||
loi.Objects = make([]minio.ObjectInfo, 0)
|
||||
loi.Prefixes = make([]string, 0)
|
||||
|
||||
for _, obj := range objects {
|
||||
loi.NextContinuationToken = obj.Name
|
||||
loi.Objects = append(loi.Objects, obj)
|
||||
}
|
||||
for _, pfx := range prefixes {
|
||||
if pfx != prefix {
|
||||
loi.Prefixes = append(loi.Prefixes, pfx)
|
||||
}
|
||||
}
|
||||
return loi, nil
|
||||
}
|
||||
|
||||
// isGWObject returns true if it is a custom object
|
||||
func isGWObject(objName string) bool {
|
||||
isEncrypted := strings.Contains(objName, defaultMinioGWPrefix)
|
||||
if !isEncrypted {
|
||||
return true
|
||||
}
|
||||
// ignore temp part.meta files
|
||||
if strings.Contains(objName, gwpartMetaJSON) {
|
||||
return false
|
||||
}
|
||||
|
||||
pfxSlice := strings.Split(objName, slashSeparator)
|
||||
var i1, i2 int
|
||||
for i := len(pfxSlice) - 1; i >= 0; i-- {
|
||||
p := pfxSlice[i]
|
||||
if p == defaultMinioGWPrefix {
|
||||
i1 = i
|
||||
}
|
||||
if p == gwdareMetaJSON {
|
||||
i2 = i
|
||||
}
|
||||
if i1 > 0 && i2 > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// incomplete uploads would have a uploadID between defaultMinioGWPrefix and gwdareMetaJSON
|
||||
return i2 > 0 && i1 > 0 && i2-i1 == 1
|
||||
}
|
||||
|
||||
// isPrefix returns true if prefix exists and is not an incomplete multipart upload entry
|
||||
func (l *s3EncObjects) isPrefix(ctx context.Context, bucket, prefix string, fetchOwner bool, startAfter string) bool {
|
||||
var continuationToken, delimiter string
|
||||
|
||||
for {
|
||||
loi, e := l.s3Objects.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, 1000, fetchOwner, startAfter)
|
||||
if e != nil {
|
||||
return false
|
||||
}
|
||||
for _, obj := range loi.Objects {
|
||||
if isGWObject(obj.Name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
continuationToken = loi.NextContinuationToken
|
||||
if !loi.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetObject reads an object from S3. Supports additional
|
||||
// parameters like offset and length which are synonymous with
|
||||
// HTTP Range requests.
|
||||
func (l *s3EncObjects) GetObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
return l.getObject(ctx, bucket, key, startOffset, length, writer, etag, opts)
|
||||
}
|
||||
|
||||
func (l *s3EncObjects) isGWEncrypted(ctx context.Context, bucket, object string) bool {
|
||||
_, err := l.s3Objects.GetObjectInfo(ctx, bucket, getDareMetaPath(object), minio.ObjectOptions{})
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// getDaremetadata fetches dare.meta from s3 backend and marshals into a structured format.
|
||||
func (l *s3EncObjects) getGWMetadata(ctx context.Context, bucket, metaFileName string) (m gwMetaV1, err error) {
|
||||
oi, err1 := l.s3Objects.GetObjectInfo(ctx, bucket, metaFileName, minio.ObjectOptions{})
|
||||
if err1 != nil {
|
||||
return m, err1
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err = l.s3Objects.GetObject(ctx, bucket, metaFileName, 0, oi.Size, &buffer, oi.ETag, minio.ObjectOptions{})
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
return readGWMetadata(ctx, buffer)
|
||||
}
|
||||
|
||||
// writes dare metadata to the s3 backend
|
||||
func (l *s3EncObjects) writeGWMetadata(ctx context.Context, bucket, metaFileName string, m gwMetaV1, o minio.ObjectOptions) error {
|
||||
reader, err := getGWMetadata(ctx, bucket, metaFileName, m)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
_, err = l.s3Objects.PutObject(ctx, bucket, metaFileName, reader, map[string]string{}, o)
|
||||
return err
|
||||
}
|
||||
|
||||
// returns path of temporary metadata json file for the upload
|
||||
func getTmpDareMetaPath(object, uploadID string) string {
|
||||
return path.Join(getGWMetaPath(object), uploadID, gwdareMetaJSON)
|
||||
}
|
||||
|
||||
// returns path of metadata json file for encrypted objects
|
||||
func getDareMetaPath(object string) string {
|
||||
return path.Join(getGWMetaPath(object), gwdareMetaJSON)
|
||||
}
|
||||
|
||||
// returns path of temporary part metadata file for multipart uploads
|
||||
func getPartMetaPath(object, uploadID string, partID int) string {
|
||||
return path.Join(object, defaultMinioGWPrefix, uploadID, strconv.Itoa(partID), gwpartMetaJSON)
|
||||
}
|
||||
|
||||
// deletes the custom dare metadata file saved at the backend
|
||||
func (l *s3EncObjects) deleteGWMetadata(ctx context.Context, bucket, metaFileName string) error {
|
||||
return l.s3Objects.DeleteObject(ctx, bucket, metaFileName)
|
||||
}
|
||||
|
||||
func (l *s3EncObjects) getObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
var o minio.ObjectOptions
|
||||
if minio.GlobalGatewaySSE.SSEC() {
|
||||
o = opts
|
||||
}
|
||||
dmeta, err := l.getGWMetadata(ctx, bucket, getDareMetaPath(key))
|
||||
if err != nil {
|
||||
// unencrypted content
|
||||
return l.s3Objects.GetObject(ctx, bucket, key, startOffset, length, writer, etag, o)
|
||||
}
|
||||
if startOffset < 0 {
|
||||
logger.LogIf(ctx, minio.InvalidRange{})
|
||||
}
|
||||
|
||||
// For negative length read everything.
|
||||
if length < 0 {
|
||||
length = dmeta.Stat.Size - startOffset
|
||||
}
|
||||
// Reply back invalid range if the input offset and length fall out of range.
|
||||
if startOffset > dmeta.Stat.Size || startOffset+length > dmeta.Stat.Size {
|
||||
logger.LogIf(ctx, minio.InvalidRange{OffsetBegin: startOffset, OffsetEnd: length, ResourceSize: dmeta.Stat.Size})
|
||||
return minio.InvalidRange{OffsetBegin: startOffset, OffsetEnd: length, ResourceSize: dmeta.Stat.Size}
|
||||
}
|
||||
// Get start part index and offset.
|
||||
_, partOffset, err := dmeta.ObjectToPartOffset(ctx, startOffset)
|
||||
if err != nil {
|
||||
return minio.InvalidRange{OffsetBegin: startOffset, OffsetEnd: length, ResourceSize: dmeta.Stat.Size}
|
||||
}
|
||||
|
||||
// Calculate endOffset according to length
|
||||
endOffset := startOffset
|
||||
if length > 0 {
|
||||
endOffset += length - 1
|
||||
}
|
||||
|
||||
// Get last part index to read given length.
|
||||
if _, _, err := dmeta.ObjectToPartOffset(ctx, endOffset); err != nil {
|
||||
return minio.InvalidRange{OffsetBegin: startOffset, OffsetEnd: length, ResourceSize: dmeta.Stat.Size}
|
||||
}
|
||||
return l.s3Objects.GetObject(ctx, bucket, key, partOffset, endOffset, writer, dmeta.ETag, o)
|
||||
}
|
||||
|
||||
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||
func (l *s3EncObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, o minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||
var opts minio.ObjectOptions
|
||||
if minio.GlobalGatewaySSE.SSEC() {
|
||||
opts = o
|
||||
}
|
||||
objInfo, err := l.GetObjectInfo(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
return l.s3Objects.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
||||
}
|
||||
objInfo.UserDefined = minio.CleanMinioInternalMetadataKeys(objInfo.UserDefined)
|
||||
fn, off, length, err := minio.NewGetObjectReader(rs, objInfo)
|
||||
if err != nil {
|
||||
return nil, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
if l.isGWEncrypted(ctx, bucket, object) {
|
||||
object = getGWContentPath(object)
|
||||
}
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
err := l.getObject(ctx, bucket, object, off, length, pw, objInfo.ETag, opts)
|
||||
pw.CloseWithError(err)
|
||||
}()
|
||||
|
||||
// Setup cleanup function to cause the above go-routine to
|
||||
// exit in case of partial read
|
||||
pipeCloser := func() { pr.Close() }
|
||||
return fn(pr, h, pipeCloser)
|
||||
}
|
||||
|
||||
// GetObjectInfo reads object info and replies back ObjectInfo
|
||||
// For custom gateway encrypted large objects, the ObjectInfo is retrieved from the dare.meta file.
|
||||
func (l *s3EncObjects) GetObjectInfo(ctx context.Context, bucket string, object string, o minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
var opts minio.ObjectOptions
|
||||
if minio.GlobalGatewaySSE.SSEC() {
|
||||
opts = o
|
||||
}
|
||||
|
||||
gwMeta, err := l.getGWMetadata(ctx, bucket, getDareMetaPath(object))
|
||||
if err != nil {
|
||||
return l.s3Objects.GetObjectInfo(ctx, bucket, object, opts)
|
||||
}
|
||||
return gwMeta.ToObjectInfo(bucket, object), nil
|
||||
}
|
||||
|
||||
// CopyObject copies an object from source bucket to a destination bucket.
|
||||
func (l *s3EncObjects) CopyObject(ctx context.Context, srcBucket string, srcObject string, dstBucket string, dstObject string, srcInfo minio.ObjectInfo, s, d minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
cpSrcDstSame := strings.EqualFold(path.Join(srcBucket, srcObject), path.Join(dstBucket, dstObject))
|
||||
if cpSrcDstSame {
|
||||
var gwMeta gwMetaV1
|
||||
if s.ServerSideEncryption != nil && d.ServerSideEncryption != nil &&
|
||||
((s.ServerSideEncryption.Type() == encrypt.SSEC && d.ServerSideEncryption.Type() == encrypt.SSEC) ||
|
||||
(s.ServerSideEncryption.Type() == encrypt.S3 && d.ServerSideEncryption.Type() == encrypt.S3)) {
|
||||
gwMeta, err = l.getGWMetadata(ctx, srcBucket, getDareMetaPath(srcObject))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
header := make(http.Header)
|
||||
if d.ServerSideEncryption != nil {
|
||||
d.ServerSideEncryption.Marshal(header)
|
||||
}
|
||||
for k, v := range header {
|
||||
srcInfo.UserDefined[k] = v[0]
|
||||
}
|
||||
gwMeta.Meta = srcInfo.UserDefined
|
||||
if err = l.writeGWMetadata(ctx, dstBucket, getDareMetaPath(dstObject), gwMeta, minio.ObjectOptions{}); err != nil {
|
||||
return objInfo, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
return gwMeta.ToObjectInfo(dstBucket, dstObject), nil
|
||||
}
|
||||
}
|
||||
return l.PutObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, srcInfo.UserDefined, d)
|
||||
}
|
||||
|
||||
// DeleteObject deletes a blob in bucket
|
||||
// For custom gateway encrypted large objects, cleans up encrypted content and metadata files
|
||||
// from the backend.
|
||||
func (l *s3EncObjects) DeleteObject(ctx context.Context, bucket string, object string) error {
|
||||
|
||||
// Get dare meta json
|
||||
if _, err := l.getGWMetadata(ctx, bucket, getDareMetaPath(object)); err != nil {
|
||||
return l.s3Objects.DeleteObject(ctx, bucket, object)
|
||||
}
|
||||
// delete encrypted object
|
||||
l.s3Objects.DeleteObject(ctx, bucket, getGWContentPath(object))
|
||||
return l.deleteGWMetadata(ctx, bucket, getDareMetaPath(object))
|
||||
}
|
||||
|
||||
// ListMultipartUploads lists all multipart uploads.
|
||||
func (l *s3EncObjects) ListMultipartUploads(ctx context.Context, bucket string, prefix string, keyMarker string, uploadIDMarker string, delimiter string, maxUploads int) (lmi minio.ListMultipartsInfo, e error) {
|
||||
|
||||
lmi, e = l.s3Objects.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
lmi.KeyMarker = strings.TrimSuffix(lmi.KeyMarker, getGWContentPath("/"))
|
||||
lmi.NextKeyMarker = strings.TrimSuffix(lmi.NextKeyMarker, getGWContentPath("/"))
|
||||
for i := range lmi.Uploads {
|
||||
lmi.Uploads[i].Object = strings.TrimSuffix(lmi.Uploads[i].Object, getGWContentPath("/"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NewMultipartUpload uploads object in multiple parts
|
||||
func (l *s3EncObjects) NewMultipartUpload(ctx context.Context, bucket string, object string, metadata map[string]string, o minio.ObjectOptions) (uploadID string, err error) {
|
||||
var opts minio.ObjectOptions
|
||||
if o.ServerSideEncryption != nil &&
|
||||
((minio.GlobalGatewaySSE.SSEC() && o.ServerSideEncryption.Type() == encrypt.SSEC) ||
|
||||
(minio.GlobalGatewaySSE.SSES3() && o.ServerSideEncryption.Type() == encrypt.S3)) {
|
||||
opts = o
|
||||
}
|
||||
if o.ServerSideEncryption == nil {
|
||||
return l.s3Objects.NewMultipartUpload(ctx, bucket, object, metadata, opts)
|
||||
}
|
||||
uploadID, err = l.s3Objects.NewMultipartUpload(ctx, bucket, getGWContentPath(object), map[string]string{}, opts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Create uploadID and write a temporary dare.meta object under object/uploadID prefix
|
||||
gwmeta := newGWMetaV1()
|
||||
gwmeta.Meta = metadata
|
||||
gwmeta.Stat.ModTime = time.Now().UTC()
|
||||
err = l.writeGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID), gwmeta, minio.ObjectOptions{})
|
||||
if err != nil {
|
||||
return uploadID, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
return uploadID, nil
|
||||
}
|
||||
|
||||
// PutObject creates a new object with the incoming data,
|
||||
func (l *s3EncObjects) PutObject(ctx context.Context, bucket string, object string, data *minio.PutObjReader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
var s3Opts minio.ObjectOptions
|
||||
if opts.ServerSideEncryption != nil &&
|
||||
((minio.GlobalGatewaySSE.SSEC() && opts.ServerSideEncryption.Type() == encrypt.SSEC) ||
|
||||
(minio.GlobalGatewaySSE.SSES3() && opts.ServerSideEncryption.Type() == encrypt.S3)) {
|
||||
s3Opts = opts
|
||||
}
|
||||
if opts.ServerSideEncryption == nil {
|
||||
defer l.deleteGWMetadata(ctx, bucket, getDareMetaPath(object))
|
||||
defer l.DeleteObject(ctx, bucket, getGWContentPath(object))
|
||||
return l.s3Objects.PutObject(ctx, bucket, object, data, metadata, s3Opts)
|
||||
}
|
||||
|
||||
oi, err := l.s3Objects.PutObject(ctx, bucket, getGWContentPath(object), data, map[string]string{}, s3Opts)
|
||||
if err != nil {
|
||||
return objInfo, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
|
||||
gwMeta := newGWMetaV1()
|
||||
gwMeta.Meta = make(map[string]string)
|
||||
for k, v := range oi.UserDefined {
|
||||
gwMeta.Meta[k] = v
|
||||
}
|
||||
for k, v := range metadata {
|
||||
gwMeta.Meta[k] = v
|
||||
}
|
||||
encMD5 := data.MD5CurrentHexString()
|
||||
|
||||
gwMeta.ETag = encMD5
|
||||
gwMeta.Stat.Size = oi.Size
|
||||
gwMeta.Stat.ModTime = time.Now().UTC()
|
||||
if err = l.writeGWMetadata(ctx, bucket, getDareMetaPath(object), gwMeta, minio.ObjectOptions{}); err != nil {
|
||||
return objInfo, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
objInfo = gwMeta.ToObjectInfo(bucket, object)
|
||||
// delete any unencrypted content of the same name created previously
|
||||
l.s3Objects.DeleteObject(ctx, bucket, object)
|
||||
return objInfo, nil
|
||||
}
|
||||
|
||||
// PutObjectPart puts a part of object in bucket
|
||||
func (l *s3EncObjects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *minio.PutObjReader, opts minio.ObjectOptions) (pi minio.PartInfo, e error) {
|
||||
|
||||
if opts.ServerSideEncryption == nil {
|
||||
return l.s3Objects.PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
|
||||
}
|
||||
|
||||
var s3Opts minio.ObjectOptions
|
||||
// for sse-s3 encryption options should not be passed to backend
|
||||
if opts.ServerSideEncryption != nil && opts.ServerSideEncryption.Type() == encrypt.SSEC && minio.GlobalGatewaySSE.SSEC() {
|
||||
s3Opts = opts
|
||||
}
|
||||
|
||||
uploadPath := getTmpGWMetaPath(object, uploadID)
|
||||
tmpDareMeta := path.Join(uploadPath, gwdareMetaJSON)
|
||||
_, err := l.s3Objects.GetObjectInfo(ctx, bucket, tmpDareMeta, minio.ObjectOptions{})
|
||||
if err != nil {
|
||||
return pi, minio.InvalidUploadID{UploadID: uploadID}
|
||||
}
|
||||
|
||||
pi, e = l.s3Objects.PutObjectPart(ctx, bucket, getGWContentPath(object), uploadID, partID, data, s3Opts)
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
gwMeta := newGWMetaV1()
|
||||
gwMeta.Parts = make([]minio.ObjectPartInfo, 1)
|
||||
// Add incoming part.
|
||||
gwMeta.Parts[0] = minio.ObjectPartInfo{
|
||||
Number: partID,
|
||||
ETag: pi.ETag,
|
||||
Size: pi.Size,
|
||||
Name: strconv.Itoa(partID),
|
||||
}
|
||||
gwMeta.ETag = data.MD5CurrentHexString() // encrypted ETag
|
||||
gwMeta.Stat.Size = pi.Size
|
||||
gwMeta.Stat.ModTime = pi.LastModified
|
||||
|
||||
if err = l.writeGWMetadata(ctx, bucket, getPartMetaPath(object, uploadID, partID), gwMeta, minio.ObjectOptions{}); err != nil {
|
||||
return pi, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
return minio.PartInfo{
|
||||
Size: gwMeta.Stat.Size,
|
||||
ETag: minio.CanonicalizeETag(gwMeta.ETag),
|
||||
LastModified: gwMeta.Stat.ModTime,
|
||||
PartNumber: partID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CopyObjectPart creates a part in a multipart upload by copying
|
||||
// existing object or a part of it.
|
||||
func (l *s3EncObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string,
|
||||
partID int, startOffset, length int64, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (p minio.PartInfo, err error) {
|
||||
return l.PutObjectPart(ctx, destBucket, destObject, uploadID, partID, srcInfo.PutObjReader, dstOpts)
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||
func (l *s3EncObjects) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (lpi minio.ListPartsInfo, e error) {
|
||||
// We do not store parts uploaded so far in the dare.meta. Only CompleteMultipartUpload finalizes the parts under upload prefix.Otherwise,
|
||||
// there could be situations of dare.meta getting corrupted by competing upload parts.
|
||||
dm, err := l.getGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID))
|
||||
if err != nil {
|
||||
return l.s3Objects.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||
}
|
||||
|
||||
lpi, err = l.s3Objects.ListObjectParts(ctx, bucket, getGWContentPath(object), uploadID, partNumberMarker, maxParts, opts)
|
||||
if err != nil {
|
||||
return lpi, err
|
||||
}
|
||||
for i, part := range lpi.Parts {
|
||||
partMeta, err := l.getGWMetadata(ctx, bucket, getPartMetaPath(object, uploadID, part.PartNumber))
|
||||
if err != nil || len(partMeta.Parts) == 0 {
|
||||
return lpi, minio.InvalidPart{}
|
||||
}
|
||||
lpi.Parts[i].ETag = partMeta.ETag
|
||||
}
|
||||
lpi.UserDefined = dm.Meta
|
||||
lpi.Object = object
|
||||
return lpi, nil
|
||||
}
|
||||
|
||||
// AbortMultipartUpload aborts a ongoing multipart upload
|
||||
func (l *s3EncObjects) AbortMultipartUpload(ctx context.Context, bucket string, object string, uploadID string) error {
|
||||
if _, err := l.getGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID)); err != nil {
|
||||
return l.s3Objects.AbortMultipartUpload(ctx, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
if err := l.s3Objects.AbortMultipartUpload(ctx, bucket, getGWContentPath(object), uploadID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploadPrefix := getTmpGWMetaPath(object, uploadID)
|
||||
var continuationToken, startAfter, delimiter string
|
||||
for {
|
||||
loi, err := l.s3Objects.ListObjectsV2(ctx, bucket, uploadPrefix, continuationToken, delimiter, 1000, false, startAfter)
|
||||
if err != nil {
|
||||
return minio.InvalidUploadID{UploadID: uploadID}
|
||||
}
|
||||
for _, obj := range loi.Objects {
|
||||
if err := l.s3Objects.DeleteObject(ctx, bucket, obj.Name); err != nil {
|
||||
return minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
startAfter = obj.Name
|
||||
}
|
||||
continuationToken = loi.NextContinuationToken
|
||||
if !loi.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteMultipartUpload completes ongoing multipart upload and finalizes object
|
||||
func (l *s3EncObjects) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []minio.CompletePart, opts minio.ObjectOptions) (oi minio.ObjectInfo, e error) {
|
||||
|
||||
tmpMeta, err := l.getGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID))
|
||||
if err != nil {
|
||||
oi, e = l.s3Objects.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||
if e == nil {
|
||||
// delete any encrypted version of object that might exist
|
||||
defer l.deleteGWMetadata(ctx, bucket, getDareMetaPath(object))
|
||||
defer l.DeleteObject(ctx, bucket, getGWContentPath(object))
|
||||
}
|
||||
return oi, e
|
||||
}
|
||||
gwMeta := newGWMetaV1()
|
||||
gwMeta.Meta = make(map[string]string)
|
||||
for k, v := range tmpMeta.Meta {
|
||||
gwMeta.Meta[k] = v
|
||||
}
|
||||
// Allocate parts similar to incoming slice.
|
||||
gwMeta.Parts = make([]minio.ObjectPartInfo, len(uploadedParts))
|
||||
|
||||
bkUploadedParts := make([]minio.CompletePart, len(uploadedParts))
|
||||
// Calculate full object size.
|
||||
var objectSize int64
|
||||
|
||||
// Validate each part and then commit to disk.
|
||||
for i, part := range uploadedParts {
|
||||
partMeta, err := l.getGWMetadata(ctx, bucket, getPartMetaPath(object, uploadID, part.PartNumber))
|
||||
if err != nil || len(partMeta.Parts) == 0 {
|
||||
return oi, minio.InvalidPart{}
|
||||
}
|
||||
bkUploadedParts[i] = minio.CompletePart{PartNumber: part.PartNumber, ETag: partMeta.Parts[0].ETag}
|
||||
gwMeta.Parts[i] = partMeta.Parts[0]
|
||||
objectSize += partMeta.Parts[0].Size
|
||||
}
|
||||
oi, e = l.s3Objects.CompleteMultipartUpload(ctx, bucket, getGWContentPath(object), uploadID, bkUploadedParts, opts)
|
||||
if e != nil {
|
||||
return oi, e
|
||||
}
|
||||
|
||||
//delete any unencrypted version of object that might be on the backend
|
||||
defer l.s3Objects.DeleteObject(ctx, bucket, object)
|
||||
|
||||
// Save the final object size and modtime.
|
||||
gwMeta.Stat.Size = objectSize
|
||||
gwMeta.Stat.ModTime = time.Now().UTC()
|
||||
gwMeta.ETag = oi.ETag
|
||||
|
||||
if err = l.writeGWMetadata(ctx, bucket, getDareMetaPath(object), gwMeta, minio.ObjectOptions{}); err != nil {
|
||||
return oi, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
// Clean up any uploaded parts that are not being committed by this CompleteMultipart operation
|
||||
var continuationToken, startAfter, delimiter string
|
||||
uploadPrefix := getTmpGWMetaPath(object, uploadID)
|
||||
done := false
|
||||
for {
|
||||
loi, lerr := l.s3Objects.ListObjectsV2(ctx, bucket, uploadPrefix, continuationToken, delimiter, 1000, false, startAfter)
|
||||
if lerr != nil {
|
||||
done = true
|
||||
break
|
||||
}
|
||||
for _, obj := range loi.Objects {
|
||||
if !strings.HasPrefix(obj.Name, uploadPrefix) {
|
||||
done = true
|
||||
break
|
||||
}
|
||||
startAfter = obj.Name
|
||||
l.s3Objects.DeleteObject(ctx, bucket, obj.Name)
|
||||
}
|
||||
continuationToken = loi.NextContinuationToken
|
||||
if !loi.IsTruncated || done {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return gwMeta.ToObjectInfo(bucket, object), nil
|
||||
}
|
||||
|
||||
// getTmpGWMetaPath returns the prefix under which uploads in progress are stored on backend
|
||||
func getTmpGWMetaPath(object, uploadID string) string {
|
||||
return path.Join(object, defaultMinioGWPrefix, uploadID)
|
||||
}
|
||||
|
||||
// getGWMetaPath returns the prefix under which custom object metadata and object are stored on backend after upload completes
|
||||
func getGWMetaPath(object string) string {
|
||||
return path.Join(object, defaultMinioGWPrefix)
|
||||
}
|
||||
|
||||
// getGWContentPath returns the prefix under which custom object is stored on backend after upload completes
|
||||
func getGWContentPath(object string) string {
|
||||
return path.Join(object, defaultMinioGWPrefix, defaultGWContentFileName)
|
||||
}
|
||||
|
||||
// Clean-up the stale incomplete encrypted multipart uploads. Should be run in a Go routine.
|
||||
func (l *s3EncObjects) cleanupStaleEncMultipartUploads(ctx context.Context, cleanupInterval, expiry time.Duration, doneCh chan struct{}) {
|
||||
ticker := time.NewTicker(cleanupInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-doneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
l.cleanupStaleEncMultipartUploadsOnGW(ctx, expiry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupStaleMultipartUploads removes old custom encryption multipart uploads on backend
|
||||
func (l *s3EncObjects) cleanupStaleEncMultipartUploadsOnGW(ctx context.Context, expiry time.Duration) {
|
||||
for {
|
||||
buckets, err := l.s3Objects.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
for _, b := range buckets {
|
||||
expParts := l.getStalePartsForBucket(ctx, b.Name, expiry)
|
||||
for k := range expParts {
|
||||
l.s3Objects.DeleteObject(ctx, b.Name, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *s3EncObjects) getStalePartsForBucket(ctx context.Context, bucket string, expiry time.Duration) (expParts map[string]string) {
|
||||
var prefix, continuationToken, delimiter, startAfter string
|
||||
expParts = make(map[string]string)
|
||||
now := time.Now()
|
||||
for {
|
||||
loi, err := l.s3Objects.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, 1000, false, startAfter)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
for _, obj := range loi.Objects {
|
||||
startAfter = obj.Name
|
||||
if !strings.Contains(obj.Name, defaultMinioGWPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isGWObject(obj.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// delete temporary part.meta or dare.meta files for incomplete uploads that are past expiry
|
||||
if (strings.HasSuffix(obj.Name, gwpartMetaJSON) || strings.HasSuffix(obj.Name, gwdareMetaJSON)) &&
|
||||
now.Sub(obj.ModTime) > expiry {
|
||||
expParts[obj.Name] = ""
|
||||
}
|
||||
}
|
||||
continuationToken = loi.NextContinuationToken
|
||||
if !loi.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *s3EncObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
var prefix, continuationToken, delimiter, startAfter string
|
||||
expParts := make(map[string]string)
|
||||
|
||||
for {
|
||||
loi, err := l.s3Objects.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, 1000, false, startAfter)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
for _, obj := range loi.Objects {
|
||||
startAfter = obj.Name
|
||||
if !strings.Contains(obj.Name, defaultMinioGWPrefix) {
|
||||
return minio.BucketNotEmpty{}
|
||||
}
|
||||
if isGWObject(obj.Name) {
|
||||
return minio.BucketNotEmpty{}
|
||||
}
|
||||
// delete temporary part.meta or dare.meta files for incomplete uploads
|
||||
if strings.HasSuffix(obj.Name, gwpartMetaJSON) || strings.HasSuffix(obj.Name, gwdareMetaJSON) {
|
||||
expParts[obj.Name] = ""
|
||||
}
|
||||
}
|
||||
continuationToken = loi.NextContinuationToken
|
||||
if !loi.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
for k := range expParts {
|
||||
l.s3Objects.DeleteObject(ctx, bucket, k)
|
||||
}
|
||||
err := l.Client.RemoveBucket(bucket)
|
||||
if err != nil {
|
||||
return minio.ErrorRespToObjectError(err, bucket)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package s3
|
||||
|
||||
import minio "github.com/minio/minio/cmd"
|
||||
|
||||
// List of header keys to be filtered, usually
|
||||
// from all S3 API http responses.
|
||||
var defaultFilterKeys = []string{
|
||||
"Connection",
|
||||
"Transfer-Encoding",
|
||||
"Accept-Ranges",
|
||||
"Date",
|
||||
"Server",
|
||||
"Vary",
|
||||
"x-amz-bucket-region",
|
||||
"x-amz-request-id",
|
||||
"x-amz-id-2",
|
||||
"Content-Security-Policy",
|
||||
"X-Xss-Protection",
|
||||
|
||||
// Add new headers to be ignored.
|
||||
}
|
||||
|
||||
// FromGatewayObjectPart converts ObjectInfo for custom part stored as object to PartInfo
|
||||
func FromGatewayObjectPart(partID int, oi minio.ObjectInfo) (pi minio.PartInfo) {
|
||||
return minio.PartInfo{
|
||||
Size: oi.Size,
|
||||
ETag: minio.CanonicalizeETag(oi.ETag),
|
||||
LastModified: oi.ModTime,
|
||||
PartNumber: partID,
|
||||
}
|
||||
}
|
||||
@@ -28,12 +28,13 @@ import (
|
||||
"github.com/minio/cli"
|
||||
miniogo "github.com/minio/minio-go"
|
||||
"github.com/minio/minio-go/pkg/credentials"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio-go/pkg/s3utils"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
|
||||
minio "github.com/minio/minio/cmd"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -223,9 +224,20 @@ func (g *S3) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &s3Objects{
|
||||
s := s3Objects{
|
||||
Client: clnt,
|
||||
}, nil
|
||||
}
|
||||
// Enables single encyption of KMS is configured.
|
||||
if minio.GlobalKMS != nil {
|
||||
encS := s3EncObjects{s}
|
||||
|
||||
// Start stale enc multipart uploads cleanup routine.
|
||||
go encS.cleanupStaleEncMultipartUploads(context.Background(),
|
||||
minio.GlobalMultipartCleanupInterval, minio.GlobalMultipartExpiry, minio.GlobalServiceDoneCh)
|
||||
|
||||
return &encS, nil
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// Production - s3 gateway is production ready.
|
||||
@@ -330,6 +342,7 @@ func (l *s3Objects) ListObjects(ctx context.Context, bucket string, prefix strin
|
||||
|
||||
// ListObjectsV2 lists all blobs in S3 bucket filtered by prefix
|
||||
func (l *s3Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, e error) {
|
||||
|
||||
result, err := l.Client.ListObjectsV2(bucket, prefix, continuationToken, fetchOwner, delimiter, maxKeys, startAfter)
|
||||
if err != nil {
|
||||
return loi, minio.ErrorRespToObjectError(err, bucket)
|
||||
@@ -387,7 +400,6 @@ func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, st
|
||||
return minio.ErrorRespToObjectError(err, bucket, key)
|
||||
}
|
||||
defer object.Close()
|
||||
|
||||
if _, err := io.Copy(writer, object); err != nil {
|
||||
return minio.ErrorRespToObjectError(err, bucket, key)
|
||||
}
|
||||
@@ -396,7 +408,11 @@ func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, st
|
||||
|
||||
// GetObjectInfo reads object info and replies back ObjectInfo
|
||||
func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
oi, err := l.Client.StatObject(bucket, object, miniogo.StatObjectOptions{GetObjectOptions: miniogo.GetObjectOptions{ServerSideEncryption: opts.ServerSideEncryption}})
|
||||
oi, err := l.Client.StatObject(bucket, object, miniogo.StatObjectOptions{
|
||||
GetObjectOptions: miniogo.GetObjectOptions{
|
||||
ServerSideEncryption: opts.ServerSideEncryption,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return minio.ObjectInfo{}, minio.ErrorRespToObjectError(err, bucket, object)
|
||||
}
|
||||
@@ -426,6 +442,18 @@ func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject
|
||||
// So preserve it by adding "REPLACE" directive to save all the metadata set by CopyObject API.
|
||||
srcInfo.UserDefined["x-amz-metadata-directive"] = "REPLACE"
|
||||
srcInfo.UserDefined["x-amz-copy-source-if-match"] = srcInfo.ETag
|
||||
header := make(http.Header)
|
||||
if srcOpts.ServerSideEncryption != nil {
|
||||
encrypt.SSECopy(srcOpts.ServerSideEncryption).Marshal(header)
|
||||
}
|
||||
|
||||
if dstOpts.ServerSideEncryption != nil {
|
||||
dstOpts.ServerSideEncryption.Marshal(header)
|
||||
}
|
||||
for k, v := range header {
|
||||
srcInfo.UserDefined[k] = v[0]
|
||||
}
|
||||
|
||||
if _, err = l.Client.CopyObject(srcBucket, srcObject, dstBucket, dstObject, srcInfo.UserDefined); err != nil {
|
||||
return objInfo, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
||||
}
|
||||
@@ -478,10 +506,21 @@ func (l *s3Objects) PutObjectPart(ctx context.Context, bucket string, object str
|
||||
// existing object or a part of it.
|
||||
func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string,
|
||||
partID int, startOffset, length int64, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (p minio.PartInfo, err error) {
|
||||
|
||||
srcInfo.UserDefined = map[string]string{
|
||||
"x-amz-copy-source-if-match": srcInfo.ETag,
|
||||
}
|
||||
header := make(http.Header)
|
||||
if srcOpts.ServerSideEncryption != nil {
|
||||
encrypt.SSECopy(srcOpts.ServerSideEncryption).Marshal(header)
|
||||
}
|
||||
|
||||
if dstOpts.ServerSideEncryption != nil {
|
||||
dstOpts.ServerSideEncryption.Marshal(header)
|
||||
}
|
||||
for k, v := range header {
|
||||
srcInfo.UserDefined[k] = v[0]
|
||||
}
|
||||
|
||||
completePart, err := l.Client.CopyObjectPart(srcBucket, srcObject, destBucket, destObject,
|
||||
uploadID, partID, startOffset, length, srcInfo.UserDefined)
|
||||
if err != nil {
|
||||
@@ -493,7 +532,7 @@ func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, de
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||
func (l *s3Objects) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (lpi minio.ListPartsInfo, e error) {
|
||||
func (l *s3Objects) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (lpi minio.ListPartsInfo, e error) {
|
||||
result, err := l.Client.ListObjectParts(bucket, object, uploadID, partNumberMarker, maxParts)
|
||||
if err != nil {
|
||||
return lpi, minio.ErrorRespToObjectError(err, bucket, object)
|
||||
@@ -557,3 +596,8 @@ func (l *s3Objects) DeleteBucketPolicy(ctx context.Context, bucket string) error
|
||||
func (l *s3Objects) IsCompressionSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEncryptionSupported returns whether server side encryption is implemented for this layer.
|
||||
func (l *s3Objects) IsEncryptionSupported() bool {
|
||||
return minio.GlobalKMS != nil || len(minio.GlobalGatewaySSE) > 0
|
||||
}
|
||||
|
||||
@@ -110,8 +110,8 @@ func TestS3ToObjectError(t *testing.T) {
|
||||
// Special test case for error that is not of type
|
||||
// miniogo.ErrorResponse
|
||||
{
|
||||
inputErr: fmt.Errorf("not a minio.ErrorResponse"),
|
||||
expectedErr: fmt.Errorf("not a minio.ErrorResponse"),
|
||||
inputErr: fmt.Errorf("not a ErrorResponse"),
|
||||
expectedErr: fmt.Errorf("not a ErrorResponse"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -496,11 +496,6 @@ func (h resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// A put method on path "/" doesn't make sense, ignore it.
|
||||
if r.Method == http.MethodPut && r.URL.Path == "/" {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Serve HTTP.
|
||||
h.handler.ServeHTTP(w, r)
|
||||
|
||||
+11
-14
@@ -20,7 +20,6 @@ import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
@@ -79,10 +78,11 @@ const (
|
||||
// date and server date during signature verification.
|
||||
globalMaxSkewTime = 15 * time.Minute // 15 minutes skew allowed.
|
||||
|
||||
// Expiry duration after which the multipart uploads are deemed stale.
|
||||
globalMultipartExpiry = time.Hour * 24 * 14 // 2 weeks.
|
||||
// Cleanup interval when the stale multipart cleanup is initiated.
|
||||
globalMultipartCleanupInterval = time.Hour * 24 // 24 hrs.
|
||||
// GlobalMultipartExpiry - Expiry duration after which the multipart uploads are deemed stale.
|
||||
GlobalMultipartExpiry = time.Hour * 24 * 14 // 2 weeks.
|
||||
// GlobalMultipartCleanupInterval - Cleanup interval when the stale multipart cleanup is initiated.
|
||||
GlobalMultipartCleanupInterval = time.Hour * 24 // 24 hrs.
|
||||
|
||||
// Refresh interval to update in-memory bucket policy cache.
|
||||
globalRefreshBucketPolicyInterval = 5 * time.Minute
|
||||
// Refresh interval to update in-memory iam config cache.
|
||||
@@ -161,12 +161,6 @@ var (
|
||||
// File to log HTTP request/response headers and body.
|
||||
globalHTTPTraceFile *os.File
|
||||
|
||||
// List of admin peers.
|
||||
globalAdminPeers = adminPeers{}
|
||||
|
||||
// Minio server user agent string.
|
||||
globalServerUserAgent = "Minio/" + ReleaseTag + " (" + runtime.GOOS + "; " + runtime.GOARCH + ")"
|
||||
|
||||
globalEndpoints EndpointList
|
||||
|
||||
// Global server's network statistics
|
||||
@@ -236,8 +230,9 @@ var (
|
||||
|
||||
// KMS key id
|
||||
globalKMSKeyID string
|
||||
// Allocated KMS
|
||||
globalKMS crypto.KMS
|
||||
|
||||
// GlobalKMS initialized KMS configuration
|
||||
GlobalKMS crypto.KMS
|
||||
|
||||
// Auto-Encryption, if enabled, turns any non-SSE-C request
|
||||
// into an SSE-S3 request. If enabled a valid, non-empty KMS
|
||||
@@ -269,6 +264,9 @@ var (
|
||||
// Deployment ID - unique per deployment
|
||||
globalDeploymentID string
|
||||
|
||||
// GlobalGatewaySSE sse options
|
||||
GlobalGatewaySSE gatewaySSE
|
||||
|
||||
// Add new variable global values here.
|
||||
)
|
||||
|
||||
@@ -356,7 +354,6 @@ func getGlobalInfo() (globalInfo map[string]interface{}) {
|
||||
"isEnvRegion": globalIsEnvRegion,
|
||||
"isSSL": globalIsSSL,
|
||||
"serverRegion": globalServerRegion,
|
||||
"serverUserAgent": globalServerUserAgent,
|
||||
// Add more relevant global settings here.
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ func (sys *IAMSys) Init(objAPI ObjectLayer) error {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-globalServiceDoneCh:
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
sys.refresh(objAPI)
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// localAdminClient - represents admin operation to be executed locally.
|
||||
type localAdminClient struct{}
|
||||
|
||||
// SignalService - sends a restart or stop signal to the local server
|
||||
func (lc localAdminClient) SignalService(s serviceSignal) error {
|
||||
switch s {
|
||||
case serviceRestart, serviceStop:
|
||||
globalServiceSignalCh <- s
|
||||
default:
|
||||
return errUnsupportedSignal
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReInitFormat - re-initialize disk format.
|
||||
func (lc localAdminClient) ReInitFormat(dryRun bool) error {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
return objectAPI.ReloadFormat(context.Background(), dryRun)
|
||||
}
|
||||
|
||||
// ServerInfo - Returns the server info of this server.
|
||||
func (lc localAdminClient) ServerInfo() (sid ServerInfoData, e error) {
|
||||
if globalBootTime.IsZero() {
|
||||
return sid, errServerNotInitialized
|
||||
}
|
||||
|
||||
// Build storage info
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
return sid, errServerNotInitialized
|
||||
}
|
||||
storage := objLayer.StorageInfo(context.Background())
|
||||
|
||||
return ServerInfoData{
|
||||
StorageInfo: storage,
|
||||
ConnStats: globalConnStats.toServerConnStats(),
|
||||
HTTPStats: globalHTTPStats.toServerHTTPStats(),
|
||||
Properties: ServerProperties{
|
||||
Uptime: UTCNow().Sub(globalBootTime),
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
SQSARN: globalNotificationSys.GetARNList(),
|
||||
Region: globalServerConfig.GetRegion(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StartProfiling - starts profiling on the local server.
|
||||
func (lc localAdminClient) StartProfiling(profiler string) error {
|
||||
if globalProfiler != nil {
|
||||
globalProfiler.Stop()
|
||||
}
|
||||
prof, err := startProfiler(profiler, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globalProfiler = prof
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadProfilingData - stops and returns profiling data of the local server.
|
||||
func (lc localAdminClient) DownloadProfilingData() ([]byte, error) {
|
||||
if globalProfiler == nil {
|
||||
return nil, errors.New("profiler not enabled")
|
||||
}
|
||||
|
||||
profilerPath := globalProfiler.Path()
|
||||
|
||||
// Stop the profiler
|
||||
globalProfiler.Stop()
|
||||
|
||||
profilerFile, err := os.Open(profilerPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(profilerFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
@@ -166,7 +166,7 @@ func startLockMaintenance(lkSrv *lockRPCReceiver) {
|
||||
for {
|
||||
// Verifies every minute for locks held more than 2minutes.
|
||||
select {
|
||||
case <-globalServiceDoneCh:
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
lkSrv.lockMaintenance(lockValidityCheckInterval)
|
||||
|
||||
+7
-8
@@ -31,14 +31,13 @@ import (
|
||||
var globalFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "config-dir, C",
|
||||
Value: getConfigDir(),
|
||||
Usage: func() string {
|
||||
usage := "Path to configuration directory."
|
||||
if getConfigDir() == "" {
|
||||
usage = usage + " This option must be set."
|
||||
}
|
||||
return usage
|
||||
}(),
|
||||
Value: defaultConfigDir.Get(),
|
||||
Usage: "[DEPRECATED] Path to legacy configuration directory.",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "certs-dir, S",
|
||||
Value: defaultCertsDir.Get(),
|
||||
Usage: "Path to certs directory.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
|
||||
+294
-50
@@ -17,11 +17,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -90,85 +92,252 @@ func (sys *NotificationSys) DeleteBucket(ctx context.Context, bucketName string)
|
||||
}()
|
||||
}
|
||||
|
||||
// ReloadFormat - calls ReloadFormat RPC call on all peers.
|
||||
func (sys *NotificationSys) ReloadFormat(dryRun bool) map[xnet.Host]error {
|
||||
errors := make(map[xnet.Host]error)
|
||||
var wg sync.WaitGroup
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
// Try to load format in three attempts, before giving up.
|
||||
for i := 0; i < 3; i++ {
|
||||
err := client.ReloadFormat(dryRun)
|
||||
if err == nil {
|
||||
break
|
||||
// A NotificationGroup is a collection of goroutines working on subtasks that are part of
|
||||
// the same overall task.
|
||||
//
|
||||
// A zero NotificationGroup is valid and does not cancel on error.
|
||||
type NotificationGroup struct {
|
||||
wg sync.WaitGroup
|
||||
errs []NotificationPeerErr
|
||||
}
|
||||
|
||||
// WithNPeers returns a new NotificationGroup with length of errs slice upto nerrs,
|
||||
// upon Wait() errors are returned collected from all tasks.
|
||||
func WithNPeers(nerrs int) *NotificationGroup {
|
||||
return &NotificationGroup{errs: make([]NotificationPeerErr, nerrs)}
|
||||
}
|
||||
|
||||
// Wait blocks until all function calls from the Go method have returned, then
|
||||
// returns the slice of errors from all function calls.
|
||||
func (g *NotificationGroup) Wait() []NotificationPeerErr {
|
||||
g.wg.Wait()
|
||||
return g.errs
|
||||
}
|
||||
|
||||
// Go calls the given function in a new goroutine.
|
||||
//
|
||||
// The first call to return a non-nil error will be
|
||||
// collected in errs slice and returned by Wait().
|
||||
func (g *NotificationGroup) Go(ctx context.Context, f func() error, index int, addr xnet.Host) {
|
||||
g.wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer g.wg.Done()
|
||||
g.errs[index] = NotificationPeerErr{
|
||||
Host: addr,
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := f(); err != nil {
|
||||
g.errs[index].Err = err
|
||||
// Last iteration log the error.
|
||||
if i == 2 {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
errors[addr] = err
|
||||
// Wait for one second and no need wait after last attempt.
|
||||
if i < 2 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}(addr, client)
|
||||
}
|
||||
wg.Wait()
|
||||
break
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return errors
|
||||
// ReloadFormat - calls ReloadFormat RPC call on all peers.
|
||||
func (sys *NotificationSys) ReloadFormat(dryRun bool) []NotificationPeerErr {
|
||||
var idx = 0
|
||||
ng := WithNPeers(len(sys.peerRPCClientMap))
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
client := client
|
||||
ng.Go(context.Background(), func() error {
|
||||
return client.ReloadFormat(dryRun)
|
||||
}, idx, addr)
|
||||
idx++
|
||||
}
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// LoadUsers - calls LoadUsers RPC call on all peers.
|
||||
func (sys *NotificationSys) LoadUsers() map[xnet.Host]error {
|
||||
errors := make(map[xnet.Host]error)
|
||||
var wg sync.WaitGroup
|
||||
func (sys *NotificationSys) LoadUsers() []NotificationPeerErr {
|
||||
var idx = 0
|
||||
ng := WithNPeers(len(sys.peerRPCClientMap))
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
// Try to load users in three attempts.
|
||||
for i := 0; i < 3; i++ {
|
||||
err := client.LoadUsers()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
errors[addr] = err
|
||||
// Wait for one second and no need wait after last attempt.
|
||||
if i < 2 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}(addr, client)
|
||||
ng.Go(context.Background(), client.LoadUsers, idx, addr)
|
||||
idx++
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return errors
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// LoadCredentials - calls LoadCredentials RPC call on all peers.
|
||||
func (sys *NotificationSys) LoadCredentials() map[xnet.Host]error {
|
||||
errors := make(map[xnet.Host]error)
|
||||
func (sys *NotificationSys) LoadCredentials() []NotificationPeerErr {
|
||||
var idx = 0
|
||||
ng := WithNPeers(len(sys.peerRPCClientMap))
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
ng.Go(context.Background(), client.LoadCredentials, idx, addr)
|
||||
idx++
|
||||
}
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// StartProfiling - start profiling on remote peers, by initiating a remote RPC.
|
||||
func (sys *NotificationSys) StartProfiling(profiler string) []NotificationPeerErr {
|
||||
var idx = 0
|
||||
ng := WithNPeers(len(sys.peerRPCClientMap))
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
client := client
|
||||
ng.Go(context.Background(), func() error {
|
||||
return client.StartProfiling(profiler)
|
||||
}, idx, addr)
|
||||
idx++
|
||||
}
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// DownloadProfilingData - download profiling data from all remote peers.
|
||||
func (sys *NotificationSys) DownloadProfilingData(ctx context.Context, writer io.Writer) bool {
|
||||
profilingDataFound := false
|
||||
|
||||
// Initialize a zip writer which will provide a zipped content
|
||||
// of profiling data of all nodes
|
||||
zipWriter := zip.NewWriter(writer)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
data, err := client.DownloadProfilingData()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
|
||||
profilingDataFound = true
|
||||
|
||||
// Send profiling data to zip as file
|
||||
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
||||
name: fmt.Sprintf("profiling-%s.pprof", addr),
|
||||
size: int64(len(data)),
|
||||
mode: 0600,
|
||||
modTime: UTCNow(),
|
||||
isDir: false,
|
||||
sys: nil,
|
||||
})
|
||||
if zerr != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, zerr)
|
||||
continue
|
||||
}
|
||||
zwriter, zerr := zipWriter.CreateHeader(header)
|
||||
if zerr != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, zerr)
|
||||
continue
|
||||
}
|
||||
if _, err = io.Copy(zwriter, bytes.NewBuffer(data)); err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
thisAddr, err := xnet.ParseHost(GetLocalPeer(globalEndpoints))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return profilingDataFound
|
||||
}
|
||||
|
||||
data, err := getProfileData()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", thisAddr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
return profilingDataFound
|
||||
}
|
||||
|
||||
profilingDataFound = true
|
||||
|
||||
// Send profiling data to zip as file
|
||||
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
||||
name: fmt.Sprintf("profiling-%s.pprof", thisAddr),
|
||||
size: int64(len(data)),
|
||||
mode: 0600,
|
||||
modTime: UTCNow(),
|
||||
isDir: false,
|
||||
sys: nil,
|
||||
})
|
||||
|
||||
zwriter, zerr := zipWriter.CreateHeader(header)
|
||||
if zerr != nil {
|
||||
return profilingDataFound
|
||||
}
|
||||
|
||||
if _, err = io.Copy(zwriter, bytes.NewBuffer(data)); err != nil {
|
||||
return profilingDataFound
|
||||
}
|
||||
|
||||
return profilingDataFound
|
||||
}
|
||||
|
||||
// SignalService - calls signal service RPC call on all peers.
|
||||
func (sys *NotificationSys) SignalService(sig serviceSignal) []NotificationPeerErr {
|
||||
var idx = 0
|
||||
ng := WithNPeers(len(sys.peerRPCClientMap))
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
client := client
|
||||
ng.Go(context.Background(), func() error {
|
||||
return client.SignalService(sig)
|
||||
}, idx, addr)
|
||||
idx++
|
||||
}
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// ServerInfo - calls ServerInfo RPC call on all peers.
|
||||
func (sys *NotificationSys) ServerInfo(ctx context.Context) []ServerInfo {
|
||||
var idx = 0
|
||||
serverInfo := make([]ServerInfo, len(sys.peerRPCClientMap))
|
||||
var wg sync.WaitGroup
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
go func(idx int, addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
// Try to load credentials in three attempts.
|
||||
// Try to fetch serverInfo remotely in three attempts.
|
||||
for i := 0; i < 3; i++ {
|
||||
err := client.LoadCredentials()
|
||||
info, err := client.ServerInfo()
|
||||
if err == nil {
|
||||
break
|
||||
serverInfo[idx] = ServerInfo{
|
||||
Addr: addr.String(),
|
||||
Data: &info,
|
||||
}
|
||||
return
|
||||
}
|
||||
serverInfo[idx] = ServerInfo{
|
||||
Addr: addr.String(),
|
||||
Data: &info,
|
||||
Error: err.Error(),
|
||||
}
|
||||
// Last iteration log the error.
|
||||
if i == 2 {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", addr.String())
|
||||
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
errors[addr] = err
|
||||
// Wait for one second and no need wait after last attempt.
|
||||
if i < 2 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}(addr, client)
|
||||
}(idx, addr, client)
|
||||
idx++
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return errors
|
||||
return serverInfo
|
||||
}
|
||||
|
||||
// SetBucketPolicy - calls SetBucketPolicy RPC call on all peers.
|
||||
@@ -512,6 +681,81 @@ func (sys *NotificationSys) Send(args eventArgs) []event.TargetIDErr {
|
||||
return sys.send(args.BucketName, args.ToEvent(), targetIDs...)
|
||||
}
|
||||
|
||||
// DrivePerfInfo - Drive speed (read and write) information
|
||||
func (sys *NotificationSys) DrivePerfInfo() []ServerDrivesPerfInfo {
|
||||
reply := make([]ServerDrivesPerfInfo, len(sys.peerRPCClientMap))
|
||||
var wg sync.WaitGroup
|
||||
var i int
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient, idx int) {
|
||||
defer wg.Done()
|
||||
di, err := client.DrivePerfInfo()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr.String())
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
di.Addr = addr.String()
|
||||
di.Error = err.Error()
|
||||
}
|
||||
reply[idx] = di
|
||||
}(addr, client, i)
|
||||
i++
|
||||
}
|
||||
wg.Wait()
|
||||
return reply
|
||||
}
|
||||
|
||||
// MemUsageInfo - Mem utilization information
|
||||
func (sys *NotificationSys) MemUsageInfo() []ServerMemUsageInfo {
|
||||
reply := make([]ServerMemUsageInfo, len(sys.peerRPCClientMap))
|
||||
var wg sync.WaitGroup
|
||||
var i int
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient, idx int) {
|
||||
defer wg.Done()
|
||||
memi, err := client.MemUsageInfo()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr.String())
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
memi.Addr = addr.String()
|
||||
memi.Error = err.Error()
|
||||
}
|
||||
reply[idx] = memi
|
||||
}(addr, client, i)
|
||||
i++
|
||||
}
|
||||
wg.Wait()
|
||||
return reply
|
||||
}
|
||||
|
||||
// CPULoadInfo - CPU utilization information
|
||||
func (sys *NotificationSys) CPULoadInfo() []ServerCPULoadInfo {
|
||||
reply := make([]ServerCPULoadInfo, len(sys.peerRPCClientMap))
|
||||
var wg sync.WaitGroup
|
||||
var i int
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient, idx int) {
|
||||
defer wg.Done()
|
||||
cpui, err := client.CPULoadInfo()
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remotePeer", addr.String())
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
cpui.Addr = addr.String()
|
||||
cpui.Error = err.Error()
|
||||
}
|
||||
reply[idx] = cpui
|
||||
}(addr, client, i)
|
||||
i++
|
||||
}
|
||||
wg.Wait()
|
||||
return reply
|
||||
}
|
||||
|
||||
// NewNotificationSys - creates new notification system object.
|
||||
func NewNotificationSys(config *serverConfig, endpoints EndpointList) *NotificationSys {
|
||||
targetList := getNotificationTargets(config)
|
||||
|
||||
@@ -103,7 +103,7 @@ type ObjectInfo struct {
|
||||
UserDefined map[string]string
|
||||
|
||||
// List of individual parts, maximum size of upto 10,000
|
||||
Parts []objectPartInfo `json:"-"`
|
||||
Parts []ObjectPartInfo `json:"-"`
|
||||
|
||||
// Implements writer and reader used by CopyObject API
|
||||
Writer io.WriteCloser `json:"-"`
|
||||
|
||||
@@ -75,7 +75,7 @@ type ObjectLayer interface {
|
||||
CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string, partID int,
|
||||
startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (info PartInfo, err error)
|
||||
PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error)
|
||||
ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int) (result ListPartsInfo, err error)
|
||||
ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error)
|
||||
AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string) error
|
||||
CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
|
||||
|
||||
@@ -1429,7 +1429,7 @@ func testListObjectPartsDiskNotFound(obj ObjectLayer, instanceType string, disks
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actualResult, actualErr := obj.ListObjectParts(context.Background(), testCase.bucket, testCase.object, testCase.uploadID, testCase.partNumberMarker, testCase.maxParts)
|
||||
actualResult, actualErr := obj.ListObjectParts(context.Background(), testCase.bucket, testCase.object, testCase.uploadID, testCase.partNumberMarker, testCase.maxParts, ObjectOptions{})
|
||||
if actualErr != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: %s: Expected to pass, but failed with: <ERROR> %s", i+1, instanceType, actualErr.Error())
|
||||
}
|
||||
@@ -1667,7 +1667,7 @@ func testListObjectParts(obj ObjectLayer, instanceType string, t TestErrHandler)
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actualResult, actualErr := obj.ListObjectParts(context.Background(), testCase.bucket, testCase.object, testCase.uploadID, testCase.partNumberMarker, testCase.maxParts)
|
||||
actualResult, actualErr := obj.ListObjectParts(context.Background(), testCase.bucket, testCase.object, testCase.uploadID, testCase.partNumberMarker, testCase.maxParts, ObjectOptions{})
|
||||
if actualErr != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: %s: Expected to pass, but failed with: <ERROR> %s", i+1, instanceType, actualErr.Error())
|
||||
}
|
||||
|
||||
+14
-2
@@ -487,7 +487,6 @@ func NewGetObjectReader(rs *HTTPRangeSpec, oi ObjectInfo, cleanUpFns ...func())
|
||||
// encrypted bytes. The header parameter is used to
|
||||
// provide encryption parameters.
|
||||
fn = func(inputReader io.Reader, h http.Header, cFns ...func()) (r *GetObjectReader, err error) {
|
||||
|
||||
copySource := h.Get(crypto.SSECopyAlgorithm) != ""
|
||||
|
||||
cFns = append(cleanUpFns, cFns...)
|
||||
@@ -577,7 +576,6 @@ func NewGetObjectReader(rs *HTTPRangeSpec, oi ObjectInfo, cleanUpFns ...func())
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
|
||||
return fn, off, length, nil
|
||||
}
|
||||
|
||||
@@ -663,3 +661,17 @@ func sealETagFn(key crypto.ObjectKey) SealMD5CurrFn {
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
// CleanMinioInternalMetadataKeys removes X-Amz-Meta- prefix from minio internal
|
||||
// encryption metadata that was sent by minio gateway
|
||||
func CleanMinioInternalMetadataKeys(metadata map[string]string) map[string]string {
|
||||
var newMeta = make(map[string]string, len(metadata))
|
||||
for k, v := range metadata {
|
||||
if strings.HasPrefix(k, "X-Amz-Meta-X-Minio-Internal-") {
|
||||
newMeta[strings.TrimPrefix(k, "X-Amz-Meta-")] = v
|
||||
} else {
|
||||
newMeta[k] = v
|
||||
}
|
||||
}
|
||||
return newMeta
|
||||
}
|
||||
|
||||
@@ -431,7 +431,7 @@ func TestGetActualSize(t *testing.T) {
|
||||
"X-Minio-Internal-actual-size": "100000001",
|
||||
"content-type": "application/octet-stream",
|
||||
"etag": "b3ff3ef3789147152fbfbc50efba4bfd-2"},
|
||||
Parts: []objectPartInfo{
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Size: 39235668,
|
||||
ActualSize: 67108864,
|
||||
@@ -450,7 +450,7 @@ func TestGetActualSize(t *testing.T) {
|
||||
"X-Minio-Internal-actual-size": "841",
|
||||
"content-type": "application/octet-stream",
|
||||
"etag": "b3ff3ef3789147152fbfbc50efba4bfd-2"},
|
||||
Parts: []objectPartInfo{},
|
||||
Parts: []ObjectPartInfo{},
|
||||
},
|
||||
result: 841,
|
||||
},
|
||||
@@ -459,7 +459,7 @@ func TestGetActualSize(t *testing.T) {
|
||||
UserDefined: map[string]string{"X-Minio-Internal-compression": "golang/snappy/LZ77",
|
||||
"content-type": "application/octet-stream",
|
||||
"etag": "b3ff3ef3789147152fbfbc50efba4bfd-2"},
|
||||
Parts: []objectPartInfo{},
|
||||
Parts: []ObjectPartInfo{},
|
||||
},
|
||||
result: -1,
|
||||
},
|
||||
@@ -482,7 +482,7 @@ func TestGetCompressedOffsets(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
objInfo: ObjectInfo{
|
||||
Parts: []objectPartInfo{
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Size: 39235668,
|
||||
ActualSize: 67108864,
|
||||
@@ -499,7 +499,7 @@ func TestGetCompressedOffsets(t *testing.T) {
|
||||
},
|
||||
{
|
||||
objInfo: ObjectInfo{
|
||||
Parts: []objectPartInfo{
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Size: 39235668,
|
||||
ActualSize: 67108864,
|
||||
@@ -516,7 +516,7 @@ func TestGetCompressedOffsets(t *testing.T) {
|
||||
},
|
||||
{
|
||||
objInfo: ObjectInfo{
|
||||
Parts: []objectPartInfo{
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Size: 39235668,
|
||||
ActualSize: 67108864,
|
||||
|
||||
+214
-194
@@ -33,8 +33,8 @@ import (
|
||||
|
||||
snappy "github.com/golang/snappy"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/klauspost/readahead"
|
||||
miniogo "github.com/minio/minio-go"
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
@@ -91,7 +91,7 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrBadRequest, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -99,6 +99,18 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
bucket := vars["bucket"]
|
||||
object := vars["object"]
|
||||
|
||||
// get gateway encryption options
|
||||
opts, err := getEncryptionOpts(ctx, r, bucket, object)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
|
||||
getObjectInfo := objectAPI.GetObjectInfo
|
||||
if api.CacheAPI() != nil {
|
||||
getObjectInfo = api.CacheAPI().GetObjectInfo
|
||||
}
|
||||
|
||||
// Check for auth type to return S3 compatible error.
|
||||
// type to return the correct error (NoSuchKey vs AccessDenied)
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, object); s3Error != ErrNone {
|
||||
@@ -122,12 +134,7 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
ConditionValues: getConditionValues(r, ""),
|
||||
IsOwner: false,
|
||||
}) {
|
||||
getObjectInfo := objectAPI.GetObjectInfo
|
||||
if api.CacheAPI() != nil {
|
||||
getObjectInfo = api.CacheAPI().GetObjectInfo
|
||||
}
|
||||
|
||||
_, err := getObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
_, err = getObjectInfo(ctx, bucket, object, opts)
|
||||
if toAPIErrorCode(ctx, err) == ErrNoSuchKey {
|
||||
s3Error = ErrNoSuchKey
|
||||
}
|
||||
@@ -148,18 +155,15 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
writeErrorResponse(w, ErrEmptyRequestBody, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
var selectReq s3select.ObjectSelectRequest
|
||||
if err := xmlDecoder(r.Body, &selectReq, r.ContentLength); err != nil {
|
||||
writeErrorResponse(w, ErrMalformedXML, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.EqualFold(string(selectReq.ExpressionType), "SQL") {
|
||||
writeErrorResponse(w, ErrInvalidExpressionType, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if len(selectReq.Expression) >= s3select.MaxExpressionLength {
|
||||
writeErrorResponse(w, ErrExpressionTooLong, r.URL, guessIsBrowserReq(r))
|
||||
s3Select, err := s3select.NewS3Select(r.Body)
|
||||
if err != nil {
|
||||
if serr, ok := err.(s3select.SelectError); ok {
|
||||
w.WriteHeader(serr.HTTPStatusCode())
|
||||
w.Write(s3select.NewErrorMessage(serr.ErrorCode(), serr.ErrorMessage()))
|
||||
} else {
|
||||
writeErrorResponse(w, ErrInternalError, r.URL, guessIsBrowserReq(r))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -167,123 +171,38 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
if api.CacheAPI() != nil {
|
||||
getObjectNInfo = api.CacheAPI().GetObjectNInfo
|
||||
}
|
||||
getObject := func(offset, length int64) (rc io.ReadCloser, err error) {
|
||||
isSuffixLength := false
|
||||
if offset < 0 {
|
||||
isSuffixLength = true
|
||||
}
|
||||
rs := &HTTPRangeSpec{
|
||||
IsSuffixLength: isSuffixLength,
|
||||
Start: offset,
|
||||
End: offset + length,
|
||||
}
|
||||
|
||||
var opts ObjectOptions
|
||||
gr, err := getObjectNInfo(ctx, bucket, object, nil, r.Header, readLock, opts)
|
||||
return getObjectNInfo(ctx, bucket, object, rs, r.Header, readLock, ObjectOptions{})
|
||||
}
|
||||
|
||||
if err = s3Select.Open(getObject); err != nil {
|
||||
if serr, ok := err.(s3select.SelectError); ok {
|
||||
w.WriteHeader(serr.HTTPStatusCode())
|
||||
w.Write(s3select.NewErrorMessage(serr.ErrorCode(), serr.ErrorMessage()))
|
||||
} else {
|
||||
writeErrorResponse(w, ErrInternalError, r.URL, guessIsBrowserReq(r))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
s3Select.Evaluate(w)
|
||||
s3Select.Close()
|
||||
|
||||
objInfo, err := getObjectInfo(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
defer gr.Close()
|
||||
|
||||
objInfo := gr.ObjInfo
|
||||
|
||||
if selectReq.InputSerialization.CompressionType == s3select.SelectCompressionGZIP {
|
||||
if !strings.Contains(objInfo.ContentType, "gzip") {
|
||||
writeErrorResponse(w, ErrInvalidDataSource, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
if selectReq.InputSerialization.CompressionType == s3select.SelectCompressionBZIP {
|
||||
if !strings.Contains(objInfo.ContentType, "bzip") {
|
||||
writeErrorResponse(w, ErrInvalidDataSource, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
if selectReq.InputSerialization.CompressionType == "" {
|
||||
selectReq.InputSerialization.CompressionType = s3select.SelectCompressionNONE
|
||||
if !strings.Contains(objInfo.ContentType, "text/csv") && !strings.Contains(objInfo.ContentType, "application/json") {
|
||||
writeErrorResponse(w, ErrInvalidDataSource, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
if !strings.EqualFold(string(selectReq.ExpressionType), "SQL") {
|
||||
writeErrorResponse(w, ErrInvalidExpressionType, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if len(selectReq.Expression) >= s3select.MaxExpressionLength {
|
||||
writeErrorResponse(w, ErrExpressionTooLong, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if selectReq.InputSerialization.CSV == nil && selectReq.InputSerialization.JSON == nil {
|
||||
writeErrorResponse(w, ErrInvalidRequestParameter, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if selectReq.OutputSerialization.CSV == nil && selectReq.OutputSerialization.JSON == nil {
|
||||
writeErrorResponse(w, ErrInvalidRequestParameter, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if selectReq.InputSerialization.CSV != nil {
|
||||
if selectReq.InputSerialization.CSV.FileHeaderInfo != s3select.CSVFileHeaderInfoUse &&
|
||||
selectReq.InputSerialization.CSV.FileHeaderInfo != s3select.CSVFileHeaderInfoNone &&
|
||||
selectReq.InputSerialization.CSV.FileHeaderInfo != s3select.CSVFileHeaderInfoIgnore &&
|
||||
selectReq.InputSerialization.CSV.FileHeaderInfo != "" {
|
||||
writeErrorResponse(w, ErrInvalidFileHeaderInfo, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if selectReq.OutputSerialization.CSV != nil {
|
||||
if selectReq.OutputSerialization.CSV.QuoteFields != s3select.CSVQuoteFieldsAlways &&
|
||||
selectReq.OutputSerialization.CSV.QuoteFields != s3select.CSVQuoteFieldsAsNeeded &&
|
||||
selectReq.OutputSerialization.CSV.QuoteFields != "" {
|
||||
writeErrorResponse(w, ErrInvalidQuoteFields, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(selectReq.InputSerialization.CSV.RecordDelimiter) > 2 {
|
||||
writeErrorResponse(w, ErrInvalidRequestParameter, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
if selectReq.InputSerialization.JSON != nil {
|
||||
if selectReq.InputSerialization.JSON.Type != s3select.JSONLinesType {
|
||||
writeErrorResponse(w, ErrInvalidJSONType, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Set encryption response headers
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if crypto.IsEncrypted(objInfo.UserDefined) {
|
||||
switch {
|
||||
case crypto.S3.IsEncrypted(objInfo.UserDefined):
|
||||
w.Header().Set(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
case crypto.SSEC.IsEncrypted(objInfo.UserDefined):
|
||||
w.Header().Set(crypto.SSECAlgorithm, r.Header.Get(crypto.SSECAlgorithm))
|
||||
w.Header().Set(crypto.SSECKeyMD5, r.Header.Get(crypto.SSECKeyMD5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader := readahead.NewReader(gr)
|
||||
defer reader.Close()
|
||||
|
||||
size := objInfo.Size
|
||||
if objInfo.IsCompressed() {
|
||||
size = objInfo.GetActualSize()
|
||||
if size < 0 {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, errInvalidDecompressedSize), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s3s, err := s3select.New(reader, size, selectReq)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Parses the select query and checks for an error
|
||||
_, _, _, _, _, _, err = s3select.ParseSelect(s3s)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Executes the query on data-set
|
||||
s3select.Execute(w, s3s)
|
||||
|
||||
// Get host and port from Request.RemoteAddr.
|
||||
host, port, err := net.SplitHostPort(handlers.GetSourceIP(r))
|
||||
@@ -322,7 +241,7 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
writeErrorResponse(w, ErrBadRequest, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrBadRequest, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -335,10 +254,13 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
opts ObjectOptions
|
||||
err error
|
||||
)
|
||||
// get gateway encryption options
|
||||
opts, err := getEncryptionOpts(ctx, r, bucket, object)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
|
||||
// Check for auth type to return S3 compatible error.
|
||||
// type to return the correct error (NoSuchKey vs AccessDenied)
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, object); s3Error != ErrNone {
|
||||
@@ -409,6 +331,7 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
objInfo := gr.ObjInfo
|
||||
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
objInfo.UserDefined = CleanMinioInternalMetadataKeys(objInfo.UserDefined)
|
||||
if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -498,7 +421,7 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponseHeadersOnly(w, ErrBadRequest)
|
||||
return
|
||||
}
|
||||
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrBadRequest, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -516,10 +439,11 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
getObjectInfo = api.CacheAPI().GetObjectInfo
|
||||
}
|
||||
|
||||
var (
|
||||
opts ObjectOptions
|
||||
err error
|
||||
)
|
||||
opts, err := getEncryptionOpts(ctx, r, bucket, object)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectAction, bucket, object); s3Error != ErrNone {
|
||||
if getRequestAuthType(r) == authTypeAnonymous {
|
||||
@@ -574,12 +498,12 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if _, err = DecryptObjectInfo(&objInfo, r.Header); err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
objInfo.UserDefined = CleanMinioInternalMetadataKeys(objInfo.UserDefined)
|
||||
}
|
||||
|
||||
// Set encryption response headers
|
||||
@@ -626,7 +550,6 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
if err != nil {
|
||||
host, port = "", ""
|
||||
}
|
||||
|
||||
// Notify object accessed via a HEAD request.
|
||||
sendEvent(eventArgs{
|
||||
EventName: event.ObjectAccessedHead,
|
||||
@@ -650,6 +573,9 @@ func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta m
|
||||
defaultMeta[k] = v
|
||||
}
|
||||
|
||||
// remove SSE Headers from source info
|
||||
crypto.RemoveSSEHeaders(defaultMeta)
|
||||
|
||||
// if x-amz-metadata-directive says REPLACE then
|
||||
// we extract metadata from the input headers.
|
||||
if isMetadataReplace(r.Header) {
|
||||
@@ -712,7 +638,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
|
||||
return
|
||||
}
|
||||
if !objectAPI.IsEncryptionSupported() && (hasServerSideEncryptionHeader(r.Header) || crypto.SSECopy.IsRequested(r.Header)) {
|
||||
if !api.EncryptionEnabled() && (hasServerSideEncryptionHeader(r.Header) || crypto.SSECopy.IsRequested(r.Header)) {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -727,12 +653,8 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
|
||||
// TODO: Reject requests where body/payload is present, for now we don't even read it.
|
||||
|
||||
// Copy source path.
|
||||
cpSrcPath, err := url.QueryUnescape(r.Header.Get("X-Amz-Copy-Source"))
|
||||
if err != nil {
|
||||
// Save unescaped string as is.
|
||||
cpSrcPath = r.Header.Get("X-Amz-Copy-Source")
|
||||
}
|
||||
// Read escaped copy source path to check for parameters.
|
||||
cpSrcPath := r.Header.Get("X-Amz-Copy-Source")
|
||||
|
||||
// Check https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectVersioning.html
|
||||
// Regardless of whether you have enabled versioning, each object in your bucket
|
||||
@@ -747,8 +669,18 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// Note that url.Parse does the unescaping
|
||||
cpSrcPath = u.Path
|
||||
}
|
||||
if vid := r.Header.Get("X-Amz-Copy-Source-Version-Id"); vid != "" {
|
||||
// Check if versionId header was added, if yes then check if
|
||||
// its non "null" value, we should error out since we do not support
|
||||
// any versions other than "null".
|
||||
if vid != "null" {
|
||||
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
srcBucket, srcObject := path2BucketAndObject(cpSrcPath)
|
||||
// If source object is empty or bucket is empty, reply back invalid copy source.
|
||||
@@ -767,8 +699,30 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponse(w, ErrInvalidMetadataDirective, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// This request header needs to be set prior to setting ObjectOptions
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
}
|
||||
|
||||
var srcOpts, dstOpts ObjectOptions
|
||||
srcOpts, err := copySrcEncryptionOpts(ctx, r, srcBucket, srcObject)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// convert copy src encryption options for GET calls
|
||||
var getOpts = ObjectOptions{}
|
||||
getSSE := encrypt.SSE(srcOpts.ServerSideEncryption)
|
||||
if getSSE != srcOpts.ServerSideEncryption {
|
||||
getOpts.ServerSideEncryption = getSSE
|
||||
}
|
||||
dstOpts, err = copyDstEncryptionOpts(ctx, r, dstBucket, dstObject, nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
@@ -791,7 +745,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
var rs *HTTPRangeSpec
|
||||
gr, err := getObjectNInfo(ctx, srcBucket, srcObject, rs, r.Header, lock, srcOpts)
|
||||
gr, err := getObjectNInfo(ctx, srcBucket, srcObject, rs, r.Header, lock, getOpts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -890,9 +844,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
rawReader := srcInfo.Reader
|
||||
pReader := NewPutObjReader(srcInfo.Reader, nil, nil)
|
||||
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
}
|
||||
var encMetadata = make(map[string]string)
|
||||
if objectAPI.IsEncryptionSupported() && !isCompressed {
|
||||
// Encryption parameters not applicable for this object.
|
||||
@@ -927,6 +878,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
// - the object is encrypted using SSE-C and two different SSE-C keys are present
|
||||
// - the object is encrypted using SSE-S3 and the SSE-S3 header is present
|
||||
// than execute a key rotation.
|
||||
var keyRotation bool
|
||||
if cpSrcDstSame && ((sseCopyC && sseC) || (sseS3 && sseCopyS3)) {
|
||||
if sseCopyC && sseC {
|
||||
oldKey, err = ParseSSECopyCustomerRequest(r.Header, srcInfo.UserDefined)
|
||||
@@ -950,12 +902,15 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
|
||||
// Since we are rotating the keys, make sure to update the metadata.
|
||||
srcInfo.metadataOnly = true
|
||||
keyRotation = true
|
||||
} else {
|
||||
if isSourceEncrypted || isTargetEncrypted {
|
||||
// We are not only copying just metadata instead
|
||||
// we are creating a new object at this point, even
|
||||
// if source and destination are same objects.
|
||||
srcInfo.metadataOnly = false
|
||||
if !keyRotation {
|
||||
srcInfo.metadataOnly = false
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the size of the target object
|
||||
@@ -965,7 +920,8 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
case !isSourceEncrypted && !isTargetEncrypted:
|
||||
targetSize = srcInfo.Size
|
||||
case isSourceEncrypted && isTargetEncrypted:
|
||||
targetSize = srcInfo.Size
|
||||
objInfo := ObjectInfo{Size: actualSize}
|
||||
targetSize = objInfo.EncryptedSize()
|
||||
case !isSourceEncrypted && isTargetEncrypted:
|
||||
targetSize = srcInfo.EncryptedSize()
|
||||
case isSourceEncrypted && !isTargetEncrypted:
|
||||
@@ -1017,7 +973,6 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
|
||||
// Ensure that metadata does not contain sensitive information
|
||||
crypto.RemoveSensitiveEntries(srcInfo.UserDefined)
|
||||
|
||||
// Check if x-amz-metadata-directive was not set to REPLACE and source,
|
||||
// desination are same objects. Apply this restriction also when
|
||||
// metadataOnly is true indicating that we are not overwriting the object.
|
||||
@@ -1115,7 +1070,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
|
||||
return
|
||||
}
|
||||
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -1276,7 +1231,17 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
rawReader := hashReader
|
||||
pReader := NewPutObjReader(rawReader, nil, nil)
|
||||
|
||||
// This request header needs to be set prior to setting ObjectOptions
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
}
|
||||
// get gateway encryption options
|
||||
var opts ObjectOptions
|
||||
opts, err = putEncryptionOpts(ctx, r, bucket, object, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
@@ -1285,9 +1250,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
}
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
}
|
||||
|
||||
var objectEncryptionKey []byte
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if hasServerSideEncryptionHeader(r.Header) && !hasSuffix(object, slashSeparator) { // handle SSE requests
|
||||
@@ -1384,7 +1347,7 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
|
||||
return
|
||||
}
|
||||
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -1392,15 +1355,26 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
bucket := vars["bucket"]
|
||||
object := vars["object"]
|
||||
|
||||
var (
|
||||
opts ObjectOptions
|
||||
err error
|
||||
)
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutObjectAction, bucket, object); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// This request header needs to be set prior to setting ObjectOptions
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
}
|
||||
|
||||
// get gateway encryption options
|
||||
var opts ObjectOptions
|
||||
var err error
|
||||
|
||||
opts, err = putEncryptionOpts(ctx, r, bucket, object, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
if _, err = objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil {
|
||||
@@ -1419,9 +1393,6 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
|
||||
var encMetadata = map[string]string{}
|
||||
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
}
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if hasServerSideEncryptionHeader(r.Header) {
|
||||
if err = setEncryptionMetadata(r, bucket, object, encMetadata); err != nil {
|
||||
@@ -1487,7 +1458,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
|
||||
return
|
||||
}
|
||||
if !objectAPI.IsEncryptionSupported() && (hasServerSideEncryptionHeader(r.Header) || crypto.SSECopy.IsRequested(r.Header)) {
|
||||
if !api.EncryptionEnabled() && (hasServerSideEncryptionHeader(r.Header) || crypto.SSECopy.IsRequested(r.Header)) {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -1501,12 +1472,8 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
// Copy source path.
|
||||
cpSrcPath, err := url.QueryUnescape(r.Header.Get("X-Amz-Copy-Source"))
|
||||
if err != nil {
|
||||
// Save unescaped string as is.
|
||||
cpSrcPath = r.Header.Get("X-Amz-Copy-Source")
|
||||
}
|
||||
// Read escaped copy source path to check for parameters.
|
||||
cpSrcPath := r.Header.Get("X-Amz-Copy-Source")
|
||||
|
||||
// Check https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectVersioning.html
|
||||
// Regardless of whether you have enabled versioning, each object in your bucket
|
||||
@@ -1521,8 +1488,18 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// Note that url.Parse does the unescaping
|
||||
cpSrcPath = u.Path
|
||||
}
|
||||
if vid := r.Header.Get("X-Amz-Copy-Source-Version-Id"); vid != "" {
|
||||
// Check if X-Amz-Copy-Source-Version-Id header was added, if yes then check if
|
||||
// its non "null" value, we should error out since we do not support
|
||||
// any versions other than "null".
|
||||
if vid != "null" {
|
||||
writeErrorResponse(w, ErrNoSuchVersion, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
srcBucket, srcObject := path2BucketAndObject(cpSrcPath)
|
||||
// If source object is empty or bucket is empty, reply back invalid copy source.
|
||||
@@ -1552,6 +1529,21 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
}
|
||||
|
||||
var srcOpts, dstOpts ObjectOptions
|
||||
srcOpts, err = copySrcEncryptionOpts(ctx, r, srcBucket, srcObject)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// convert copy src and dst encryption options for GET/PUT calls
|
||||
var getOpts = ObjectOptions{}
|
||||
if srcOpts.ServerSideEncryption != nil {
|
||||
getOpts.ServerSideEncryption = encrypt.SSE(srcOpts.ServerSideEncryption)
|
||||
}
|
||||
dstOpts, err = copyDstEncryptionOpts(ctx, r, dstBucket, dstObject, nil)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
@@ -1582,7 +1574,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
}
|
||||
}
|
||||
|
||||
gr, err := getObjectNInfo(ctx, srcBucket, srcObject, rs, r.Header, readLock, srcOpts)
|
||||
gr, err := getObjectNInfo(ctx, srcBucket, srcObject, rs, r.Header, readLock, getOpts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1627,7 +1619,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
var reader io.Reader
|
||||
|
||||
var li ListPartsInfo
|
||||
li, err = objectAPI.ListObjectParts(ctx, dstBucket, dstObject, uploadID, 0, 1)
|
||||
li, err = objectAPI.ListObjectParts(ctx, dstBucket, dstObject, uploadID, 0, 1, dstOpts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1669,7 +1661,19 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
isEncrypted := false
|
||||
var objectEncryptionKey []byte
|
||||
if objectAPI.IsEncryptionSupported() && !isCompressed {
|
||||
li, lerr := objectAPI.ListObjectParts(ctx, dstBucket, dstObject, uploadID, 0, 1, dstOpts)
|
||||
if lerr != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, lerr), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
li.UserDefined = CleanMinioInternalMetadataKeys(li.UserDefined)
|
||||
dstOpts, err = copyDstEncryptionOpts(ctx, r, dstBucket, dstObject, li.UserDefined)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if crypto.IsEncrypted(li.UserDefined) {
|
||||
isEncrypted = true
|
||||
if !crypto.SSEC.IsRequested(r.Header) && crypto.SSEC.IsEncrypted(li.UserDefined) {
|
||||
writeErrorResponse(w, ErrSSEMultipartEncrypted, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1714,7 +1718,6 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
pReader = NewPutObjReader(rawReader, srcInfo.Reader, objectEncryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
srcInfo.PutObjReader = pReader
|
||||
// Copy source object to destination, if source and destination
|
||||
// object is same then only metadata is updated.
|
||||
@@ -1724,6 +1727,9 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if isEncrypted {
|
||||
partInfo.ETag = tryDecryptETag(objectEncryptionKey, partInfo.ETag, crypto.SSEC.IsRequested(r.Header))
|
||||
}
|
||||
|
||||
if isEncrypted {
|
||||
partInfo.ETag = tryDecryptETag(objectEncryptionKey, partInfo.ETag, crypto.SSEC.IsRequested(r.Header))
|
||||
@@ -1751,7 +1757,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r)) // SSE-KMS is not supported
|
||||
return
|
||||
}
|
||||
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
|
||||
if !api.EncryptionEnabled() && hasServerSideEncryptionHeader(r.Header) {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -1856,8 +1862,17 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
var pipeReader *io.PipeReader
|
||||
var pipeWriter *io.PipeWriter
|
||||
|
||||
// get encryption options
|
||||
var opts ObjectOptions
|
||||
if crypto.SSEC.IsRequested(r.Header) {
|
||||
opts, err = putEncryptionOpts(ctx, r, bucket, object, nil)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
var li ListPartsInfo
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1)
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1897,10 +1912,8 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
rawReader := hashReader
|
||||
pReader := NewPutObjReader(rawReader, nil, nil)
|
||||
var opts ObjectOptions
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
@@ -1914,17 +1927,25 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
var objectEncryptionKey []byte
|
||||
if objectAPI.IsEncryptionSupported() && !isCompressed {
|
||||
var li ListPartsInfo
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1)
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
li.UserDefined = CleanMinioInternalMetadataKeys(li.UserDefined)
|
||||
if crypto.IsEncrypted(li.UserDefined) {
|
||||
if !crypto.SSEC.IsRequested(r.Header) && crypto.SSEC.IsEncrypted(li.UserDefined) {
|
||||
writeErrorResponse(w, ErrSSEMultipartEncrypted, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
isEncrypted = true // to detect SSE-S3 encryption
|
||||
opts, err = putEncryptionOpts(ctx, r, bucket, object, li.UserDefined)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
var key []byte
|
||||
if crypto.SSEC.IsRequested(r.Header) {
|
||||
key, err = ParseSSECustomerRequest(r)
|
||||
@@ -1940,7 +1961,6 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
var partIDbin [4]byte
|
||||
binary.LittleEndian.PutUint32(partIDbin[:], uint32(partID)) // marshal part ID
|
||||
|
||||
@@ -2067,16 +2087,16 @@ func (api objectAPIHandlers) ListObjectPartsHandler(w http.ResponseWriter, r *ht
|
||||
writeErrorResponse(w, ErrInvalidMaxParts, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
listPartsInfo, err := objectAPI.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts)
|
||||
var opts ObjectOptions
|
||||
listPartsInfo, err := objectAPI.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
var ssec bool
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
var li ListPartsInfo
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1)
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -2169,10 +2189,9 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
var objectEncryptionKey []byte
|
||||
var opts ObjectOptions
|
||||
var isEncrypted, ssec bool
|
||||
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
var li ListPartsInfo
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1)
|
||||
li, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, 0, 1, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -2181,6 +2200,8 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
isEncrypted = true
|
||||
ssec = crypto.SSEC.IsEncrypted(li.UserDefined)
|
||||
var key []byte
|
||||
isEncrypted = true
|
||||
ssec = crypto.SSEC.IsEncrypted(li.UserDefined)
|
||||
if crypto.S3.IsEncrypted(li.UserDefined) {
|
||||
// Calculating object encryption key
|
||||
objectEncryptionKey, err = decryptObjectInfo(key, bucket, object, li.UserDefined)
|
||||
@@ -2193,13 +2214,11 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
}
|
||||
|
||||
partsMap := make(map[string]PartInfo)
|
||||
var listPartsInfo ListPartsInfo
|
||||
|
||||
if isEncrypted {
|
||||
var partNumberMarker int
|
||||
maxParts := 1000
|
||||
for {
|
||||
listPartsInfo, err = objectAPI.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts)
|
||||
listPartsInfo, err := objectAPI.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -2213,6 +2232,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete parts.
|
||||
var completeParts []CompletePart
|
||||
for _, part := range complMultipartUpload.Parts {
|
||||
|
||||
+107
-13
@@ -25,6 +25,7 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"io/ioutil"
|
||||
@@ -1523,14 +1524,15 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
|
||||
// test cases with inputs and expected result for Copy Object.
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
copySourceHeader string // data for "X-Amz-Copy-Source" header. Contains the object to be copied in the URL.
|
||||
copySourceRange string // data for "X-Amz-Copy-Source-Range" header, contains the byte range offsets of data to be copied.
|
||||
uploadID string // uploadID of the transaction.
|
||||
invalidPartNumber bool // Sets an invalid multipart.
|
||||
maximumPartNumber bool // Sets a maximum parts.
|
||||
accessKey string
|
||||
secretKey string
|
||||
bucketName string
|
||||
copySourceHeader string // data for "X-Amz-Copy-Source" header. Contains the object to be copied in the URL.
|
||||
copySourceVersionID string // data for "X-Amz-Copy-Source-Version-Id" header.
|
||||
copySourceRange string // data for "X-Amz-Copy-Source-Range" header, contains the byte range offsets of data to be copied.
|
||||
uploadID string // uploadID of the transaction.
|
||||
invalidPartNumber bool // Sets an invalid multipart.
|
||||
maximumPartNumber bool // Sets a maximum parts.
|
||||
accessKey string
|
||||
secretKey string
|
||||
// expected output.
|
||||
expectedRespStatus int
|
||||
}{
|
||||
@@ -1694,6 +1696,44 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 14, copy part 1 from from newObject1 with null versionId
|
||||
{
|
||||
bucketName: bucketName,
|
||||
uploadID: uploadID,
|
||||
copySourceHeader: url.QueryEscape("/"+bucketName+"/"+objectName) + "?versionId=null",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 15, copy part 1 from from newObject1 with non null versionId
|
||||
{
|
||||
bucketName: bucketName,
|
||||
uploadID: uploadID,
|
||||
copySourceHeader: url.QueryEscape("/"+bucketName+"/"+objectName) + "?versionId=17",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
},
|
||||
// Test case - 16, copy part 1 from from newObject1 with null X-Amz-Copy-Source-Version-Id
|
||||
{
|
||||
bucketName: bucketName,
|
||||
uploadID: uploadID,
|
||||
copySourceHeader: url.QueryEscape("/" + bucketName + "/" + objectName),
|
||||
copySourceVersionID: "null",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 16, copy part 1 from from newObject1 with non null X-Amz-Copy-Source-Version-Id
|
||||
{
|
||||
bucketName: bucketName,
|
||||
uploadID: uploadID,
|
||||
copySourceHeader: url.QueryEscape("/" + bucketName + "/" + objectName),
|
||||
copySourceVersionID: "17",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
@@ -1717,6 +1757,9 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
if testCase.copySourceHeader != "" {
|
||||
req.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
|
||||
}
|
||||
if testCase.copySourceVersionID != "" {
|
||||
req.Header.Set("X-Amz-Copy-Source-Version-Id", testCase.copySourceVersionID)
|
||||
}
|
||||
if testCase.copySourceRange != "" {
|
||||
req.Header.Set("X-Amz-Copy-Source-Range", testCase.copySourceRange)
|
||||
}
|
||||
@@ -1731,7 +1774,7 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
// See if the new part has been uploaded.
|
||||
// testing whether the copy was successful.
|
||||
var results ListPartsInfo
|
||||
results, err = obj.ListObjectParts(context.Background(), testCase.bucketName, testObject, testCase.uploadID, 0, 1)
|
||||
results, err = obj.ListObjectParts(context.Background(), testCase.bucketName, testObject, testCase.uploadID, 0, 1, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: %s: Failed to look for copied object part: <ERROR> %s", i+1, instanceType, err)
|
||||
}
|
||||
@@ -1752,6 +1795,9 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
if testCase.copySourceHeader != "" {
|
||||
reqV2.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
|
||||
}
|
||||
if testCase.copySourceVersionID != "" {
|
||||
reqV2.Header.Set("X-Amz-Copy-Source-Version-Id", testCase.copySourceVersionID)
|
||||
}
|
||||
if testCase.copySourceRange != "" {
|
||||
reqV2.Header.Set("X-Amz-Copy-Source-Range", testCase.copySourceRange)
|
||||
}
|
||||
@@ -1802,7 +1848,10 @@ func TestAPICopyObjectHandler(t *testing.T) {
|
||||
func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
|
||||
objectName := "test-object"
|
||||
objectName := "test?object" // use file with ? to test URL parsing...
|
||||
if runtime.GOOS == "windows" {
|
||||
objectName = "test-object" // ...except on Windows
|
||||
}
|
||||
// object used for anonymous HTTP request test.
|
||||
anonObject := "anon-object"
|
||||
var err error
|
||||
@@ -1861,6 +1910,7 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
||||
bucketName string
|
||||
newObjectName string // name of the newly copied object.
|
||||
copySourceHeader string // data for "X-Amz-Copy-Source" header. Contains the object to be copied in the URL.
|
||||
copySourceVersionID string // data for "X-Amz-Copy-Source-Version-Id" header.
|
||||
copyModifiedHeader string // data for "X-Amz-Copy-Source-If-Modified-Since" header
|
||||
copyUnmodifiedHeader string // data for "X-Amz-Copy-Source-If-Unmodified-Since" header
|
||||
metadataGarbage bool
|
||||
@@ -2071,6 +2121,44 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 17, copy metadata from newObject1 with null versionId
|
||||
{
|
||||
bucketName: bucketName,
|
||||
newObjectName: "newObject1",
|
||||
copySourceHeader: url.QueryEscape("/"+bucketName+"/"+objectName) + "?versionId=null",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 18, copy metadata from newObject1 with non null versionId
|
||||
{
|
||||
bucketName: bucketName,
|
||||
newObjectName: "newObject1",
|
||||
copySourceHeader: url.QueryEscape("/"+bucketName+"/"+objectName) + "?versionId=17",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
},
|
||||
// Test case - 19, copy metadata from newObject1 with null X-Amz-Copy-Source-Version-Id
|
||||
{
|
||||
bucketName: bucketName,
|
||||
newObjectName: "newObject1",
|
||||
copySourceHeader: url.QueryEscape("/" + bucketName + "/" + objectName),
|
||||
copySourceVersionID: "null",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 20, copy metadata from newObject1 with non null X-Amz-Copy-Source-Version-Id
|
||||
{
|
||||
bucketName: bucketName,
|
||||
newObjectName: "newObject1",
|
||||
copySourceHeader: url.QueryEscape("/" + bucketName + "/" + objectName),
|
||||
copySourceVersionID: "17",
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
@@ -2089,6 +2177,9 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
||||
if testCase.copySourceHeader != "" {
|
||||
req.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
|
||||
}
|
||||
if testCase.copySourceVersionID != "" {
|
||||
req.Header.Set("X-Amz-Copy-Source-Version-Id", testCase.copySourceVersionID)
|
||||
}
|
||||
if testCase.copyModifiedHeader != "" {
|
||||
req.Header.Set("X-Amz-Copy-Source-If-Modified-Since", testCase.copyModifiedHeader)
|
||||
}
|
||||
@@ -2150,6 +2241,9 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
||||
if testCase.copySourceHeader != "" {
|
||||
reqV2.Header.Set("X-Amz-Copy-Source", testCase.copySourceHeader)
|
||||
}
|
||||
if testCase.copySourceVersionID != "" {
|
||||
reqV2.Header.Set("X-Amz-Copy-Source-Version-Id", testCase.copySourceVersionID)
|
||||
}
|
||||
if testCase.copyModifiedHeader != "" {
|
||||
reqV2.Header.Set("X-Amz-Copy-Source-If-Modified-Since", testCase.copyModifiedHeader)
|
||||
}
|
||||
@@ -2245,7 +2339,7 @@ func testAPINewMultipartHandler(obj ObjectLayer, instanceType, bucketName string
|
||||
t.Fatalf("Error decoding the recorded response Body")
|
||||
}
|
||||
// verify the uploadID my making an attempt to list parts.
|
||||
_, err = obj.ListObjectParts(context.Background(), bucketName, objectName, multipartResponse.UploadID, 0, 1)
|
||||
_, err = obj.ListObjectParts(context.Background(), bucketName, objectName, multipartResponse.UploadID, 0, 1, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid UploadID: <ERROR> %s", err)
|
||||
}
|
||||
@@ -2297,7 +2391,7 @@ func testAPINewMultipartHandler(obj ObjectLayer, instanceType, bucketName string
|
||||
t.Fatalf("Error decoding the recorded response Body")
|
||||
}
|
||||
// verify the uploadID my making an attempt to list parts.
|
||||
_, err = obj.ListObjectParts(context.Background(), bucketName, objectName, multipartResponse.UploadID, 0, 1)
|
||||
_, err = obj.ListObjectParts(context.Background(), bucketName, objectName, multipartResponse.UploadID, 0, 1, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid UploadID: <ERROR> %s", err)
|
||||
}
|
||||
@@ -2412,7 +2506,7 @@ func testAPINewMultipartHandlerParallel(obj ObjectLayer, instanceType, bucketNam
|
||||
wg.Wait()
|
||||
// Validate the upload ID by an attempt to list parts using it.
|
||||
for _, uploadID := range testUploads.uploads {
|
||||
_, err := obj.ListObjectParts(context.Background(), bucketName, objectName, uploadID, 0, 1)
|
||||
_, err := obj.ListObjectParts(context.Background(), bucketName, objectName, uploadID, 0, 1, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid UploadID: <ERROR> %s", err)
|
||||
}
|
||||
|
||||
@@ -141,6 +141,65 @@ func (rpcClient *PeerRPCClient) LoadCredentials() error {
|
||||
return rpcClient.Call(peerServiceName+".LoadCredentials", &args, &reply)
|
||||
}
|
||||
|
||||
// DrivePerfInfo - returns drive performance info for remote server.
|
||||
func (rpcClient *PeerRPCClient) DrivePerfInfo() (ServerDrivesPerfInfo, error) {
|
||||
args := AuthArgs{}
|
||||
var reply ServerDrivesPerfInfo
|
||||
|
||||
err := rpcClient.Call(peerServiceName+".DrivePerfInfo", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// MemUsageInfo - returns mem utilization info for remote server
|
||||
func (rpcClient *PeerRPCClient) MemUsageInfo() (ServerMemUsageInfo, error) {
|
||||
args := AuthArgs{}
|
||||
var reply ServerMemUsageInfo
|
||||
|
||||
err := rpcClient.Call(peerServiceName+".MemUsageInfo", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// CPULoadInfo - returns cpu performance info for remote server
|
||||
func (rpcClient *PeerRPCClient) CPULoadInfo() (ServerCPULoadInfo, error) {
|
||||
args := AuthArgs{}
|
||||
var reply ServerCPULoadInfo
|
||||
|
||||
err := rpcClient.Call(peerServiceName+".CPULoadInfo", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// StartProfiling - starts profiling on the remote server.
|
||||
func (rpcClient *PeerRPCClient) StartProfiling(profiler string) error {
|
||||
args := StartProfilingArgs{Profiler: profiler}
|
||||
reply := VoidReply{}
|
||||
return rpcClient.Call(peerServiceName+".StartProfiling", &args, &reply)
|
||||
}
|
||||
|
||||
// DownloadProfilingData - download already running profiling on the remote server.
|
||||
func (rpcClient *PeerRPCClient) DownloadProfilingData() ([]byte, error) {
|
||||
args := AuthArgs{}
|
||||
var reply []byte
|
||||
err := rpcClient.Call(peerServiceName+".DownloadProfilingData", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// SignalService - calls load server info RPC.
|
||||
func (rpcClient *PeerRPCClient) SignalService(sig serviceSignal) error {
|
||||
args := SignalServiceArgs{Sig: sig}
|
||||
reply := VoidReply{}
|
||||
|
||||
return rpcClient.Call(peerServiceName+".SignalService", &args, &reply)
|
||||
}
|
||||
|
||||
// ServerInfo - calls load server info RPC.
|
||||
func (rpcClient *PeerRPCClient) ServerInfo() (ServerInfoData, error) {
|
||||
args := AuthArgs{}
|
||||
reply := ServerInfoData{}
|
||||
|
||||
err := rpcClient.Call(peerServiceName+".ServerInfo", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// NewPeerRPCClient - returns new peer RPC client.
|
||||
func NewPeerRPCClient(host *xnet.Host) (*PeerRPCClient, error) {
|
||||
scheme := "http"
|
||||
|
||||
+147
-2
@@ -20,6 +20,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
@@ -30,7 +32,7 @@ import (
|
||||
)
|
||||
|
||||
const peerServiceName = "Peer"
|
||||
const peerServiceSubPath = "/s3/remote"
|
||||
const peerServiceSubPath = "/peer/remote"
|
||||
|
||||
var peerServicePath = path.Join(minioReservedBucketPath, peerServiceSubPath)
|
||||
|
||||
@@ -118,7 +120,8 @@ type ListenBucketNotificationArgs struct {
|
||||
Addr xnet.Host `json:"addr"`
|
||||
}
|
||||
|
||||
// ListenBucketNotification - handles listen bucket notification RPC call. It creates PeerRPCClient target which pushes requested events to target in remote peer.
|
||||
// ListenBucketNotification - handles listen bucket notification RPC call.
|
||||
// It creates PeerRPCClient target which pushes requested events to target in remote peer.
|
||||
func (receiver *peerRPCReceiver) ListenBucketNotification(args *ListenBucketNotificationArgs, reply *VoidReply) error {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
@@ -238,6 +241,148 @@ func (receiver *peerRPCReceiver) LoadCredentials(args *AuthArgs, reply *VoidRepl
|
||||
return globalConfigSys.Load(newObjectLayerFn())
|
||||
}
|
||||
|
||||
// DrivePerfInfo - handles drive performance RPC call
|
||||
func (receiver *peerRPCReceiver) DrivePerfInfo(args *AuthArgs, reply *ServerDrivesPerfInfo) error {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
*reply = localEndpointsDrivePerf(globalEndpoints)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CPULoadInfo - handles cpu performance RPC call
|
||||
func (receiver *peerRPCReceiver) CPULoadInfo(args *AuthArgs, reply *ServerCPULoadInfo) error {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
*reply = localEndpointsCPULoad(globalEndpoints)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MemUsageInfo - handles mem utilization RPC call
|
||||
func (receiver *peerRPCReceiver) MemUsageInfo(args *AuthArgs, reply *ServerMemUsageInfo) error {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
*reply = localEndpointsMemUsage(globalEndpoints)
|
||||
return 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]
|
||||
}
|
||||
|
||||
// StartProfilingArgs - holds the RPC argument for StartingProfiling RPC call
|
||||
type StartProfilingArgs struct {
|
||||
AuthArgs
|
||||
Profiler string
|
||||
}
|
||||
|
||||
// StartProfiling - profiling server receiver.
|
||||
func (receiver *peerRPCReceiver) StartProfiling(args *StartProfilingArgs, reply *VoidReply) error {
|
||||
if globalProfiler != nil {
|
||||
globalProfiler.Stop()
|
||||
}
|
||||
var err error
|
||||
globalProfiler, err = startProfiler(args.Profiler, "")
|
||||
return err
|
||||
}
|
||||
|
||||
// DownloadProfilingData - download profiling data.
|
||||
func (receiver *peerRPCReceiver) DownloadProfilingData(args *AuthArgs, reply *[]byte) error {
|
||||
var err error
|
||||
*reply, err = getProfileData()
|
||||
return err
|
||||
}
|
||||
|
||||
var errUnsupportedSignal = fmt.Errorf("unsupported signal: only restart and stop signals are supported")
|
||||
|
||||
// SignalServiceArgs - send event RPC arguments.
|
||||
type SignalServiceArgs struct {
|
||||
AuthArgs
|
||||
Sig serviceSignal
|
||||
}
|
||||
|
||||
// SignalService - signal service receiver.
|
||||
func (receiver *peerRPCReceiver) SignalService(args *SignalServiceArgs, reply *VoidReply) error {
|
||||
switch args.Sig {
|
||||
case serviceRestart, serviceStop:
|
||||
globalServiceSignalCh <- args.Sig
|
||||
default:
|
||||
return errUnsupportedSignal
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerInfo - server info receiver.
|
||||
func (receiver *peerRPCReceiver) ServerInfo(args *AuthArgs, reply *ServerInfoData) error {
|
||||
if globalBootTime.IsZero() {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
// Build storage info
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
// Server info data.
|
||||
*reply = ServerInfoData{
|
||||
StorageInfo: objLayer.StorageInfo(context.Background()),
|
||||
ConnStats: globalConnStats.toServerConnStats(),
|
||||
HTTPStats: globalHTTPStats.toServerHTTPStats(),
|
||||
Properties: ServerProperties{
|
||||
Uptime: UTCNow().Sub(globalBootTime),
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
SQSARN: globalNotificationSys.GetARNList(),
|
||||
Region: globalServerConfig.GetRegion(),
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPeerRPCServer - returns new peer RPC server.
|
||||
func NewPeerRPCServer() (*xrpc.Server, error) {
|
||||
rpcServer := xrpc.NewServer()
|
||||
|
||||
+1
-1
@@ -141,7 +141,7 @@ func (sys *PolicySys) Init(objAPI ObjectLayer) error {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-globalServiceDoneCh:
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
sys.refresh(objAPI)
|
||||
|
||||
+1
-1
@@ -202,7 +202,7 @@ func newPosix(path string) (*posix, error) {
|
||||
}
|
||||
|
||||
if !p.diskMount {
|
||||
go p.diskUsage(globalServiceDoneCh)
|
||||
go p.diskUsage(GlobalServiceDoneCh)
|
||||
}
|
||||
|
||||
// Success.
|
||||
|
||||
@@ -29,14 +29,14 @@ import (
|
||||
|
||||
// startWithConds - map which indicates if a given condition supports starts-with policy operator
|
||||
var startsWithConds = map[string]bool{
|
||||
"$acl": true,
|
||||
"$bucket": false,
|
||||
"$cache-control": true,
|
||||
"$content-type": true,
|
||||
"$content-disposition": true,
|
||||
"$content-encoding": true,
|
||||
"$expires": true,
|
||||
"$key": true,
|
||||
"$acl": true,
|
||||
"$bucket": false,
|
||||
"$cache-control": true,
|
||||
"$content-type": true,
|
||||
"$content-disposition": true,
|
||||
"$content-encoding": true,
|
||||
"$expires": true,
|
||||
"$key": true,
|
||||
"$success_action_redirect": true,
|
||||
"$redirect": true,
|
||||
"$success_action_status": false,
|
||||
|
||||
+4
-7
@@ -41,7 +41,7 @@ func registerDistXLRouters(router *mux.Router, endpoints EndpointList) {
|
||||
// Register distributed namespace lock.
|
||||
registerDistNSLockRouter(router)
|
||||
|
||||
// Register S3 peer communication router.
|
||||
// Register peer communication router.
|
||||
registerPeerRPCRouter(router)
|
||||
}
|
||||
|
||||
@@ -101,12 +101,9 @@ func configureServerHandler(endpoints EndpointList) (http.Handler, error) {
|
||||
registerDistXLRouters(router, endpoints)
|
||||
}
|
||||
|
||||
// Add STS router only enabled if etcd is configured.
|
||||
// Add STS router always.
|
||||
registerSTSRouter(router)
|
||||
|
||||
// Add Admin RPC router
|
||||
registerAdminRPCRouter(router)
|
||||
|
||||
// Add Admin router, all APIs are enabled in server mode.
|
||||
registerAdminRouter(router, true)
|
||||
|
||||
@@ -123,8 +120,8 @@ func configureServerHandler(endpoints EndpointList) (http.Handler, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add API router.
|
||||
registerAPIRouter(router)
|
||||
// Add API router, additionally all server mode support encryption.
|
||||
registerAPIRouter(router, true)
|
||||
|
||||
// Register rest of the handlers.
|
||||
return registerHandlers(router, globalHandlers...), nil
|
||||
|
||||
+1
-7
@@ -207,16 +207,13 @@ func serverMain(ctx *cli.Context) {
|
||||
// Handle all server command args.
|
||||
serverHandleCmdArgs(ctx)
|
||||
|
||||
// Create certs path.
|
||||
logger.FatalIf(createConfigDir(), "Unable to initialize configuration files")
|
||||
|
||||
// Check and load TLS certificates.
|
||||
var err error
|
||||
globalPublicCerts, globalTLSCerts, globalIsSSL, err = getTLSConfig()
|
||||
logger.FatalIf(err, "Unable to load the TLS configuration")
|
||||
|
||||
// Check and load Root CAs.
|
||||
globalRootCAs, err = getRootCAs(getCADir())
|
||||
globalRootCAs, err = getRootCAs(globalCertsCADir.Get())
|
||||
logger.FatalIf(err, "Failed to read root CAs (%v)", err)
|
||||
|
||||
// Handle all server environment vars.
|
||||
@@ -296,9 +293,6 @@ func serverMain(ctx *cli.Context) {
|
||||
logger.Fatal(uiErrUnexpectedError(err), "Unable to configure one of server's RPC services")
|
||||
}
|
||||
|
||||
// Initialize Admin Peers inter-node communication only in distributed setup.
|
||||
initGlobalAdminPeers(globalEndpoints)
|
||||
|
||||
var getCert certs.GetCertificateFunc
|
||||
if globalTLSCerts != nil {
|
||||
getCert = globalTLSCerts.GetCertificate
|
||||
|
||||
+3
-3
@@ -34,12 +34,12 @@ const (
|
||||
// Global service signal channel.
|
||||
var globalServiceSignalCh chan serviceSignal
|
||||
|
||||
// Global service done channel.
|
||||
var globalServiceDoneCh chan struct{}
|
||||
// GlobalServiceDoneCh - Global service done channel.
|
||||
var GlobalServiceDoneCh chan struct{}
|
||||
|
||||
// Initialize service mutex once.
|
||||
func init() {
|
||||
globalServiceDoneCh = make(chan struct{}, 1)
|
||||
GlobalServiceDoneCh = make(chan struct{}, 1)
|
||||
globalServiceSignalCh = make(chan serviceSignal)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// AssumedRoleUser - The identifiers for the temporary security credentials that
|
||||
// the operation returns. Please also see https://docs.aws.amazon.com/goto/WebAPI/sts-2011-06-15/AssumedRoleUser
|
||||
type AssumedRoleUser struct {
|
||||
// The ARN of the temporary security credentials that are returned from the
|
||||
// AssumeRole action. For more information about ARNs and how to use them in
|
||||
// policies, see IAM Identifiers (http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html)
|
||||
// in Using IAM.
|
||||
//
|
||||
// Arn is a required field
|
||||
Arn string
|
||||
|
||||
// A unique identifier that contains the role ID and the role session name of
|
||||
// the role that is being assumed. The role ID is generated by AWS when the
|
||||
// role is created.
|
||||
//
|
||||
// AssumedRoleId is a required field
|
||||
AssumedRoleID string `xml:"AssumeRoleId"`
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
|
||||
// AssumeRoleWithWebIdentityResponse contains the result of successful AssumeRoleWithWebIdentity request.
|
||||
type AssumeRoleWithWebIdentityResponse struct {
|
||||
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithWebIdentityResponse" json:"-"`
|
||||
Result WebIdentityResult `xml:"AssumeRoleWithWebIdentityResult"`
|
||||
ResponseMetadata struct {
|
||||
RequestID string `xml:"RequestId,omitempty"`
|
||||
} `xml:"ResponseMetadata,omitempty"`
|
||||
}
|
||||
|
||||
// WebIdentityResult - Contains the response to a successful AssumeRoleWithWebIdentity
|
||||
// request, including temporary credentials that can be used to make Minio API requests.
|
||||
type WebIdentityResult struct {
|
||||
// The identifiers for the temporary security credentials that the operation
|
||||
// returns.
|
||||
AssumedRoleUser AssumedRoleUser `xml:",omitempty"`
|
||||
|
||||
// The intended audience (also known as client ID) of the web identity token.
|
||||
// This is traditionally the client identifier issued to the application that
|
||||
// requested the client grants.
|
||||
Audience string `xml:",omitempty"`
|
||||
|
||||
// The temporary security credentials, which include an access key ID, a secret
|
||||
// access key, and a security (or session) token.
|
||||
//
|
||||
// Note: The size of the security token that STS APIs return is not fixed. We
|
||||
// strongly recommend that you make no assumptions about the maximum size. As
|
||||
// of this writing, the typical size is less than 4096 bytes, but that can vary.
|
||||
// Also, future updates to AWS might require larger sizes.
|
||||
Credentials auth.Credentials `xml:",omitempty"`
|
||||
|
||||
// A percentage value that indicates the size of the policy in packed form.
|
||||
// The service rejects any policy with a packed size greater than 100 percent,
|
||||
// which means the policy exceeded the allowed space.
|
||||
PackedPolicySize int `xml:",omitempty"`
|
||||
|
||||
// The issuing authority of the web identity token presented. For OpenID Connect
|
||||
// ID tokens, this contains the value of the iss field. For OAuth 2.0 access tokens,
|
||||
// this contains the value of the ProviderId parameter that was passed in the
|
||||
// AssumeRoleWithWebIdentity request.
|
||||
Provider string `xml:",omitempty"`
|
||||
|
||||
// The unique user identifier that is returned by the identity provider.
|
||||
// This identifier is associated with the Token that was submitted
|
||||
// with the AssumeRoleWithWebIdentity call. The identifier is typically unique to
|
||||
// the user and the application that acquired the WebIdentityToken (pairwise identifier).
|
||||
// For OpenID Connect ID tokens, this field contains the value returned by the identity
|
||||
// provider as the token's sub (Subject) claim.
|
||||
SubjectFromWebIdentityToken string `xml:",omitempty"`
|
||||
}
|
||||
|
||||
// AssumeRoleWithClientGrantsResponse contains the result of successful AssumeRoleWithClientGrants request.
|
||||
type AssumeRoleWithClientGrantsResponse struct {
|
||||
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithClientGrantsResponse" json:"-"`
|
||||
Result ClientGrantsResult `xml:"AssumeRoleWithClientGrantsResult"`
|
||||
ResponseMetadata struct {
|
||||
RequestID string `xml:"RequestId,omitempty"`
|
||||
} `xml:"ResponseMetadata,omitempty"`
|
||||
}
|
||||
|
||||
// ClientGrantsResult - Contains the response to a successful AssumeRoleWithClientGrants
|
||||
// request, including temporary credentials that can be used to make Minio API requests.
|
||||
type ClientGrantsResult struct {
|
||||
// The identifiers for the temporary security credentials that the operation
|
||||
// returns.
|
||||
AssumedRoleUser AssumedRoleUser `xml:",omitempty"`
|
||||
|
||||
// The intended audience (also known as client ID) of the web identity token.
|
||||
// This is traditionally the client identifier issued to the application that
|
||||
// requested the client grants.
|
||||
Audience string `xml:",omitempty"`
|
||||
|
||||
// The temporary security credentials, which include an access key ID, a secret
|
||||
// access key, and a security (or session) token.
|
||||
//
|
||||
// Note: The size of the security token that STS APIs return is not fixed. We
|
||||
// strongly recommend that you make no assumptions about the maximum size. As
|
||||
// of this writing, the typical size is less than 4096 bytes, but that can vary.
|
||||
// Also, future updates to AWS might require larger sizes.
|
||||
Credentials auth.Credentials `xml:",omitempty"`
|
||||
|
||||
// A percentage value that indicates the size of the policy in packed form.
|
||||
// The service rejects any policy with a packed size greater than 100 percent,
|
||||
// which means the policy exceeded the allowed space.
|
||||
PackedPolicySize int `xml:",omitempty"`
|
||||
|
||||
// The issuing authority of the web identity token presented. For OpenID Connect
|
||||
// ID tokens, this contains the value of the iss field. For OAuth 2.0 access tokens,
|
||||
// this contains the value of the ProviderId parameter that was passed in the
|
||||
// AssumeRoleWithClientGrants request.
|
||||
Provider string `xml:",omitempty"`
|
||||
|
||||
// The unique user identifier that is returned by the identity provider.
|
||||
// This identifier is associated with the Token that was submitted
|
||||
// with the AssumeRoleWithClientGrants call. The identifier is typically unique to
|
||||
// the user and the application that acquired the ClientGrantsToken (pairwise identifier).
|
||||
// For OpenID Connect ID tokens, this field contains the value returned by the identity
|
||||
// provider as the token's sub (Subject) claim.
|
||||
SubjectFromToken string `xml:",omitempty"`
|
||||
}
|
||||
+7
-1
@@ -56,6 +56,7 @@ const (
|
||||
ErrSTSNone STSErrorCode = iota
|
||||
ErrSTSMissingParameter
|
||||
ErrSTSInvalidParameterValue
|
||||
ErrSTSWebIdentityExpiredToken
|
||||
ErrSTSClientGrantsExpiredToken
|
||||
ErrSTSInvalidClientGrantsToken
|
||||
ErrSTSMalformedPolicyDocument
|
||||
@@ -76,9 +77,14 @@ var stsErrCodeResponse = map[STSErrorCode]STSError{
|
||||
Description: "An invalid or out-of-range value was supplied for the input parameter.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSWebIdentityExpiredToken: {
|
||||
Code: "ExpiredToken",
|
||||
Description: "The web identity token that was passed is expired or is not valid. Get a new identity token from the identity provider and then retry the request.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSClientGrantsExpiredToken: {
|
||||
Code: "ExpiredToken",
|
||||
Description: "The client grants that was passed is expired or is not valid.",
|
||||
Description: "The client grants that was passed is expired or is not valid. Get a new client grants token from the identity provider and then retry the request.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSTSInvalidClientGrantsToken: {
|
||||
|
||||
+100
-63
@@ -17,7 +17,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -45,82 +44,104 @@ func registerSTSRouter(router *mux.Router) {
|
||||
// AssumeRoleWithClientGrants
|
||||
stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithClientGrants)).
|
||||
Queries("Action", "AssumeRoleWithClientGrants").
|
||||
Queries("Version", stsAPIVersion).
|
||||
Queries("Token", "{Token:.*}")
|
||||
|
||||
// AssumeRoleWithWebIdentity
|
||||
stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithWebIdentity)).
|
||||
Queries("Action", "AssumeRoleWithWebIdentity").
|
||||
Queries("Version", stsAPIVersion).
|
||||
Queries("WebIdentityToken", "{Token:.*}")
|
||||
|
||||
}
|
||||
|
||||
// AssumedRoleUser - The identifiers for the temporary security credentials that
|
||||
// the operation returns. Please also see https://docs.aws.amazon.com/goto/WebAPI/sts-2011-06-15/AssumedRoleUser
|
||||
type AssumedRoleUser struct {
|
||||
// The ARN of the temporary security credentials that are returned from the
|
||||
// AssumeRole action. For more information about ARNs and how to use them in
|
||||
// policies, see IAM Identifiers (http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html)
|
||||
// in Using IAM.
|
||||
//
|
||||
// Arn is a required field
|
||||
Arn string
|
||||
// AssumeRoleWithWebIdentity - implementation of AWS STS API supporting OAuth2.0
|
||||
// users from web identity provider such as Facebook, Google, or any OpenID
|
||||
// Connect-compatible identity provider.
|
||||
//
|
||||
// Eg:-
|
||||
// $ curl https://minio:9000/?Action=AssumeRoleWithWebIdentity&WebIdentityToken=<jwt>
|
||||
func (sts *stsAPIHandlers) AssumeRoleWithWebIdentity(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AssumeRoleWithWebIdentity")
|
||||
|
||||
// A unique identifier that contains the role ID and the role session name of
|
||||
// the role that is being assumed. The role ID is generated by AWS when the
|
||||
// role is created.
|
||||
//
|
||||
// AssumedRoleId is a required field
|
||||
AssumedRoleID string `xml:"AssumeRoleId"`
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
defer logger.AuditLog(w, r, "AssumeRoleWithWebIdentity", nil)
|
||||
|
||||
// AssumeRoleWithClientGrantsResponse contains the result of successful AssumeRoleWithClientGrants request.
|
||||
type AssumeRoleWithClientGrantsResponse struct {
|
||||
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleWithClientGrantsResponse" json:"-"`
|
||||
Result ClientGrantsResult `xml:"AssumeRoleWithClientGrantsResult"`
|
||||
ResponseMetadata struct {
|
||||
RequestID string `xml:"RequestId,omitempty"`
|
||||
} `xml:"ResponseMetadata,omitempty"`
|
||||
}
|
||||
if globalIAMValidators == nil {
|
||||
writeSTSErrorResponse(w, ErrSTSNotInitialized)
|
||||
return
|
||||
}
|
||||
|
||||
// ClientGrantsResult - Contains the response to a successful AssumeRoleWithClientGrants
|
||||
// request, including temporary credentials that can be used to make Minio API requests.
|
||||
type ClientGrantsResult struct {
|
||||
// The identifiers for the temporary security credentials that the operation
|
||||
// returns.
|
||||
AssumedRoleUser AssumedRoleUser `xml:",omitempty"`
|
||||
// NOTE: this API only accepts JWT tokens.
|
||||
v, err := globalIAMValidators.Get("jwt")
|
||||
if err != nil {
|
||||
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
|
||||
return
|
||||
}
|
||||
|
||||
// The intended audience (also known as client ID) of the web identity token.
|
||||
// This is traditionally the client identifier issued to the application that
|
||||
// requested the client grants.
|
||||
Audience string `xml:",omitempty"`
|
||||
vars := mux.Vars(r)
|
||||
m, err := v.Validate(vars["Token"], r.URL.Query().Get("DurationSeconds"))
|
||||
if err != nil {
|
||||
switch err {
|
||||
case validator.ErrTokenExpired:
|
||||
writeSTSErrorResponse(w, ErrSTSWebIdentityExpiredToken)
|
||||
case validator.ErrInvalidDuration:
|
||||
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
|
||||
default:
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, ErrSTSInvalidParameterValue)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// The temporary security credentials, which include an access key ID, a secret
|
||||
// access key, and a security (or session) token.
|
||||
//
|
||||
// Note: The size of the security token that STS APIs return is not fixed. We
|
||||
// strongly recommend that you make no assumptions about the maximum size. As
|
||||
// of this writing, the typical size is less than 4096 bytes, but that can vary.
|
||||
// Also, future updates to AWS might require larger sizes.
|
||||
Credentials auth.Credentials `xml:",omitempty"`
|
||||
secret := globalServerConfig.GetCredential().SecretKey
|
||||
cred, err := auth.GetNewCredentialsWithMetadata(m, secret)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, ErrSTSInternalError)
|
||||
return
|
||||
}
|
||||
|
||||
// A percentage value that indicates the size of the policy in packed form.
|
||||
// The service rejects any policy with a packed size greater than 100 percent,
|
||||
// which means the policy exceeded the allowed space.
|
||||
PackedPolicySize int `xml:",omitempty"`
|
||||
// JWT has requested a custom claim with policy value set.
|
||||
// This is a Minio STS API specific value, this value should
|
||||
// be set and configured on your identity provider as part of
|
||||
// JWT custom claims.
|
||||
var policyName string
|
||||
if v, ok := m["policy"]; ok {
|
||||
policyName, _ = v.(string)
|
||||
}
|
||||
|
||||
// The issuing authority of the web identity token presented. For OpenID Connect
|
||||
// ID tokens, this contains the value of the iss field. For OAuth 2.0 access tokens,
|
||||
// this contains the value of the ProviderId parameter that was passed in the
|
||||
// AssumeRoleWithClientGrants request.
|
||||
Provider string `xml:",omitempty"`
|
||||
var subFromToken string
|
||||
if v, ok := m["sub"]; ok {
|
||||
subFromToken, _ = v.(string)
|
||||
}
|
||||
|
||||
// The unique user identifier that is returned by the identity provider.
|
||||
// This identifier is associated with the Token that was submitted
|
||||
// with the AssumeRoleWithClientGrants call. The identifier is typically unique to
|
||||
// the user and the application that acquired the ClientGrantsToken (pairwise identifier).
|
||||
// For OpenID Connect ID tokens, this field contains the value returned by the identity
|
||||
// provider as the token's sub (Subject) claim.
|
||||
SubjectFromToken string `xml:",omitempty"`
|
||||
// Set the newly generated credentials.
|
||||
if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeSTSErrorResponse(w, ErrSTSInternalError)
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload temp users
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
|
||||
encodedSuccessResponse := encodeResponse(&AssumeRoleWithWebIdentityResponse{
|
||||
Result: WebIdentityResult{
|
||||
Credentials: cred,
|
||||
SubjectFromWebIdentityToken: subFromToken,
|
||||
},
|
||||
})
|
||||
|
||||
writeSuccessResponseXML(w, encodedSuccessResponse)
|
||||
}
|
||||
|
||||
// AssumeRoleWithClientGrants - implementation of AWS STS extension API supporting
|
||||
// OAuth2.0 client grants.
|
||||
// OAuth2.0 client credential grants.
|
||||
//
|
||||
// Eg:-
|
||||
// $ curl https://minio:9000/?Action=AssumeRoleWithClientGrants&Token=<jwt>
|
||||
@@ -173,6 +194,11 @@ func (sts *stsAPIHandlers) AssumeRoleWithClientGrants(w http.ResponseWriter, r *
|
||||
policyName, _ = v.(string)
|
||||
}
|
||||
|
||||
var subFromToken string
|
||||
if v, ok := m["sub"]; ok {
|
||||
subFromToken, _ = v.(string)
|
||||
}
|
||||
|
||||
// Set the newly generated credentials.
|
||||
if err = globalIAMSys.SetTempUser(cred.AccessKey, cred, policyName); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -180,8 +206,19 @@ func (sts *stsAPIHandlers) AssumeRoleWithClientGrants(w http.ResponseWriter, r *
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload temp users
|
||||
for _, nerr := range globalNotificationSys.LoadUsers() {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
|
||||
encodedSuccessResponse := encodeResponse(&AssumeRoleWithClientGrantsResponse{
|
||||
Result: ClientGrantsResult{Credentials: cred},
|
||||
Result: ClientGrantsResult{
|
||||
Credentials: cred,
|
||||
SubjectFromToken: subFromToken,
|
||||
},
|
||||
})
|
||||
|
||||
writeSuccessResponseXML(w, encodedSuccessResponse)
|
||||
|
||||
@@ -404,7 +404,7 @@ func StartTestServer(t TestErrHandler, instanceType string) TestServer {
|
||||
|
||||
// Sets the global config path to empty string.
|
||||
func resetGlobalConfigPath() {
|
||||
setConfigDir("")
|
||||
globalConfigDir = &ConfigDir{path: ""}
|
||||
}
|
||||
|
||||
// sets globalObjectAPI to `nil`.
|
||||
@@ -2115,7 +2115,7 @@ func registerBucketLevelFunc(bucket *mux.Router, api objectAPIHandlers, apiFunct
|
||||
func registerAPIFunctions(muxRouter *mux.Router, objLayer ObjectLayer, apiFunctions ...string) {
|
||||
if len(apiFunctions) == 0 {
|
||||
// Register all api endpoints by default.
|
||||
registerAPIRouter(muxRouter)
|
||||
registerAPIRouter(muxRouter, true)
|
||||
return
|
||||
}
|
||||
// API Router.
|
||||
@@ -2133,8 +2133,9 @@ func registerAPIFunctions(muxRouter *mux.Router, objLayer ObjectLayer, apiFuncti
|
||||
// to underlying cache layer to manage object layer operation and disk caching
|
||||
// operation
|
||||
api := objectAPIHandlers{
|
||||
ObjectAPI: newObjectLayerFn,
|
||||
CacheAPI: newCacheObjectsFn,
|
||||
ObjectAPI: newObjectLayerFn,
|
||||
CacheAPI: newCacheObjectsFn,
|
||||
EncryptionEnabled: func() bool { return true },
|
||||
}
|
||||
|
||||
// Register ListBuckets handler.
|
||||
@@ -2155,7 +2156,7 @@ func initTestAPIEndPoints(objLayer ObjectLayer, apiFunctions []string) http.Hand
|
||||
registerAPIFunctions(muxRouter, objLayer, apiFunctions...)
|
||||
return muxRouter
|
||||
}
|
||||
registerAPIRouter(muxRouter)
|
||||
registerAPIRouter(muxRouter, true)
|
||||
return muxRouter
|
||||
}
|
||||
|
||||
|
||||
@@ -203,4 +203,16 @@ Example 1:
|
||||
"Please check the passed value",
|
||||
"Compress extensions/mime-types are delimited by `,`. For eg, MINIO_COMPRESS_ATTR=\"A,B,C\"",
|
||||
)
|
||||
|
||||
uiErrInvalidGWSSEValue = newUIErrFn(
|
||||
"Invalid gateway SSE value",
|
||||
"Please check the passed value",
|
||||
"MINIO_GATEWAY_SSE: Gateway SSE accepts only C and S3 as valid values. Delimit by `;` to set more than one value",
|
||||
)
|
||||
|
||||
uiErrInvalidGWSSEEnvValue = newUIErrFn(
|
||||
"Invalid gateway SSE configuration",
|
||||
"",
|
||||
"Refer to https://docs.minio.io/docs/minio-kms-quickstart-guide.html for setting up SSE",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -208,6 +208,26 @@ func (p profilerWrapper) Path() string {
|
||||
return p.pathFn()
|
||||
}
|
||||
|
||||
// Returns current profile data, returns error if there is no active
|
||||
// profiling in progress. Stops an active profile.
|
||||
func getProfileData() ([]byte, error) {
|
||||
if globalProfiler == nil {
|
||||
return nil, errors.New("profiler not enabled")
|
||||
}
|
||||
|
||||
profilerPath := globalProfiler.Path()
|
||||
|
||||
// Stop the profiler
|
||||
globalProfiler.Stop()
|
||||
|
||||
profilerFile, err := os.Open(profilerPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ioutil.ReadAll(profilerFile)
|
||||
}
|
||||
|
||||
// Starts a profiler returns nil if profiler is not enabled, caller needs to handle this.
|
||||
func startProfiler(profilerType, dirPath string) (interface {
|
||||
Stop()
|
||||
|
||||
+10
-5
@@ -752,10 +752,10 @@ func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *Se
|
||||
|
||||
if errs := globalNotificationSys.LoadCredentials(); len(errs) != 0 {
|
||||
reply.PeerErrMsgs = make(map[string]string)
|
||||
for host, err := range errs {
|
||||
err = fmt.Errorf("Unable to update credentials on server %v: %v", host, err)
|
||||
for _, nerr := range errs {
|
||||
err = fmt.Errorf("Unable to update credentials on server %v: %v", nerr.Host, nerr.Err)
|
||||
logger.LogIf(context.Background(), err)
|
||||
reply.PeerErrMsgs[host.String()] = err.Error()
|
||||
reply.PeerErrMsgs[nerr.Host.String()] = err.Error()
|
||||
}
|
||||
} else {
|
||||
reply.Token, err = authenticateWeb(creds.AccessKey, creds.SecretKey)
|
||||
@@ -931,7 +931,13 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
pReader = NewPutObjReader(hashReader, nil, nil)
|
||||
|
||||
// get gateway encryption options
|
||||
var opts ObjectOptions
|
||||
opts, err = putEncryptionOpts(ctx, r, bucket, object, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||
return
|
||||
}
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if hasServerSideEncryptionHeader(r.Header) && !hasSuffix(object, slashSeparator) { // handle SSE requests
|
||||
rawReader := hashReader
|
||||
@@ -954,7 +960,6 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
// Ensure that metadata does not contain sensitive information
|
||||
crypto.RemoveSensitiveEntries(metadata)
|
||||
|
||||
var opts ObjectOptions
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
if _, err = objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil {
|
||||
|
||||
+5
-5
@@ -234,7 +234,7 @@ func (s *xlSets) monitorAndConnectEndpoints(monitorInterval time.Duration) {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-globalServiceDoneCh:
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case <-s.disksConnectDoneCh:
|
||||
return
|
||||
@@ -288,7 +288,7 @@ func newXLSets(endpoints EndpointList, format *formatXLV3, setCount int, drivesP
|
||||
nsMutex: mutex,
|
||||
bp: bp,
|
||||
}
|
||||
go s.sets[i].cleanupStaleMultipartUploads(context.Background(), globalMultipartCleanupInterval, globalMultipartExpiry, globalServiceDoneCh)
|
||||
go s.sets[i].cleanupStaleMultipartUploads(context.Background(), GlobalMultipartCleanupInterval, GlobalMultipartExpiry, GlobalServiceDoneCh)
|
||||
}
|
||||
|
||||
// Connect disks right away, but wait until we have `format.json` quorum.
|
||||
@@ -521,7 +521,7 @@ func (s *xlSets) IsListenBucketSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsEncryptionSupported returns whether server side encryption is applicable for this layer.
|
||||
// IsEncryptionSupported returns whether server side encryption is implemented for this layer.
|
||||
func (s *xlSets) IsEncryptionSupported() bool {
|
||||
return s.getHashedSet("").IsEncryptionSupported()
|
||||
}
|
||||
@@ -840,8 +840,8 @@ func (s *xlSets) PutObjectPart(ctx context.Context, bucket, object, uploadID str
|
||||
}
|
||||
|
||||
// ListObjectParts - lists all uploaded parts to an object in hashedSet.
|
||||
func (s *xlSets) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int) (result ListPartsInfo, err error) {
|
||||
return s.getHashedSet(object).ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts)
|
||||
func (s *xlSets) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
|
||||
return s.getHashedSet(object).ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||
}
|
||||
|
||||
// Aborts an in-progress multipart operation on hashedSet based on the object name.
|
||||
|
||||
+1
-1
@@ -299,7 +299,7 @@ func (xl xlObjects) IsListenBucketSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsEncryptionSupported returns whether server side encryption is applicable for this layer.
|
||||
// IsEncryptionSupported returns whether server side encryption is implemented for this layer.
|
||||
func (xl xlObjects) IsEncryptionSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func partsMetaFromModTimes(modTimes []time.Time, algorithm BitrotAlgorithm, chec
|
||||
Stat: statInfo{
|
||||
ModTime: modTime,
|
||||
},
|
||||
Parts: []objectPartInfo{
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Name: "part.1",
|
||||
},
|
||||
|
||||
@@ -31,9 +31,9 @@ import (
|
||||
|
||||
const erasureAlgorithmKlauspost = "klauspost/reedsolomon/vandermonde"
|
||||
|
||||
// objectPartInfo Info of each part kept in the multipart metadata
|
||||
// ObjectPartInfo Info of each part kept in the multipart metadata
|
||||
// file after CompleteMultipartUpload() is called.
|
||||
type objectPartInfo struct {
|
||||
type ObjectPartInfo struct {
|
||||
Number int `json:"number"`
|
||||
Name string `json:"name"`
|
||||
ETag string `json:"etag"`
|
||||
@@ -42,7 +42,7 @@ type objectPartInfo struct {
|
||||
}
|
||||
|
||||
// byObjectPartNumber is a collection satisfying sort.Interface.
|
||||
type byObjectPartNumber []objectPartInfo
|
||||
type byObjectPartNumber []ObjectPartInfo
|
||||
|
||||
func (t byObjectPartNumber) Len() int { return len(t) }
|
||||
func (t byObjectPartNumber) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
||||
@@ -153,7 +153,7 @@ type xlMetaV1 struct {
|
||||
// Metadata map for current object `xl.json`.
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
// Captures all the individual object `xl.json`.
|
||||
Parts []objectPartInfo `json:"parts,omitempty"`
|
||||
Parts []ObjectPartInfo `json:"parts,omitempty"`
|
||||
}
|
||||
|
||||
// XL metadata constants.
|
||||
@@ -243,7 +243,7 @@ func (m xlMetaV1) ToObjectInfo(bucket, object string) ObjectInfo {
|
||||
}
|
||||
|
||||
// objectPartIndex - returns the index of matching object part number.
|
||||
func objectPartIndex(parts []objectPartInfo, partNumber int) int {
|
||||
func objectPartIndex(parts []ObjectPartInfo, partNumber int) int {
|
||||
for i, part := range parts {
|
||||
if partNumber == part.Number {
|
||||
return i
|
||||
@@ -254,7 +254,7 @@ func objectPartIndex(parts []objectPartInfo, partNumber int) int {
|
||||
|
||||
// AddObjectPart - add a new object part in order.
|
||||
func (m *xlMetaV1) AddObjectPart(partNumber int, partName string, partETag string, partSize int64, actualSize int64) {
|
||||
partInfo := objectPartInfo{
|
||||
partInfo := ObjectPartInfo{
|
||||
Number: partNumber,
|
||||
Name: partName,
|
||||
ETag: partETag,
|
||||
@@ -351,7 +351,7 @@ func pickValidXLMeta(ctx context.Context, metaArr []xlMetaV1, modTime time.Time,
|
||||
var objMetadataOpIgnoredErrs = append(baseIgnoredErrs, errDiskAccessDenied, errVolumeNotFound, errFileNotFound, errFileAccessDenied, errCorruptedFormat)
|
||||
|
||||
// readXLMetaParts - returns the XL Metadata Parts from xl.json of one of the disks picked at random.
|
||||
func (xl xlObjects) readXLMetaParts(ctx context.Context, bucket, object string) (xlMetaParts []objectPartInfo, xlMeta map[string]string, err error) {
|
||||
func (xl xlObjects) readXLMetaParts(ctx context.Context, bucket, object string) (xlMetaParts []ObjectPartInfo, xlMeta map[string]string, err error) {
|
||||
var ignoredErrs []error
|
||||
for _, disk := range xl.getLoadBalancedDisks() {
|
||||
if disk == nil {
|
||||
|
||||
@@ -493,7 +493,7 @@ func (xl xlObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
||||
// Implements S3 compatible ListObjectParts API. The resulting
|
||||
// ListPartsInfo structure is marshaled directly into XML and
|
||||
// replied back to the client.
|
||||
func (xl xlObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int) (result ListPartsInfo, e error) {
|
||||
func (xl xlObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int, opts ObjectOptions) (result ListPartsInfo, e error) {
|
||||
if err := checkListPartsArgs(ctx, bucket, object, xl); err != nil {
|
||||
return result, err
|
||||
}
|
||||
@@ -656,7 +656,7 @@ func (xl xlObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
var currentXLMeta = xlMeta
|
||||
|
||||
// Allocate parts similar to incoming slice.
|
||||
xlMeta.Parts = make([]objectPartInfo, len(parts))
|
||||
xlMeta.Parts = make([]ObjectPartInfo, len(parts))
|
||||
|
||||
// Validate each part and then commit to disk.
|
||||
for i, part := range parts {
|
||||
@@ -695,7 +695,7 @@ func (xl xlObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
objectActualSize += currentXLMeta.Parts[partIdx].ActualSize
|
||||
|
||||
// Add incoming parts.
|
||||
xlMeta.Parts[i] = objectPartInfo{
|
||||
xlMeta.Parts[i] = ObjectPartInfo{
|
||||
Number: part.PartNumber,
|
||||
ETag: part.ETag,
|
||||
Size: currentXLMeta.Parts[partIdx].Size,
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestXLCleanupStaleMultipartUploads(t *testing.T) {
|
||||
|
||||
// Close the go-routine, we are going to
|
||||
// manually start it and test in this test case.
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
@@ -48,14 +48,14 @@ func TestXLCleanupStaleMultipartUploads(t *testing.T) {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
|
||||
go xl.cleanupStaleMultipartUploads(context.Background(), 20*time.Millisecond, 0, globalServiceDoneCh)
|
||||
go xl.cleanupStaleMultipartUploads(context.Background(), 20*time.Millisecond, 0, GlobalServiceDoneCh)
|
||||
|
||||
// Wait for 40ms such that - we have given enough time for
|
||||
// cleanup routine to kick in.
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
|
||||
// Close the routine we do not need it anymore.
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
|
||||
// Check if upload id was already purged.
|
||||
if err = obj.AbortMultipartUpload(context.Background(), bucketName, objectName, uploadID); err != nil {
|
||||
|
||||
+1
-1
@@ -526,7 +526,7 @@ func (xl xlObjects) renameCorruptedObject(ctx context.Context, bucket, object st
|
||||
},
|
||||
}
|
||||
|
||||
validMeta.Parts = []objectPartInfo{
|
||||
validMeta.Parts = []ObjectPartInfo{
|
||||
{
|
||||
Number: 1,
|
||||
Name: "part.1",
|
||||
|
||||
+4
-4
@@ -192,12 +192,12 @@ func parseXLErasureInfo(ctx context.Context, xlMetaBuf []byte) (ErasureInfo, err
|
||||
return erasure, nil
|
||||
}
|
||||
|
||||
func parseXLParts(xlMetaBuf []byte) []objectPartInfo {
|
||||
func parseXLParts(xlMetaBuf []byte) []ObjectPartInfo {
|
||||
// Parse the XL Parts.
|
||||
partsResult := gjson.GetBytes(xlMetaBuf, "parts").Array()
|
||||
partInfo := make([]objectPartInfo, len(partsResult))
|
||||
partInfo := make([]ObjectPartInfo, len(partsResult))
|
||||
for i, p := range partsResult {
|
||||
info := objectPartInfo{}
|
||||
info := ObjectPartInfo{}
|
||||
info.Number = int(p.Get("number").Int())
|
||||
info.Name = p.Get("name").String()
|
||||
info.ETag = p.Get("etag").String()
|
||||
@@ -249,7 +249,7 @@ func xlMetaV1UnmarshalJSON(ctx context.Context, xlMetaBuf []byte) (xlMeta xlMeta
|
||||
}
|
||||
|
||||
// read xl.json from the given disk, parse and return xlV1MetaV1.Parts.
|
||||
func readXLMetaParts(ctx context.Context, disk StorageAPI, bucket string, object string) ([]objectPartInfo, map[string]string, error) {
|
||||
func readXLMetaParts(ctx context.Context, disk StorageAPI, bucket string, object string) ([]ObjectPartInfo, map[string]string, error) {
|
||||
// Reads entire `xl.json`.
|
||||
xlMetaBuf, err := disk.ReadAll(bucket, path.Join(object, xlMetaJSONFile))
|
||||
if err != nil {
|
||||
|
||||
@@ -174,7 +174,7 @@ func (m *xlMetaV1) AddTestObjectCheckSum(checkSumNum int, name string, algorithm
|
||||
|
||||
// AddTestObjectPart - add a new object part in order.
|
||||
func (m *xlMetaV1) AddTestObjectPart(partNumber int, partName string, partETag string, partSize int64) {
|
||||
partInfo := objectPartInfo{
|
||||
partInfo := ObjectPartInfo{
|
||||
Number: partNumber,
|
||||
Name: partName,
|
||||
ETag: partETag,
|
||||
@@ -201,7 +201,7 @@ func getSampleXLMeta(totalParts int) xlMetaV1 {
|
||||
// Number of checksum info == total parts.
|
||||
xlMeta.Erasure.Checksums = make([]ChecksumInfo, totalParts)
|
||||
// total number of parts.
|
||||
xlMeta.Parts = make([]objectPartInfo, totalParts)
|
||||
xlMeta.Parts = make([]ObjectPartInfo, totalParts)
|
||||
for i := 0; i < totalParts; i++ {
|
||||
partName := "part." + strconv.Itoa(i+1)
|
||||
// hard coding hash and algo value for the checksum, Since we are benchmarking the parsing of xl.json the magnitude doesn't affect the test,
|
||||
|
||||
@@ -163,6 +163,7 @@ The Minio server configuration file is stored on the backend in json format. The
|
||||
| `clientId` | _string_ | Unique ID for the MQTT broker to identify Minio |
|
||||
| `username` | _string_ | Username to connect to the MQTT server (if required) |
|
||||
| `password` | _string_ | Password to connect to the MQTT server (if required) |
|
||||
| `queueDir` | _string_ | Persistent store for events when MQTT broker is offline |
|
||||
|
||||
An example configuration for MQTT is shown below:
|
||||
|
||||
@@ -175,10 +176,14 @@ An example configuration for MQTT is shown below:
|
||||
"qos": 1,
|
||||
"clientId": "minio",
|
||||
"username": "",
|
||||
"password": ""
|
||||
"password": "",
|
||||
"queueDir": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
MQTT supports persistent event store in the client-side. The persistent store will backup events when the MQTT broker goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field in the mqtt config. For eg, the `queueDir` can be `/home/events`.
|
||||
|
||||
|
||||
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally.
|
||||
```sh
|
||||
$ mc admin config get myminio/ > /tmp/myconfig
|
||||
@@ -1031,7 +1036,7 @@ An example configuration for NSQ is shown below:
|
||||
"nsqdAddress": "127.0.0.1:4150",
|
||||
"topic": "minio",
|
||||
"tls": {
|
||||
"enable": false,
|
||||
"enable": false,
|
||||
"skipVerify": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
|
||||
## Configuration Directory
|
||||
|
||||
The default configuration directory is `${HOME}/.minio`. Till the release `RELEASE.2018-08-02T23-11-36Z`, Minio server configuration file (`config.json`) was stored in the configuration directory. However for releases beyond `RELEASE.2018-08-18T03-49-57Z`, the configuration file (only), has been migrated to the storage back-end (storage back-end is the directory passed to Minio server while starting the server).
|
||||
Till Minio release `RELEASE.2018-08-02T23-11-36Z`, Minio server configuration file (`config.json`) was stored in the configuration directory specified by `--config-dir` or defaulted to `${HOME}/.minio`. However from releases after `RELEASE.2018-08-18T03-49-57Z`, the configuration file (only), has been migrated to the storage backend (storage backend is the directory passed to Minio server while starting the server).
|
||||
|
||||
You can override the default configuration directory using `--config-dir` command-line option. Please note that this won't have an effect on the `config.json` file as it is always stored on the backend storage, along with data. Minio server generates a new `config.json` with auto-generated access credentials when its started for the first time.
|
||||
You can specify the location of your existing config using `--config-dir`, Minio will migrate the `config.json` to your backend storage. Your current `config.json` will be renamed upon successful migration as `config.json.deprecated` in your current `--config-dir`. All your existing configurations are honored after this migration.
|
||||
|
||||
Additionally `--config-dir` is now a legacy option which will is scheduled for removal in future, so please update your local startup, ansible scripts accordingly.
|
||||
|
||||
```sh
|
||||
minio server --config-dir /etc/minio /data
|
||||
minio server /data
|
||||
```
|
||||
|
||||
### Certificate Directory
|
||||
|
||||
TLS certificates are stored under ``${HOME}/.minio/certs`` directory. You need to place certificates here to enable `HTTPS` based access. Read more about [How to secure access to Minio server with TLS](https://docs.minio.io/docs/how-to-secure-access-to-minio-server-with-tls).
|
||||
TLS certificates by default are stored under ``${HOME}/.minio/certs`` directory. You need to place certificates here to enable `HTTPS` based access. Read more about [How to secure access to Minio server with TLS](https://docs.minio.io/docs/how-to-secure-access-to-minio-server-with-tls).
|
||||
|
||||
Following is the directory structure for Minio server with TLS certificates.
|
||||
|
||||
@@ -25,6 +27,7 @@ $ tree ~/.minio
|
||||
│ └── public.crt
|
||||
```
|
||||
|
||||
You can provide a custom certs directory using `--certs-dir` command line option.
|
||||
|
||||
### Accessing configuration file
|
||||
|
||||
@@ -32,8 +35,6 @@ All configuration changes can be made using [`mc admin config` get/set commands]
|
||||
|
||||
#### Editing configuration file fields
|
||||
|
||||
|
||||
|
||||
##### Get current configuration for Minio deployment
|
||||
|
||||
```sh
|
||||
@@ -62,6 +63,8 @@ The `mc admin` config API will evolve soon to be able to configure specific fiel
|
||||
|``credential.accessKey`` | _string_ | Access key of minimum 3 characters in length. You may override this field with `MINIO_ACCESS_KEY` environment variable.|
|
||||
|``credential.secretKey`` | _string_ | Secret key of minimum 8 characters in length. You may override this field with `MINIO_SECRET_KEY` environment variable.|
|
||||
|
||||
> NOTE: In distributed setup it is mandatory to use environment variables `MINIO_ACCESS_KEY` and `MINIO_SECRET_KEY` for credentials.
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -86,7 +86,8 @@
|
||||
"username": "",
|
||||
"password": "",
|
||||
"reconnectInterval": 0,
|
||||
"keepAliveInterval": 0
|
||||
"keepAliveInterval": 0,
|
||||
"queueDir": ""
|
||||
}
|
||||
},
|
||||
"mysql": {
|
||||
|
||||
+15
-1
@@ -108,7 +108,22 @@ export MINIO_SSE_VAULT_NAMESPACE=ns1
|
||||
|
||||
Note: If [Vault Namespaces](https://learn.hashicorp.com/vault/operations/namespaces) are in use, MINIO_SSE_VAULT_NAMESPACE variable needs to be set before setting approle and transit secrets engine.
|
||||
|
||||
Minio gateway to S3 supports encryption. Three encryption modes are possible - encryption can be set to ``pass-through`` to backend, ``single encryption`` (at the gateway) or ``double encryption`` (single encryption at gateway and pass through to backend). This can be specified by setting MINIO_GATEWAY_SSE and KMS environment variables set in Step 2.1.2.
|
||||
|
||||
If MINIO_GATEWAY_SSE and KMS are not setup, all encryption headers are passed through to the backend. If KMS environment variables are set up, ``single encryption`` is automatically performed at the gateway and encrypted object is saved at the backend.
|
||||
|
||||
To specify ``double encryption``, MINIO_GATEWAY_SSE environment variable needs to be set to "s3" for sse-s3
|
||||
and "c" for sse-c encryption. More than one encryption option can be set, delimited by ";". Objects are encrypted at the gateway and the gateway also does a pass-through to backend. Note that in the case of SSE-C encryption, gateway derives a unique SSE-C key for pass through from the SSE-C client key using a KDF.
|
||||
|
||||
```sh
|
||||
export MINIO_GATEWAY_SSE="s3;c"
|
||||
export MINIO_SSE_VAULT_APPROLE_ID=9b56cc08-8258-45d5-24a3-679876769126
|
||||
export MINIO_SSE_VAULT_APPROLE_SECRET=4e30c52f-13e4-a6f5-0763-d50e8cb4321f
|
||||
export MINIO_SSE_VAULT_ENDPOINT=https://vault-endpoint-ip:8200
|
||||
export MINIO_SSE_VAULT_KEY_NAME=my-minio-key
|
||||
export MINIO_SSE_VAULT_AUTH_TYPE=approle
|
||||
minio gateway s3
|
||||
```
|
||||
|
||||
#### 2.2 Specify a master key
|
||||
|
||||
@@ -126,7 +141,6 @@ head -c 32 /dev/urandom | xxd -c 32 -ps
|
||||
```
|
||||
|
||||
### 3. Test your setup
|
||||
|
||||
To test this setup, start minio server with environment variables set in Step 3, and server is ready to handle SSE-S3 requests.
|
||||
|
||||
### Auto-Encryption
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user