mirror of
https://github.com/pgsty/minio.git
synced 2026-11-03 22:32:04 +03:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35d19a4ae2 | |||
| f767a2538a | |||
| 0c75395abe | |||
| 188cf1d5ce | |||
| d96584ef58 | |||
| d42496cc74 | |||
| 2c096c569f | |||
| 816459d10f | |||
| 3efcd6fa46 | |||
| d2f42d830f | |||
| 7686340621 |
@@ -48,7 +48,7 @@ export class Login extends React.Component {
|
||||
|
||||
handleSubmit(event) {
|
||||
event.preventDefault()
|
||||
const { showAlert, history } = this.props
|
||||
const { showAlert, clearAlert, history } = this.props
|
||||
let message = ""
|
||||
if (this.state.accessKey === "") {
|
||||
message = "Access Key cannot be empty"
|
||||
@@ -66,6 +66,9 @@ export class Login extends React.Component {
|
||||
password: this.state.secretKey
|
||||
})
|
||||
.then(res => {
|
||||
// Clear alerts from previous login attempts
|
||||
clearAlert()
|
||||
|
||||
history.push("/")
|
||||
})
|
||||
.catch(e => {
|
||||
@@ -145,4 +148,7 @@ const mapDispatchToProps = dispatch => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => state, mapDispatchToProps)(Login)
|
||||
export default connect(
|
||||
state => state,
|
||||
mapDispatchToProps
|
||||
)(Login)
|
||||
|
||||
+37
-26
File diff suppressed because one or more lines are too long
@@ -65,6 +65,7 @@ const (
|
||||
ErrBadDigest
|
||||
ErrEntityTooSmall
|
||||
ErrEntityTooLarge
|
||||
ErrPolicyTooLarge
|
||||
ErrIncompleteBody
|
||||
ErrInternalError
|
||||
ErrInvalidAccessKeyID
|
||||
@@ -398,6 +399,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Your proposed upload exceeds the maximum allowed object size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrPolicyTooLarge: {
|
||||
Code: "PolicyTooLarge",
|
||||
Description: "Policy exceeds the maximum allowed document size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrIncompleteBody: {
|
||||
Code: "IncompleteBody",
|
||||
Description: "You did not provide the number of bytes specified by the Content-Length HTTP header.",
|
||||
|
||||
+12
-27
@@ -263,16 +263,6 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
var s3Error APIErrorCode
|
||||
if s3Error = checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, ""); s3Error != ErrNone {
|
||||
// In the event access is denied, a 200 response should still be returned
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
if s3Error != ErrAccessDenied {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Content-Length is required and should be non-zero
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
if r.ContentLength <= 0 {
|
||||
@@ -324,37 +314,32 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
deleteObject = api.CacheAPI().DeleteObject
|
||||
}
|
||||
|
||||
var dErrs = make([]error, len(deleteObjects.Objects))
|
||||
var dErrs = make([]APIErrorCode, len(deleteObjects.Objects))
|
||||
for index, object := range deleteObjects.Objects {
|
||||
// If the request is denied access, each item
|
||||
// should be marked as 'AccessDenied'
|
||||
if s3Error == ErrAccessDenied {
|
||||
dErrs[index] = PrefixAccessDenied{
|
||||
Bucket: bucket,
|
||||
Object: object.ObjectName,
|
||||
if dErrs[index] = checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, object.ObjectName); dErrs[index] != ErrNone {
|
||||
if dErrs[index] == ErrSignatureDoesNotMatch || dErrs[index] == ErrInvalidAccessKeyID {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(dErrs[index]), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
dErrs[index] = deleteObject(ctx, bucket, object.ObjectName)
|
||||
err := deleteObject(ctx, bucket, object.ObjectName)
|
||||
if err != nil {
|
||||
dErrs[index] = toAPIErrorCode(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect deleted objects and errors if any.
|
||||
var deletedObjects []ObjectIdentifier
|
||||
var deleteErrors []DeleteError
|
||||
for index, err := range dErrs {
|
||||
for index, errCode := range dErrs {
|
||||
object := deleteObjects.Objects[index]
|
||||
// Success deleted objects are collected separately.
|
||||
if err == nil {
|
||||
if errCode == ErrNone || errCode == ErrNoSuchKey {
|
||||
deletedObjects = append(deletedObjects, object)
|
||||
continue
|
||||
}
|
||||
if _, ok := err.(ObjectNotFound); ok {
|
||||
// If the object is not found it should be
|
||||
// accounted as deleted as per S3 spec.
|
||||
deletedObjects = append(deletedObjects, object)
|
||||
continue
|
||||
}
|
||||
apiErr := toAPIError(ctx, err)
|
||||
apiErr := getAPIError(errCode)
|
||||
// Error during delete should be collected separately.
|
||||
deleteErrors = append(deleteErrors, DeleteError{
|
||||
Code: apiErr.Code,
|
||||
|
||||
@@ -71,7 +71,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
|
||||
// Error out if Content-Length is beyond allowed size.
|
||||
if r.ContentLength > maxBucketPolicySize {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL, guessIsBrowserReq(r))
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPolicyTooLarge), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+6
-7
@@ -200,9 +200,12 @@ func (c cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string,
|
||||
return cacheReader, nil
|
||||
} else if err != nil {
|
||||
if _, ok := err.(ObjectNotFound); ok {
|
||||
cacheReader.Close()
|
||||
// Delete cached entry if backend object was deleted.
|
||||
dcache.Delete(ctx, bucket, object)
|
||||
if cacheErr == nil {
|
||||
cacheReader.Close()
|
||||
// Delete cached entry if backend object
|
||||
// was deleted.
|
||||
dcache.Delete(ctx, bucket, object)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -455,10 +458,6 @@ func (c cacheObjects) listCacheObjects(ctx context.Context, bucket, prefix, mark
|
||||
eof = true
|
||||
break
|
||||
}
|
||||
// For any walk error return right away.
|
||||
if walkResult.err != nil {
|
||||
return result, toObjectErr(walkResult.err, bucket, prefix)
|
||||
}
|
||||
|
||||
entry := walkResult.entry
|
||||
var objInfo ObjectInfo
|
||||
|
||||
@@ -44,7 +44,6 @@ func TestCreateServerEndpoints(t *testing.T) {
|
||||
{":9000", []string{"/export1{1...32}", "/export1{1...32}"}, false},
|
||||
// Same host cannot export same disk on two ports - special case localhost.
|
||||
{":9001", []string{"http://localhost:900{1...2}/export{1...64}"}, false},
|
||||
|
||||
// Valid inputs.
|
||||
{":9000", []string{"/export1"}, true},
|
||||
{":9000", []string{"/export1", "/export2", "/export3", "/export4"}, true},
|
||||
|
||||
+100
-7
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
@@ -26,7 +27,9 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/cpu"
|
||||
@@ -44,6 +47,8 @@ const (
|
||||
|
||||
// URLEndpointType - URL style endpoint type enum.
|
||||
URLEndpointType
|
||||
|
||||
retryInterval = 5 // In Seconds.
|
||||
)
|
||||
|
||||
// Endpoint - any type of endpoint.
|
||||
@@ -51,6 +56,7 @@ type Endpoint struct {
|
||||
*url.URL
|
||||
IsLocal bool
|
||||
SetIndex int
|
||||
HostName string
|
||||
}
|
||||
|
||||
func (endpoint Endpoint) String() string {
|
||||
@@ -75,6 +81,19 @@ func (endpoint Endpoint) IsHTTPS() bool {
|
||||
return endpoint.Scheme == "https"
|
||||
}
|
||||
|
||||
// UpdateIsLocal - resolves the host and updates if it is local or not.
|
||||
func (endpoint *Endpoint) UpdateIsLocal() error {
|
||||
if !endpoint.IsLocal {
|
||||
isLocal, err := isLocalHost(endpoint.HostName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpoint.IsLocal = isLocal
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewEndpoint - returns new endpoint based on given arguments.
|
||||
func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||
// isEmptyPath - check whether given path is not empty.
|
||||
@@ -87,6 +106,7 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||
}
|
||||
|
||||
var isLocal bool
|
||||
var host string
|
||||
u, err := url.Parse(arg)
|
||||
if err == nil && u.Host != "" {
|
||||
// URL style of endpoint.
|
||||
@@ -98,7 +118,7 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||
return ep, fmt.Errorf("invalid URL endpoint format")
|
||||
}
|
||||
|
||||
var host, port string
|
||||
var port string
|
||||
host, port, err = net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "missing port in address") {
|
||||
@@ -150,10 +170,6 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||
}
|
||||
}
|
||||
|
||||
isLocal, err = isLocalHost(host)
|
||||
if err != nil {
|
||||
return ep, err
|
||||
}
|
||||
} else {
|
||||
// Only check if the arg is an ip address and ask for scheme since its absent.
|
||||
// localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as
|
||||
@@ -166,8 +182,9 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||
}
|
||||
|
||||
return Endpoint{
|
||||
URL: u,
|
||||
IsLocal: isLocal,
|
||||
URL: u,
|
||||
IsLocal: isLocal,
|
||||
HostName: host,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -200,6 +217,74 @@ func (endpoints EndpointList) GetString(i int) string {
|
||||
return endpoints[i].String()
|
||||
}
|
||||
|
||||
// UpdateIsLocal - resolves the host and discovers the local host.
|
||||
func (endpoints EndpointList) UpdateIsLocal() error {
|
||||
var epsResolved int
|
||||
var foundLocal bool
|
||||
resolvedList := make([]bool, len(endpoints))
|
||||
// Mark the starting time
|
||||
startTime := time.Now()
|
||||
keepAliveTicker := time.NewTicker(retryInterval * time.Second)
|
||||
defer keepAliveTicker.Stop()
|
||||
for {
|
||||
// Break if the local endpoint is found already. Or all the endpoints are resolved.
|
||||
if foundLocal || (epsResolved == len(endpoints)) {
|
||||
break
|
||||
}
|
||||
// Retry infinitely on Kubernetes and Docker swarm.
|
||||
// This is needed as the remote hosts are sometime
|
||||
// not available immediately.
|
||||
select {
|
||||
case <-globalOSSignalCh:
|
||||
return fmt.Errorf("The endpoint resolution got interrupted")
|
||||
default:
|
||||
for i, resolved := range resolvedList {
|
||||
if resolved {
|
||||
continue
|
||||
}
|
||||
|
||||
// return err if not Docker or Kubernetes
|
||||
// We use IsDocker() method to check for Docker Swarm environment
|
||||
// as there is no reliable way to clearly identify Swarm from
|
||||
// Docker environment.
|
||||
isLocal, err := isLocalHost(endpoints[i].HostName)
|
||||
if err != nil {
|
||||
if !IsDocker() && !IsKubernetes() {
|
||||
return err
|
||||
}
|
||||
// time elapsed
|
||||
timeElapsed := time.Since(startTime)
|
||||
// log error only if more than 1s elapsed
|
||||
if timeElapsed > time.Second {
|
||||
// log the message to console about the host not being
|
||||
// resolveable.
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("host", endpoints[i].HostName)
|
||||
reqInfo.AppendTags("elapsedTime", humanize.RelTime(startTime, startTime.Add(timeElapsed), "elapsed", ""))
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
} else {
|
||||
resolvedList[i] = true
|
||||
endpoints[i].IsLocal = isLocal
|
||||
epsResolved++
|
||||
if !foundLocal {
|
||||
foundLocal = isLocal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the tick, if the there exist a local endpoint in discovery.
|
||||
// Non docker/kubernetes environment does not need to wait.
|
||||
if !foundLocal && (IsDocker() && IsKubernetes()) {
|
||||
<-keepAliveTicker.C
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// localEndpointsMemUsage - returns ServerMemUsageInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func localEndpointsMemUsage(endpoints EndpointList) ServerMemUsageInfo {
|
||||
@@ -302,6 +387,7 @@ func NewEndpointList(args ...string) (endpoints EndpointList, err error) {
|
||||
uniqueArgs.Add(arg)
|
||||
endpoints = append(endpoints, endpoint)
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
@@ -341,6 +427,9 @@ func CreateEndpoints(serverAddr string, args ...[]string) (string, EndpointList,
|
||||
if err != nil {
|
||||
return serverAddr, endpoints, setupType, err
|
||||
}
|
||||
if err := endpoint.UpdateIsLocal(); err != nil {
|
||||
return serverAddr, endpoints, setupType, err
|
||||
}
|
||||
if endpoint.Type() != PathEndpointType {
|
||||
return serverAddr, endpoints, setupType, uiErrInvalidFSEndpoint(nil).Msg("use path style endpoint for FS setup")
|
||||
}
|
||||
@@ -381,6 +470,10 @@ func CreateEndpoints(serverAddr string, args ...[]string) (string, EndpointList,
|
||||
return serverAddr, endpoints, setupType, nil
|
||||
}
|
||||
|
||||
if err := endpoints.UpdateIsLocal(); err != nil {
|
||||
return serverAddr, endpoints, setupType, uiErrInvalidErasureEndpoints(nil).Msg(err.Error())
|
||||
}
|
||||
|
||||
// Here all endpoints are URL style.
|
||||
endpointPathSet := set.NewStringSet()
|
||||
localEndpointCount := 0
|
||||
|
||||
+43
-29
@@ -49,11 +49,11 @@ func TestNewEndpoint(t *testing.T) {
|
||||
{"http:path", Endpoint{URL: &url.URL{Path: "http:path"}, IsLocal: true}, PathEndpointType, nil},
|
||||
{"http:/path", Endpoint{URL: &url.URL{Path: "http:/path"}, IsLocal: true}, PathEndpointType, nil},
|
||||
{"http:///path", Endpoint{URL: &url.URL{Path: "http:/path"}, IsLocal: true}, PathEndpointType, nil},
|
||||
{"http://localhost/path", Endpoint{URL: u1, IsLocal: true}, URLEndpointType, nil},
|
||||
{"http://localhost/path//", Endpoint{URL: u1, IsLocal: true}, URLEndpointType, nil},
|
||||
{"https://example.org/path", Endpoint{URL: u2}, URLEndpointType, nil},
|
||||
{"http://127.0.0.1:8080/path", Endpoint{URL: u3, IsLocal: true}, URLEndpointType, nil},
|
||||
{"http://192.168.253.200/path", Endpoint{URL: u4}, URLEndpointType, nil},
|
||||
{"http://localhost/path", Endpoint{URL: u1, IsLocal: true, HostName: "localhost"}, URLEndpointType, nil},
|
||||
{"http://localhost/path//", Endpoint{URL: u1, IsLocal: true, HostName: "localhost"}, URLEndpointType, nil},
|
||||
{"https://example.org/path", Endpoint{URL: u2, IsLocal: false, HostName: "example.org"}, URLEndpointType, nil},
|
||||
{"http://127.0.0.1:8080/path", Endpoint{URL: u3, IsLocal: true, HostName: "127.0.0.1"}, URLEndpointType, nil},
|
||||
{"http://192.168.253.200/path", Endpoint{URL: u4, IsLocal: false, HostName: "192.168.253.200"}, URLEndpointType, nil},
|
||||
{"", Endpoint{}, -1, fmt.Errorf("empty or root endpoint is not supported")},
|
||||
{"/", Endpoint{}, -1, fmt.Errorf("empty or root endpoint is not supported")},
|
||||
{`\`, Endpoint{}, -1, fmt.Errorf("empty or root endpoint is not supported")},
|
||||
@@ -71,6 +71,10 @@ func TestNewEndpoint(t *testing.T) {
|
||||
|
||||
for _, testCase := range testCases {
|
||||
endpoint, err := NewEndpoint(testCase.arg)
|
||||
if err == nil {
|
||||
err = endpoint.UpdateIsLocal()
|
||||
}
|
||||
|
||||
if testCase.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
@@ -261,46 +265,46 @@ func TestCreateEndpoints(t *testing.T) {
|
||||
|
||||
// DistXL type
|
||||
{"127.0.0.1:10000", [][]string{{case1Endpoint1, case1Endpoint2, "http://example.org/d3", "http://example.com/d4"}}, "127.0.0.1:10000", EndpointList{
|
||||
Endpoint{URL: case1URLs[0], IsLocal: case1LocalFlags[0]},
|
||||
Endpoint{URL: case1URLs[1], IsLocal: case1LocalFlags[1]},
|
||||
Endpoint{URL: case1URLs[2], IsLocal: case1LocalFlags[2]},
|
||||
Endpoint{URL: case1URLs[3], IsLocal: case1LocalFlags[3]},
|
||||
Endpoint{URL: case1URLs[0], IsLocal: case1LocalFlags[0], HostName: nonLoopBackIP},
|
||||
Endpoint{URL: case1URLs[1], IsLocal: case1LocalFlags[1], HostName: nonLoopBackIP},
|
||||
Endpoint{URL: case1URLs[2], IsLocal: case1LocalFlags[2], HostName: "example.org"},
|
||||
Endpoint{URL: case1URLs[3], IsLocal: case1LocalFlags[3], HostName: "example.com"},
|
||||
}, DistXLSetupType, nil},
|
||||
|
||||
{"127.0.0.1:10000", [][]string{{case2Endpoint1, case2Endpoint2, "http://example.org/d3", "http://example.com/d4"}}, "127.0.0.1:10000", EndpointList{
|
||||
Endpoint{URL: case2URLs[0], IsLocal: case2LocalFlags[0]},
|
||||
Endpoint{URL: case2URLs[1], IsLocal: case2LocalFlags[1]},
|
||||
Endpoint{URL: case2URLs[2], IsLocal: case2LocalFlags[2]},
|
||||
Endpoint{URL: case2URLs[3], IsLocal: case2LocalFlags[3]},
|
||||
Endpoint{URL: case2URLs[0], IsLocal: case2LocalFlags[0], HostName: nonLoopBackIP},
|
||||
Endpoint{URL: case2URLs[1], IsLocal: case2LocalFlags[1], HostName: nonLoopBackIP},
|
||||
Endpoint{URL: case2URLs[2], IsLocal: case2LocalFlags[2], HostName: "example.org"},
|
||||
Endpoint{URL: case2URLs[3], IsLocal: case2LocalFlags[3], HostName: "example.com"},
|
||||
}, DistXLSetupType, nil},
|
||||
|
||||
{":80", [][]string{{case3Endpoint1, "http://example.org:9000/d2", "http://example.com/d3", "http://example.net/d4"}}, ":80", EndpointList{
|
||||
Endpoint{URL: case3URLs[0], IsLocal: case3LocalFlags[0]},
|
||||
Endpoint{URL: case3URLs[1], IsLocal: case3LocalFlags[1]},
|
||||
Endpoint{URL: case3URLs[2], IsLocal: case3LocalFlags[2]},
|
||||
Endpoint{URL: case3URLs[3], IsLocal: case3LocalFlags[3]},
|
||||
Endpoint{URL: case3URLs[0], IsLocal: case3LocalFlags[0], HostName: nonLoopBackIP},
|
||||
Endpoint{URL: case3URLs[1], IsLocal: case3LocalFlags[1], HostName: "example.org"},
|
||||
Endpoint{URL: case3URLs[2], IsLocal: case3LocalFlags[2], HostName: "example.com"},
|
||||
Endpoint{URL: case3URLs[3], IsLocal: case3LocalFlags[3], HostName: "example.net"},
|
||||
}, DistXLSetupType, nil},
|
||||
|
||||
{":9000", [][]string{{case4Endpoint1, "http://example.org/d2", "http://example.com/d3", "http://example.net/d4"}}, ":9000", EndpointList{
|
||||
Endpoint{URL: case4URLs[0], IsLocal: case4LocalFlags[0]},
|
||||
Endpoint{URL: case4URLs[1], IsLocal: case4LocalFlags[1]},
|
||||
Endpoint{URL: case4URLs[2], IsLocal: case4LocalFlags[2]},
|
||||
Endpoint{URL: case4URLs[3], IsLocal: case4LocalFlags[3]},
|
||||
Endpoint{URL: case4URLs[0], IsLocal: case4LocalFlags[0], HostName: nonLoopBackIP},
|
||||
Endpoint{URL: case4URLs[1], IsLocal: case4LocalFlags[1], HostName: "example.org"},
|
||||
Endpoint{URL: case4URLs[2], IsLocal: case4LocalFlags[2], HostName: "example.com"},
|
||||
Endpoint{URL: case4URLs[3], IsLocal: case4LocalFlags[3], HostName: "example.net"},
|
||||
}, DistXLSetupType, nil},
|
||||
|
||||
{":9000", [][]string{{case5Endpoint1, case5Endpoint2, case5Endpoint3, case5Endpoint4}}, ":9000", EndpointList{
|
||||
Endpoint{URL: case5URLs[0], IsLocal: case5LocalFlags[0]},
|
||||
Endpoint{URL: case5URLs[1], IsLocal: case5LocalFlags[1]},
|
||||
Endpoint{URL: case5URLs[2], IsLocal: case5LocalFlags[2]},
|
||||
Endpoint{URL: case5URLs[3], IsLocal: case5LocalFlags[3]},
|
||||
Endpoint{URL: case5URLs[0], IsLocal: case5LocalFlags[0], HostName: nonLoopBackIP},
|
||||
Endpoint{URL: case5URLs[1], IsLocal: case5LocalFlags[1], HostName: nonLoopBackIP},
|
||||
Endpoint{URL: case5URLs[2], IsLocal: case5LocalFlags[2], HostName: nonLoopBackIP},
|
||||
Endpoint{URL: case5URLs[3], IsLocal: case5LocalFlags[3], HostName: nonLoopBackIP},
|
||||
}, DistXLSetupType, nil},
|
||||
|
||||
// DistXL Setup using only local host.
|
||||
{":9003", [][]string{{"http://localhost:9000/d1", "http://localhost:9001/d2", "http://127.0.0.1:9002/d3", case6Endpoint}}, ":9003", EndpointList{
|
||||
Endpoint{URL: case6URLs[0], IsLocal: case6LocalFlags[0]},
|
||||
Endpoint{URL: case6URLs[1], IsLocal: case6LocalFlags[1]},
|
||||
Endpoint{URL: case6URLs[2], IsLocal: case6LocalFlags[2]},
|
||||
Endpoint{URL: case6URLs[3], IsLocal: case6LocalFlags[3]},
|
||||
Endpoint{URL: case6URLs[0], IsLocal: case6LocalFlags[0], HostName: "localhost"},
|
||||
Endpoint{URL: case6URLs[1], IsLocal: case6LocalFlags[1], HostName: "localhost"},
|
||||
Endpoint{URL: case6URLs[2], IsLocal: case6LocalFlags[2], HostName: "127.0.0.1"},
|
||||
Endpoint{URL: case6URLs[3], IsLocal: case6LocalFlags[3], HostName: nonLoopBackIP},
|
||||
}, DistXLSetupType, nil},
|
||||
}
|
||||
|
||||
@@ -357,6 +361,11 @@ func TestGetLocalPeer(t *testing.T) {
|
||||
|
||||
for i, testCase := range testCases {
|
||||
endpoints, _ := NewEndpointList(testCase.endpointArgs...)
|
||||
if !endpoints[0].IsLocal {
|
||||
if err := endpoints.UpdateIsLocal(); err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
}
|
||||
remotePeer := GetLocalPeer(endpoints)
|
||||
if remotePeer != testCase.expectedResult {
|
||||
t.Fatalf("Test %d: expected: %v, got: %v", i+1, testCase.expectedResult, remotePeer)
|
||||
@@ -384,6 +393,11 @@ func TestGetRemotePeers(t *testing.T) {
|
||||
|
||||
for _, testCase := range testCases {
|
||||
endpoints, _ := NewEndpointList(testCase.endpointArgs...)
|
||||
if !endpoints[0].IsLocal {
|
||||
if err := endpoints.UpdateIsLocal(); err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
}
|
||||
remotePeers := GetRemotePeers(endpoints)
|
||||
if !reflect.DeepEqual(remotePeers, testCase.expectedResult) {
|
||||
t.Fatalf("expected: %v, got: %v", testCase.expectedResult, remotePeers)
|
||||
|
||||
+2
-2
@@ -1010,8 +1010,8 @@ func (fs *FSObjects) listDirFactory(isLeaf IsLeafFunc) ListDirFunc {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return
|
||||
}
|
||||
entries, delayIsLeaf = filterListEntries(bucket, prefixDir, entries, prefixEntry, isLeaf)
|
||||
return entries, delayIsLeaf
|
||||
sort.Strings(entries)
|
||||
return filterListEntries(bucket, prefixDir, entries, prefixEntry, isLeaf)
|
||||
}
|
||||
|
||||
// Return list factory instance.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -349,6 +350,11 @@ func ErrorRespToObjectError(err error, params ...string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ComputeCompleteMultipartMD5 calculates MD5 ETag for complete multipart responses
|
||||
func ComputeCompleteMultipartMD5(parts []CompletePart) (string, error) {
|
||||
return getCompleteMultipartMD5(context.Background(), parts)
|
||||
}
|
||||
|
||||
// parse gateway sse env variable
|
||||
func parseGatewaySSE(s string) (gatewaySSE, error) {
|
||||
l := strings.Split(s, ";")
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/cli"
|
||||
miniogopolicy "github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio/cmd"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
@@ -1202,19 +1203,21 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
|
||||
if err != nil {
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
if len(metadata.Metadata) > 0 {
|
||||
objBlob.Metadata, objBlob.Properties, err = s3MetaToAzureProperties(ctx, metadata.Metadata)
|
||||
if err != nil {
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
err = objBlob.SetProperties(nil)
|
||||
if err != nil {
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
err = objBlob.SetMetadata(nil)
|
||||
if err != nil {
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
objBlob.Metadata, objBlob.Properties, err = s3MetaToAzureProperties(ctx, metadata.Metadata)
|
||||
if err != nil {
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
objBlob.Metadata["md5sum"], err = cmd.ComputeCompleteMultipartMD5(uploadedParts)
|
||||
if err != nil {
|
||||
return objInfo, err
|
||||
}
|
||||
err = objBlob.SetProperties(nil)
|
||||
if err != nil {
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
err = objBlob.SetMetadata(nil)
|
||||
if err != nil {
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
var partNumberMarker int
|
||||
for {
|
||||
|
||||
@@ -31,9 +31,7 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/sys"
|
||||
"github.com/rs/cors"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// HandlerFunc - useful to chain different middleware http.Handler
|
||||
@@ -737,46 +735,6 @@ func setBucketForwardingHandler(h http.Handler) http.Handler {
|
||||
return bucketForwardingHandler{fwd, h}
|
||||
}
|
||||
|
||||
// setRateLimitHandler middleware limits the throughput to h using a
|
||||
// rate.Limiter token bucket configured with maxOpenFileLimit and
|
||||
// burst set to 1. The request will idle for up to 1*time.Second.
|
||||
// If the limiter detects the deadline will be exceeded, the request is
|
||||
// canceled immediately.
|
||||
func setRateLimitHandler(h http.Handler) http.Handler {
|
||||
_, maxLimit, err := sys.GetMaxOpenFileLimit()
|
||||
logger.FatalIf(err, "Unable to get maximum open file limit")
|
||||
// Burst value is set to 1 to allow only maxOpenFileLimit
|
||||
// requests to happen at once.
|
||||
l := rate.NewLimiter(rate.Limit(maxLimit), 1)
|
||||
return rateLimit{l, h}
|
||||
}
|
||||
|
||||
type rateLimit struct {
|
||||
*rate.Limiter
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (l rateLimit) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// create a new context from the request with the wait timeout
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
|
||||
defer cancel() // always cancel the context!
|
||||
|
||||
// Wait errors out if the request cannot be processed within
|
||||
// the deadline. time/rate tries to reserve a slot if possible
|
||||
// with in the given duration if it's not possible then Wait(ctx)
|
||||
// returns an error and we cancel the request with ErrSlowDown
|
||||
// error message to the client. This context wait also ensures
|
||||
// requests doomed to fail are terminated early, preventing a
|
||||
// potential pileup on the server.
|
||||
if err := l.Wait(ctx); err != nil {
|
||||
// Send an S3 compatible error, SlowDown.
|
||||
writeErrorResponse(context.Background(), w, errorCodes.ToAPIErr(ErrSlowDown), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
l.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// customHeaderHandler sets x-amz-request-id, x-minio-deployment-id header.
|
||||
// Previously, this value was set right before a response was sent to
|
||||
// the client. So, logger and Error response XML were not using this
|
||||
|
||||
@@ -208,13 +208,6 @@ var (
|
||||
// Max allowed disk cache percentage
|
||||
globalCacheMaxUse = 80
|
||||
|
||||
// RPC V1 - Initial version
|
||||
// RPC V2 - format.json XL version changed to 2
|
||||
// RPC V3 - format.json XL version changed to 3
|
||||
// RPC V4 - ReadFile() arguments signature changed
|
||||
// Current RPC version
|
||||
globalRPCAPIVersion = RPCVersion{4, 0, 0}
|
||||
|
||||
// Allocated etcd endpoint for config and bucket DNS.
|
||||
globalEtcdClient *etcd.Client
|
||||
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"net/url"
|
||||
|
||||
"github.com/minio/dsync"
|
||||
"github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/cmd/rest"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
// lockRESTClient is authenticable lock REST client
|
||||
type lockRESTClient struct {
|
||||
lockSync sync.RWMutex
|
||||
host *xnet.Host
|
||||
restClient *rest.Client
|
||||
serverURL *url.URL
|
||||
connected bool
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
// ServerAddr - dsync.NetLocker interface compatible method.
|
||||
func (client *lockRESTClient) ServerAddr() string {
|
||||
return client.serverURL.Host
|
||||
}
|
||||
|
||||
// ServiceEndpoint - dsync.NetLocker interface compatible method.
|
||||
func (client *lockRESTClient) ServiceEndpoint() string {
|
||||
return client.serverURL.Path
|
||||
}
|
||||
|
||||
// check if the host is up or if it is fine
|
||||
// to make a call to the lock rest server.
|
||||
func (client *lockRESTClient) isHostUp() bool {
|
||||
client.lockSync.Lock()
|
||||
defer client.lockSync.Unlock()
|
||||
|
||||
if client.connected {
|
||||
return true
|
||||
}
|
||||
select {
|
||||
case <-client.timer.C:
|
||||
client.connected = true
|
||||
client.timer = nil
|
||||
return true
|
||||
default:
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Mark the host as down if there is a Network error.
|
||||
func (client *lockRESTClient) markHostDown() {
|
||||
client.lockSync.Lock()
|
||||
defer client.lockSync.Unlock()
|
||||
|
||||
if !client.connected {
|
||||
return
|
||||
}
|
||||
client.connected = false
|
||||
client.timer = time.NewTimer(defaultRetryUnit * 5)
|
||||
}
|
||||
|
||||
// Wrapper to restClient.Call to handle network errors, in case of network error the connection is marked disconnected
|
||||
// permanently. The only way to restore the connection is at the xl-sets layer by xlsets.monitorAndConnectEndpoints()
|
||||
// after verifying format.json
|
||||
func (client *lockRESTClient) call(method string, values url.Values, body io.Reader, length int64) (respBody io.ReadCloser, err error) {
|
||||
|
||||
if !client.isHostUp() {
|
||||
return nil, errors.New("Lock rest server node is down")
|
||||
}
|
||||
|
||||
if values == nil {
|
||||
values = make(url.Values)
|
||||
}
|
||||
|
||||
respBody, err = client.restClient.Call(method, values, body, length)
|
||||
|
||||
if err == nil {
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
if isNetworkError(err) {
|
||||
client.markHostDown()
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Stringer provides a canonicalized representation of node.
|
||||
func (client *lockRESTClient) String() string {
|
||||
return client.host.String()
|
||||
}
|
||||
|
||||
// IsOnline - returns whether REST client failed to connect or not.
|
||||
func (client *lockRESTClient) IsOnline() bool {
|
||||
return client.connected
|
||||
}
|
||||
|
||||
// Close - marks the client as closed.
|
||||
func (client *lockRESTClient) Close() error {
|
||||
client.connected = false
|
||||
client.restClient.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// restCall makes a call to the lock REST server.
|
||||
func (client *lockRESTClient) restCall(call string, args dsync.LockArgs) (reply bool, err error) {
|
||||
|
||||
reader := bytes.NewBuffer(make([]byte, 0, 2048))
|
||||
err = gob.NewEncoder(reader).Encode(args)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
respBody, err := client.call(call, nil, reader, -1)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var resp lockResponse
|
||||
defer http.DrainBody(respBody)
|
||||
err = gob.NewDecoder(respBody).Decode(&resp)
|
||||
|
||||
if err != nil || !resp.Success {
|
||||
reqInfo := &logger.ReqInfo{}
|
||||
reqInfo.AppendTags("resource", args.Resource)
|
||||
reqInfo.AppendTags("serveraddress", args.ServerAddr)
|
||||
reqInfo.AppendTags("serviceendpoint", args.ServiceEndpoint)
|
||||
reqInfo.AppendTags("source", args.Source)
|
||||
reqInfo.AppendTags("uid", args.UID)
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return resp.Success, err
|
||||
}
|
||||
|
||||
// RLock calls read lock REST API.
|
||||
func (client *lockRESTClient) RLock(args dsync.LockArgs) (reply bool, err error) {
|
||||
return client.restCall(lockRESTMethodRLock, args)
|
||||
}
|
||||
|
||||
// Lock calls lock REST API.
|
||||
func (client *lockRESTClient) Lock(args dsync.LockArgs) (reply bool, err error) {
|
||||
return client.restCall(lockRESTMethodLock, args)
|
||||
}
|
||||
|
||||
// RUnlock calls read unlock REST API.
|
||||
func (client *lockRESTClient) RUnlock(args dsync.LockArgs) (reply bool, err error) {
|
||||
return client.restCall(lockRESTMethodRUnlock, args)
|
||||
}
|
||||
|
||||
// Unlock calls write unlock RPC.
|
||||
func (client *lockRESTClient) Unlock(args dsync.LockArgs) (reply bool, err error) {
|
||||
return client.restCall(lockRESTMethodUnlock, args)
|
||||
}
|
||||
|
||||
// ForceUnlock calls force unlock RPC.
|
||||
func (client *lockRESTClient) ForceUnlock(args dsync.LockArgs) (reply bool, err error) {
|
||||
return client.restCall(lockRESTMethodForceUnlock, args)
|
||||
}
|
||||
|
||||
// Expired calls expired RPC.
|
||||
func (client *lockRESTClient) Expired(args dsync.LockArgs) (reply bool, err error) {
|
||||
return client.restCall(lockRESTMethodExpired, args)
|
||||
}
|
||||
|
||||
// Returns a lock rest client.
|
||||
func newlockRESTClient(peer *xnet.Host) *lockRESTClient {
|
||||
|
||||
scheme := "http"
|
||||
if globalIsSSL {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
serverURL := &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: peer.String(),
|
||||
Path: lockRESTPath,
|
||||
}
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if globalIsSSL {
|
||||
tlsConfig = &tls.Config{
|
||||
ServerName: peer.Name,
|
||||
RootCAs: globalRootCAs,
|
||||
NextProtos: []string{"http/1.1"}, // Force http1.1
|
||||
}
|
||||
}
|
||||
|
||||
restClient, err := rest.NewClient(serverURL, tlsConfig, rest.DefaultRESTTimeout, newAuthToken)
|
||||
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return &lockRESTClient{serverURL: serverURL, host: peer, restClient: restClient, connected: false, timer: time.NewTimer(defaultRetryUnit * 5)}
|
||||
}
|
||||
|
||||
return &lockRESTClient{serverURL: serverURL, host: peer, restClient: restClient, connected: true}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2017 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -24,14 +24,14 @@ import (
|
||||
)
|
||||
|
||||
// Tests lock rpc client.
|
||||
func TestLockRPCClient(t *testing.T) {
|
||||
func TestLockRESTlient(t *testing.T) {
|
||||
host, err := xnet.ParseHost("localhost:9000")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
lkClient, err := NewLockRPCClient(host)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
lkClient := newlockRESTClient(host)
|
||||
if lkClient.connected == false {
|
||||
t.Fatalf("unexpected error. connection failed")
|
||||
}
|
||||
|
||||
// Attempt all calls.
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2016, 2017, 2018, 2019 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,17 +19,36 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
const lockRESTVersion = "v1"
|
||||
const lockRESTPath = minioReservedBucketPath + "/lock/" + lockRESTVersion
|
||||
|
||||
var lockServicePath = path.Join(minioReservedBucketPath, lockServiceSubPath)
|
||||
|
||||
const (
|
||||
lockRESTMethodLock = "lock"
|
||||
lockRESTMethodRLock = "rlock"
|
||||
lockRESTMethodUnlock = "unlock"
|
||||
lockRESTMethodRUnlock = "runlock"
|
||||
lockRESTMethodForceUnlock = "forceunlock"
|
||||
lockRESTMethodExpired = "expired"
|
||||
)
|
||||
|
||||
// nameLockRequesterInfoPair is a helper type for lock maintenance
|
||||
type nameLockRequesterInfoPair struct {
|
||||
name string
|
||||
lri lockRequesterInfo
|
||||
}
|
||||
|
||||
type lockResponse struct {
|
||||
Success bool
|
||||
}
|
||||
|
||||
// Similar to removeEntry but only removes an entry only if the lock entry exists in map.
|
||||
func (l *localLocker) removeEntryIfExists(nlrip nameLockRequesterInfoPair) {
|
||||
// Check if entry is still in map (could have been removed altogether by 'concurrent' (R)Unlock of last entry)
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2016, 2017, 2018, 2019 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,10 +19,36 @@ package cmd
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Helper function to create a lock server for testing
|
||||
func createLockTestServer(t *testing.T) (string, *lockRESTServer, string) {
|
||||
obj, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
|
||||
locker := &lockRESTServer{
|
||||
ll: localLocker{
|
||||
mutex: sync.Mutex{},
|
||||
serviceEndpoint: "rpc-path",
|
||||
lockMap: make(map[string][]lockRequesterInfo),
|
||||
},
|
||||
}
|
||||
creds := globalServerConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return fsDir, locker, token
|
||||
}
|
||||
|
||||
// Test function to remove lock entries from map only in case they still exist based on name & uid combination
|
||||
func TestLockRpcServerRemoveEntryIfExists(t *testing.T) {
|
||||
testPath, locker, _ := createLockTestServer(t)
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2019 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/dsync"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
const (
|
||||
// Lock rpc server endpoint.
|
||||
lockServiceSubPath = "/lock"
|
||||
|
||||
// Lock maintenance interval.
|
||||
lockMaintenanceInterval = 1 * time.Minute
|
||||
|
||||
// Lock validity check interval.
|
||||
lockValidityCheckInterval = 2 * time.Minute
|
||||
)
|
||||
|
||||
// To abstract a node over network.
|
||||
type lockRESTServer struct {
|
||||
ll localLocker
|
||||
}
|
||||
|
||||
func (l *lockRESTServer) writeErrorResponse(w http.ResponseWriter, err error) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte(err.Error()))
|
||||
}
|
||||
|
||||
// IsValid - To authenticate and verify the time difference.
|
||||
func (l *lockRESTServer) IsValid(w http.ResponseWriter, r *http.Request) bool {
|
||||
if err := storageServerRequestValidate(r); err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// LockHandler - Acquires a lock.
|
||||
func (l *lockRESTServer) LockHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !l.IsValid(w, r) {
|
||||
l.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := newContext(r, w, "Lock")
|
||||
|
||||
var lockArgs dsync.LockArgs
|
||||
if r.ContentLength < 0 {
|
||||
l.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode(&lockArgs)
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
success, err := l.ll.Lock(lockArgs)
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
resp := lockResponse{Success: success}
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(resp))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// UnlockHandler - releases the acquired lock.
|
||||
func (l *lockRESTServer) UnlockHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !l.IsValid(w, r) {
|
||||
l.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := newContext(r, w, "Unlock")
|
||||
|
||||
var lockArgs dsync.LockArgs
|
||||
if r.ContentLength < 0 {
|
||||
l.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode(&lockArgs)
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
success, err := l.ll.Unlock(lockArgs)
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
resp := lockResponse{Success: success}
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(resp))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// LockHandler - Acquires an RLock.
|
||||
func (l *lockRESTServer) RLockHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !l.IsValid(w, r) {
|
||||
l.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := newContext(r, w, "RLock")
|
||||
var lockArgs dsync.LockArgs
|
||||
if r.ContentLength < 0 {
|
||||
l.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode(&lockArgs)
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
success, err := l.ll.RLock(lockArgs)
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
resp := lockResponse{Success: success}
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(resp))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// RUnlockHandler - releases the acquired read lock.
|
||||
func (l *lockRESTServer) RUnlockHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !l.IsValid(w, r) {
|
||||
l.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := newContext(r, w, "RUnlock")
|
||||
var lockArgs dsync.LockArgs
|
||||
if r.ContentLength < 0 {
|
||||
l.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode(&lockArgs)
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
success, err := l.ll.RUnlock(lockArgs)
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
resp := lockResponse{Success: success}
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(resp))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// ForceUnlockHandler - force releases the acquired lock.
|
||||
func (l *lockRESTServer) ForceUnlockHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !l.IsValid(w, r) {
|
||||
l.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := newContext(r, w, "ForceUnlock")
|
||||
|
||||
var lockArgs dsync.LockArgs
|
||||
if r.ContentLength < 0 {
|
||||
l.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode(&lockArgs)
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
success, err := l.ll.ForceUnlock(lockArgs)
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
resp := lockResponse{Success: success}
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(resp))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// ExpiredHandler - query expired lock status.
|
||||
func (l *lockRESTServer) ExpiredHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !l.IsValid(w, r) {
|
||||
l.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := newContext(r, w, "Expired")
|
||||
|
||||
var lockArgs dsync.LockArgs
|
||||
if r.ContentLength < 0 {
|
||||
l.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode(&lockArgs)
|
||||
if err != nil {
|
||||
l.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
success := true
|
||||
l.ll.mutex.Lock()
|
||||
defer l.ll.mutex.Unlock()
|
||||
// Lock found, proceed to verify if belongs to given uid.
|
||||
if lri, ok := l.ll.lockMap[lockArgs.Resource]; ok {
|
||||
// Check whether uid is still active
|
||||
for _, entry := range lri {
|
||||
if entry.UID == lockArgs.UID {
|
||||
success = false // When uid found, lock is still active so return not expired.
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// When we get here lock is no longer active due to either dsync.LockArgs.Resource
|
||||
// being absent from map or uid not found for given dsync.LockArgs.Resource
|
||||
resp := lockResponse{Success: success}
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(resp))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// lockMaintenance loops over locks that have been active for some time and checks back
|
||||
// with the original server whether it is still alive or not
|
||||
//
|
||||
// Following logic inside ignores the errors generated for Dsync.Active operation.
|
||||
// - server at client down
|
||||
// - some network error (and server is up normally)
|
||||
//
|
||||
// We will ignore the error, and we will retry later to get a resolve on this lock
|
||||
func (l *lockRESTServer) lockMaintenance(interval time.Duration) {
|
||||
l.ll.mutex.Lock()
|
||||
// Get list of long lived locks to check for staleness.
|
||||
nlripLongLived := getLongLivedLocks(l.ll.lockMap, interval)
|
||||
l.ll.mutex.Unlock()
|
||||
|
||||
// Validate if long lived locks are indeed clean.
|
||||
for _, nlrip := range nlripLongLived {
|
||||
// Initialize client based on the long live locks.
|
||||
host, err := xnet.ParseHost(nlrip.lri.Node)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
c := newlockRESTClient(host)
|
||||
if !c.connected {
|
||||
continue
|
||||
}
|
||||
|
||||
// Call back to original server verify whether the lock is still active (based on name & uid)
|
||||
expired, _ := c.Expired(dsync.LockArgs{
|
||||
UID: nlrip.lri.UID,
|
||||
Resource: nlrip.name,
|
||||
})
|
||||
|
||||
// Close the connection regardless of the call response.
|
||||
c.Close()
|
||||
|
||||
// For successful response, verify if lock is indeed active or stale.
|
||||
if expired {
|
||||
// The lock is no longer active at server that originated the lock
|
||||
// So remove the lock from the map.
|
||||
l.ll.mutex.Lock()
|
||||
l.ll.removeEntryIfExists(nlrip) // Purge the stale entry if it exists.
|
||||
l.ll.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start lock maintenance from all lock servers.
|
||||
func startLockMaintenance(lkSrv *lockRESTServer) {
|
||||
// Initialize a new ticker with a minute between each ticks.
|
||||
ticker := time.NewTicker(lockMaintenanceInterval)
|
||||
// Stop the timer upon service closure and cleanup the go-routine.
|
||||
defer ticker.Stop()
|
||||
|
||||
// Start with random sleep time, so as to avoid "synchronous checks" between servers
|
||||
time.Sleep(time.Duration(rand.Float64() * float64(lockMaintenanceInterval)))
|
||||
for {
|
||||
// Verifies every minute for locks held more than 2 minutes.
|
||||
select {
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
lkSrv.lockMaintenance(lockValidityCheckInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// registerLockRESTHandlers - register lock rest router.
|
||||
func registerLockRESTHandlers(router *mux.Router) {
|
||||
subrouter := router.PathPrefix(lockRESTPath).Subrouter()
|
||||
subrouter.Methods(http.MethodPost).Path("/" + lockRESTMethodLock).HandlerFunc(httpTraceHdrs(globalLockServer.LockHandler))
|
||||
subrouter.Methods(http.MethodPost).Path("/" + lockRESTMethodRLock).HandlerFunc(httpTraceHdrs(globalLockServer.RLockHandler))
|
||||
subrouter.Methods(http.MethodPost).Path("/" + lockRESTMethodUnlock).HandlerFunc(httpTraceHdrs(globalLockServer.UnlockHandler))
|
||||
subrouter.Methods(http.MethodPost).Path("/" + lockRESTMethodRUnlock).HandlerFunc(httpTraceHdrs(globalLockServer.RUnlockHandler))
|
||||
subrouter.Methods(http.MethodPost).Path("/" + lockRESTMethodForceUnlock).HandlerFunc(httpTraceHdrs(globalLockServer.ForceUnlockHandler))
|
||||
subrouter.Methods(http.MethodPost).Path("/" + lockRESTMethodExpired).HandlerFunc(httpTraceAll(globalLockServer.ExpiredHandler))
|
||||
router.NotFoundHandler = http.HandlerFunc(httpTraceAll(notFoundHandler))
|
||||
|
||||
// Start lock maintenance from all lock servers.
|
||||
go startLockMaintenance(globalLockServer)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2016 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/minio/dsync"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
// LockRPCClient is authenticable lock RPC client compatible to dsync.NetLocker
|
||||
type LockRPCClient struct {
|
||||
*RPCClient
|
||||
}
|
||||
|
||||
// ServerAddr - dsync.NetLocker interface compatible method.
|
||||
func (lockRPC *LockRPCClient) ServerAddr() string {
|
||||
url := lockRPC.ServiceURL()
|
||||
return url.Host
|
||||
}
|
||||
|
||||
// ServiceEndpoint - dsync.NetLocker interface compatible method.
|
||||
func (lockRPC *LockRPCClient) ServiceEndpoint() string {
|
||||
url := lockRPC.ServiceURL()
|
||||
return url.Path
|
||||
}
|
||||
|
||||
// RLock calls read lock RPC.
|
||||
func (lockRPC *LockRPCClient) RLock(args dsync.LockArgs) (reply bool, err error) {
|
||||
err = lockRPC.Call(lockServiceName+".RLock", &LockArgs{LockArgs: args}, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// Lock calls write lock RPC.
|
||||
func (lockRPC *LockRPCClient) Lock(args dsync.LockArgs) (reply bool, err error) {
|
||||
err = lockRPC.Call(lockServiceName+".Lock", &LockArgs{LockArgs: args}, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// RUnlock calls read unlock RPC.
|
||||
func (lockRPC *LockRPCClient) RUnlock(args dsync.LockArgs) (reply bool, err error) {
|
||||
err = lockRPC.Call(lockServiceName+".RUnlock", &LockArgs{LockArgs: args}, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// Unlock calls write unlock RPC.
|
||||
func (lockRPC *LockRPCClient) Unlock(args dsync.LockArgs) (reply bool, err error) {
|
||||
err = lockRPC.Call(lockServiceName+".Unlock", &LockArgs{LockArgs: args}, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// ForceUnlock calls force unlock RPC.
|
||||
func (lockRPC *LockRPCClient) ForceUnlock(args dsync.LockArgs) (reply bool, err error) {
|
||||
err = lockRPC.Call(lockServiceName+".ForceUnlock", &LockArgs{LockArgs: args}, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// Expired calls expired RPC.
|
||||
func (lockRPC *LockRPCClient) Expired(args dsync.LockArgs) (reply bool, err error) {
|
||||
err = lockRPC.Call(lockServiceName+".Expired", &LockArgs{LockArgs: args}, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// NewLockRPCClient - returns new lock RPC client.
|
||||
func NewLockRPCClient(host *xnet.Host) (*LockRPCClient, error) {
|
||||
scheme := "http"
|
||||
if globalIsSSL {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
serviceURL := &xnet.URL{
|
||||
Scheme: scheme,
|
||||
Host: host.String(),
|
||||
Path: lockServicePath,
|
||||
}
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if globalIsSSL {
|
||||
tlsConfig = &tls.Config{
|
||||
ServerName: host.Name,
|
||||
RootCAs: globalRootCAs,
|
||||
}
|
||||
}
|
||||
|
||||
rpcClient, err := NewRPCClient(
|
||||
RPCClientArgs{
|
||||
NewAuthTokenFunc: newAuthToken,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceName: lockServiceName,
|
||||
ServiceURL: serviceURL,
|
||||
TLSConfig: tlsConfig,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &LockRPCClient{rpcClient}, nil
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2016, 2017, 2018, 2019 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/dsync"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
xrpc "github.com/minio/minio/cmd/rpc"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
const (
|
||||
// Lock rpc server endpoint.
|
||||
lockServiceSubPath = "/lock"
|
||||
|
||||
// Lock rpc service name.
|
||||
lockServiceName = "Dsync"
|
||||
|
||||
// Lock maintenance interval.
|
||||
lockMaintenanceInterval = 1 * time.Minute
|
||||
|
||||
// Lock validity check interval.
|
||||
lockValidityCheckInterval = 2 * time.Minute
|
||||
)
|
||||
|
||||
var lockServicePath = path.Join(minioReservedBucketPath, lockServiceSubPath)
|
||||
|
||||
// LockArgs represents arguments for any authenticated lock RPC call.
|
||||
type LockArgs struct {
|
||||
AuthArgs
|
||||
LockArgs dsync.LockArgs
|
||||
}
|
||||
|
||||
// lockRPCReceiver is type for RPC handlers
|
||||
type lockRPCReceiver struct {
|
||||
ll localLocker
|
||||
}
|
||||
|
||||
// Lock - rpc handler for (single) write lock operation.
|
||||
func (l *lockRPCReceiver) Lock(args *LockArgs, reply *bool) (err error) {
|
||||
*reply, err = l.ll.Lock(args.LockArgs)
|
||||
return err
|
||||
}
|
||||
|
||||
// Unlock - rpc handler for (single) write unlock operation.
|
||||
func (l *lockRPCReceiver) Unlock(args *LockArgs, reply *bool) (err error) {
|
||||
*reply, err = l.ll.Unlock(args.LockArgs)
|
||||
return err
|
||||
}
|
||||
|
||||
// RLock - rpc handler for read lock operation.
|
||||
func (l *lockRPCReceiver) RLock(args *LockArgs, reply *bool) (err error) {
|
||||
*reply, err = l.ll.RLock(args.LockArgs)
|
||||
return err
|
||||
}
|
||||
|
||||
// RUnlock - rpc handler for read unlock operation.
|
||||
func (l *lockRPCReceiver) RUnlock(args *LockArgs, reply *bool) (err error) {
|
||||
*reply, err = l.ll.RUnlock(args.LockArgs)
|
||||
return err
|
||||
}
|
||||
|
||||
// ForceUnlock - rpc handler for force unlock operation.
|
||||
func (l *lockRPCReceiver) ForceUnlock(args *LockArgs, reply *bool) (err error) {
|
||||
*reply, err = l.ll.ForceUnlock(args.LockArgs)
|
||||
return err
|
||||
}
|
||||
|
||||
// Expired - rpc handler for expired lock status.
|
||||
func (l *lockRPCReceiver) Expired(args *LockArgs, reply *bool) error {
|
||||
l.ll.mutex.Lock()
|
||||
defer l.ll.mutex.Unlock()
|
||||
// Lock found, proceed to verify if belongs to given uid.
|
||||
if lri, ok := l.ll.lockMap[args.LockArgs.Resource]; ok {
|
||||
// Check whether uid is still active
|
||||
for _, entry := range lri {
|
||||
if entry.UID == args.LockArgs.UID {
|
||||
*reply = false // When uid found, lock is still active so return not expired.
|
||||
return nil // When uid found *reply is set to true.
|
||||
}
|
||||
}
|
||||
}
|
||||
// When we get here lock is no longer active due to either args.LockArgs.Resource
|
||||
// being absent from map or uid not found for given args.LockArgs.Resource
|
||||
*reply = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// lockMaintenance loops over locks that have been active for some time and checks back
|
||||
// with the original server whether it is still alive or not
|
||||
//
|
||||
// Following logic inside ignores the errors generated for Dsync.Active operation.
|
||||
// - server at client down
|
||||
// - some network error (and server is up normally)
|
||||
//
|
||||
// We will ignore the error, and we will retry later to get a resolve on this lock
|
||||
func (l *lockRPCReceiver) lockMaintenance(interval time.Duration) {
|
||||
l.ll.mutex.Lock()
|
||||
// Get list of long lived locks to check for staleness.
|
||||
nlripLongLived := getLongLivedLocks(l.ll.lockMap, interval)
|
||||
l.ll.mutex.Unlock()
|
||||
|
||||
// Validate if long lived locks are indeed clean.
|
||||
for _, nlrip := range nlripLongLived {
|
||||
// Initialize client based on the long live locks.
|
||||
host, err := xnet.ParseHost(nlrip.lri.Node)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
c, err := NewLockRPCClient(host)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Call back to original server verify whether the lock is still active (based on name & uid)
|
||||
expired, _ := c.Expired(dsync.LockArgs{
|
||||
UID: nlrip.lri.UID,
|
||||
Resource: nlrip.name,
|
||||
})
|
||||
|
||||
// Close the connection regardless of the call response.
|
||||
c.Close()
|
||||
|
||||
// For successful response, verify if lock is indeed active or stale.
|
||||
if expired {
|
||||
// The lock is no longer active at server that originated the lock
|
||||
// So remove the lock from the map.
|
||||
l.ll.mutex.Lock()
|
||||
l.ll.removeEntryIfExists(nlrip) // Purge the stale entry if it exists.
|
||||
l.ll.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start lock maintenance from all lock servers.
|
||||
func startLockMaintenance(lkSrv *lockRPCReceiver) {
|
||||
// Initialize a new ticker with a minute between each ticks.
|
||||
ticker := time.NewTicker(lockMaintenanceInterval)
|
||||
// Stop the timer upon service closure and cleanup the go-routine.
|
||||
defer ticker.Stop()
|
||||
|
||||
// Start with random sleep time, so as to avoid "synchronous checks" between servers
|
||||
time.Sleep(time.Duration(rand.Float64() * float64(lockMaintenanceInterval)))
|
||||
for {
|
||||
// Verifies every minute for locks held more than 2minutes.
|
||||
select {
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
lkSrv.lockMaintenance(lockValidityCheckInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewLockRPCServer - returns new lock RPC server.
|
||||
func NewLockRPCServer() (*xrpc.Server, error) {
|
||||
rpcServer := xrpc.NewServer()
|
||||
if err := rpcServer.RegisterName(lockServiceName, globalLockServer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rpcServer, nil
|
||||
}
|
||||
|
||||
// Register distributed NS lock handlers.
|
||||
func registerDistNSLockRouter(router *mux.Router) {
|
||||
rpcServer, err := NewLockRPCServer()
|
||||
logger.FatalIf(err, "Unable to initialize Lock RPC Server")
|
||||
|
||||
// Start lock maintenance from all lock servers.
|
||||
go startLockMaintenance(globalLockServer)
|
||||
|
||||
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
|
||||
subrouter.Path(lockServiceSubPath).HandlerFunc(httpTraceHdrs(rpcServer.ServeHTTP))
|
||||
}
|
||||
@@ -1,538 +0,0 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2016, 2017, 2018, 2019 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/dsync"
|
||||
)
|
||||
|
||||
// Helper function to test equality of locks (without taking timing info into account)
|
||||
func testLockEquality(lriLeft, lriRight []lockRequesterInfo) bool {
|
||||
if len(lriLeft) != len(lriRight) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(lriLeft); i++ {
|
||||
if lriLeft[i].Writer != lriRight[i].Writer ||
|
||||
lriLeft[i].Node != lriRight[i].Node ||
|
||||
lriLeft[i].ServiceEndpoint != lriRight[i].ServiceEndpoint ||
|
||||
lriLeft[i].UID != lriRight[i].UID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Helper function to create a lock server for testing
|
||||
func createLockTestServer(t *testing.T) (string, *lockRPCReceiver, string) {
|
||||
obj, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
|
||||
locker := &lockRPCReceiver{
|
||||
ll: localLocker{
|
||||
mutex: sync.Mutex{},
|
||||
serviceEndpoint: "rpc-path",
|
||||
lockMap: make(map[string][]lockRequesterInfo),
|
||||
},
|
||||
}
|
||||
creds := globalServerConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return fsDir, locker, token
|
||||
}
|
||||
|
||||
// Test Lock functionality
|
||||
func TestLockRpcServerLock(t *testing.T) {
|
||||
testPath, locker, token := createLockTestServer(t)
|
||||
defer os.RemoveAll(testPath)
|
||||
|
||||
la := LockArgs{
|
||||
AuthArgs: AuthArgs{
|
||||
Token: token,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
},
|
||||
LockArgs: dsync.LockArgs{
|
||||
UID: "0123-4567",
|
||||
Resource: "name",
|
||||
ServerAddr: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
}}
|
||||
|
||||
// Claim a lock
|
||||
var result bool
|
||||
err := locker.Lock(&la, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else {
|
||||
if !result {
|
||||
t.Errorf("Expected %#v, got %#v", true, result)
|
||||
} else {
|
||||
gotLri := locker.ll.lockMap["name"]
|
||||
expectedLri := []lockRequesterInfo{
|
||||
{
|
||||
Writer: true,
|
||||
Node: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
UID: "0123-4567",
|
||||
},
|
||||
}
|
||||
if !testLockEquality(expectedLri, gotLri) {
|
||||
t.Errorf("Expected %#v, got %#v", expectedLri, gotLri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to claim same lock again (will fail)
|
||||
la2 := LockArgs{
|
||||
AuthArgs: AuthArgs{
|
||||
Token: token,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
},
|
||||
LockArgs: dsync.LockArgs{
|
||||
UID: "89ab-cdef",
|
||||
Resource: "name",
|
||||
ServerAddr: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
}}
|
||||
|
||||
err = locker.Lock(&la2, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else {
|
||||
if result {
|
||||
t.Errorf("Expected %#v, got %#v", false, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test Unlock functionality
|
||||
func TestLockRpcServerUnlock(t *testing.T) {
|
||||
testPath, locker, token := createLockTestServer(t)
|
||||
defer os.RemoveAll(testPath)
|
||||
|
||||
la := LockArgs{
|
||||
AuthArgs: AuthArgs{
|
||||
Token: token,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
},
|
||||
LockArgs: dsync.LockArgs{
|
||||
UID: "0123-4567",
|
||||
Resource: "name",
|
||||
ServerAddr: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
}}
|
||||
|
||||
// First test return of error when attempting to unlock a lock that does not exist
|
||||
var result bool
|
||||
err := locker.Unlock(&la, &result)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %#v", nil)
|
||||
}
|
||||
|
||||
// Create lock (so that we can release)
|
||||
err = locker.Lock(&la, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else if !result {
|
||||
t.Errorf("Expected %#v, got %#v", true, result)
|
||||
}
|
||||
|
||||
// Finally test successful release of lock
|
||||
err = locker.Unlock(&la, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else {
|
||||
if !result {
|
||||
t.Errorf("Expected %#v, got %#v", true, result)
|
||||
} else {
|
||||
gotLri := locker.ll.lockMap["name"]
|
||||
expectedLri := []lockRequesterInfo(nil)
|
||||
if !testLockEquality(expectedLri, gotLri) {
|
||||
t.Errorf("Expected %#v, got %#v", expectedLri, gotLri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test RLock functionality
|
||||
func TestLockRpcServerRLock(t *testing.T) {
|
||||
testPath, locker, token := createLockTestServer(t)
|
||||
defer os.RemoveAll(testPath)
|
||||
|
||||
la := LockArgs{
|
||||
AuthArgs: AuthArgs{
|
||||
Token: token,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
},
|
||||
LockArgs: dsync.LockArgs{
|
||||
UID: "0123-4567",
|
||||
Resource: "name",
|
||||
ServerAddr: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
}}
|
||||
|
||||
// Claim a lock
|
||||
var result bool
|
||||
err := locker.RLock(&la, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else {
|
||||
if !result {
|
||||
t.Errorf("Expected %#v, got %#v", true, result)
|
||||
} else {
|
||||
gotLri := locker.ll.lockMap["name"]
|
||||
expectedLri := []lockRequesterInfo{
|
||||
{
|
||||
Writer: false,
|
||||
Node: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
UID: "0123-4567",
|
||||
},
|
||||
}
|
||||
if !testLockEquality(expectedLri, gotLri) {
|
||||
t.Errorf("Expected %#v, got %#v", expectedLri, gotLri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to claim same again (will succeed)
|
||||
la2 := LockArgs{
|
||||
AuthArgs: AuthArgs{
|
||||
Token: token,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
},
|
||||
LockArgs: dsync.LockArgs{
|
||||
UID: "89ab-cdef",
|
||||
Resource: "name",
|
||||
ServerAddr: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
}}
|
||||
|
||||
err = locker.RLock(&la2, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else {
|
||||
if !result {
|
||||
t.Errorf("Expected %#v, got %#v", true, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test RUnlock functionality
|
||||
func TestLockRpcServerRUnlock(t *testing.T) {
|
||||
testPath, locker, token := createLockTestServer(t)
|
||||
defer os.RemoveAll(testPath)
|
||||
|
||||
la := LockArgs{
|
||||
AuthArgs: AuthArgs{
|
||||
Token: token,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
},
|
||||
LockArgs: dsync.LockArgs{
|
||||
UID: "0123-4567",
|
||||
Resource: "name",
|
||||
ServerAddr: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
}}
|
||||
|
||||
// First test return of error when attempting to unlock a read-lock that does not exist
|
||||
var result bool
|
||||
err := locker.Unlock(&la, &result)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %#v", nil)
|
||||
}
|
||||
|
||||
// Create first lock ... (so that we can release)
|
||||
err = locker.RLock(&la, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else if !result {
|
||||
t.Errorf("Expected %#v, got %#v", true, result)
|
||||
}
|
||||
|
||||
// Try to claim same again (will succeed)
|
||||
la2 := LockArgs{
|
||||
AuthArgs: AuthArgs{
|
||||
Token: token,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
},
|
||||
LockArgs: dsync.LockArgs{
|
||||
UID: "89ab-cdef",
|
||||
Resource: "name",
|
||||
ServerAddr: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
}}
|
||||
|
||||
// ... and create a second lock on same resource
|
||||
err = locker.RLock(&la2, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else if !result {
|
||||
t.Errorf("Expected %#v, got %#v", true, result)
|
||||
}
|
||||
|
||||
// Test successful release of first read lock
|
||||
err = locker.RUnlock(&la, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else {
|
||||
if !result {
|
||||
t.Errorf("Expected %#v, got %#v", true, result)
|
||||
} else {
|
||||
gotLri := locker.ll.lockMap["name"]
|
||||
expectedLri := []lockRequesterInfo{
|
||||
{
|
||||
Writer: false,
|
||||
Node: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
UID: "89ab-cdef",
|
||||
},
|
||||
}
|
||||
if !testLockEquality(expectedLri, gotLri) {
|
||||
t.Errorf("Expected %#v, got %#v", expectedLri, gotLri)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Finally test successful release of second (and last) read lock
|
||||
err = locker.RUnlock(&la2, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else {
|
||||
if !result {
|
||||
t.Errorf("Expected %#v, got %#v", true, result)
|
||||
} else {
|
||||
gotLri := locker.ll.lockMap["name"]
|
||||
expectedLri := []lockRequesterInfo(nil)
|
||||
if !testLockEquality(expectedLri, gotLri) {
|
||||
t.Errorf("Expected %#v, got %#v", expectedLri, gotLri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test ForceUnlock functionality
|
||||
func TestLockRpcServerForceUnlock(t *testing.T) {
|
||||
testPath, locker, token := createLockTestServer(t)
|
||||
defer os.RemoveAll(testPath)
|
||||
|
||||
laForce := LockArgs{
|
||||
AuthArgs: AuthArgs{
|
||||
Token: token,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
},
|
||||
LockArgs: dsync.LockArgs{
|
||||
UID: "1234-5678",
|
||||
Resource: "name",
|
||||
ServerAddr: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
}}
|
||||
|
||||
// First test that UID should be empty
|
||||
var result bool
|
||||
err := locker.ForceUnlock(&laForce, &result)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %#v", nil)
|
||||
}
|
||||
|
||||
// Then test force unlock of a lock that does not exist (not returning an error)
|
||||
laForce.LockArgs.UID = ""
|
||||
err = locker.ForceUnlock(&laForce, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %#v", err)
|
||||
}
|
||||
|
||||
la := LockArgs{
|
||||
AuthArgs: AuthArgs{
|
||||
Token: token,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
},
|
||||
LockArgs: dsync.LockArgs{
|
||||
UID: "0123-4567",
|
||||
Resource: "name",
|
||||
ServerAddr: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
}}
|
||||
|
||||
// Create lock ... (so that we can force unlock)
|
||||
err = locker.Lock(&la, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else if !result {
|
||||
t.Errorf("Expected %#v, got %#v", true, result)
|
||||
}
|
||||
|
||||
// Forcefully unlock the lock (not returning an error)
|
||||
err = locker.ForceUnlock(&laForce, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %#v", err)
|
||||
}
|
||||
|
||||
// Try to get lock again (should be granted)
|
||||
err = locker.Lock(&la, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else if !result {
|
||||
t.Errorf("Expected %#v, got %#v", true, result)
|
||||
}
|
||||
|
||||
// Finally forcefully unlock the lock once again
|
||||
err = locker.ForceUnlock(&laForce, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %#v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test Expired functionality
|
||||
func TestLockRpcServerExpired(t *testing.T) {
|
||||
testPath, locker, token := createLockTestServer(t)
|
||||
defer os.RemoveAll(testPath)
|
||||
|
||||
la := LockArgs{
|
||||
AuthArgs: AuthArgs{
|
||||
Token: token,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
},
|
||||
LockArgs: dsync.LockArgs{
|
||||
UID: "0123-4567",
|
||||
Resource: "name",
|
||||
ServerAddr: "node",
|
||||
ServiceEndpoint: "rpc-path",
|
||||
}}
|
||||
|
||||
// Unknown lock at server will return expired = true
|
||||
var expired bool
|
||||
err := locker.Expired(&la, &expired)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %#v", err)
|
||||
} else {
|
||||
if !expired {
|
||||
t.Errorf("Expected %#v, got %#v", true, expired)
|
||||
}
|
||||
}
|
||||
|
||||
// Create lock (so that we can test that it is not expired)
|
||||
var result bool
|
||||
err = locker.Lock(&la, &result)
|
||||
if err != nil {
|
||||
t.Errorf("Expected %#v, got %#v", nil, err)
|
||||
} else if !result {
|
||||
t.Errorf("Expected %#v, got %#v", true, result)
|
||||
}
|
||||
|
||||
err = locker.Expired(&la, &expired)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %#v", err)
|
||||
} else {
|
||||
if expired {
|
||||
t.Errorf("Expected %#v, got %#v", false, expired)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test initialization of lock server.
|
||||
func TestLockServerInit(t *testing.T) {
|
||||
if runtime.GOOS == globalWindowsOSName {
|
||||
return
|
||||
}
|
||||
|
||||
obj, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(fsDir)
|
||||
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
|
||||
currentIsDistXL := globalIsDistXL
|
||||
currentLockServer := globalLockServer
|
||||
defer func() {
|
||||
globalIsDistXL = currentIsDistXL
|
||||
globalLockServer = currentLockServer
|
||||
}()
|
||||
|
||||
case1Endpoints := mustGetNewEndpointList(
|
||||
"http://localhost:9000/mnt/disk1",
|
||||
"http://1.1.1.2:9000/mnt/disk2",
|
||||
"http://1.1.2.1:9000/mnt/disk3",
|
||||
"http://1.1.2.2:9000/mnt/disk4",
|
||||
)
|
||||
for i := range case1Endpoints {
|
||||
if case1Endpoints[i].Host == "localhost:9000" {
|
||||
case1Endpoints[i].IsLocal = true
|
||||
}
|
||||
}
|
||||
|
||||
case2Endpoints := mustGetNewEndpointList(
|
||||
"http://localhost:9000/mnt/disk1",
|
||||
"http://localhost:9000/mnt/disk2",
|
||||
"http://1.1.2.1:9000/mnt/disk3",
|
||||
"http://1.1.2.2:9000/mnt/disk4",
|
||||
)
|
||||
for i := range case2Endpoints {
|
||||
if case2Endpoints[i].Host == "localhost:9000" {
|
||||
case2Endpoints[i].IsLocal = true
|
||||
}
|
||||
}
|
||||
|
||||
globalMinioHost = ""
|
||||
testCases := []struct {
|
||||
isDistXL bool
|
||||
endpoints EndpointList
|
||||
}{
|
||||
// Test - 1 one lock server initialized.
|
||||
{true, case1Endpoints},
|
||||
// Test - similar endpoint hosts should
|
||||
// converge to single lock server
|
||||
// initialized.
|
||||
{true, case2Endpoints},
|
||||
}
|
||||
|
||||
// Validates lock server initialization.
|
||||
for i, testCase := range testCases {
|
||||
globalIsDistXL = testCase.isDistXL
|
||||
globalLockServer = nil
|
||||
_, _ = newDsyncNodes(testCase.endpoints)
|
||||
if globalLockServer == nil && testCase.isDistXL {
|
||||
t.Errorf("Test %d: Expected initialized lock RPC receiver, but got uninitialized", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
var globalNSMutex *nsLockMap
|
||||
|
||||
// Global lock server one per server.
|
||||
var globalLockServer *lockRPCReceiver
|
||||
var globalLockServer *lockRESTServer
|
||||
|
||||
// Instance of dsync for distributed clients.
|
||||
var globalDsync *dsync.Dsync
|
||||
@@ -66,7 +66,7 @@ func newDsyncNodes(endpoints EndpointList) (clnts []dsync.NetLocker, myNode int)
|
||||
if endpoint.IsLocal {
|
||||
myNode = len(clnts)
|
||||
|
||||
receiver := &lockRPCReceiver{
|
||||
receiver := &lockRESTServer{
|
||||
ll: localLocker{
|
||||
serverAddr: endpoint.Host,
|
||||
serviceEndpoint: lockServicePath,
|
||||
@@ -79,8 +79,7 @@ func newDsyncNodes(endpoints EndpointList) (clnts []dsync.NetLocker, myNode int)
|
||||
} else {
|
||||
host, err := xnet.ParseHost(endpoint.Host)
|
||||
logger.FatalIf(err, "Unable to parse Lock RPC Host")
|
||||
locker, err = NewLockRPCClient(host)
|
||||
logger.FatalIf(err, "Unable to initialize Lock RPC Client")
|
||||
locker = newlockRESTClient(host)
|
||||
}
|
||||
|
||||
clnts = append(clnts, locker)
|
||||
|
||||
@@ -111,11 +111,11 @@ func (d *naughtyDisk) DeleteVol(volume string) (err error) {
|
||||
return d.disk.DeleteVol(volume)
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) ListDir(volume, path string, count int) (entries []string, err error) {
|
||||
func (d *naughtyDisk) ListDir(volume, path string, count int, leafFile string) (entries []string, err error) {
|
||||
if err := d.calcError(); err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
return d.disk.ListDir(volume, path, count)
|
||||
return d.disk.ListDir(volume, path, count, leafFile)
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) ReadFile(volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error) {
|
||||
|
||||
+1
-37
@@ -17,7 +17,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -27,9 +26,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
@@ -102,40 +99,7 @@ func getHostIP(host string) (ipList set.StringSet, err error) {
|
||||
var ips []net.IP
|
||||
|
||||
if ips, err = net.LookupIP(host); err != nil {
|
||||
// return err if not Docker or Kubernetes
|
||||
// We use IsDocker() method to check for Docker Swarm environment
|
||||
// as there is no reliable way to clearly identify Swarm from
|
||||
// Docker environment.
|
||||
if !IsDocker() && !IsKubernetes() {
|
||||
return ipList, err
|
||||
}
|
||||
|
||||
// channel to indicate completion of host resolution
|
||||
doneCh := make(chan struct{})
|
||||
// Indicate retry routine to exit cleanly, upon this function return.
|
||||
defer close(doneCh)
|
||||
// Mark the starting time
|
||||
startTime := time.Now()
|
||||
// wait for hosts to resolve in exponentialbackoff manner
|
||||
for range newRetryTimerSimple(doneCh) {
|
||||
// Retry infinitely on Kubernetes and Docker swarm.
|
||||
// This is needed as the remote hosts are sometime
|
||||
// not available immediately.
|
||||
if ips, err = net.LookupIP(host); err == nil {
|
||||
break
|
||||
}
|
||||
// time elapsed
|
||||
timeElapsed := time.Since(startTime)
|
||||
// log error only if more than 1s elapsed
|
||||
if timeElapsed > time.Second {
|
||||
// log the message to console about the host not being
|
||||
// resolveable.
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("host", host)
|
||||
reqInfo.AppendTags("elapsedTime", humanize.RelTime(startTime, startTime.Add(timeElapsed), "elapsed", ""))
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
return ipList, err
|
||||
}
|
||||
|
||||
ipList = set.NewStringSet()
|
||||
|
||||
@@ -501,7 +501,6 @@ func (sys *NotificationSys) RemoteTargetExist(bucketName string, targetID event.
|
||||
|
||||
// ListenBucketNotificationArgs - listen bucket notification RPC arguments.
|
||||
type ListenBucketNotificationArgs struct {
|
||||
AuthArgs `json:"-"`
|
||||
BucketName string `json:"-"`
|
||||
EventNames []event.Name `json:"eventNames"`
|
||||
Pattern string `json:"pattern"`
|
||||
|
||||
@@ -116,7 +116,7 @@ func cleanupDir(ctx context.Context, storage StorageAPI, volume, dirPath string)
|
||||
}
|
||||
|
||||
// If it's a directory, list and call delFunc() for each entry.
|
||||
entries, err := storage.ListDir(volume, entryPath, -1)
|
||||
entries, err := storage.ListDir(volume, entryPath, -1, "")
|
||||
// If entryPath prefix never existed, safe to ignore.
|
||||
if err == errFileNotFound {
|
||||
return nil
|
||||
@@ -218,14 +218,6 @@ func listObjects(ctx context.Context, obj ObjectLayer, bucket, prefix, marker, d
|
||||
eof = true
|
||||
break
|
||||
}
|
||||
// For any walk error return right away.
|
||||
if walkResult.err != nil {
|
||||
// File not found is a valid case.
|
||||
if walkResult.err == errFileNotFound {
|
||||
continue
|
||||
}
|
||||
return loi, toObjectErr(walkResult.err, bucket, prefix)
|
||||
}
|
||||
|
||||
var objInfo ObjectInfo
|
||||
var err error
|
||||
@@ -235,6 +227,14 @@ func listObjects(ctx context.Context, obj ObjectLayer, bucket, prefix, marker, d
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if err == errFileNotFound {
|
||||
err = nil
|
||||
objInfo = ObjectInfo{
|
||||
Bucket: bucket,
|
||||
Name: walkResult.entry,
|
||||
IsDir: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
objInfo, err = getObjInfo(ctx, bucket, walkResult.entry)
|
||||
|
||||
@@ -27,6 +27,22 @@ import (
|
||||
// underlying storage layer.
|
||||
func toObjectErr(err error, params ...string) error {
|
||||
switch err {
|
||||
case errDiskNotFound:
|
||||
switch len(params) {
|
||||
case 1:
|
||||
err = BucketNotFound{Bucket: params[0]}
|
||||
case 2:
|
||||
err = ObjectNotFound{
|
||||
Bucket: params[0],
|
||||
Object: params[1],
|
||||
}
|
||||
case 3:
|
||||
err = InvalidUploadID{
|
||||
Bucket: params[0],
|
||||
Object: params[1],
|
||||
UploadID: params[2],
|
||||
}
|
||||
}
|
||||
case errVolumeNotFound:
|
||||
if len(params) >= 1 {
|
||||
err = BucketNotFound{Bucket: params[0]}
|
||||
@@ -63,11 +79,18 @@ func toObjectErr(err error, params ...string) error {
|
||||
}
|
||||
}
|
||||
case errFileNotFound:
|
||||
if len(params) >= 2 {
|
||||
switch len(params) {
|
||||
case 2:
|
||||
err = ObjectNotFound{
|
||||
Bucket: params[0],
|
||||
Object: params[1],
|
||||
}
|
||||
case 3:
|
||||
err = InvalidUploadID{
|
||||
Bucket: params[0],
|
||||
Object: params[1],
|
||||
UploadID: params[2],
|
||||
}
|
||||
}
|
||||
case errFileNameTooLong:
|
||||
if len(params) >= 2 {
|
||||
@@ -321,6 +344,8 @@ func (e MalformedUploadID) Error() string {
|
||||
|
||||
// InvalidUploadID invalid upload id.
|
||||
type InvalidUploadID struct {
|
||||
Bucket string
|
||||
Object string
|
||||
UploadID string
|
||||
}
|
||||
|
||||
|
||||
@@ -228,8 +228,7 @@ func testPutObjectPartDiskNotFound(obj ObjectLayer, instanceType string, disks [
|
||||
}
|
||||
|
||||
// This causes quorum failure verify.
|
||||
disks = disks[len(disks)-3:]
|
||||
for _, disk := range disks {
|
||||
for _, disk := range disks[len(disks)-3:] {
|
||||
os.RemoveAll(disk)
|
||||
}
|
||||
|
||||
@@ -240,9 +239,26 @@ func testPutObjectPartDiskNotFound(obj ObjectLayer, instanceType string, disks [
|
||||
t.Fatalf("Test %s: expected to fail but passed instead", instanceType)
|
||||
}
|
||||
// as majority of xl.json are not available, we expect uploadID to be not available.
|
||||
expectedErr := InvalidUploadID{UploadID: testCase.uploadID}
|
||||
if err.Error() != expectedErr.Error() {
|
||||
t.Fatalf("Test %s: expected error %s, got %s instead.", instanceType, expectedErr, err)
|
||||
expectedErr1 := InsufficientReadQuorum{}
|
||||
if err.Error() != expectedErr1.Error() {
|
||||
t.Fatalf("Test %s: expected error %s, got %s instead.", instanceType, expectedErr1, err)
|
||||
}
|
||||
|
||||
// This causes invalid upload id.
|
||||
for _, disk := range disks {
|
||||
os.RemoveAll(disk)
|
||||
}
|
||||
|
||||
// Object part upload should fail with bucket not found.
|
||||
_, err = obj.PutObjectPart(context.Background(), testCase.bucketName, testCase.objName, testCase.uploadID, testCase.PartID, mustGetPutObjReader(t, bytes.NewBufferString(testCase.inputReaderData), testCase.intputDataSize, testCase.inputMd5, sha256sum), ObjectOptions{})
|
||||
if err == nil {
|
||||
t.Fatalf("Test %s: expected to fail but passed instead", instanceType)
|
||||
}
|
||||
|
||||
// As all disks at not available, bucket not found.
|
||||
expectedErr2 := BucketNotFound{Bucket: testCase.bucketName}
|
||||
if err.Error() != expectedErr2.Error() {
|
||||
t.Fatalf("Test %s: expected error %s, got %s instead.", instanceType, expectedErr2, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -79,7 +78,7 @@ func parseDirents(dirPath string, buf []byte) (entries []string, err error) {
|
||||
// On Linux XFS does not implement d_type for on disk
|
||||
// format << v5. Fall back to OsStat().
|
||||
var fi os.FileInfo
|
||||
fi, err = os.Stat(path.Join(dirPath, name))
|
||||
fi, err = os.Stat(pathJoin(dirPath, name))
|
||||
if err != nil {
|
||||
// If file does not exist, we continue and skip it.
|
||||
// Could happen if it was deleted in the middle while
|
||||
|
||||
@@ -20,7 +20,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
@@ -82,7 +81,7 @@ func readDirN(dirPath string, count int) (entries []string, err error) {
|
||||
case data.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0:
|
||||
// If its symbolic link, follow the link using os.Stat()
|
||||
var fi os.FileInfo
|
||||
fi, err = os.Stat(path.Join(dirPath, name))
|
||||
fi, err = os.Stat(pathJoin(dirPath, name))
|
||||
if err != nil {
|
||||
// If file does not exist, we continue and skip it.
|
||||
// Could happen if it was deleted in the middle while
|
||||
|
||||
+15
-3
@@ -651,7 +651,7 @@ func (s *posix) DeleteVol(volume string) (err error) {
|
||||
|
||||
// ListDir - return all the entries at the given directory path.
|
||||
// If an entry is a directory it will be returned with a trailing "/".
|
||||
func (s *posix) ListDir(volume, dirPath string, count int) (entries []string, err error) {
|
||||
func (s *posix) ListDir(volume, dirPath string, count int, leafFile string) (entries []string, err error) {
|
||||
defer func() {
|
||||
if err == errFaultyDisk {
|
||||
atomic.AddInt32(&s.ioErrCount, 1)
|
||||
@@ -684,9 +684,21 @@ func (s *posix) ListDir(volume, dirPath string, count int) (entries []string, er
|
||||
|
||||
dirPath = pathJoin(volumeDir, dirPath)
|
||||
if count > 0 {
|
||||
return readDirN(dirPath, count)
|
||||
entries, err = readDirN(dirPath, count)
|
||||
} else {
|
||||
entries, err = readDir(dirPath)
|
||||
}
|
||||
return readDir(dirPath)
|
||||
|
||||
// If leaf file is specified, filter out the entries.
|
||||
if leafFile != "" {
|
||||
for i, entry := range entries {
|
||||
if _, serr := os.Stat(pathJoin(dirPath, entry, leafFile)); serr == nil {
|
||||
entries[i] = strings.TrimSuffix(entry, slashSeparator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries, err
|
||||
}
|
||||
|
||||
// ReadAll reads from r until an error or EOF and returns the data it read.
|
||||
|
||||
+1
-1
@@ -839,7 +839,7 @@ func TestPosixPosixListDir(t *testing.T) {
|
||||
} else {
|
||||
t.Errorf("Expected the StorageAPI to be of type *posix")
|
||||
}
|
||||
dirList, err = posixStorage.ListDir(testCase.srcVol, testCase.srcPath, -1)
|
||||
dirList, err = posixStorage.ListDir(testCase.srcVol, testCase.srcPath, -1, "")
|
||||
if err != testCase.expectedErr {
|
||||
t.Fatalf("TestPosix case %d: Expected: \"%s\", got: \"%s\"", i+1, testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
+1
-3
@@ -42,7 +42,7 @@ func registerDistXLRouters(router *mux.Router, endpoints EndpointList) {
|
||||
registerPeerRESTHandlers(router)
|
||||
|
||||
// Register distributed namespace lock.
|
||||
registerDistNSLockRouter(router)
|
||||
registerLockRESTHandlers(router)
|
||||
|
||||
}
|
||||
|
||||
@@ -54,8 +54,6 @@ var globalHandlers = []HandlerFunc{
|
||||
addSecurityHeaders,
|
||||
// Forward path style requests to actual host in a bucket federated setup.
|
||||
setBucketForwardingHandler,
|
||||
// Ratelimit the incoming requests using a token bucket algorithm
|
||||
setRateLimitHandler,
|
||||
// Validate all the incoming requests.
|
||||
setRequestValidityHandler,
|
||||
// Network statistics
|
||||
|
||||
-281
@@ -1,281 +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 (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
xrpc "github.com/minio/minio/cmd/rpc"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
// DefaultSkewTime - skew time is 15 minutes between minio peers.
|
||||
const DefaultSkewTime = 15 * time.Minute
|
||||
|
||||
// defaultRPCTimeout - default RPC timeout is one minute.
|
||||
const defaultRPCTimeout = 5 * time.Minute
|
||||
|
||||
// defaultRPCRetryTime - default RPC time to wait before retry after a network error
|
||||
const defaultRPCRetryTime = 1 * time.Minute
|
||||
|
||||
var errRPCRetry = fmt.Errorf("rpc: retry error")
|
||||
|
||||
func isNetError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if uerr, isURLError := err.(*url.Error); isURLError {
|
||||
if uerr.Timeout() {
|
||||
return true
|
||||
}
|
||||
|
||||
err = uerr.Err
|
||||
}
|
||||
|
||||
_, isNetOpError := err.(*net.OpError)
|
||||
return isNetOpError
|
||||
}
|
||||
|
||||
// RPCVersion - RPC semantic version based on semver 2.0.0 https://semver.org/.
|
||||
type RPCVersion struct {
|
||||
Major uint64
|
||||
Minor uint64
|
||||
Patch uint64
|
||||
}
|
||||
|
||||
// Compare - compares given version with this version.
|
||||
func (v RPCVersion) Compare(o RPCVersion) int {
|
||||
compare := func(v1, v2 uint64) int {
|
||||
if v1 == v2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if v1 > v2 {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
if r := compare(v.Major, o.Major); r != 0 {
|
||||
return r
|
||||
}
|
||||
|
||||
if r := compare(v.Minor, o.Minor); r != 0 {
|
||||
return r
|
||||
}
|
||||
|
||||
return compare(v.Patch, o.Patch)
|
||||
}
|
||||
|
||||
func (v RPCVersion) String() string {
|
||||
return fmt.Sprintf("%v.%v.%v", v.Major, v.Minor, v.Patch)
|
||||
}
|
||||
|
||||
// AuthArgs - base argument for any RPC call for authentication.
|
||||
type AuthArgs struct {
|
||||
Token string
|
||||
RPCVersion RPCVersion
|
||||
RequestTime time.Time
|
||||
}
|
||||
|
||||
// Authenticate - checks if given arguments are valid to allow RPC call.
|
||||
// This is xrpc.Authenticator and is called in RPC server.
|
||||
func (args AuthArgs) Authenticate() error {
|
||||
// Check whether request time is within acceptable skew time.
|
||||
utcNow := time.Now().UTC()
|
||||
if args.RequestTime.Sub(utcNow) > DefaultSkewTime || utcNow.Sub(args.RequestTime) > DefaultSkewTime {
|
||||
return fmt.Errorf("client time %v is too apart with server time %v", args.RequestTime, utcNow)
|
||||
}
|
||||
|
||||
if globalRPCAPIVersion.Compare(args.RPCVersion) != 0 {
|
||||
return fmt.Errorf("version mismatch. expected: %v, received: %v", globalRPCAPIVersion, args.RPCVersion)
|
||||
}
|
||||
|
||||
if !isAuthTokenValid(args.Token) {
|
||||
return errAuthentication
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAuthArgs - sets given authentication arguments to this args. This is called in RPC client.
|
||||
func (args *AuthArgs) SetAuthArgs(authArgs AuthArgs) {
|
||||
*args = authArgs
|
||||
}
|
||||
|
||||
// VoidReply - void (empty) RPC reply.
|
||||
type VoidReply struct{}
|
||||
|
||||
// RPCClientArgs - RPC client arguments.
|
||||
type RPCClientArgs struct {
|
||||
NewAuthTokenFunc func() string
|
||||
RPCVersion RPCVersion
|
||||
ServiceName string
|
||||
ServiceURL *xnet.URL
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// validate - checks whether given args are valid or not.
|
||||
func (args RPCClientArgs) validate() error {
|
||||
if args.NewAuthTokenFunc == nil {
|
||||
return fmt.Errorf("NewAuthTokenFunc must not be empty")
|
||||
}
|
||||
|
||||
if args.ServiceName == "" {
|
||||
return fmt.Errorf("ServiceName must not be empty")
|
||||
}
|
||||
|
||||
if args.ServiceURL.Scheme != "http" && args.ServiceURL.Scheme != "https" {
|
||||
return fmt.Errorf("unknown RPC URL %v", args.ServiceURL)
|
||||
}
|
||||
|
||||
if args.ServiceURL.User != nil || args.ServiceURL.ForceQuery || args.ServiceURL.RawQuery != "" || args.ServiceURL.Fragment != "" {
|
||||
return fmt.Errorf("unknown RPC URL %v", args.ServiceURL)
|
||||
}
|
||||
|
||||
if args.ServiceURL.Scheme == "https" && args.TLSConfig == nil {
|
||||
return fmt.Errorf("tls configuration must not be empty for https url %v", args.ServiceURL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RPCClient - base RPC client.
|
||||
type RPCClient struct {
|
||||
sync.RWMutex
|
||||
args RPCClientArgs
|
||||
authToken string
|
||||
rpcClient *xrpc.Client
|
||||
retryTicker *time.Ticker
|
||||
}
|
||||
|
||||
func (client *RPCClient) setRetryTicker(ticker *time.Ticker) {
|
||||
if ticker == nil {
|
||||
client.RLock()
|
||||
isNil := client.retryTicker == nil
|
||||
client.RUnlock()
|
||||
if isNil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
client.Lock()
|
||||
defer client.Unlock()
|
||||
|
||||
if client.retryTicker != nil {
|
||||
client.retryTicker.Stop()
|
||||
}
|
||||
|
||||
client.retryTicker = ticker
|
||||
}
|
||||
|
||||
// Call - calls servicemethod on remote server.
|
||||
func (client *RPCClient) Call(serviceMethod string, args interface {
|
||||
SetAuthArgs(args AuthArgs)
|
||||
}, reply interface{}) (err error) {
|
||||
lockedCall := func() error {
|
||||
client.RLock()
|
||||
retryTicker := client.retryTicker
|
||||
client.RUnlock()
|
||||
if retryTicker != nil {
|
||||
select {
|
||||
case <-retryTicker.C:
|
||||
default:
|
||||
return errRPCRetry
|
||||
}
|
||||
}
|
||||
|
||||
client.RLock()
|
||||
authToken := client.authToken
|
||||
client.RUnlock()
|
||||
|
||||
// Make RPC call.
|
||||
args.SetAuthArgs(AuthArgs{authToken, client.args.RPCVersion, time.Now().UTC()})
|
||||
return client.rpcClient.Call(serviceMethod, args, reply)
|
||||
}
|
||||
|
||||
call := func() error {
|
||||
err = lockedCall()
|
||||
|
||||
if err == errRPCRetry {
|
||||
return err
|
||||
}
|
||||
|
||||
if isNetError(err) {
|
||||
client.setRetryTicker(time.NewTicker(defaultRPCRetryTime))
|
||||
} else {
|
||||
client.setRetryTicker(nil)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// If authentication error is received, retry the same call only once
|
||||
// with new authentication token.
|
||||
if err = call(); err == nil {
|
||||
return nil
|
||||
}
|
||||
if err.Error() != errAuthentication.Error() {
|
||||
return err
|
||||
}
|
||||
|
||||
client.Lock()
|
||||
client.authToken = client.args.NewAuthTokenFunc()
|
||||
client.Unlock()
|
||||
return call()
|
||||
}
|
||||
|
||||
// Close - closes underneath RPC client.
|
||||
func (client *RPCClient) Close() error {
|
||||
client.Lock()
|
||||
defer client.Unlock()
|
||||
|
||||
client.authToken = ""
|
||||
return client.rpcClient.Close()
|
||||
}
|
||||
|
||||
// ServiceURL - returns service URL used for RPC call.
|
||||
func (client *RPCClient) ServiceURL() *xnet.URL {
|
||||
// Take copy of ServiceURL
|
||||
u := *(client.args.ServiceURL)
|
||||
return &u
|
||||
}
|
||||
|
||||
// NewRPCClient - returns new RPC client.
|
||||
func NewRPCClient(args RPCClientArgs) (*RPCClient, error) {
|
||||
if err := args.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rpcClient, err := xrpc.NewClient(args.ServiceURL, args.TLSConfig, defaultRPCTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &RPCClient{
|
||||
args: args,
|
||||
authToken: args.NewAuthTokenFunc(),
|
||||
rpcClient: rpcClient,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,142 +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 rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
// DefaultRPCTimeout - default RPC timeout is one minute.
|
||||
const DefaultRPCTimeout = 1 * time.Minute
|
||||
|
||||
// Client - http based RPC client.
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
httpIdleConnsCloser func()
|
||||
serviceURL *xnet.URL
|
||||
}
|
||||
|
||||
// Call - calls service method on RPC server.
|
||||
func (client *Client) Call(serviceMethod string, args, reply interface{}) error {
|
||||
replyKind := reflect.TypeOf(reply).Kind()
|
||||
if replyKind != reflect.Ptr {
|
||||
return fmt.Errorf("rpc reply must be a pointer type, but found %v", replyKind)
|
||||
}
|
||||
|
||||
argBuf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
|
||||
if err := gobEncodeBuf(args, argBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
callRequest := CallRequest{
|
||||
Method: serviceMethod,
|
||||
ArgBytes: argBuf.Bytes(),
|
||||
}
|
||||
|
||||
reqBuf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
if err := gob.NewEncoder(reqBuf).Encode(callRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := client.httpClient.Post(client.serviceURL.String(), "", reqBuf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer xhttp.DrainBody(response.Body)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%v rpc call failed with error code %v", serviceMethod, response.StatusCode)
|
||||
}
|
||||
|
||||
var callResponse CallResponse
|
||||
if err := gob.NewDecoder(response.Body).Decode(&callResponse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if callResponse.Error != "" {
|
||||
return errors.New(callResponse.Error)
|
||||
}
|
||||
|
||||
return gobDecode(callResponse.ReplyBytes, reply)
|
||||
}
|
||||
|
||||
// Close closes all idle connections of the underlying http client
|
||||
func (client *Client) Close() error {
|
||||
if client.httpIdleConnsCloser != nil {
|
||||
client.httpIdleConnsCloser()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newCustomDialContext(timeout time.Duration) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: timeout,
|
||||
KeepAlive: timeout,
|
||||
DualStack: true,
|
||||
}
|
||||
|
||||
conn, err := dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return xhttp.NewTimeoutConn(conn, timeout, timeout), nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient - returns new RPC client.
|
||||
func NewClient(serviceURL *xnet.URL, tlsConfig *tls.Config, timeout time.Duration) (*Client, error) {
|
||||
// Transport is exactly same as Go default in https://golang.org/pkg/net/http/#RoundTripper
|
||||
// except custom DialContext and TLSClientConfig.
|
||||
tr := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: newCustomDialContext(timeout),
|
||||
MaxIdleConnsPerHost: 4096,
|
||||
MaxIdleConns: 4096,
|
||||
IdleConnTimeout: 120 * time.Second,
|
||||
TLSHandshakeTimeout: 30 * time.Second,
|
||||
ExpectContinueTimeout: 10 * time.Second,
|
||||
TLSClientConfig: tlsConfig,
|
||||
DisableCompression: true,
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
// If TLS is enabled configure http2
|
||||
if err := http2.ConfigureTransport(tr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &Client{
|
||||
httpClient: &http.Client{Transport: tr},
|
||||
httpIdleConnsCloser: tr.CloseIdleConnections,
|
||||
serviceURL: serviceURL,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,75 +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 rpc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
func TestClientCall(t *testing.T) {
|
||||
rpcServer := NewServer()
|
||||
if err := rpcServer.RegisterName("Arith", &Arith{}); err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rpcServer.ServeHTTP(w, r)
|
||||
}))
|
||||
defer httpServer.Close()
|
||||
|
||||
url, err := xnet.ParseURL(httpServer.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
rpcClient, err := NewClient(url, nil, DefaultRPCTimeout)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient initialization error %v", err)
|
||||
}
|
||||
|
||||
var reply int
|
||||
var boolReply bool
|
||||
var intArg int
|
||||
|
||||
testCases := []struct {
|
||||
serviceMethod string
|
||||
args interface{}
|
||||
reply interface{}
|
||||
expectErr bool
|
||||
}{
|
||||
{"Arith.Multiply", Args{7, 8}, &reply, false},
|
||||
{"Arith.Multiply", &Args{7, 8}, &reply, false},
|
||||
// rpc reply must be a pointer type but found int error.
|
||||
{"Arith.Multiply", &Args{7, 8}, reply, true},
|
||||
// gob: type mismatch in decoder: want struct type rpc.Args; got non-struct error.
|
||||
{"Arith.Multiply", intArg, &reply, true},
|
||||
// gob: decoding into local type *bool, received remote type int error.
|
||||
{"Arith.Multiply", &Args{7, 8}, &boolReply, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := rpcClient.Call(testCase.serviceMethod, testCase.args, testCase.reply)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +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 rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A Pool is a type-safe wrapper around a sync.Pool.
|
||||
type Pool struct {
|
||||
p *sync.Pool
|
||||
}
|
||||
|
||||
// NewPool constructs a new Pool.
|
||||
func NewPool() Pool {
|
||||
return Pool{p: &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &bytes.Buffer{}
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// Get retrieves a bytes.Buffer from the pool, creating one if necessary.
|
||||
func (p Pool) Get() *bytes.Buffer {
|
||||
buf := p.p.Get().(*bytes.Buffer)
|
||||
return buf
|
||||
}
|
||||
|
||||
// Put - returns a bytes.Buffer to the pool.
|
||||
func (p Pool) Put(buf *bytes.Buffer) {
|
||||
buf.Reset()
|
||||
p.p.Put(buf)
|
||||
}
|
||||
@@ -1,256 +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 rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Authenticator - validator of first argument of any RPC call.
|
||||
type Authenticator interface {
|
||||
// Method to validate first argument of any RPC call.
|
||||
Authenticate() error
|
||||
}
|
||||
|
||||
// reflect.Type of error interface.
|
||||
var errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
|
||||
// reflect.Type of Authenticator interface.
|
||||
var authenticatorType = reflect.TypeOf((*Authenticator)(nil)).Elem()
|
||||
|
||||
func gobEncodeBuf(e interface{}, buf *bytes.Buffer) error {
|
||||
return gob.NewEncoder(buf).Encode(e)
|
||||
}
|
||||
|
||||
func gobDecode(data []byte, e interface{}) error {
|
||||
return gob.NewDecoder(bytes.NewReader(data)).Decode(e)
|
||||
}
|
||||
|
||||
// Returns whether given type is exported or builin type or not.
|
||||
func isExportedOrBuiltinType(t reflect.Type) bool {
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
rune, _ := utf8.DecodeRuneInString(t.Name())
|
||||
return unicode.IsUpper(rune) || t.PkgPath() == ""
|
||||
}
|
||||
|
||||
// Makes method name map from given type.
|
||||
func getMethodMap(receiverType reflect.Type) map[string]reflect.Method {
|
||||
methodMap := make(map[string]reflect.Method)
|
||||
for i := 0; i < receiverType.NumMethod(); i++ {
|
||||
// Method.PkgPath is empty for this package.
|
||||
method := receiverType.Method(i)
|
||||
|
||||
// Methods must have three arguments (receiver, args, reply)
|
||||
if method.Type.NumIn() != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
// First argument must be exported.
|
||||
if !isExportedOrBuiltinType(method.Type.In(1)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// First argument must be Authenticator.
|
||||
if !method.Type.In(1).Implements(authenticatorType) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Second argument must be exported or builtin type.
|
||||
if !isExportedOrBuiltinType(method.Type.In(2)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Second argument must be a pointer.
|
||||
if method.Type.In(2).Kind() != reflect.Ptr {
|
||||
continue
|
||||
}
|
||||
|
||||
// Method must return one value.
|
||||
if method.Type.NumOut() != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// The return type of the method must be error.
|
||||
if method.Type.Out(0) != errorType {
|
||||
continue
|
||||
}
|
||||
|
||||
methodMap[method.Name] = method
|
||||
}
|
||||
|
||||
return methodMap
|
||||
}
|
||||
|
||||
// Server - HTTP based RPC server.
|
||||
type Server struct {
|
||||
serviceName string
|
||||
receiverValue reflect.Value
|
||||
methodMap map[string]reflect.Method
|
||||
}
|
||||
|
||||
// RegisterName - registers receiver with given name to handle RPC requests.
|
||||
func (server *Server) RegisterName(name string, receiver interface{}) error {
|
||||
server.serviceName = name
|
||||
|
||||
server.receiverValue = reflect.ValueOf(receiver)
|
||||
if !reflect.Indirect(server.receiverValue).IsValid() {
|
||||
return fmt.Errorf("nil receiver")
|
||||
}
|
||||
|
||||
receiverName := reflect.Indirect(server.receiverValue).Type().Name()
|
||||
receiverType := reflect.TypeOf(receiver)
|
||||
server.methodMap = getMethodMap(receiverType)
|
||||
if len(server.methodMap) == 0 {
|
||||
str := "rpc.Register: type " + receiverName + " has no exported methods of suitable type"
|
||||
|
||||
// To help the user, see if a pointer receiver would work.
|
||||
if len(getMethodMap(reflect.PtrTo(receiverType))) != 0 {
|
||||
str += " (hint: pass a pointer to value of that type)"
|
||||
}
|
||||
|
||||
return errors.New(str)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// call - call service method in receiver.
|
||||
func (server *Server) call(serviceMethod string, argBytes []byte, replyBytes *bytes.Buffer) (err error) {
|
||||
tokens := strings.SplitN(serviceMethod, ".", 2)
|
||||
if len(tokens) != 2 {
|
||||
return fmt.Errorf("invalid service/method request ill-formed %v", serviceMethod)
|
||||
}
|
||||
|
||||
serviceName := tokens[0]
|
||||
if serviceName != server.serviceName {
|
||||
return fmt.Errorf("can't find service %v", serviceName)
|
||||
}
|
||||
|
||||
methodName := tokens[1]
|
||||
method, found := server.methodMap[methodName]
|
||||
if !found {
|
||||
return fmt.Errorf("can't find method %v", methodName)
|
||||
}
|
||||
|
||||
var argv reflect.Value
|
||||
|
||||
// Decode the argument value.
|
||||
argIsValue := false // if true, need to indirect before calling.
|
||||
if method.Type.In(1).Kind() == reflect.Ptr {
|
||||
argv = reflect.New(method.Type.In(1).Elem())
|
||||
} else {
|
||||
argv = reflect.New(method.Type.In(1))
|
||||
argIsValue = true
|
||||
}
|
||||
|
||||
if err = gobDecode(argBytes, argv.Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if argIsValue {
|
||||
argv = argv.Elem()
|
||||
}
|
||||
|
||||
// call Authenticate() method.
|
||||
authMethod, ok := method.Type.In(1).MethodByName("Authenticate")
|
||||
if !ok {
|
||||
panic("Authenticate() method not found. This should not happen.")
|
||||
}
|
||||
returnValues := authMethod.Func.Call([]reflect.Value{argv})
|
||||
errInter := returnValues[0].Interface()
|
||||
if errInter != nil {
|
||||
err = errInter.(error)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
replyv := reflect.New(method.Type.In(2).Elem())
|
||||
|
||||
switch method.Type.In(2).Elem().Kind() {
|
||||
case reflect.Map:
|
||||
replyv.Elem().Set(reflect.MakeMap(method.Type.In(2).Elem()))
|
||||
case reflect.Slice:
|
||||
replyv.Elem().Set(reflect.MakeSlice(method.Type.In(2).Elem(), 0, 0))
|
||||
}
|
||||
|
||||
returnValues = method.Func.Call([]reflect.Value{server.receiverValue, argv, replyv})
|
||||
errInter = returnValues[0].Interface()
|
||||
if errInter != nil {
|
||||
err = errInter.(error)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gobEncodeBuf(replyv.Interface(), replyBytes)
|
||||
}
|
||||
|
||||
// CallRequest - RPC call request parameters.
|
||||
type CallRequest struct {
|
||||
Method string
|
||||
ArgBytes []byte
|
||||
}
|
||||
|
||||
// CallResponse - RPC call response parameters.
|
||||
type CallResponse struct {
|
||||
Error string
|
||||
ReplyBytes []byte
|
||||
}
|
||||
|
||||
// ServeHTTP - handles RPC on HTTP request.
|
||||
func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var callRequest CallRequest
|
||||
if err := gob.NewDecoder(req.Body).Decode(&callRequest); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
callResponse := CallResponse{}
|
||||
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
|
||||
if err := server.call(callRequest.Method, callRequest.ArgBytes, buf); err != nil {
|
||||
callResponse.Error = err.Error()
|
||||
}
|
||||
callResponse.ReplyBytes = buf.Bytes()
|
||||
|
||||
gob.NewEncoder(w).Encode(callResponse)
|
||||
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// NewServer - returns new RPC server.
|
||||
func NewServer() *Server {
|
||||
return &Server{}
|
||||
}
|
||||
@@ -1,349 +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 rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func gobEncode(e interface{}) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
err := gob.NewEncoder(&buf).Encode(e)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
type Args struct {
|
||||
A, B int
|
||||
}
|
||||
|
||||
func (a *Args) Authenticate() (err error) {
|
||||
if a.A == 0 && a.B == 0 {
|
||||
err = errors.New("authenticated failed")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
type Arith struct{}
|
||||
|
||||
func (t *Arith) Multiply(args *Args, reply *int) error {
|
||||
*reply = args.A * args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Arith) Divide(args *Args, quo *Quotient) error {
|
||||
if args.B == 0 {
|
||||
return errors.New("divide by zero")
|
||||
}
|
||||
quo.Quo = args.A / args.B
|
||||
quo.Rem = args.A % args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
type mytype int
|
||||
|
||||
type Auth struct{}
|
||||
|
||||
func (a Auth) Authenticate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// exported method.
|
||||
func (t mytype) Foo(a *Auth, b *int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// incompatible method because of first argument is not Authenticator.
|
||||
func (t *mytype) Bar(a, b *int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// incompatible method because of error is not returned.
|
||||
func (t mytype) IncompatFoo(a, b *int) {
|
||||
}
|
||||
|
||||
// incompatible method because of second argument is not a pointer.
|
||||
func (t *mytype) IncompatBar(a *int, b int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestIsExportedOrBuiltinType(t *testing.T) {
|
||||
var i int
|
||||
case1Type := reflect.TypeOf(i)
|
||||
|
||||
var iptr *int
|
||||
case2Type := reflect.TypeOf(iptr)
|
||||
|
||||
var a Arith
|
||||
case3Type := reflect.TypeOf(a)
|
||||
|
||||
var aptr *Arith
|
||||
case4Type := reflect.TypeOf(aptr)
|
||||
|
||||
var m mytype
|
||||
case5Type := reflect.TypeOf(m)
|
||||
|
||||
var mptr *mytype
|
||||
case6Type := reflect.TypeOf(mptr)
|
||||
|
||||
testCases := []struct {
|
||||
t reflect.Type
|
||||
expectedResult bool
|
||||
}{
|
||||
{case1Type, true},
|
||||
{case2Type, true},
|
||||
{case3Type, true},
|
||||
{case4Type, true},
|
||||
// Type.Name() starts with lower case and Type.PkgPath() is not empty.
|
||||
{case5Type, false},
|
||||
// Type.Name() starts with lower case and Type.PkgPath() is not empty.
|
||||
{case6Type, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := isExportedOrBuiltinType(testCase.t)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMethodMap(t *testing.T) {
|
||||
var a Arith
|
||||
case1Type := reflect.TypeOf(a)
|
||||
|
||||
var aptr *Arith
|
||||
case2Type := reflect.TypeOf(aptr)
|
||||
|
||||
var m mytype
|
||||
case3Type := reflect.TypeOf(m)
|
||||
|
||||
var mptr *mytype
|
||||
case4Type := reflect.TypeOf(mptr)
|
||||
|
||||
testCases := []struct {
|
||||
t reflect.Type
|
||||
expectedResult int
|
||||
}{
|
||||
// No methods exported.
|
||||
{case1Type, 0},
|
||||
// Multiply and Divide methods are exported.
|
||||
{case2Type, 2},
|
||||
// Foo method is exported.
|
||||
{case3Type, 1},
|
||||
// Foo method is exported.
|
||||
{case4Type, 1},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
m := getMethodMap(testCase.t)
|
||||
result := len(m)
|
||||
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerRegisterName(t *testing.T) {
|
||||
case1Receiver := &Arith{}
|
||||
var case2Receiver mytype
|
||||
var case3Receiver *Arith
|
||||
i := 0
|
||||
var case4Receiver = &i
|
||||
var case5Receiver Arith
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
receiver interface{}
|
||||
expectErr bool
|
||||
}{
|
||||
{"Arith", case1Receiver, false},
|
||||
{"arith", case1Receiver, false},
|
||||
{"Arith", case2Receiver, false},
|
||||
// nil receiver error.
|
||||
{"Arith", nil, true},
|
||||
// nil receiver error.
|
||||
{"Arith", case3Receiver, true},
|
||||
// rpc.Register: type Arith has no exported methods of suitable type error.
|
||||
{"Arith", case4Receiver, true},
|
||||
// rpc.Register: type Arith has no exported methods of suitable type (hint: pass a pointer to value of that type) error.
|
||||
{"Arith", case5Receiver, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := NewServer().RegisterName(testCase.name, testCase.receiver)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerCall(t *testing.T) {
|
||||
server1 := NewServer()
|
||||
if err := server1.RegisterName("Arith", &Arith{}); err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
server2 := NewServer()
|
||||
if err := server2.RegisterName("arith", &Arith{}); err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
case1ArgBytes, err := gobEncode(&Args{7, 8})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
reply := 7 * 8
|
||||
case1ExpectedResult, err := gobEncode(&reply)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
case2ArgBytes, err := gobEncode(&Args{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
server *Server
|
||||
serviceMethod string
|
||||
argBytes []byte
|
||||
expectedResult []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{server1, "Arith.Multiply", case1ArgBytes, case1ExpectedResult, false},
|
||||
{server2, "arith.Multiply", case1ArgBytes, case1ExpectedResult, false},
|
||||
// invalid service/method request ill-formed error.
|
||||
{server1, "Multiply", nil, nil, true},
|
||||
// can't find service error.
|
||||
{server1, "arith.Multiply", nil, nil, true},
|
||||
// can't find method error.
|
||||
{server1, "Arith.Add", nil, nil, true},
|
||||
// gob decode error.
|
||||
{server1, "Arith.Multiply", []byte{10}, nil, true},
|
||||
// authentication error.
|
||||
{server1, "Arith.Multiply", case2ArgBytes, nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
|
||||
err := testCase.server.call(testCase.serviceMethod, testCase.argBytes, buf)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: error: expected: %v, got: %v\n", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(buf.Bytes(), testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, buf.Bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerServeHTTP(t *testing.T) {
|
||||
server1 := NewServer()
|
||||
if err := server1.RegisterName("Arith", &Arith{}); err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
argBytes, err := gobEncode(&Args{7, 8})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
requestBodyData, err := gobEncode(CallRequest{Method: "Arith.Multiply", ArgBytes: argBytes})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
case1Request, err := http.NewRequest("POST", "http://localhost:12345/", bytes.NewReader(requestBodyData))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
reply := 7 * 8
|
||||
replyBytes, err := gobEncode(&reply)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
case1Result, err := gobEncode(CallResponse{ReplyBytes: replyBytes})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
case2Request, err := http.NewRequest("GET", "http://localhost:12345/", bytes.NewReader([]byte{}))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
case3Request, err := http.NewRequest("POST", "http://localhost:12345/", bytes.NewReader([]byte{10, 20}))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
requestBodyData, err = gobEncode(CallRequest{Method: "Arith.Add", ArgBytes: argBytes})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
case4Request, err := http.NewRequest("POST", "http://localhost:12345/", bytes.NewReader(requestBodyData))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
case4Result, err := gobEncode(CallResponse{Error: "can't find method Add"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
server *Server
|
||||
httpRequest *http.Request
|
||||
expectedCode int
|
||||
expectedResult []byte
|
||||
}{
|
||||
{server1, case1Request, http.StatusOK, case1Result},
|
||||
{server1, case2Request, http.StatusMethodNotAllowed, nil},
|
||||
{server1, case3Request, http.StatusBadRequest, nil},
|
||||
{server1, case4Request, http.StatusOK, case4Result},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
writer := httptest.NewRecorder()
|
||||
testCase.server.ServeHTTP(writer, testCase.httpRequest)
|
||||
if writer.Code != testCase.expectedCode {
|
||||
t.Fatalf("case %v: code: expected: %v, got: %v\n", i+1, testCase.expectedCode, writer.Code)
|
||||
}
|
||||
|
||||
if testCase.expectedCode == http.StatusOK {
|
||||
result := writer.Body.Bytes()
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: result: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-402
@@ -1,402 +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 (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
xrpc "github.com/minio/minio/cmd/rpc"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
func TestAuthArgsAuthenticate(t *testing.T) {
|
||||
tmpGlobalServerConfig := globalServerConfig
|
||||
defer func() {
|
||||
globalServerConfig = tmpGlobalServerConfig
|
||||
}()
|
||||
globalServerConfig = newServerConfig()
|
||||
|
||||
case1Args := AuthArgs{
|
||||
Token: newAuthToken(),
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
|
||||
case2Args := AuthArgs{
|
||||
Token: newAuthToken(),
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow().Add(15 * time.Minute),
|
||||
}
|
||||
|
||||
case3Args := AuthArgs{
|
||||
Token: newAuthToken(),
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow().Add(-16 * time.Minute),
|
||||
}
|
||||
|
||||
case4Args := AuthArgs{
|
||||
Token: newAuthToken(),
|
||||
RPCVersion: RPCVersion{99, 99, 99},
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
|
||||
case5Args := AuthArgs{
|
||||
Token: "invalid-token",
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
args AuthArgs
|
||||
expectErr bool
|
||||
}{
|
||||
{case1Args, false},
|
||||
{case2Args, false},
|
||||
{case3Args, true},
|
||||
{case4Args, true},
|
||||
{case5Args, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := testCase.args.Authenticate()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthArgsSetAuthArgs(t *testing.T) {
|
||||
tmpGlobalServerConfig := globalServerConfig
|
||||
defer func() {
|
||||
globalServerConfig = tmpGlobalServerConfig
|
||||
}()
|
||||
globalServerConfig = newServerConfig()
|
||||
|
||||
case1Args := AuthArgs{
|
||||
Token: newAuthToken(),
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
|
||||
case2Args := AuthArgs{
|
||||
Token: newAuthToken(),
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow().Add(15 * time.Minute),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
args *AuthArgs
|
||||
authArgs AuthArgs
|
||||
expectedResult *AuthArgs
|
||||
}{
|
||||
{&AuthArgs{}, case1Args, &case1Args},
|
||||
{&case2Args, case1Args, &case1Args},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
testCase.args.SetAuthArgs(testCase.authArgs)
|
||||
result := testCase.args
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRPCClientArgsValidate(t *testing.T) {
|
||||
case1URL, err := xnet.ParseURL("http://localhost:12345/rpc")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
case1Args := RPCClientArgs{
|
||||
NewAuthTokenFunc: newAuthToken,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceName: "Arith",
|
||||
ServiceURL: case1URL,
|
||||
TLSConfig: nil,
|
||||
}
|
||||
|
||||
case2URL, err := xnet.ParseURL("https://localhost:12345/rpc")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
case2Args := RPCClientArgs{
|
||||
NewAuthTokenFunc: newAuthToken,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceName: "Arith",
|
||||
ServiceURL: case1URL,
|
||||
TLSConfig: &tls.Config{},
|
||||
}
|
||||
|
||||
case3Args := RPCClientArgs{
|
||||
NewAuthTokenFunc: nil,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceName: "Arith",
|
||||
ServiceURL: case1URL,
|
||||
TLSConfig: &tls.Config{},
|
||||
}
|
||||
|
||||
case4Args := RPCClientArgs{
|
||||
NewAuthTokenFunc: newAuthToken,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceURL: case1URL,
|
||||
TLSConfig: &tls.Config{},
|
||||
}
|
||||
|
||||
case5URL, err := xnet.ParseURL("ftp://localhost:12345/rpc")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
case5Args := RPCClientArgs{
|
||||
NewAuthTokenFunc: newAuthToken,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceName: "Arith",
|
||||
ServiceURL: case5URL,
|
||||
TLSConfig: &tls.Config{},
|
||||
}
|
||||
|
||||
case6URL, err := xnet.ParseURL("http://localhost:12345/rpc?location")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
case6Args := RPCClientArgs{
|
||||
NewAuthTokenFunc: newAuthToken,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceName: "Arith",
|
||||
ServiceURL: case6URL,
|
||||
TLSConfig: &tls.Config{},
|
||||
}
|
||||
|
||||
case7Args := RPCClientArgs{
|
||||
NewAuthTokenFunc: newAuthToken,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceName: "Arith",
|
||||
ServiceURL: case2URL,
|
||||
TLSConfig: nil,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
args RPCClientArgs
|
||||
expectErr bool
|
||||
}{
|
||||
{case1Args, false},
|
||||
{case2Args, false},
|
||||
// NewAuthTokenFunc must not be empty error.
|
||||
{case3Args, true},
|
||||
// ServiceName must not be empty.
|
||||
{case4Args, true},
|
||||
// unknown RPC URL error.
|
||||
{case5Args, true},
|
||||
// unknown RPC URL error.
|
||||
{case6Args, true},
|
||||
// tls configuration must not be empty for https url error.
|
||||
{case7Args, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := testCase.args.validate()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Args struct {
|
||||
AuthArgs
|
||||
A, B int
|
||||
}
|
||||
|
||||
type Quotient struct {
|
||||
Quo, Rem int
|
||||
}
|
||||
|
||||
type Arith struct{}
|
||||
|
||||
func (t *Arith) Multiply(args *Args, reply *int) error {
|
||||
*reply = args.A * args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Arith) Divide(args *Args, quo *Quotient) error {
|
||||
if args.B == 0 {
|
||||
return errors.New("divide by zero")
|
||||
}
|
||||
quo.Quo = args.A / args.B
|
||||
quo.Rem = args.A % args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRPCClientCall(t *testing.T) {
|
||||
tmpGlobalServerConfig := globalServerConfig
|
||||
defer func() {
|
||||
globalServerConfig = tmpGlobalServerConfig
|
||||
}()
|
||||
globalServerConfig = newServerConfig()
|
||||
|
||||
rpcServer := xrpc.NewServer()
|
||||
if err := rpcServer.RegisterName("Arith", &Arith{}); err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rpcServer.ServeHTTP(w, r)
|
||||
}))
|
||||
defer httpServer.Close()
|
||||
|
||||
url, err := xnet.ParseURL(httpServer.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
rpcClient, err := NewRPCClient(RPCClientArgs{
|
||||
NewAuthTokenFunc: newAuthToken,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceName: "Arith",
|
||||
ServiceURL: url,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
var case1Result int
|
||||
case1ExpectedResult := 19 * 8
|
||||
|
||||
testCases := []struct {
|
||||
serviceMethod string
|
||||
args *Args
|
||||
result interface{}
|
||||
changeConfig bool
|
||||
expectedResult interface{}
|
||||
expectErr bool
|
||||
}{
|
||||
{"Arith.Multiply", &Args{A: 19, B: 8}, &case1Result, false, &case1ExpectedResult, false},
|
||||
{"Arith.Divide", &Args{A: 19, B: 8}, &Quotient{}, false, &Quotient{2, 3}, false},
|
||||
{"Arith.Multiply", &Args{A: 19, B: 8}, &case1Result, true, &case1ExpectedResult, false},
|
||||
{"Arith.Divide", &Args{A: 19, B: 8}, &Quotient{}, true, &Quotient{2, 3}, false},
|
||||
{"Arith.Divide", &Args{A: 19, B: 0}, &Quotient{}, false, nil, true},
|
||||
{"Arith.Divide", &Args{A: 19, B: 8}, &case1Result, false, nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
if testCase.changeConfig {
|
||||
globalServerConfig = newServerConfig()
|
||||
}
|
||||
|
||||
err := rpcClient.Call(testCase.serviceMethod, testCase.args, testCase.result)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
|
||||
if !testCase.expectErr {
|
||||
if !reflect.DeepEqual(testCase.result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, testCase.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRPCClientClose(t *testing.T) {
|
||||
tmpGlobalServerConfig := globalServerConfig
|
||||
defer func() {
|
||||
globalServerConfig = tmpGlobalServerConfig
|
||||
}()
|
||||
globalServerConfig = newServerConfig()
|
||||
|
||||
url, err := xnet.ParseURL("http://localhost:12345")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
rpcClient, err := NewRPCClient(RPCClientArgs{
|
||||
NewAuthTokenFunc: newAuthToken,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceName: "Arith",
|
||||
ServiceURL: url,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
rpcClient *RPCClient
|
||||
expectErr bool
|
||||
}{
|
||||
{rpcClient, false},
|
||||
// Double close.
|
||||
{rpcClient, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := testCase.rpcClient.Close()
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRPCClientServiceURL(t *testing.T) {
|
||||
tmpGlobalServerConfig := globalServerConfig
|
||||
defer func() {
|
||||
globalServerConfig = tmpGlobalServerConfig
|
||||
}()
|
||||
globalServerConfig = newServerConfig()
|
||||
|
||||
url, err := xnet.ParseURL("http://localhost:12345")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
rpcClient, err := NewRPCClient(RPCClientArgs{
|
||||
NewAuthTokenFunc: newAuthToken,
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
ServiceName: "Arith",
|
||||
ServiceURL: url,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
case1Result, err := xnet.ParseURL("http://localhost:12345")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
testCases := []struct {
|
||||
rpcClient *RPCClient
|
||||
expectedResult *xnet.URL
|
||||
}{
|
||||
{rpcClient, case1Result},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result := testCase.rpcClient.ServiceURL()
|
||||
|
||||
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -200,6 +200,8 @@ func serverMain(ctx *cli.Context) {
|
||||
cli.ShowCommandHelpAndExit(ctx, "server", 1)
|
||||
}
|
||||
|
||||
signal.Notify(globalOSSignalCh, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// Disable logging until server initialization is complete, any
|
||||
// error during initialization will be shown as a fatal message
|
||||
logger.Disable = true
|
||||
@@ -305,8 +307,6 @@ func serverMain(ctx *cli.Context) {
|
||||
globalHTTPServerErrorCh <- globalHTTPServer.Start()
|
||||
}()
|
||||
|
||||
signal.Notify(globalOSSignalCh, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
newObject, err := newObjectLayer(globalEndpoints)
|
||||
if err != nil {
|
||||
// Stop watching for any certificate changes.
|
||||
|
||||
@@ -39,7 +39,7 @@ type StorageAPI interface {
|
||||
DeleteVol(volume string) (err error)
|
||||
|
||||
// File operations.
|
||||
ListDir(volume, dirPath string, count int) ([]string, error)
|
||||
ListDir(volume, dirPath string, count int, leafFile string) ([]string, error)
|
||||
ReadFile(volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error)
|
||||
AppendFile(volume string, path string, buf []byte) (err error)
|
||||
CreateFile(volume, path string, size int64, reader io.Reader) error
|
||||
|
||||
@@ -315,11 +315,12 @@ func (client *storageRESTClient) ReadFile(volume, path string, offset int64, buf
|
||||
}
|
||||
|
||||
// ListDir - lists a directory.
|
||||
func (client *storageRESTClient) ListDir(volume, dirPath string, count int) (entries []string, err error) {
|
||||
func (client *storageRESTClient) ListDir(volume, dirPath string, count int, leafFile string) (entries []string, err error) {
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
values.Set(storageRESTDirPath, dirPath)
|
||||
values.Set(storageRESTCount, strconv.Itoa(count))
|
||||
values.Set(storageRESTLeafFile, leafFile)
|
||||
respBody, err := client.call(storageRESTMethodListDir, values, nil, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -405,6 +406,7 @@ func newStorageRESTClient(endpoint Endpoint) (*storageRESTClient, error) {
|
||||
tlsConfig = &tls.Config{
|
||||
ServerName: host.Name,
|
||||
RootCAs: globalRootCAs,
|
||||
NextProtos: []string{"http/1.1"}, // Force http1.1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package cmd
|
||||
|
||||
const storageRESTVersion = "v4"
|
||||
const storageRESTVersion = "v5"
|
||||
const storageRESTPath = minioReservedBucketPath + "/storage/" + storageRESTVersion + "/"
|
||||
|
||||
const (
|
||||
@@ -50,6 +50,7 @@ const (
|
||||
storageRESTOffset = "offset"
|
||||
storageRESTLength = "length"
|
||||
storageRESTCount = "count"
|
||||
storageRESTLeafFile = "leaf-file"
|
||||
storageRESTBitrotAlgo = "bitrot-algo"
|
||||
storageRESTBitrotHash = "bitrot-hash"
|
||||
storageRESTInstanceID = "instance-id"
|
||||
|
||||
@@ -47,6 +47,9 @@ func (s *storageRESTServer) writeErrorResponse(w http.ResponseWriter, err error)
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// DefaultSkewTime - skew time is 15 minutes between minio peers.
|
||||
const DefaultSkewTime = 15 * time.Minute
|
||||
|
||||
// Authenticates storage client's requests and validates for skewed time.
|
||||
func storageServerRequestValidate(r *http.Request) error {
|
||||
_, owner, err := webRequestAuthenticate(r)
|
||||
@@ -358,12 +361,13 @@ func (s *storageRESTServer) ListDirHandler(w http.ResponseWriter, r *http.Reques
|
||||
vars := mux.Vars(r)
|
||||
volume := vars[storageRESTVolume]
|
||||
dirPath := vars[storageRESTDirPath]
|
||||
leafFile := vars[storageRESTLeafFile]
|
||||
count, err := strconv.Atoi(vars[storageRESTCount])
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
entries, err := s.storage.ListDir(volume, dirPath, count)
|
||||
entries, err := s.storage.ListDir(volume, dirPath, count, leafFile)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
@@ -440,7 +444,7 @@ func registerStorageRESTHandlers(router *mux.Router, endpoints EndpointList) {
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodReadFileStream).HandlerFunc(httpTraceHdrs(server.ReadFileStreamHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath, storageRESTOffset, storageRESTLength)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodListDir).HandlerFunc(httpTraceHdrs(server.ListDirHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTDirPath, storageRESTCount)...)
|
||||
Queries(restQueries(storageRESTVolume, storageRESTDirPath, storageRESTCount, storageRESTLeafFile)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodDeleteFile).HandlerFunc(httpTraceHdrs(server.DeleteFileHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodRenameFile).HandlerFunc(httpTraceHdrs(server.RenameFileHandler)).
|
||||
|
||||
@@ -260,7 +260,7 @@ func testStorageAPIListDir(t *testing.T, storage StorageAPI) {
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := storage.ListDir(testCase.volumeName, testCase.prefix, -1)
|
||||
result, err := storage.ListDir(testCase.volumeName, testCase.prefix, -1, "")
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
@@ -508,6 +508,10 @@ func newStorageRESTHTTPServerClient(t *testing.T) (*httptest.Server, *storageRES
|
||||
t.Fatalf("NewEndpoint failed %v", endpoint)
|
||||
}
|
||||
|
||||
if err := endpoint.UpdateIsLocal(); err != nil {
|
||||
t.Fatalf("UpdateIsLocal failed %v", err)
|
||||
}
|
||||
|
||||
registerStorageRESTHandlers(router, EndpointList{endpoint})
|
||||
restClient, err := newStorageRESTClient(endpoint)
|
||||
if err != nil {
|
||||
|
||||
+12
-5
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/iam/validator"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -49,13 +50,19 @@ func registerSTSRouter(router *mux.Router) {
|
||||
stsRouter := router.NewRoute().PathPrefix("/").Subrouter()
|
||||
|
||||
// Assume roles with no JWT, handles AssumeRole.
|
||||
stsRouter.Methods("POST").HeadersRegexp("Content-Type", "application/x-www-form-urlencoded*").
|
||||
HeadersRegexp("Authorization", "AWS4-HMAC-SHA256*").
|
||||
HandlerFunc(httpTraceAll(sts.AssumeRole))
|
||||
stsRouter.Methods("POST").MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {
|
||||
ctypeOk := wildcard.MatchSimple("application/x-www-form-urlencoded*", r.Header.Get("Content-Type"))
|
||||
authOk := wildcard.MatchSimple("AWS4-HMAC-SHA256*", r.Header.Get("Authorization"))
|
||||
noQueries := len(r.URL.Query()) == 0
|
||||
return ctypeOk && authOk && noQueries
|
||||
}).HandlerFunc(httpTraceAll(sts.AssumeRole))
|
||||
|
||||
// Assume roles with JWT handler, handles both ClientGrants and WebIdentity.
|
||||
stsRouter.Methods("POST").HeadersRegexp("Content-Type", "application/x-www-form-urlencoded*").
|
||||
HandlerFunc(httpTraceAll(sts.AssumeRoleWithJWT))
|
||||
stsRouter.Methods("POST").MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {
|
||||
ctypeOk := wildcard.MatchSimple("application/x-www-form-urlencoded*", r.Header.Get("Content-Type"))
|
||||
noQueries := len(r.URL.Query()) == 0
|
||||
return ctypeOk && noQueries
|
||||
}).HandlerFunc(httpTraceAll(sts.AssumeRoleWithJWT))
|
||||
|
||||
// AssumeRoleWithClientGrants
|
||||
stsRouter.Methods("POST").HandlerFunc(httpTraceAll(sts.AssumeRoleWithClientGrants)).
|
||||
|
||||
+9
-9
@@ -25,7 +25,6 @@ import (
|
||||
// TreeWalkResult - Tree walk result carries results of tree walking.
|
||||
type TreeWalkResult struct {
|
||||
entry string
|
||||
err error
|
||||
end bool
|
||||
}
|
||||
|
||||
@@ -99,10 +98,8 @@ type IsLeafFunc func(string, string) bool
|
||||
// IsLeafDirFunc - A function isLeafDir of type isLeafDirFunc is used to detect if an entry represents an empty directory.
|
||||
type IsLeafDirFunc func(string, string) bool
|
||||
|
||||
// Note: input entries are expected to be sorted.
|
||||
func filterListEntries(bucket, prefixDir string, entries []string, prefixEntry string, isLeaf IsLeafFunc) ([]string, bool) {
|
||||
// Listing needs to be sorted.
|
||||
sort.Strings(entries)
|
||||
|
||||
// Filter entries that have the prefix prefixEntry.
|
||||
entries = filterMatchingPrefix(entries, prefixEntry)
|
||||
|
||||
@@ -168,9 +165,10 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
|
||||
for i, entry := range entries {
|
||||
var leaf, leafDir bool
|
||||
|
||||
pentry := pathJoin(prefixDir, entry)
|
||||
// Decision to do isLeaf check was pushed from listDir() to here.
|
||||
if delayIsLeaf {
|
||||
leaf = isLeaf(bucket, pathJoin(prefixDir, entry))
|
||||
leaf = isLeaf(bucket, pentry)
|
||||
if leaf {
|
||||
entry = strings.TrimSuffix(entry, slashSeparator)
|
||||
}
|
||||
@@ -179,7 +177,7 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
|
||||
}
|
||||
|
||||
if strings.HasSuffix(entry, slashSeparator) {
|
||||
leafDir = isLeafDir(bucket, pathJoin(prefixDir, entry))
|
||||
leafDir = isLeafDir(bucket, pentry)
|
||||
}
|
||||
|
||||
isDir := !leafDir && !leaf
|
||||
@@ -209,17 +207,19 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
|
||||
// markIsEnd is passed to this entry's treeWalk() so that treeWalker.end can be marked
|
||||
// true at the end of the treeWalk stream.
|
||||
markIsEnd := i == len(entries)-1 && isEnd
|
||||
if tErr := doTreeWalk(ctx, bucket, pathJoin(prefixDir, entry), prefixMatch, markerArg, recursive, listDir, isLeaf, isLeafDir, resultCh, endWalkCh, markIsEnd); tErr != nil {
|
||||
return tErr
|
||||
if err := doTreeWalk(ctx, bucket, pentry, prefixMatch, markerArg, recursive,
|
||||
listDir, isLeaf, isLeafDir, resultCh, endWalkCh, markIsEnd); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// EOF is set if we are at last entry and the caller indicated we at the end.
|
||||
isEOF := ((i == len(entries)-1) && isEnd)
|
||||
select {
|
||||
case <-endWalkCh:
|
||||
return errWalkAbort
|
||||
case resultCh <- TreeWalkResult{entry: pathJoin(prefixDir, entry), end: isEOF}:
|
||||
case resultCh <- TreeWalkResult{entry: pentry, end: isEOF}:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ func TestTreeWalk(t *testing.T) {
|
||||
}
|
||||
|
||||
isLeafDir := func(volume, prefix string) bool {
|
||||
entries, listErr := disk.ListDir(volume, prefix, 1)
|
||||
entries, listErr := disk.ListDir(volume, prefix, 1, "")
|
||||
if listErr != nil {
|
||||
return false
|
||||
}
|
||||
@@ -233,7 +233,7 @@ func TestTreeWalkTimeout(t *testing.T) {
|
||||
}
|
||||
|
||||
isLeafDir := func(volume, prefix string) bool {
|
||||
entries, listErr := disk.ListDir(volume, prefix, 1)
|
||||
entries, listErr := disk.ListDir(volume, prefix, 1, "")
|
||||
if listErr != nil {
|
||||
return false
|
||||
}
|
||||
@@ -379,7 +379,7 @@ func TestRecursiveTreeWalk(t *testing.T) {
|
||||
}
|
||||
|
||||
isLeafDir := func(volume, prefix string) bool {
|
||||
entries, listErr := disk1.ListDir(volume, prefix, 1)
|
||||
entries, listErr := disk1.ListDir(volume, prefix, 1, "")
|
||||
if listErr != nil {
|
||||
return false
|
||||
}
|
||||
@@ -494,7 +494,7 @@ func TestSortedness(t *testing.T) {
|
||||
}
|
||||
|
||||
isLeafDir := func(volume, prefix string) bool {
|
||||
entries, listErr := disk1.ListDir(volume, prefix, 1)
|
||||
entries, listErr := disk1.ListDir(volume, prefix, 1, "")
|
||||
if listErr != nil {
|
||||
return false
|
||||
}
|
||||
@@ -577,7 +577,7 @@ func TestTreeWalkIsEnd(t *testing.T) {
|
||||
}
|
||||
|
||||
isLeafDir := func(volume, prefix string) bool {
|
||||
entries, listErr := disk1.ListDir(volume, prefix, 1)
|
||||
entries, listErr := disk1.ListDir(volume, prefix, 1, "")
|
||||
if listErr != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
+31
-12
@@ -122,7 +122,7 @@ type StorageInfoRep struct {
|
||||
}
|
||||
|
||||
// StorageInfo - web call to gather storage usage statistics.
|
||||
func (web *webAPIHandlers) StorageInfo(r *http.Request, args *AuthArgs, reply *StorageInfoRep) error {
|
||||
func (web *webAPIHandlers) StorageInfo(r *http.Request, args *WebGenericArgs, reply *StorageInfoRep) error {
|
||||
objectAPI := web.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
return toJSONError(errServerNotInitialized)
|
||||
@@ -583,7 +583,22 @@ func (web *webAPIHandlers) RemoveObject(r *http.Request, args *RemoveObjectArgs,
|
||||
|
||||
claims, owner, authErr := webRequestAuthenticate(r)
|
||||
if authErr != nil {
|
||||
return toJSONError(authErr)
|
||||
if authErr == errNoAuthToken {
|
||||
// Check if all objects are allowed to be deleted anonymously
|
||||
for _, object := range args.Objects {
|
||||
if !globalPolicySys.IsAllowed(policy.Args{
|
||||
Action: policy.DeleteObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, "", ""),
|
||||
IsOwner: false,
|
||||
ObjectName: object,
|
||||
}) {
|
||||
return toJSONError(errAuthentication)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return toJSONError(authErr)
|
||||
}
|
||||
}
|
||||
|
||||
if args.BucketName == "" || len(args.Objects) == 0 {
|
||||
@@ -640,16 +655,20 @@ next:
|
||||
return toJSONError(errMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.DeleteObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, "", claims.Subject),
|
||||
IsOwner: owner,
|
||||
ObjectName: objectName,
|
||||
}) {
|
||||
return toJSONError(errAccessDenied)
|
||||
// Check for permissions only in the case of
|
||||
// non-anonymous login. For anonymous login, policy has already
|
||||
// been checked.
|
||||
if authErr != errNoAuthToken {
|
||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: claims.Subject,
|
||||
Action: iampolicy.DeleteObjectAction,
|
||||
BucketName: args.BucketName,
|
||||
ConditionValues: getConditionValues(r, "", claims.Subject),
|
||||
IsOwner: owner,
|
||||
ObjectName: objectName,
|
||||
}) {
|
||||
return toJSONError(errAccessDenied)
|
||||
}
|
||||
}
|
||||
|
||||
if err = deleteObject(context.Background(), objectAPI, web.CacheAPI(), args.BucketName, objectName, r); err != nil {
|
||||
|
||||
@@ -189,9 +189,7 @@ func testStorageInfoWebHandler(obj ObjectLayer, instanceType string, t TestErrHa
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
storageInfoRequest := AuthArgs{
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
}
|
||||
storageInfoRequest := &WebGenericArgs{}
|
||||
storageInfoReply := &StorageInfoRep{}
|
||||
req, err := newTestWebRPCRequest("Web.StorageInfo", authorization, storageInfoRequest)
|
||||
if err != nil {
|
||||
@@ -224,9 +222,7 @@ func testServerInfoWebHandler(obj ObjectLayer, instanceType string, t TestErrHan
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serverInfoRequest := AuthArgs{
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
}
|
||||
serverInfoRequest := &WebGenericArgs{}
|
||||
serverInfoReply := &ServerInfoRep{}
|
||||
req, err := newTestWebRPCRequest("Web.ServerInfo", authorization, serverInfoRequest)
|
||||
if err != nil {
|
||||
@@ -1527,11 +1523,8 @@ func TestWebCheckAuthorization(t *testing.T) {
|
||||
"PresignedGet",
|
||||
}
|
||||
for _, rpcCall := range webRPCs {
|
||||
args := &AuthArgs{
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
}
|
||||
reply := &WebGenericRep{}
|
||||
req, nerr := newTestWebRPCRequest("Web."+rpcCall, "Bearer fooauthorization", args)
|
||||
req, nerr := newTestWebRPCRequest("Web."+rpcCall, "Bearer fooauthorization", &WebGenericArgs{})
|
||||
if nerr != nil {
|
||||
t.Fatalf("Test %s: Failed to create HTTP request: <ERROR> %v", rpcCall, nerr)
|
||||
}
|
||||
@@ -1634,7 +1627,7 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
|
||||
RepArgs interface{}
|
||||
}{
|
||||
{"MakeBucket", MakeBucketArgs{BucketName: bucketName}, WebGenericRep{}},
|
||||
{"ListBuckets", AuthArgs{RPCVersion: globalRPCAPIVersion}, ListBucketsRep{}},
|
||||
{"ListBuckets", WebGenericArgs{}, ListBucketsRep{}},
|
||||
{"ListObjects", ListObjectsArgs{BucketName: bucketName, Prefix: ""}, ListObjectsRep{}},
|
||||
{"GetBucketPolicy", GetBucketPolicyArgs{BucketName: bucketName, Prefix: ""}, GetBucketPolicyRep{}},
|
||||
{"SetBucketPolicy", SetBucketPolicyWebArgs{BucketName: bucketName, Prefix: "", Policy: "none"}, WebGenericRep{}},
|
||||
@@ -1658,9 +1651,7 @@ func TestWebObjectLayerFaultyDisks(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test Web.StorageInfo
|
||||
storageInfoRequest := AuthArgs{
|
||||
RPCVersion: globalRPCAPIVersion,
|
||||
}
|
||||
storageInfoRequest := &WebGenericArgs{}
|
||||
storageInfoReply := &StorageInfoRep{}
|
||||
req, err := newTestWebRPCRequest("Web.StorageInfo", authorization, storageInfoRequest)
|
||||
if err != nil {
|
||||
|
||||
+2
-17
@@ -654,7 +654,7 @@ func listDirSetsFactory(ctx context.Context, isLeaf IsLeafFunc, isLeafDir IsLeaf
|
||||
wg.Add(1)
|
||||
go func(index int, disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
diskEntries[index], _ = disk.ListDir(bucket, prefixDir, -1)
|
||||
diskEntries[index], _ = disk.ListDir(bucket, prefixDir, -1, xlMetaJSONFile)
|
||||
}(index, disk)
|
||||
}
|
||||
|
||||
@@ -713,21 +713,10 @@ func listDirSetsFactory(ctx context.Context, isLeaf IsLeafFunc, isLeafDir IsLeaf
|
||||
// value through the walk channel receives the data properly lexically sorted.
|
||||
func (s *xlSets) ListObjects(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
||||
isLeaf := func(bucket, entry string) bool {
|
||||
entry = strings.TrimSuffix(entry, slashSeparator)
|
||||
// Verify if we are at the leaf, a leaf is where we
|
||||
// see `xl.json` inside a directory.
|
||||
return s.getHashedSet(entry).isObject(bucket, entry)
|
||||
return !hasSuffix(entry, slashSeparator)
|
||||
}
|
||||
|
||||
isLeafDir := func(bucket, entry string) bool {
|
||||
// Verify prefixes in all sets.
|
||||
var ok bool
|
||||
for _, set := range s.sets {
|
||||
ok = set.isObjectDir(bucket, entry)
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1271,10 +1260,6 @@ func (s *xlSets) HealObjects(ctx context.Context, bucket, prefix string, healObj
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
// For any walk error return right away.
|
||||
if walkResult.err != nil {
|
||||
return toObjectErr(walkResult.err, bucket, prefix)
|
||||
}
|
||||
if err := healObjectFn(bucket, strings.TrimSuffix(walkResult.entry, slashSeparator+xlMetaJSONFile)); err != nil {
|
||||
return toObjectErr(err, bucket, walkResult.entry)
|
||||
}
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ func (xl xlObjects) isObjectDir(bucket, prefix string) (ok bool) {
|
||||
go func(index int, disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
// Check if 'prefix' is an object on this 'disk', else continue the check the next disk
|
||||
entries, err := disk.ListDir(bucket, prefix, 1)
|
||||
entries, err := disk.ListDir(bucket, prefix, 1, "")
|
||||
if err != nil {
|
||||
errs[index] = err
|
||||
return
|
||||
|
||||
+83
-44
@@ -341,7 +341,7 @@ func (xl xlObjects) healObject(ctx context.Context, bucket string, object string
|
||||
}
|
||||
|
||||
// List and delete the object directory,
|
||||
files, derr := disk.ListDir(bucket, object, -1)
|
||||
files, derr := disk.ListDir(bucket, object, -1, "")
|
||||
if derr == nil {
|
||||
for _, entry := range files {
|
||||
_ = disk.DeleteFile(bucket,
|
||||
@@ -478,52 +478,51 @@ func (xl xlObjects) healObjectDir(ctx context.Context, bucket, object string, dr
|
||||
hr.Before.Drives = make([]madmin.HealDriveInfo, len(storageDisks))
|
||||
hr.After.Drives = make([]madmin.HealDriveInfo, len(storageDisks))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Prepare object creation in all disks
|
||||
for i, d := range storageDisks {
|
||||
wg.Add(1)
|
||||
go func(idx int, disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
if disk == nil {
|
||||
hr.Before.Drives[idx] = madmin.HealDriveInfo{State: madmin.DriveStateOffline}
|
||||
hr.After.Drives[idx] = madmin.HealDriveInfo{State: madmin.DriveStateOffline}
|
||||
return
|
||||
errs := statAllDirs(ctx, storageDisks, bucket, object)
|
||||
if isObjectDirDangling(errs) {
|
||||
for i, err := range errs {
|
||||
if err == nil {
|
||||
storageDisks[i].DeleteFile(bucket, object)
|
||||
}
|
||||
|
||||
drive := disk.String()
|
||||
hr.Before.Drives[idx] = madmin.HealDriveInfo{UUID: "", Endpoint: drive, State: madmin.DriveStateOffline}
|
||||
hr.After.Drives[idx] = madmin.HealDriveInfo{UUID: "", Endpoint: drive, State: madmin.DriveStateOffline}
|
||||
|
||||
_, statErr := disk.StatVol(pathJoin(bucket, object))
|
||||
switch statErr {
|
||||
case nil:
|
||||
hr.Before.Drives[idx].State = madmin.DriveStateOk
|
||||
hr.After.Drives[idx].State = madmin.DriveStateOk
|
||||
// Object is fine in this disk, nothing to be done anymore, exiting
|
||||
return
|
||||
case errVolumeNotFound:
|
||||
hr.Before.Drives[idx].State = madmin.DriveStateMissing
|
||||
hr.After.Drives[idx].State = madmin.DriveStateMissing
|
||||
default:
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return
|
||||
}
|
||||
|
||||
if err := disk.MakeVol(pathJoin(bucket, object)); err == nil || err == errVolumeExists {
|
||||
hr.After.Drives[idx].State = madmin.DriveStateOk
|
||||
} else {
|
||||
logger.LogIf(ctx, err)
|
||||
hr.After.Drives[idx].State = madmin.DriveStateOffline
|
||||
}
|
||||
}(i, d)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
// Prepare object creation in all disks
|
||||
for i, err := range errs {
|
||||
var drive string
|
||||
if storageDisks[i] != nil {
|
||||
drive = storageDisks[i].String()
|
||||
}
|
||||
switch err {
|
||||
case errDiskNotFound:
|
||||
hr.Before.Drives[i] = madmin.HealDriveInfo{State: madmin.DriveStateOffline}
|
||||
hr.After.Drives[i] = madmin.HealDriveInfo{State: madmin.DriveStateOffline}
|
||||
case errVolumeNotFound:
|
||||
hr.Before.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateMissing}
|
||||
hr.After.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateMissing}
|
||||
default:
|
||||
hr.Before.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateCorrupt}
|
||||
hr.After.Drives[i] = madmin.HealDriveInfo{Endpoint: drive, State: madmin.DriveStateCorrupt}
|
||||
}
|
||||
}
|
||||
if dryRun {
|
||||
return hr, nil
|
||||
}
|
||||
for i, err := range errs {
|
||||
switch err {
|
||||
case errVolumeNotFound:
|
||||
merr := storageDisks[i].MakeVol(pathJoin(bucket, object))
|
||||
switch merr {
|
||||
case nil, errVolumeExists:
|
||||
hr.After.Drives[i].State = madmin.DriveStateOk
|
||||
case errDiskNotFound:
|
||||
hr.After.Drives[i].State = madmin.DriveStateOffline
|
||||
default:
|
||||
logger.LogIf(ctx, merr)
|
||||
hr.After.Drives[i].State = madmin.DriveStateCorrupt
|
||||
}
|
||||
}
|
||||
}
|
||||
return hr, nil
|
||||
}
|
||||
|
||||
@@ -587,6 +586,46 @@ func defaultHealResult(latestXLMeta xlMetaV1, storageDisks []StorageAPI, errs []
|
||||
return result
|
||||
}
|
||||
|
||||
// Stat all directories.
|
||||
func statAllDirs(ctx context.Context, storageDisks []StorageAPI, bucket, prefix string) []error {
|
||||
var errs = make([]error, len(storageDisks))
|
||||
var wg sync.WaitGroup
|
||||
for index, disk := range storageDisks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(index int, disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
entries, err := disk.ListDir(bucket, prefix, 1, "")
|
||||
if err != nil {
|
||||
errs[index] = err
|
||||
return
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
errs[index] = errVolumeNotEmpty
|
||||
return
|
||||
}
|
||||
}(index, disk)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return errs
|
||||
}
|
||||
|
||||
// ObjectDir is considered dangling/corrupted if any only
|
||||
// if total disks - a combination of corrupted and missing
|
||||
// files is lesser than N/2+1 number of disks.
|
||||
func isObjectDirDangling(errs []error) (ok bool) {
|
||||
var notFoundDir int
|
||||
for _, readErr := range errs {
|
||||
if readErr == errFileNotFound {
|
||||
notFoundDir++
|
||||
}
|
||||
}
|
||||
return notFoundDir > len(errs)/2
|
||||
}
|
||||
|
||||
// Object is considered dangling/corrupted if any only
|
||||
// if total disks - a combination of corrupted and missing
|
||||
// files is lesser than number of data blocks.
|
||||
|
||||
@@ -34,7 +34,7 @@ func listDirFactory(ctx context.Context, isLeaf IsLeafFunc, disks ...StorageAPI)
|
||||
var entries []string
|
||||
var newEntries []string
|
||||
var err error
|
||||
entries, err = disk.ListDir(bucket, prefixDir, -1)
|
||||
entries, err = disk.ListDir(bucket, prefixDir, -1, xlMetaJSONFile)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -89,10 +89,6 @@ func (xl xlObjects) listObjects(ctx context.Context, bucket, prefix, marker, del
|
||||
eof = true
|
||||
break
|
||||
}
|
||||
// For any walk error return right away.
|
||||
if walkResult.err != nil {
|
||||
return loi, toObjectErr(walkResult.err, bucket, prefix)
|
||||
}
|
||||
entry := walkResult.entry
|
||||
var objInfo ObjectInfo
|
||||
if hasSuffix(entry, slashSeparator) {
|
||||
@@ -108,8 +104,10 @@ func (xl xlObjects) listObjects(ctx context.Context, bucket, prefix, marker, del
|
||||
// Ignore errFileNotFound as the object might have got
|
||||
// deleted in the interim period of listing and getObjectInfo(),
|
||||
// ignore quorum error as it might be an entry from an outdated disk.
|
||||
switch err {
|
||||
case errFileNotFound, errXLReadQuorum:
|
||||
if IsErrIgnored(err, []error{
|
||||
errFileNotFound,
|
||||
errXLReadQuorum,
|
||||
}...) {
|
||||
continue
|
||||
}
|
||||
return loi, toObjectErr(err, bucket, prefix)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2016, 2017, 2017 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2016-2019 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
@@ -74,8 +75,8 @@ func (c ChecksumInfo) MarshalJSON() ([]byte, error) {
|
||||
|
||||
// UnmarshalJSON - should never be called, instead xlMetaV1UnmarshalJSON() should be used.
|
||||
func (c *ChecksumInfo) UnmarshalJSON(data []byte) error {
|
||||
logger.LogIf(context.Background(), errUnexpected)
|
||||
var info checksumInfoJSON
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
if err := json.Unmarshal(data, &info); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+19
-17
@@ -47,9 +47,10 @@ func (xl xlObjects) getMultipartSHADir(bucket, object string) string {
|
||||
return getSHA256Hash([]byte(pathJoin(bucket, object)))
|
||||
}
|
||||
|
||||
// isUploadIDExists - verify if a given uploadID exists and is valid.
|
||||
func (xl xlObjects) isUploadIDExists(ctx context.Context, bucket, object, uploadID string) bool {
|
||||
return xl.isObject(minioMetaMultipartBucket, xl.getUploadIDDir(bucket, object, uploadID))
|
||||
// checkUploadIDExists - verify if a given uploadID exists and is valid.
|
||||
func (xl xlObjects) checkUploadIDExists(ctx context.Context, bucket, object, uploadID string) error {
|
||||
_, err := xl.getObjectInfo(ctx, minioMetaMultipartBucket, xl.getUploadIDDir(bucket, object, uploadID))
|
||||
return err
|
||||
}
|
||||
|
||||
// Removes part given by partName belonging to a mulitpart upload from minioMetaBucket
|
||||
@@ -163,7 +164,7 @@ func (xl xlObjects) ListMultipartUploads(ctx context.Context, bucket, object, ke
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
uploadIDs, err := disk.ListDir(minioMetaMultipartBucket, xl.getMultipartSHADir(bucket, object), -1)
|
||||
uploadIDs, err := disk.ListDir(minioMetaMultipartBucket, xl.getMultipartSHADir(bucket, object), -1, "")
|
||||
if err != nil {
|
||||
if err == errFileNotFound {
|
||||
return result, nil
|
||||
@@ -308,9 +309,9 @@ func (xl xlObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
||||
}
|
||||
|
||||
// Validates if upload ID exists.
|
||||
if !xl.isUploadIDExists(ctx, bucket, object, uploadID) {
|
||||
if err := xl.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
|
||||
preUploadIDLock.RUnlock()
|
||||
return pi, InvalidUploadID{UploadID: uploadID}
|
||||
return pi, toObjectErr(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
// Read metadata associated with the object from all disks.
|
||||
@@ -406,9 +407,9 @@ func (xl xlObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
||||
}
|
||||
defer postUploadIDLock.Unlock()
|
||||
|
||||
// Validate again if upload ID still exists.
|
||||
if !xl.isUploadIDExists(ctx, bucket, object, uploadID) {
|
||||
return pi, InvalidUploadID{UploadID: uploadID}
|
||||
// Validates if upload ID exists.
|
||||
if err := xl.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
|
||||
return pi, toObjectErr(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
// Rename temporary part file to its final location.
|
||||
@@ -499,8 +500,8 @@ func (xl xlObjects) ListObjectParts(ctx context.Context, bucket, object, uploadI
|
||||
}
|
||||
defer uploadIDLock.Unlock()
|
||||
|
||||
if !xl.isUploadIDExists(ctx, bucket, object, uploadID) {
|
||||
return result, InvalidUploadID{UploadID: uploadID}
|
||||
if err := xl.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
|
||||
return result, toObjectErr(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
uploadIDPath := xl.getUploadIDDir(bucket, object, uploadID)
|
||||
@@ -617,8 +618,8 @@ func (xl xlObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
}
|
||||
defer uploadIDLock.Unlock()
|
||||
|
||||
if !xl.isUploadIDExists(ctx, bucket, object, uploadID) {
|
||||
return oi, InvalidUploadID{UploadID: uploadID}
|
||||
if err := xl.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
|
||||
return oi, toObjectErr(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
// Check if an object is present as one of the parent dir.
|
||||
@@ -821,8 +822,9 @@ func (xl xlObjects) AbortMultipartUpload(ctx context.Context, bucket, object, up
|
||||
}
|
||||
defer uploadIDLock.Unlock()
|
||||
|
||||
if !xl.isUploadIDExists(ctx, bucket, object, uploadID) {
|
||||
return InvalidUploadID{UploadID: uploadID}
|
||||
// Validates if upload ID exists.
|
||||
if err := xl.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
|
||||
return toObjectErr(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
// Read metadata associated with the object from all disks.
|
||||
@@ -871,12 +873,12 @@ func (xl xlObjects) cleanupStaleMultipartUploads(ctx context.Context, cleanupInt
|
||||
// Remove the old multipart uploads on the given disk.
|
||||
func (xl xlObjects) cleanupStaleMultipartUploadsOnDisk(ctx context.Context, disk StorageAPI, expiry time.Duration) {
|
||||
now := time.Now()
|
||||
shaDirs, err := disk.ListDir(minioMetaMultipartBucket, "", -1)
|
||||
shaDirs, err := disk.ListDir(minioMetaMultipartBucket, "", -1, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, shaDir := range shaDirs {
|
||||
uploadIDDirs, err := disk.ListDir(minioMetaMultipartBucket, shaDir, -1)
|
||||
uploadIDDirs, err := disk.ListDir(minioMetaMultipartBucket, shaDir, -1, "")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
+24
-24
@@ -173,10 +173,6 @@ func (xl xlObjects) GetObjectNInfo(ctx context.Context, bucket, object string, r
|
||||
// Handler directory request by returning a reader that
|
||||
// returns no bytes.
|
||||
if hasSuffix(object, slashSeparator) {
|
||||
if !xl.isObjectDir(bucket, object) {
|
||||
nsUnlocker()
|
||||
return nil, toObjectErr(errFileNotFound, bucket, object)
|
||||
}
|
||||
var objInfo ObjectInfo
|
||||
if objInfo, err = xl.getObjectInfoDir(ctx, bucket, object); err != nil {
|
||||
nsUnlocker()
|
||||
@@ -375,19 +371,16 @@ func (xl xlObjects) getObjectInfoDir(ctx context.Context, bucket, object string)
|
||||
wg.Add(1)
|
||||
go func(index int, disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
if _, err := disk.StatVol(pathJoin(bucket, object)); err != nil {
|
||||
// Since we are re-purposing StatVol, an object which
|
||||
// is a directory if it doesn't exist should be
|
||||
// returned as errFileNotFound instead, convert
|
||||
// the error right here accordingly.
|
||||
if err == errVolumeNotFound {
|
||||
err = errFileNotFound
|
||||
} else if err == errVolumeAccessDenied {
|
||||
err = errFileAccessDenied
|
||||
}
|
||||
|
||||
// Save error to reduce it later
|
||||
// Check if 'prefix' is an object on this 'disk'.
|
||||
entries, err := disk.ListDir(bucket, object, 1, "")
|
||||
if err != nil {
|
||||
errs[index] = err
|
||||
return
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
// Not a directory if not empty.
|
||||
errs[index] = errFileNotFound
|
||||
return
|
||||
}
|
||||
}(index, disk)
|
||||
}
|
||||
@@ -412,13 +405,11 @@ func (xl xlObjects) GetObjectInfo(ctx context.Context, bucket, object string, op
|
||||
}
|
||||
|
||||
if hasSuffix(object, slashSeparator) {
|
||||
if !xl.isObjectDir(bucket, object) {
|
||||
return oi, toObjectErr(errFileNotFound, bucket, object)
|
||||
info, err := xl.getObjectInfoDir(ctx, bucket, object)
|
||||
if err != nil {
|
||||
return oi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
if oi, e = xl.getObjectInfoDir(ctx, bucket, object); e != nil {
|
||||
return oi, toObjectErr(e, bucket, object)
|
||||
}
|
||||
return oi, nil
|
||||
return info, nil
|
||||
}
|
||||
|
||||
info, err := xl.getObjectInfo(ctx, bucket, object)
|
||||
@@ -879,8 +870,17 @@ func (xl xlObjects) DeleteObject(ctx context.Context, bucket, object string) (er
|
||||
var writeQuorum int
|
||||
var isObjectDir = hasSuffix(object, slashSeparator)
|
||||
|
||||
if isObjectDir && !xl.isObjectDir(bucket, object) {
|
||||
return toObjectErr(errFileNotFound, bucket, object)
|
||||
if isObjectDir {
|
||||
_, err = xl.getObjectInfoDir(ctx, bucket, object)
|
||||
if err == errXLReadQuorum {
|
||||
if isObjectDirDangling(statAllDirs(ctx, xl.getDisks(), bucket, object)) {
|
||||
// If object is indeed dangling, purge it.
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return toObjectErr(err, bucket, object)
|
||||
}
|
||||
}
|
||||
|
||||
if isObjectDir {
|
||||
|
||||
+4
-80
@@ -18,13 +18,13 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"hash/crc32"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
@@ -139,59 +139,6 @@ func parseXLFormat(xlMetaBuf []byte) string {
|
||||
return gjson.GetBytes(xlMetaBuf, "format").String()
|
||||
}
|
||||
|
||||
func parseXLRelease(xlMetaBuf []byte) string {
|
||||
return gjson.GetBytes(xlMetaBuf, "minio.release").String()
|
||||
}
|
||||
|
||||
func parseXLErasureInfo(ctx context.Context, xlMetaBuf []byte) (ErasureInfo, error) {
|
||||
erasure := ErasureInfo{}
|
||||
erasureResult := gjson.GetBytes(xlMetaBuf, "erasure")
|
||||
// parse the xlV1Meta.Erasure.Distribution.
|
||||
disResult := erasureResult.Get("distribution").Array()
|
||||
|
||||
distribution := make([]int, len(disResult))
|
||||
for i, dis := range disResult {
|
||||
distribution[i] = int(dis.Int())
|
||||
}
|
||||
erasure.Distribution = distribution
|
||||
|
||||
erasure.Algorithm = erasureResult.Get("algorithm").String()
|
||||
erasure.DataBlocks = int(erasureResult.Get("data").Int())
|
||||
erasure.ParityBlocks = int(erasureResult.Get("parity").Int())
|
||||
erasure.BlockSize = erasureResult.Get("blockSize").Int()
|
||||
erasure.Index = int(erasureResult.Get("index").Int())
|
||||
|
||||
checkSumsResult := erasureResult.Get("checksum").Array()
|
||||
|
||||
// Check for scenario where checksum information missing for some parts.
|
||||
partsResult := gjson.GetBytes(xlMetaBuf, "parts").Array()
|
||||
if len(checkSumsResult) != len(partsResult) {
|
||||
return erasure, errCorruptedFormat
|
||||
}
|
||||
|
||||
// Parse xlMetaV1.Erasure.Checksum array.
|
||||
checkSums := make([]ChecksumInfo, len(checkSumsResult))
|
||||
for i, v := range checkSumsResult {
|
||||
algorithm := BitrotAlgorithmFromString(v.Get("algorithm").String())
|
||||
if !algorithm.Available() {
|
||||
logger.LogIf(ctx, errBitrotHashAlgoInvalid)
|
||||
return erasure, errBitrotHashAlgoInvalid
|
||||
}
|
||||
hash, err := hex.DecodeString(v.Get("hash").String())
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return erasure, err
|
||||
}
|
||||
name := v.Get("name").String()
|
||||
if name == "" {
|
||||
return erasure, errCorruptedFormat
|
||||
}
|
||||
checkSums[i] = ChecksumInfo{Name: name, Algorithm: algorithm, Hash: hash}
|
||||
}
|
||||
erasure.Checksums = checkSums
|
||||
return erasure, nil
|
||||
}
|
||||
|
||||
func parseXLParts(xlMetaBuf []byte) []ObjectPartInfo {
|
||||
// Parse the XL Parts.
|
||||
partsResult := gjson.GetBytes(xlMetaBuf, "parts").Array()
|
||||
@@ -220,32 +167,9 @@ func parseXLMetaMap(xlMetaBuf []byte) map[string]string {
|
||||
|
||||
// Constructs XLMetaV1 using `gjson` lib to retrieve each field.
|
||||
func xlMetaV1UnmarshalJSON(ctx context.Context, xlMetaBuf []byte) (xlMeta xlMetaV1, e error) {
|
||||
// obtain version.
|
||||
xlMeta.Version = parseXLVersion(xlMetaBuf)
|
||||
// obtain format.
|
||||
xlMeta.Format = parseXLFormat(xlMetaBuf)
|
||||
// Parse xlMetaV1.Stat .
|
||||
stat, err := parseXLStat(xlMetaBuf)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return xlMeta, err
|
||||
}
|
||||
|
||||
xlMeta.Stat = stat
|
||||
// parse the xlV1Meta.Erasure fields.
|
||||
xlMeta.Erasure, err = parseXLErasureInfo(ctx, xlMetaBuf)
|
||||
if err != nil {
|
||||
return xlMeta, err
|
||||
}
|
||||
|
||||
// Parse the XL Parts.
|
||||
xlMeta.Parts = parseXLParts(xlMetaBuf)
|
||||
// Get the xlMetaV1.Realse field.
|
||||
xlMeta.Minio.Release = parseXLRelease(xlMetaBuf)
|
||||
// parse xlMetaV1.
|
||||
xlMeta.Meta = parseXLMetaMap(xlMetaBuf)
|
||||
|
||||
return xlMeta, nil
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
e = json.Unmarshal(xlMetaBuf, &xlMeta)
|
||||
return xlMeta, e
|
||||
}
|
||||
|
||||
// read xl.json from the given disk, parse and return xlV1MetaV1.Parts.
|
||||
|
||||
@@ -5,7 +5,7 @@ version: '2'
|
||||
# 9001 through 9004.
|
||||
services:
|
||||
minio1:
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
volumes:
|
||||
- data1:/data
|
||||
ports:
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
MINIO_SECRET_KEY: minio123
|
||||
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
|
||||
minio2:
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
volumes:
|
||||
- data2:/data
|
||||
ports:
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
MINIO_SECRET_KEY: minio123
|
||||
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
|
||||
minio3:
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
volumes:
|
||||
- data3:/data
|
||||
ports:
|
||||
@@ -35,7 +35,7 @@ services:
|
||||
MINIO_SECRET_KEY: minio123
|
||||
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
|
||||
minio4:
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
volumes:
|
||||
- data4:/data
|
||||
ports:
|
||||
|
||||
@@ -2,7 +2,7 @@ version: '3.1'
|
||||
|
||||
services:
|
||||
minio1:
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
hostname: minio1
|
||||
volumes:
|
||||
- minio1-data:/export
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
- access_key
|
||||
|
||||
minio2:
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
hostname: minio2
|
||||
volumes:
|
||||
- minio2-data:/export
|
||||
@@ -46,7 +46,7 @@ services:
|
||||
- access_key
|
||||
|
||||
minio3:
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
hostname: minio3
|
||||
volumes:
|
||||
- minio3-data:/export
|
||||
@@ -68,7 +68,7 @@ services:
|
||||
- access_key
|
||||
|
||||
minio4:
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
hostname: minio4
|
||||
volumes:
|
||||
- minio4-data:/export
|
||||
|
||||
@@ -2,7 +2,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
minio1:
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
hostname: minio1
|
||||
volumes:
|
||||
- minio1-data:/export
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
command: server http://minio1/export http://minio2/export http://minio3/export http://minio4/export
|
||||
|
||||
minio2:
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
hostname: minio2
|
||||
volumes:
|
||||
- minio2-data:/export
|
||||
@@ -46,7 +46,7 @@ services:
|
||||
command: server http://minio1/export http://minio2/export http://minio3/export http://minio4/export
|
||||
|
||||
minio3:
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
hostname: minio3
|
||||
volumes:
|
||||
- minio3-data:/export
|
||||
@@ -68,7 +68,7 @@ services:
|
||||
command: server http://minio1/export http://minio2/export http://minio3/export http://minio4/export
|
||||
|
||||
minio4:
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
hostname: minio4
|
||||
volumes:
|
||||
- minio4-data:/export
|
||||
|
||||
@@ -30,7 +30,7 @@ spec:
|
||||
value: "minio"
|
||||
- name: MINIO_SECRET_KEY
|
||||
value: "minio123"
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
# Unfortunately you must manually define each server. Perhaps autodiscovery via DNS can be implemented in the future.
|
||||
args:
|
||||
- server
|
||||
|
||||
@@ -21,7 +21,7 @@ spec:
|
||||
value: "minio"
|
||||
- name: MINIO_SECRET_KEY
|
||||
value: "minio123"
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
args:
|
||||
- server
|
||||
- http://minio-0.minio.default.svc.cluster.local/data
|
||||
|
||||
@@ -21,7 +21,7 @@ spec:
|
||||
containers:
|
||||
- name: minio
|
||||
# Pulls the default Minio image from Docker Hub
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
args:
|
||||
- gateway
|
||||
- gcs
|
||||
|
||||
@@ -29,7 +29,7 @@ spec:
|
||||
- name: data
|
||||
mountPath: "/data"
|
||||
# Pulls the lastest Minio image from Docker Hub
|
||||
image: minio/minio:RELEASE.2019-04-09T01-22-30Z
|
||||
image: minio/minio:RELEASE.2019-04-18T21-44-59Z
|
||||
args:
|
||||
- server
|
||||
- /data
|
||||
|
||||
@@ -43,6 +43,7 @@ require (
|
||||
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c // indirect
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf
|
||||
github.com/jonboulle/clockwork v0.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.6
|
||||
github.com/klauspost/compress v1.4.1 // indirect
|
||||
github.com/klauspost/cpuid v1.2.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.1
|
||||
@@ -93,10 +94,12 @@ require (
|
||||
go.uber.org/atomic v1.3.2
|
||||
go.uber.org/multierr v1.1.0 // indirect
|
||||
go.uber.org/zap v1.9.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3
|
||||
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422 // indirect
|
||||
golang.org/x/net v0.0.0-20190415214537-1da14a5a36f2
|
||||
golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
|
||||
golang.org/x/tools v0.0.0-20190418152851-6732636ccdfd // indirect
|
||||
google.golang.org/api v0.3.0
|
||||
gopkg.in/Shopify/sarama.v1 v1.20.0
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
||||
|
||||
@@ -305,6 +305,7 @@ github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
@@ -407,7 +408,9 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/pointerstructure v0.0.0-20170205204203-f2329fcfa9e2/go.mod h1:KMNPMpc0BU/kZEgyDhBplsDn/mjnJMhyMjq4MWboN20=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/gnatsd v1.4.1 h1:RconcfDeWpKCD6QIIwiVFcvForlXpWeJP7i5/lDLy44=
|
||||
@@ -594,6 +597,10 @@ golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0F
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 h1:bselrhR0Or1vomJZC8ZIjWtbDmn9OYFLX5Ik9alpJpE=
|
||||
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190417174047-f416ebab96af h1:6qGQw30u837TXZbCmLFR9AVA+RjJU1LIbvk0oIkDZGY=
|
||||
golang.org/x/crypto v0.0.0-20190417174047-f416ebab96af/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480 h1:O5YqonU5IWby+w98jVUG9h7zlCWCcH4RHyPVReBmhzk=
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -602,6 +609,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -621,6 +630,8 @@ golang.org/x/net v0.0.0-20190318221613-d196dffd7c2b/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190324223953-e3b2ff56ed87/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190415214537-1da14a5a36f2 h1:iC0Y6EDq+rhnAePxGvJs2kzUAYcwESqdcGRPzEUfzTU=
|
||||
golang.org/x/net v0.0.0-20190415214537-1da14a5a36f2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20180603041954-1e0a3fa8ba9a/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -659,6 +670,10 @@ golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqY
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67 h1:1Fzlr8kkDLQwqMP8GxrhptBLqZG/EDpiATneiZHY998=
|
||||
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190416152802-12500544f89f h1:1ZH9RnjNgLzh6YrsRp/c6ddZ8Lq0fq9xztNOoWJ2sz4=
|
||||
golang.org/x/sys v0.0.0-20190416152802-12500544f89f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be h1:mI+jhqkn68ybP0ORJqunXn+fq+Eeb4hHKqLQcFICjAc=
|
||||
golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -675,6 +690,10 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190318200714-bb1270c20edf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190417223002-a5870b403859 h1:9IwAPkJAVzY3ES7jydtY2zc5YltyS6lQuXUJoqoWQYg=
|
||||
golang.org/x/tools v0.0.0-20190417223002-a5870b403859/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190418152851-6732636ccdfd h1:xDnl4DRcQ4FyjjmlpchohoK4Tl5GeV1JhqzFNp9OFhI=
|
||||
golang.org/x/tools v0.0.0-20190418152851-6732636ccdfd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/api v0.0.0-20180603000442-8e296ef26005/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20180916000451-19ff8768a5c0/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
|
||||
Reference in New Issue
Block a user