mirror of
https://github.com/minio/minio.git
synced 2024-12-24 06:05:55 -05:00
Peer RPCs for bucket notifications (#2877)
* Implements a Peer RPC router that sends info to all Minio servers in the cluster. * Bucket notifications are propagated to all nodes via this RPC router. * Bucket listener configuration is persisted to separate object layer file (`listener.json`) and peer RPCs are used to communicate changes throughout the cluster. * When events are generated, RPC calls to send them to other servers where bucket listeners may be connected is implemented. * Some bucket notification tests are now disabled as they cannot work in the new design. * Minor fix in `funcFromPC` to use `path.Join`
This commit is contained in:
parent
a5921b5743
commit
6199aa0707
@ -32,30 +32,32 @@ type keyFilter struct {
|
|||||||
FilterRules []filterRule `xml:"FilterRule,omitempty"`
|
FilterRules []filterRule `xml:"FilterRule,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common elements of service notification.
|
type filterStruct struct {
|
||||||
type serviceConfig struct {
|
Key keyFilter `xml:"S3Key,omitempty" json:"S3Key,omitempty"`
|
||||||
Events []string `xml:"Event"`
|
}
|
||||||
Filter struct {
|
|
||||||
Key keyFilter `xml:"S3Key,omitempty"`
|
// ServiceConfig - Common elements of service notification.
|
||||||
}
|
type ServiceConfig struct {
|
||||||
ID string `xml:"Id"`
|
Events []string `xml:"Event" json:"Event"`
|
||||||
|
Filter filterStruct `xml:"Filter" json:"Filter"`
|
||||||
|
ID string `xml:"Id" json:"Id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue SQS configuration.
|
// Queue SQS configuration.
|
||||||
type queueConfig struct {
|
type queueConfig struct {
|
||||||
serviceConfig
|
ServiceConfig
|
||||||
QueueARN string `xml:"Queue"`
|
QueueARN string `xml:"Queue"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Topic SNS configuration, this is a compliance field not used by minio yet.
|
// Topic SNS configuration, this is a compliance field not used by minio yet.
|
||||||
type topicConfig struct {
|
type topicConfig struct {
|
||||||
serviceConfig
|
ServiceConfig
|
||||||
TopicARN string `xml:"Topic"`
|
TopicARN string `xml:"Topic" json:"Topic"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lambda function configuration, this is a compliance field not used by minio yet.
|
// Lambda function configuration, this is a compliance field not used by minio yet.
|
||||||
type lambdaConfig struct {
|
type lambdaConfig struct {
|
||||||
serviceConfig
|
ServiceConfig
|
||||||
LambdaARN string `xml:"CloudFunction"`
|
LambdaARN string `xml:"CloudFunction"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,10 +66,16 @@ type lambdaConfig struct {
|
|||||||
type notificationConfig struct {
|
type notificationConfig struct {
|
||||||
XMLName xml.Name `xml:"NotificationConfiguration"`
|
XMLName xml.Name `xml:"NotificationConfiguration"`
|
||||||
QueueConfigs []queueConfig `xml:"QueueConfiguration"`
|
QueueConfigs []queueConfig `xml:"QueueConfiguration"`
|
||||||
TopicConfigs []topicConfig `xml:"TopicConfiguration"`
|
|
||||||
LambdaConfigs []lambdaConfig `xml:"CloudFunctionConfiguration"`
|
LambdaConfigs []lambdaConfig `xml:"CloudFunctionConfiguration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listenerConfig structure represents run-time notification
|
||||||
|
// configuration for live listeners
|
||||||
|
type listenerConfig struct {
|
||||||
|
TopicConfig topicConfig `json:"TopicConfiguration"`
|
||||||
|
TargetServer string `json:"TargetServer"`
|
||||||
|
}
|
||||||
|
|
||||||
// Internal error used to signal notifications not set.
|
// Internal error used to signal notifications not set.
|
||||||
var errNoSuchNotifications = errors.New("The specified bucket does not have bucket notifications")
|
var errNoSuchNotifications = errors.New("The specified bucket does not have bucket notifications")
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
bucketConfigPrefix = "buckets"
|
bucketConfigPrefix = "buckets"
|
||||||
bucketNotificationConfig = "notification.xml"
|
bucketNotificationConfig = "notification.xml"
|
||||||
|
bucketListenerConfig = "listener.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetBucketNotificationHandler - This implementation of the GET
|
// GetBucketNotificationHandler - This implementation of the GET
|
||||||
@ -117,11 +118,10 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
|
|
||||||
// Reads the incoming notification configuration.
|
// Reads the incoming notification configuration.
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
var bufferSize int64
|
|
||||||
if r.ContentLength >= 0 {
|
if r.ContentLength >= 0 {
|
||||||
bufferSize, err = io.CopyN(&buffer, r.Body, r.ContentLength)
|
_, err = io.CopyN(&buffer, r.Body, r.ContentLength)
|
||||||
} else {
|
} else {
|
||||||
bufferSize, err = io.Copy(&buffer, r.Body)
|
_, err = io.Copy(&buffer, r.Body)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorIf(err, "Unable to read incoming body.")
|
errorIf(err, "Unable to read incoming body.")
|
||||||
@ -144,24 +144,39 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed to save notification configuration.
|
// Put bucket notification config.
|
||||||
notificationConfigPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
err = PutBucketNotificationConfig(bucket, ¬ificationCfg, objectAPI)
|
||||||
sha256sum := ""
|
|
||||||
var metadata map[string]string
|
|
||||||
_, err = objectAPI.PutObject(minioMetaBucket, notificationConfigPath, bufferSize, bytes.NewReader(buffer.Bytes()), metadata, sha256sum)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorIf(err, "Unable to write bucket notification configuration.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set bucket notification config.
|
|
||||||
globalEventNotifier.SetBucketNotificationConfig(bucket, ¬ificationCfg)
|
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
writeSuccessResponse(w, nil)
|
writeSuccessResponse(w, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PutBucketNotificationConfig - Put a new notification config for a
|
||||||
|
// bucket (overwrites any previous config) persistently, updates
|
||||||
|
// global in-memory state, and notify other nodes in the cluster (if
|
||||||
|
// any)
|
||||||
|
func PutBucketNotificationConfig(bucket string, ncfg *notificationConfig, objAPI ObjectLayer) error {
|
||||||
|
if ncfg == nil {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
// persist config to disk
|
||||||
|
err := persistNotificationConfig(bucket, ncfg, objAPI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to persist Bucket notification config to object layer - config=%v errMsg=%v", *ncfg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All servers (including local) are told to update in-memory
|
||||||
|
// config
|
||||||
|
S3PeersUpdateBucketNotification(bucket, ncfg)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// writeNotification marshals notification message before writing to client.
|
// writeNotification marshals notification message before writing to client.
|
||||||
func writeNotification(w http.ResponseWriter, notification map[string][]NotificationEvent) error {
|
func writeNotification(w http.ResponseWriter, notification map[string][]NotificationEvent) error {
|
||||||
// Invalid response writer.
|
// Invalid response writer.
|
||||||
@ -172,7 +187,7 @@ func writeNotification(w http.ResponseWriter, notification map[string][]Notifica
|
|||||||
if notification == nil {
|
if notification == nil {
|
||||||
return errInvalidArgument
|
return errInvalidArgument
|
||||||
}
|
}
|
||||||
// Marshal notification data into XML and write to client.
|
// Marshal notification data into JSON and write to client.
|
||||||
notificationBytes, err := json.Marshal(¬ification)
|
notificationBytes, err := json.Marshal(¬ification)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -251,13 +266,18 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
|
|
||||||
_, err := objAPI.GetBucketInfo(bucket)
|
_, err := objAPI.GetBucketInfo(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorIf(err, "Unable to bucket info.")
|
errorIf(err, "Unable to get bucket info.")
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accountID := fmt.Sprintf("%d", time.Now().UTC().UnixNano())
|
accountID := fmt.Sprintf("%d", time.Now().UTC().UnixNano())
|
||||||
accountARN := "arn:minio:sns:" + serverConfig.GetRegion() + accountID + ":listen"
|
accountARN := fmt.Sprintf(
|
||||||
|
"arn:minio:sqs:%s:%s:listen-%s",
|
||||||
|
serverConfig.GetRegion(),
|
||||||
|
accountID,
|
||||||
|
globalMinioAddr,
|
||||||
|
)
|
||||||
var filterRules []filterRule
|
var filterRules []filterRule
|
||||||
if prefix != "" {
|
if prefix != "" {
|
||||||
filterRules = append(filterRules, filterRule{
|
filterRules = append(filterRules, filterRule{
|
||||||
@ -272,13 +292,14 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make topic configuration corresponding to this ListenBucketNotification request.
|
// Make topic configuration corresponding to this
|
||||||
|
// ListenBucketNotification request.
|
||||||
topicCfg := &topicConfig{
|
topicCfg := &topicConfig{
|
||||||
TopicARN: accountARN,
|
TopicARN: accountARN,
|
||||||
serviceConfig: serviceConfig{
|
ServiceConfig: ServiceConfig{
|
||||||
Events: events,
|
Events: events,
|
||||||
Filter: struct {
|
Filter: struct {
|
||||||
Key keyFilter `xml:"S3Key,omitempty"`
|
Key keyFilter `xml:"S3Key,omitempty" json:"S3Key,omitempty"`
|
||||||
}{
|
}{
|
||||||
Key: keyFilter{
|
Key: keyFilter{
|
||||||
FilterRules: filterRules,
|
FilterRules: filterRules,
|
||||||
@ -288,29 +309,93 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add topic config to bucket notification config.
|
// Setup a listening channel that will receive notifications
|
||||||
if err = globalEventNotifier.AddTopicConfig(bucket, topicCfg); err != nil {
|
// from the RPC handler.
|
||||||
|
nEventCh := make(chan []NotificationEvent)
|
||||||
|
defer close(nEventCh)
|
||||||
|
// Add channel for listener events
|
||||||
|
globalEventNotifier.AddListenerChan(accountARN, nEventCh)
|
||||||
|
// Remove listener channel after the writer has closed or the
|
||||||
|
// client disconnected.
|
||||||
|
defer globalEventNotifier.RemoveListenerChan(accountARN)
|
||||||
|
|
||||||
|
// Update topic config to bucket config and persist - as soon
|
||||||
|
// as this call compelets, events may start appearing in
|
||||||
|
// nEventCh
|
||||||
|
lc := listenerConfig{
|
||||||
|
TopicConfig: *topicCfg,
|
||||||
|
TargetServer: globalMinioAddr,
|
||||||
|
}
|
||||||
|
err = AddBucketListenerConfig(bucket, &lc, objAPI)
|
||||||
|
if err != nil {
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer RemoveBucketListenerConfig(bucket, &lc, objAPI)
|
||||||
|
|
||||||
// Add all common headers.
|
// Add all common headers.
|
||||||
setCommonHeaders(w)
|
setCommonHeaders(w)
|
||||||
|
|
||||||
// Create a new notification event channel.
|
|
||||||
nEventCh := make(chan []NotificationEvent)
|
|
||||||
// Close the listener channel.
|
|
||||||
defer close(nEventCh)
|
|
||||||
|
|
||||||
// Set sns target.
|
|
||||||
globalEventNotifier.SetSNSTarget(accountARN, nEventCh)
|
|
||||||
// Remove sns listener after the writer has closed or the client disconnected.
|
|
||||||
defer globalEventNotifier.RemoveSNSTarget(accountARN, nEventCh)
|
|
||||||
|
|
||||||
// Start sending bucket notifications.
|
// Start sending bucket notifications.
|
||||||
sendBucketNotification(w, nEventCh)
|
sendBucketNotification(w, nEventCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddBucketListenerConfig - Updates on disk state of listeners, and
|
||||||
|
// updates all peers with the change in listener config.
|
||||||
|
func AddBucketListenerConfig(bucket string, lcfg *listenerConfig, objAPI ObjectLayer) error {
|
||||||
|
if lcfg == nil {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
listenerCfgs := globalEventNotifier.GetBucketListenerConfig(bucket)
|
||||||
|
|
||||||
|
// add new lid to listeners and persist to object layer.
|
||||||
|
listenerCfgs = append(listenerCfgs, *lcfg)
|
||||||
|
|
||||||
|
// update persistent config
|
||||||
|
err := persistListenerConfig(bucket, listenerCfgs, objAPI)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "Error persisting listener config when adding a listener.")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// persistence success - now update in-memory globals on all
|
||||||
|
// peers (including local)
|
||||||
|
S3PeersUpdateBucketListener(bucket, listenerCfgs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveBucketListenerConfig - removes a given bucket notification config
|
||||||
|
func RemoveBucketListenerConfig(bucket string, lcfg *listenerConfig, objAPI ObjectLayer) {
|
||||||
|
listenerCfgs := globalEventNotifier.GetBucketListenerConfig(bucket)
|
||||||
|
|
||||||
|
// remove listener with matching ARN - if not found ignore and
|
||||||
|
// exit.
|
||||||
|
var updatedLcfgs []listenerConfig
|
||||||
|
found := false
|
||||||
|
for k, configuredLcfg := range listenerCfgs {
|
||||||
|
if configuredLcfg.TopicConfig.TopicARN == lcfg.TopicConfig.TopicARN {
|
||||||
|
updatedLcfgs = append(listenerCfgs[:k],
|
||||||
|
listenerCfgs[k+1:]...)
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// update persistent config
|
||||||
|
err := persistListenerConfig(bucket, updatedLcfgs, objAPI)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "Error persisting listener config when removing a listener.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// persistence success - now update in-memory globals on all
|
||||||
|
// peers (including local)
|
||||||
|
S3PeersUpdateBucketListener(bucket, updatedLcfgs)
|
||||||
|
}
|
||||||
|
|
||||||
// Removes notification.xml for a given bucket, only used during DeleteBucket.
|
// Removes notification.xml for a given bucket, only used during DeleteBucket.
|
||||||
func removeNotificationConfig(bucket string, objAPI ObjectLayer) error {
|
func removeNotificationConfig(bucket string, objAPI ObjectLayer) error {
|
||||||
// Verify bucket is valid.
|
// Verify bucket is valid.
|
||||||
|
@ -5,11 +5,16 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Implement a dummy flush writer.
|
// Implement a dummy flush writer.
|
||||||
@ -156,325 +161,50 @@ func TestSendBucketNotification(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGetBucketNotificationHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
|
||||||
// get random bucket name.
|
|
||||||
randBucket := getRandomBucketName()
|
|
||||||
noNotificationBucket := "nonotification"
|
|
||||||
invalidBucket := "Invalid\\Bucket"
|
|
||||||
|
|
||||||
// Create buckets for the following test cases.
|
|
||||||
for _, bucket := range []string{randBucket, noNotificationBucket} {
|
|
||||||
err := obj.MakeBucket(bucket)
|
|
||||||
if err != nil {
|
|
||||||
// failed to create newbucket, abort.
|
|
||||||
t.Fatalf("Failed to create bucket %s %s : %s", bucket,
|
|
||||||
instanceType, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize sample bucket notification config.
|
|
||||||
sampleNotificationBytes := []byte("<NotificationConfiguration><TopicConfiguration>" +
|
|
||||||
"<Event>s3:ObjectCreated:*</Event><Event>s3:ObjectRemoved:*</Event><Filter>" +
|
|
||||||
"<S3Key></S3Key></Filter><Id></Id><Topic>arn:minio:sns:us-east-1:1474332374:listen</Topic>" +
|
|
||||||
"</TopicConfiguration></NotificationConfiguration>")
|
|
||||||
|
|
||||||
emptyNotificationBytes := []byte("<NotificationConfiguration></NotificationConfiguration>")
|
|
||||||
|
|
||||||
// Register the API end points with XL/FS object layer.
|
|
||||||
apiRouter := initTestAPIEndPoints(obj, []string{
|
|
||||||
"GetBucketNotification",
|
|
||||||
"PutBucketNotification",
|
|
||||||
})
|
|
||||||
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig("us-east-1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root folder after the test ends.
|
|
||||||
defer removeAll(rootPath)
|
|
||||||
|
|
||||||
credentials := serverConfig.GetCredential()
|
|
||||||
|
|
||||||
//Initialize global event notifier with mock queue targets.
|
|
||||||
err = initEventNotifier(obj)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %s: Failed to initialize mock event notifier %v",
|
|
||||||
instanceType, err)
|
|
||||||
}
|
|
||||||
// Initialize httptest recorder.
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
|
|
||||||
// Prepare notification config for one of the test cases.
|
|
||||||
req, err := newTestSignedRequestV4("PUT", getPutBucketNotificationURL("", randBucket),
|
|
||||||
int64(len(sampleNotificationBytes)), bytes.NewReader(sampleNotificationBytes),
|
|
||||||
credentials.AccessKeyID, credentials.SecretAccessKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d %s: Failed to create HTTP request for PutBucketNotification: <ERROR> %v",
|
|
||||||
1, instanceType, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
apiRouter.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
type testKind int
|
|
||||||
const (
|
|
||||||
CompareBytes testKind = iota
|
|
||||||
CheckStatus
|
|
||||||
InvalidAuth
|
|
||||||
)
|
|
||||||
testCases := []struct {
|
|
||||||
bucketName string
|
|
||||||
kind testKind
|
|
||||||
expectedNotificationBytes []byte
|
|
||||||
expectedHTTPCode int
|
|
||||||
}{
|
|
||||||
{randBucket, CompareBytes, sampleNotificationBytes, http.StatusOK},
|
|
||||||
{randBucket, InvalidAuth, nil, http.StatusBadRequest},
|
|
||||||
{noNotificationBucket, CompareBytes, emptyNotificationBytes, http.StatusOK},
|
|
||||||
{invalidBucket, CheckStatus, nil, http.StatusBadRequest},
|
|
||||||
}
|
|
||||||
signatureMismatchCode := getAPIError(ErrContentSHA256Mismatch).Code
|
|
||||||
for i, test := range testCases {
|
|
||||||
testRec := httptest.NewRecorder()
|
|
||||||
testReq, tErr := newTestSignedRequestV4("GET", getGetBucketNotificationURL("", test.bucketName),
|
|
||||||
int64(0), nil, credentials.AccessKeyID, credentials.SecretAccessKey)
|
|
||||||
if tErr != nil {
|
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP testRequest for GetBucketNotification: <ERROR> %v",
|
|
||||||
i+1, instanceType, tErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set X-Amz-Content-SHA256 in header different from what was used to calculate Signature.
|
|
||||||
if test.kind == InvalidAuth {
|
|
||||||
// Triggering a authentication type check failure.
|
|
||||||
testReq.Header.Set("x-amz-content-sha256", "somethingElse")
|
|
||||||
}
|
|
||||||
|
|
||||||
apiRouter.ServeHTTP(testRec, testReq)
|
|
||||||
|
|
||||||
switch test.kind {
|
|
||||||
case CompareBytes:
|
|
||||||
rspBytes, rErr := ioutil.ReadAll(testRec.Body)
|
|
||||||
if rErr != nil {
|
|
||||||
t.Errorf("Test %d: %s: Failed to read response body: <ERROR> %v", i+1, instanceType, rErr)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(rspBytes, test.expectedNotificationBytes) {
|
|
||||||
t.Errorf("Test %d: %s: Notification config doesn't match expected value %s: <ERROR> %v",
|
|
||||||
i+1, instanceType, string(test.expectedNotificationBytes), err)
|
|
||||||
}
|
|
||||||
case InvalidAuth:
|
|
||||||
rspBytes, rErr := ioutil.ReadAll(testRec.Body)
|
|
||||||
if rErr != nil {
|
|
||||||
t.Errorf("Test %d: %s: Failed to read response body: <ERROR> %v", i+1, instanceType, rErr)
|
|
||||||
}
|
|
||||||
var errCode APIError
|
|
||||||
xErr := xml.Unmarshal(rspBytes, &errCode)
|
|
||||||
if xErr != nil {
|
|
||||||
t.Errorf("Test %d: %s: Failed to unmarshal error XML: <ERROR> %v", i+1, instanceType, xErr)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if errCode.Code != signatureMismatchCode {
|
|
||||||
t.Errorf("Test %d: %s: Expected error code %s but received %s: <ERROR> %v", i+1,
|
|
||||||
instanceType, signatureMismatchCode, errCode.Code, err)
|
|
||||||
|
|
||||||
}
|
|
||||||
fallthrough
|
|
||||||
case CheckStatus:
|
|
||||||
if testRec.Code != test.expectedHTTPCode {
|
|
||||||
t.Errorf("Test %d: %s: expected HTTP code %d, but received %d: <ERROR> %v",
|
|
||||||
i+1, instanceType, test.expectedHTTPCode, testRec.Code, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nil Object layer
|
|
||||||
nilAPIRouter := initTestAPIEndPoints(nil, []string{
|
|
||||||
"GetBucketNotification",
|
|
||||||
"PutBucketNotification",
|
|
||||||
})
|
|
||||||
testRec := httptest.NewRecorder()
|
|
||||||
testReq, tErr := newTestSignedRequestV4("GET", getGetBucketNotificationURL("", randBucket),
|
|
||||||
int64(0), nil, credentials.AccessKeyID, credentials.SecretAccessKey)
|
|
||||||
if tErr != nil {
|
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP testRequest for GetBucketNotification: <ERROR> %v",
|
|
||||||
len(testCases)+1, instanceType, tErr)
|
|
||||||
}
|
|
||||||
nilAPIRouter.ServeHTTP(testRec, testReq)
|
|
||||||
if testRec.Code != http.StatusServiceUnavailable {
|
|
||||||
t.Errorf("Test %d: %s: expected HTTP code %d, but received %d: <ERROR> %v",
|
|
||||||
len(testCases)+1, instanceType, http.StatusServiceUnavailable, testRec.Code, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetBucketNotificationHandler(t *testing.T) {
|
|
||||||
ExecObjectLayerTest(t, testGetBucketNotificationHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPutBucketNotificationHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
|
||||||
invalidBucket := "Invalid\\Bucket"
|
|
||||||
// get random bucket name.
|
|
||||||
randBucket := getRandomBucketName()
|
|
||||||
|
|
||||||
err := obj.MakeBucket(randBucket)
|
|
||||||
if err != nil {
|
|
||||||
// failed to create randBucket, abort.
|
|
||||||
t.Fatalf("Failed to create bucket %s %s : %s", randBucket,
|
|
||||||
instanceType, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sampleNotificationBytes := []byte("<NotificationConfiguration><TopicConfiguration>" +
|
|
||||||
"<Event>s3:ObjectCreated:*</Event><Event>s3:ObjectRemoved:*</Event><Filter>" +
|
|
||||||
"<S3Key></S3Key></Filter><Id></Id><Topic>arn:minio:sns:us-east-1:1474332374:listen</Topic>" +
|
|
||||||
"</TopicConfiguration></NotificationConfiguration>")
|
|
||||||
|
|
||||||
// Register the API end points with XL/FS object layer.
|
|
||||||
apiRouter := initTestAPIEndPoints(obj, []string{
|
|
||||||
"GetBucketNotification",
|
|
||||||
"PutBucketNotification",
|
|
||||||
})
|
|
||||||
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig("us-east-1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root folder after the test ends.
|
|
||||||
defer removeAll(rootPath)
|
|
||||||
|
|
||||||
credentials := serverConfig.GetCredential()
|
|
||||||
|
|
||||||
//Initialize global event notifier with mock queue targets.
|
|
||||||
err = initEventNotifier(obj)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %s: Failed to initialize mock event notifier %v",
|
|
||||||
instanceType, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
signatureMismatchError := getAPIError(ErrContentSHA256Mismatch)
|
|
||||||
missingContentLengthError := getAPIError(ErrMissingContentLength)
|
|
||||||
type testKind int
|
|
||||||
const (
|
|
||||||
CompareBytes testKind = iota
|
|
||||||
CheckStatus
|
|
||||||
InvalidAuth
|
|
||||||
MissingContentLength
|
|
||||||
ChunkedEncoding
|
|
||||||
)
|
|
||||||
testCases := []struct {
|
|
||||||
bucketName string
|
|
||||||
kind testKind
|
|
||||||
expectedNotificationBytes []byte
|
|
||||||
expectedHTTPCode int
|
|
||||||
expectedAPIError string
|
|
||||||
}{
|
|
||||||
{randBucket, CompareBytes, sampleNotificationBytes, http.StatusOK, ""},
|
|
||||||
{randBucket, ChunkedEncoding, sampleNotificationBytes, http.StatusOK, ""},
|
|
||||||
{randBucket, InvalidAuth, nil, signatureMismatchError.HTTPStatusCode, signatureMismatchError.Code},
|
|
||||||
{randBucket, MissingContentLength, nil, missingContentLengthError.HTTPStatusCode, missingContentLengthError.Code},
|
|
||||||
{invalidBucket, CheckStatus, nil, http.StatusBadRequest, ""},
|
|
||||||
}
|
|
||||||
for i, test := range testCases {
|
|
||||||
testRec := httptest.NewRecorder()
|
|
||||||
testReq, tErr := newTestSignedRequestV4("PUT", getPutBucketNotificationURL("", test.bucketName),
|
|
||||||
int64(len(test.expectedNotificationBytes)), bytes.NewReader(test.expectedNotificationBytes),
|
|
||||||
credentials.AccessKeyID, credentials.SecretAccessKey)
|
|
||||||
if tErr != nil {
|
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP testRequest for PutBucketNotification: <ERROR> %v",
|
|
||||||
i+1, instanceType, tErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set X-Amz-Content-SHA256 in header different from what was used to calculate Signature.
|
|
||||||
switch test.kind {
|
|
||||||
case InvalidAuth:
|
|
||||||
// Triggering a authentication type check failure.
|
|
||||||
testReq.Header.Set("x-amz-content-sha256", "somethingElse")
|
|
||||||
case MissingContentLength:
|
|
||||||
testReq.ContentLength = -1
|
|
||||||
case ChunkedEncoding:
|
|
||||||
testReq.ContentLength = -1
|
|
||||||
testReq.TransferEncoding = append(testReq.TransferEncoding, "chunked")
|
|
||||||
}
|
|
||||||
|
|
||||||
apiRouter.ServeHTTP(testRec, testReq)
|
|
||||||
|
|
||||||
switch test.kind {
|
|
||||||
case CompareBytes:
|
|
||||||
|
|
||||||
testReq, tErr = newTestSignedRequestV4("GET", getGetBucketNotificationURL("", test.bucketName),
|
|
||||||
int64(0), nil, credentials.AccessKeyID, credentials.SecretAccessKey)
|
|
||||||
if tErr != nil {
|
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP testRequest for GetBucketNotification: <ERROR> %v",
|
|
||||||
i+1, instanceType, tErr)
|
|
||||||
}
|
|
||||||
apiRouter.ServeHTTP(testRec, testReq)
|
|
||||||
|
|
||||||
rspBytes, rErr := ioutil.ReadAll(testRec.Body)
|
|
||||||
if rErr != nil {
|
|
||||||
t.Errorf("Test %d: %s: Failed to read response body: <ERROR> %v", i+1, instanceType, rErr)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(rspBytes, test.expectedNotificationBytes) {
|
|
||||||
t.Errorf("Test %d: %s: Notification config doesn't match expected value %s: <ERROR> %v",
|
|
||||||
i+1, instanceType, string(test.expectedNotificationBytes), err)
|
|
||||||
}
|
|
||||||
case MissingContentLength, InvalidAuth:
|
|
||||||
rspBytes, rErr := ioutil.ReadAll(testRec.Body)
|
|
||||||
if rErr != nil {
|
|
||||||
t.Errorf("Test %d: %s: Failed to read response body: <ERROR> %v", i+1, instanceType, rErr)
|
|
||||||
}
|
|
||||||
var errCode APIError
|
|
||||||
xErr := xml.Unmarshal(rspBytes, &errCode)
|
|
||||||
if xErr != nil {
|
|
||||||
t.Errorf("Test %d: %s: Failed to unmarshal error XML: <ERROR> %v", i+1, instanceType, xErr)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if errCode.Code != test.expectedAPIError {
|
|
||||||
t.Errorf("Test %d: %s: Expected error code %s but received %s: <ERROR> %v", i+1,
|
|
||||||
instanceType, test.expectedAPIError, errCode.Code, err)
|
|
||||||
|
|
||||||
}
|
|
||||||
fallthrough
|
|
||||||
case CheckStatus:
|
|
||||||
if testRec.Code != test.expectedHTTPCode {
|
|
||||||
t.Errorf("Test %d: %s: expected HTTP code %d, but received %d: <ERROR> %v",
|
|
||||||
i+1, instanceType, test.expectedHTTPCode, testRec.Code, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nil Object layer
|
|
||||||
nilAPIRouter := initTestAPIEndPoints(nil, []string{
|
|
||||||
"GetBucketNotification",
|
|
||||||
"PutBucketNotification",
|
|
||||||
})
|
|
||||||
testRec := httptest.NewRecorder()
|
|
||||||
testReq, tErr := newTestSignedRequestV4("PUT", getPutBucketNotificationURL("", randBucket),
|
|
||||||
int64(len(sampleNotificationBytes)), bytes.NewReader(sampleNotificationBytes),
|
|
||||||
credentials.AccessKeyID, credentials.SecretAccessKey)
|
|
||||||
if tErr != nil {
|
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP testRequest for PutBucketNotification: <ERROR> %v",
|
|
||||||
len(testCases)+1, instanceType, tErr)
|
|
||||||
}
|
|
||||||
nilAPIRouter.ServeHTTP(testRec, testReq)
|
|
||||||
if testRec.Code != http.StatusServiceUnavailable {
|
|
||||||
t.Errorf("Test %d: %s: expected HTTP code %d, but received %d: <ERROR> %v",
|
|
||||||
len(testCases)+1, instanceType, http.StatusServiceUnavailable, testRec.Code, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutBucketNotificationHandler(t *testing.T) {
|
|
||||||
ExecObjectLayerTest(t, testPutBucketNotificationHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testListenBucketNotificationHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
func testListenBucketNotificationHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
||||||
|
// Register the API end points with XL/FS object layer.
|
||||||
|
apiRouter := initTestAPIEndPoints(obj, []string{
|
||||||
|
"ListenBucketNotification",
|
||||||
|
})
|
||||||
|
mux, ok := apiRouter.(*mux.Router)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Unable to setup test")
|
||||||
|
}
|
||||||
|
registerS3PeerRPCRouter(mux)
|
||||||
|
|
||||||
|
testServer := httptest.NewServer(apiRouter)
|
||||||
|
defer testServer.Close()
|
||||||
|
|
||||||
|
// initialize the server and obtain the credentials and root.
|
||||||
|
// credentials are necessary to sign the HTTP request.
|
||||||
|
rootPath, err := newTestConfig("us-east-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init Test config failed")
|
||||||
|
}
|
||||||
|
// remove the root folder after the test ends.
|
||||||
|
defer removeAll(rootPath)
|
||||||
|
|
||||||
|
credentials := serverConfig.GetCredential()
|
||||||
|
|
||||||
|
// setup port and minio addr
|
||||||
|
_, portStr, err := net.SplitHostPort(testServer.Listener.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Initialisation error: %v", err)
|
||||||
|
}
|
||||||
|
globalMinioPort, err = strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Initialisation error: %v", err)
|
||||||
|
}
|
||||||
|
globalMinioAddr = fmt.Sprintf(":%d", globalMinioPort)
|
||||||
|
// initialize the peer client(s)
|
||||||
|
initGlobalS3Peers([]string{})
|
||||||
|
|
||||||
invalidBucket := "Invalid\\Bucket"
|
invalidBucket := "Invalid\\Bucket"
|
||||||
noNotificationBucket := "nonotificationbucket"
|
noNotificationBucket := "nonotificationbucket"
|
||||||
// get random bucket name.
|
// get random bucket name.
|
||||||
randBucket := getRandomBucketName()
|
randBucket := getRandomBucketName()
|
||||||
for _, bucket := range []string{randBucket, noNotificationBucket} {
|
for _, bucket := range []string{randBucket, noNotificationBucket} {
|
||||||
err := obj.MakeBucket(bucket)
|
err = obj.MakeBucket(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// failed to create bucket, abort.
|
// failed to create bucket, abort.
|
||||||
t.Fatalf("Failed to create bucket %s %s : %s", bucket,
|
t.Fatalf("Failed to create bucket %s %s : %s", bucket,
|
||||||
@ -482,43 +212,16 @@ func testListenBucketNotificationHandler(obj ObjectLayer, instanceType string, t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sampleNotificationBytes := []byte("<NotificationConfiguration><TopicConfiguration>" +
|
|
||||||
"<Event>s3:ObjectCreated:*</Event><Event>s3:ObjectRemoved:*</Event><Filter>" +
|
|
||||||
"<S3Key></S3Key></Filter><Id></Id><Topic>arn:minio:sns:us-east-1:1474332374:listen</Topic>" +
|
|
||||||
"</TopicConfiguration></NotificationConfiguration>")
|
|
||||||
|
|
||||||
// Register the API end points with XL/FS object layer.
|
|
||||||
apiRouter := initTestAPIEndPoints(obj, []string{
|
|
||||||
"PutBucketNotification",
|
|
||||||
"ListenBucketNotification",
|
|
||||||
"PutObject",
|
|
||||||
})
|
|
||||||
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig("us-east-1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root folder after the test ends.
|
|
||||||
defer removeAll(rootPath)
|
|
||||||
|
|
||||||
credentials := serverConfig.GetCredential()
|
|
||||||
|
|
||||||
// Initialize global event notifier with mock queue targets.
|
// Initialize global event notifier with mock queue targets.
|
||||||
err = initEventNotifier(obj)
|
err = initEventNotifier(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %s: Failed to initialize mock event notifier %v",
|
t.Fatalf("Test %s: Failed to initialize mock event notifier %v",
|
||||||
instanceType, err)
|
instanceType, err)
|
||||||
}
|
}
|
||||||
testRec := httptest.NewRecorder()
|
|
||||||
testReq, tErr := newTestSignedRequestV4("PUT", getPutBucketNotificationURL("", randBucket),
|
var testRec *httptest.ResponseRecorder
|
||||||
int64(len(sampleNotificationBytes)), bytes.NewReader(sampleNotificationBytes),
|
var testReq *http.Request
|
||||||
credentials.AccessKeyID, credentials.SecretAccessKey)
|
var tErr error
|
||||||
if tErr != nil {
|
|
||||||
t.Fatalf("%s: Failed to create HTTP testRequest for PutBucketNotification: <ERROR> %v", instanceType, tErr)
|
|
||||||
}
|
|
||||||
apiRouter.ServeHTTP(testRec, testReq)
|
|
||||||
|
|
||||||
signatureMismatchError := getAPIError(ErrContentSHA256Mismatch)
|
signatureMismatchError := getAPIError(ErrContentSHA256Mismatch)
|
||||||
type testKind int
|
type testKind int
|
||||||
|
@ -265,25 +265,6 @@ func checkDuplicateQueueConfigs(configs []queueConfig) APIErrorCode {
|
|||||||
return ErrNone
|
return ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check all the topic configs for any duplicates.
|
|
||||||
func checkDuplicateTopicConfigs(configs []topicConfig) APIErrorCode {
|
|
||||||
var topicConfigARNS []string
|
|
||||||
|
|
||||||
// Navigate through each configs and count the entries.
|
|
||||||
for _, config := range configs {
|
|
||||||
topicConfigARNS = append(topicConfigARNS, config.TopicARN)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there are any duplicate counts.
|
|
||||||
if err := checkDuplicates(topicConfigARNS); err != nil {
|
|
||||||
errorIf(err, "Invalid topic configs found.")
|
|
||||||
return ErrOverlappingConfigs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success.
|
|
||||||
return ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validates all the bucket notification configuration for their validity,
|
// Validates all the bucket notification configuration for their validity,
|
||||||
// if one of the config is malformed or has invalid data it is rejected.
|
// if one of the config is malformed or has invalid data it is rejected.
|
||||||
// Configuration is never applied partially.
|
// Configuration is never applied partially.
|
||||||
@ -292,10 +273,6 @@ func validateNotificationConfig(nConfig notificationConfig) APIErrorCode {
|
|||||||
if s3Error := validateQueueConfigs(nConfig.QueueConfigs); s3Error != ErrNone {
|
if s3Error := validateQueueConfigs(nConfig.QueueConfigs); s3Error != ErrNone {
|
||||||
return s3Error
|
return s3Error
|
||||||
}
|
}
|
||||||
// Validate all topic configs.
|
|
||||||
if s3Error := validateTopicConfigs(nConfig.TopicConfigs); s3Error != ErrNone {
|
|
||||||
return s3Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate queue configs.
|
// Check for duplicate queue configs.
|
||||||
if len(nConfig.QueueConfigs) > 1 {
|
if len(nConfig.QueueConfigs) > 1 {
|
||||||
@ -304,13 +281,6 @@ func validateNotificationConfig(nConfig notificationConfig) APIErrorCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate topic configs.
|
|
||||||
if len(nConfig.TopicConfigs) > 1 {
|
|
||||||
if s3Error := checkDuplicateTopicConfigs(nConfig.TopicConfigs); s3Error != ErrNone {
|
|
||||||
return s3Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add validation for other configurations.
|
// Add validation for other configurations.
|
||||||
return ErrNone
|
return ErrNone
|
||||||
}
|
}
|
||||||
|
@ -57,42 +57,6 @@ func TestCheckDuplicateConfigs(t *testing.T) {
|
|||||||
t.Errorf("Test %d: Expected %d, got %d", i+1, testCase.expectedErrCode, errCode)
|
t.Errorf("Test %d: Expected %d, got %d", i+1, testCase.expectedErrCode, errCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test cases for SNS topic config.
|
|
||||||
topicTestCases := []struct {
|
|
||||||
tConfigs []topicConfig
|
|
||||||
expectedErrCode APIErrorCode
|
|
||||||
}{
|
|
||||||
// Error out for duplicate configs.
|
|
||||||
{
|
|
||||||
tConfigs: []topicConfig{
|
|
||||||
{
|
|
||||||
TopicARN: "arn:minio:sns:us-east-1:1:listen",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
TopicARN: "arn:minio:sns:us-east-1:1:listen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedErrCode: ErrOverlappingConfigs,
|
|
||||||
},
|
|
||||||
// Valid config.
|
|
||||||
{
|
|
||||||
tConfigs: []topicConfig{
|
|
||||||
{
|
|
||||||
TopicARN: "arn:minio:sns:us-east-1:1:listen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedErrCode: ErrNone,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... validate for duplicate topic configs.
|
|
||||||
for i, testCase := range topicTestCases {
|
|
||||||
errCode := checkDuplicateTopicConfigs(testCase.tConfigs)
|
|
||||||
if errCode != testCase.expectedErrCode {
|
|
||||||
t.Errorf("Test %d: Expected %d, got %d", i+1, testCase.expectedErrCode, errCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests for validating filter rules.
|
// Tests for validating filter rules.
|
||||||
|
@ -31,24 +31,6 @@ const (
|
|||||||
controlPath = "/control"
|
controlPath = "/control"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Find local node through the command line arguments.
|
|
||||||
func getLocalAddress(srvCmdConfig serverCmdConfig) string {
|
|
||||||
if !srvCmdConfig.isDistXL {
|
|
||||||
return fmt.Sprintf(":%d", globalMinioPort)
|
|
||||||
}
|
|
||||||
for _, export := range srvCmdConfig.disks {
|
|
||||||
// Validates if remote disk is local.
|
|
||||||
if isLocalStorage(export) {
|
|
||||||
var host string
|
|
||||||
if idx := strings.LastIndex(export, ":"); idx != -1 {
|
|
||||||
host = export[:idx]
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s:%d", host, globalMinioPort)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializes remote control clients for making remote requests.
|
// Initializes remote control clients for making remote requests.
|
||||||
func initRemoteControlClients(srvCmdConfig serverCmdConfig) []*AuthRPCClient {
|
func initRemoteControlClients(srvCmdConfig serverCmdConfig) []*AuthRPCClient {
|
||||||
if !srvCmdConfig.isDistXL {
|
if !srvCmdConfig.isDistXL {
|
||||||
|
@ -16,70 +16,7 @@
|
|||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tests fetch local address.
|
|
||||||
func TestLocalAddress(t *testing.T) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
testCases := []struct {
|
|
||||||
srvCmdConfig serverCmdConfig
|
|
||||||
localAddr string
|
|
||||||
}{
|
|
||||||
// Test 1 - local address is found.
|
|
||||||
{
|
|
||||||
srvCmdConfig: serverCmdConfig{
|
|
||||||
isDistXL: true,
|
|
||||||
disks: []string{
|
|
||||||
"localhost:/mnt/disk1",
|
|
||||||
"1.1.1.2:/mnt/disk2",
|
|
||||||
"1.1.2.1:/mnt/disk3",
|
|
||||||
"1.1.2.2:/mnt/disk4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
localAddr: "localhost:9000",
|
|
||||||
},
|
|
||||||
// Test 2 - local address is everything.
|
|
||||||
{
|
|
||||||
srvCmdConfig: serverCmdConfig{
|
|
||||||
isDistXL: false,
|
|
||||||
disks: []string{
|
|
||||||
"/mnt/disk1",
|
|
||||||
"/mnt/disk2",
|
|
||||||
"/mnt/disk3",
|
|
||||||
"/mnt/disk4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
localAddr: ":9000",
|
|
||||||
},
|
|
||||||
// Test 3 - local address is not found.
|
|
||||||
{
|
|
||||||
srvCmdConfig: serverCmdConfig{
|
|
||||||
isDistXL: true,
|
|
||||||
disks: []string{
|
|
||||||
"1.1.1.1:/mnt/disk1",
|
|
||||||
"1.1.1.2:/mnt/disk2",
|
|
||||||
"1.1.2.1:/mnt/disk3",
|
|
||||||
"1.1.2.2:/mnt/disk4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
localAddr: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validates fetching local address.
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
localAddr := getLocalAddress(testCase.srvCmdConfig)
|
|
||||||
if localAddr != testCase.localAddr {
|
|
||||||
t.Fatalf("Test %d: Expected %s, got %s", i+1, testCase.localAddr, localAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests initialization of remote controller clients.
|
// Tests initialization of remote controller clients.
|
||||||
func TestInitRemoteControlClients(t *testing.T) {
|
func TestInitRemoteControlClients(t *testing.T) {
|
||||||
|
@ -18,6 +18,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@ -29,14 +30,51 @@ import (
|
|||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Global event notification queue. This is the queue that would be used to send all notifications.
|
type externalNotifier struct {
|
||||||
type eventNotifier struct {
|
// Per-bucket notification config. This is updated via
|
||||||
rwMutex *sync.RWMutex
|
// PutBucketNotification API.
|
||||||
|
|
||||||
// Collection of 'bucket' and notification config.
|
|
||||||
notificationConfigs map[string]*notificationConfig
|
notificationConfigs map[string]*notificationConfig
|
||||||
snsTargets map[string][]chan []NotificationEvent
|
|
||||||
queueTargets map[string]*logrus.Logger
|
// An external target keeps a connection to an external
|
||||||
|
// service to which events are to be sent. It is a mapping
|
||||||
|
// from an ARN to a log object
|
||||||
|
targets map[string]*logrus.Logger
|
||||||
|
|
||||||
|
rwMutex *sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type internalNotifier struct {
|
||||||
|
// per-bucket listener configuration. This is updated
|
||||||
|
// when listeners connect or disconnect.
|
||||||
|
listenerConfigs map[string][]listenerConfig
|
||||||
|
|
||||||
|
// An internal target is a peer Minio server, that is
|
||||||
|
// connected to a listening client. Here, targets is a map of
|
||||||
|
// listener ARN to log object.
|
||||||
|
targets map[string]*listenerLogger
|
||||||
|
|
||||||
|
// Connected listeners is a map of listener ARNs to channels
|
||||||
|
// on which the ListenBucket API handler go routine is waiting
|
||||||
|
// for events to send to a client.
|
||||||
|
connectedListeners map[string]chan []NotificationEvent
|
||||||
|
|
||||||
|
rwMutex *sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global event notification configuration. This structure has state
|
||||||
|
// about configured external notifications, and run-time configuration
|
||||||
|
// for listener notifications.
|
||||||
|
type eventNotifier struct {
|
||||||
|
|
||||||
|
// `external` here refers to notification configuration to
|
||||||
|
// send events to supported external systems
|
||||||
|
external externalNotifier
|
||||||
|
|
||||||
|
// `internal` refers to notification configuration for live
|
||||||
|
// listening clients. Events for a client are send from all
|
||||||
|
// servers, internally to a particular server that is
|
||||||
|
// connected to the client.
|
||||||
|
internal internalNotifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// Represents data to be sent with notification event.
|
// Represents data to be sent with notification event.
|
||||||
@ -54,7 +92,8 @@ func newNotificationEvent(event eventData) NotificationEvent {
|
|||||||
region := serverConfig.GetRegion()
|
region := serverConfig.GetRegion()
|
||||||
tnow := time.Now().UTC()
|
tnow := time.Now().UTC()
|
||||||
sequencer := fmt.Sprintf("%X", tnow.UnixNano())
|
sequencer := fmt.Sprintf("%X", tnow.UnixNano())
|
||||||
// Following blocks fills in all the necessary details of s3 event message structure.
|
// Following blocks fills in all the necessary details of s3
|
||||||
|
// event message structure.
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html
|
// http://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html
|
||||||
nEvent := NotificationEvent{
|
nEvent := NotificationEvent{
|
||||||
EventVersion: "2.0",
|
EventVersion: "2.0",
|
||||||
@ -96,85 +135,147 @@ func newNotificationEvent(event eventData) NotificationEvent {
|
|||||||
return nEvent
|
return nEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the saved queue target.
|
// Fetch the external target. No locking needed here since this map is
|
||||||
func (en eventNotifier) GetQueueTarget(queueARN string) *logrus.Logger {
|
// never written after initial startup.
|
||||||
return en.queueTargets[queueARN]
|
func (en eventNotifier) GetExternalTarget(queueARN string) *logrus.Logger {
|
||||||
|
return en.external.targets[queueARN]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (en eventNotifier) GetSNSTarget(snsARN string) []chan []NotificationEvent {
|
func (en eventNotifier) GetInternalTarget(arn string) *listenerLogger {
|
||||||
en.rwMutex.RLock()
|
en.internal.rwMutex.RLock()
|
||||||
defer en.rwMutex.RUnlock()
|
defer en.internal.rwMutex.RUnlock()
|
||||||
return en.snsTargets[snsARN]
|
return en.internal.targets[arn]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a new sns target for an input sns ARN.
|
// Set a new sns target for an input sns ARN.
|
||||||
func (en *eventNotifier) SetSNSTarget(snsARN string, listenerCh chan []NotificationEvent) error {
|
func (en *eventNotifier) AddListenerChan(snsARN string, listenerCh chan []NotificationEvent) error {
|
||||||
en.rwMutex.Lock()
|
|
||||||
defer en.rwMutex.Unlock()
|
|
||||||
if listenerCh == nil {
|
if listenerCh == nil {
|
||||||
return errInvalidArgument
|
return errInvalidArgument
|
||||||
}
|
}
|
||||||
en.snsTargets[snsARN] = append(en.snsTargets[snsARN], listenerCh)
|
en.internal.rwMutex.Lock()
|
||||||
|
defer en.internal.rwMutex.Unlock()
|
||||||
|
en.internal.connectedListeners[snsARN] = listenerCh
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove sns target for an input sns ARN.
|
// Remove sns target for an input sns ARN.
|
||||||
func (en *eventNotifier) RemoveSNSTarget(snsARN string, listenerCh chan []NotificationEvent) {
|
func (en *eventNotifier) RemoveListenerChan(snsARN string) {
|
||||||
en.rwMutex.Lock()
|
en.internal.rwMutex.Lock()
|
||||||
defer en.rwMutex.Unlock()
|
defer en.internal.rwMutex.Unlock()
|
||||||
snsTarget, ok := en.snsTargets[snsARN]
|
if en.internal.connectedListeners != nil {
|
||||||
|
delete(en.internal.connectedListeners, snsARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (en *eventNotifier) SendListenerEvent(arn string, event []NotificationEvent) error {
|
||||||
|
en.internal.rwMutex.Lock()
|
||||||
|
defer en.internal.rwMutex.Unlock()
|
||||||
|
|
||||||
|
ch, ok := en.internal.connectedListeners[arn]
|
||||||
if ok {
|
if ok {
|
||||||
for i, savedListenerCh := range snsTarget {
|
ch <- event
|
||||||
if listenerCh == savedListenerCh {
|
}
|
||||||
snsTarget = append(snsTarget[:i], snsTarget[i+1:]...)
|
// If the channel is not present we ignore the event.
|
||||||
if len(snsTarget) == 0 {
|
return nil
|
||||||
delete(en.snsTargets, snsARN)
|
}
|
||||||
break
|
|
||||||
}
|
// Fetch bucket notification config for an input bucket.
|
||||||
en.snsTargets[snsARN] = snsTarget
|
func (en eventNotifier) GetBucketNotificationConfig(bucket string) *notificationConfig {
|
||||||
|
en.external.rwMutex.RLock()
|
||||||
|
defer en.external.rwMutex.RUnlock()
|
||||||
|
return en.external.notificationConfigs[bucket]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (en *eventNotifier) SetBucketNotificationConfig(bucket string, ncfg *notificationConfig) {
|
||||||
|
en.external.rwMutex.Lock()
|
||||||
|
if ncfg == nil {
|
||||||
|
delete(en.external.notificationConfigs, bucket)
|
||||||
|
} else {
|
||||||
|
en.external.notificationConfigs[bucket] = ncfg
|
||||||
|
}
|
||||||
|
en.external.rwMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (en *eventNotifier) GetBucketListenerConfig(bucket string) []listenerConfig {
|
||||||
|
en.internal.rwMutex.RLock()
|
||||||
|
defer en.internal.rwMutex.RUnlock()
|
||||||
|
return en.internal.listenerConfigs[bucket]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (en *eventNotifier) SetBucketListenerConfig(bucket string, lcfg []listenerConfig) error {
|
||||||
|
en.internal.rwMutex.Lock()
|
||||||
|
defer en.internal.rwMutex.Unlock()
|
||||||
|
if lcfg == nil {
|
||||||
|
delete(en.internal.listenerConfigs, bucket)
|
||||||
|
} else {
|
||||||
|
en.internal.listenerConfigs[bucket] = lcfg
|
||||||
|
}
|
||||||
|
// close all existing loggers and initialize again.
|
||||||
|
for _, v := range en.internal.targets {
|
||||||
|
v.lconn.Close()
|
||||||
|
}
|
||||||
|
en.internal.targets = make(map[string]*listenerLogger)
|
||||||
|
for _, lc := range lcfg {
|
||||||
|
logger, err := newListenerLogger(lc.TopicConfig.TopicARN,
|
||||||
|
lc.TargetServer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
en.internal.targets[lc.TopicConfig.TopicARN] = logger
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func eventNotifyForBucketNotifications(eventType, objectName, bucketName string,
|
||||||
|
nEvent []NotificationEvent) {
|
||||||
|
nConfig := globalEventNotifier.GetBucketNotificationConfig(bucketName)
|
||||||
|
if nConfig == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Validate if the event and object match the queue configs.
|
||||||
|
for _, qConfig := range nConfig.QueueConfigs {
|
||||||
|
eventMatch := eventMatch(eventType, qConfig.Events)
|
||||||
|
ruleMatch := filterRuleMatch(objectName, qConfig.Filter.Key.FilterRules)
|
||||||
|
if eventMatch && ruleMatch {
|
||||||
|
targetLog := globalEventNotifier.GetExternalTarget(qConfig.QueueARN)
|
||||||
|
if targetLog != nil {
|
||||||
|
targetLog.WithFields(logrus.Fields{
|
||||||
|
"Key": path.Join(bucketName, objectName),
|
||||||
|
"EventType": eventType,
|
||||||
|
"Records": nEvent,
|
||||||
|
}).Info()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch bucket notification config for an input bucket.
|
func eventNotifyForBucketListeners(eventType, objectName, bucketName string,
|
||||||
func (en eventNotifier) GetBucketNotificationConfig(bucket string) *notificationConfig {
|
nEvent []NotificationEvent) {
|
||||||
en.rwMutex.RLock()
|
lCfgs := globalEventNotifier.GetBucketListenerConfig(bucketName)
|
||||||
defer en.rwMutex.RUnlock()
|
if lCfgs == nil {
|
||||||
return en.notificationConfigs[bucket]
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// Set a new notification config for a bucket, this operation will overwrite any previous
|
|
||||||
// notification configs for the bucket.
|
|
||||||
func (en *eventNotifier) SetBucketNotificationConfig(bucket string, notificationCfg *notificationConfig) error {
|
|
||||||
en.rwMutex.Lock()
|
|
||||||
defer en.rwMutex.Unlock()
|
|
||||||
if notificationCfg == nil {
|
|
||||||
return errInvalidArgument
|
|
||||||
}
|
}
|
||||||
en.notificationConfigs[bucket] = notificationCfg
|
// Validate if the event and object match listener configs
|
||||||
return nil
|
for _, lcfg := range lCfgs {
|
||||||
}
|
ruleMatch := filterRuleMatch(objectName, lcfg.TopicConfig.Filter.Key.FilterRules)
|
||||||
|
eventMatch := eventMatch(eventType, lcfg.TopicConfig.Events)
|
||||||
func (en *eventNotifier) AddTopicConfig(bucket string, topicCfg *topicConfig) error {
|
if eventMatch && ruleMatch {
|
||||||
en.rwMutex.Lock()
|
targetLog := globalEventNotifier.GetInternalTarget(
|
||||||
defer en.rwMutex.Unlock()
|
lcfg.TopicConfig.TopicARN)
|
||||||
if topicCfg == nil {
|
if targetLog != nil && targetLog.log != nil {
|
||||||
return errInvalidArgument
|
targetLog.log.WithFields(logrus.Fields{
|
||||||
}
|
"Key": path.Join(bucketName, objectName),
|
||||||
notificationCfg := en.notificationConfigs[bucket]
|
"EventType": eventType,
|
||||||
if notificationCfg == nil {
|
"Records": nEvent,
|
||||||
en.notificationConfigs[bucket] = ¬ificationConfig{
|
}).Info()
|
||||||
TopicConfigs: []topicConfig{*topicCfg},
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
notificationCfg.TopicConfigs = append(notificationCfg.TopicConfigs, *topicCfg)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eventNotify notifies an event to relevant targets based on their
|
// eventNotify notifies an event to relevant targets based on their
|
||||||
// bucket notification configs.
|
// bucket configuration (notifications and listeners).
|
||||||
func eventNotify(event eventData) {
|
func eventNotify(event eventData) {
|
||||||
// Notifies a new event.
|
// Notifies a new event.
|
||||||
// List of events reported through this function are
|
// List of events reported through this function are
|
||||||
@ -184,15 +285,6 @@ func eventNotify(event eventData) {
|
|||||||
// - s3:ObjectCreated:CompleteMultipartUpload
|
// - s3:ObjectCreated:CompleteMultipartUpload
|
||||||
// - s3:ObjectRemoved:Delete
|
// - s3:ObjectRemoved:Delete
|
||||||
|
|
||||||
nConfig := globalEventNotifier.GetBucketNotificationConfig(event.Bucket)
|
|
||||||
// No bucket notifications enabled, drop the event notification.
|
|
||||||
if nConfig == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(nConfig.QueueConfigs) == 0 && len(nConfig.TopicConfigs) == 0 && len(nConfig.LambdaConfigs) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event type.
|
// Event type.
|
||||||
eventType := event.Type.String()
|
eventType := event.Type.String()
|
||||||
|
|
||||||
@ -202,43 +294,26 @@ func eventNotify(event eventData) {
|
|||||||
// Save the notification event to be sent.
|
// Save the notification event to be sent.
|
||||||
notificationEvent := []NotificationEvent{newNotificationEvent(event)}
|
notificationEvent := []NotificationEvent{newNotificationEvent(event)}
|
||||||
|
|
||||||
// Validate if the event and object match the queue configs.
|
// Notify external targets.
|
||||||
for _, qConfig := range nConfig.QueueConfigs {
|
eventNotifyForBucketNotifications(eventType, objectName, event.Bucket,
|
||||||
eventMatch := eventMatch(eventType, qConfig.Events)
|
notificationEvent)
|
||||||
ruleMatch := filterRuleMatch(objectName, qConfig.Filter.Key.FilterRules)
|
|
||||||
if eventMatch && ruleMatch {
|
// Notify internal targets.
|
||||||
targetLog := globalEventNotifier.GetQueueTarget(qConfig.QueueARN)
|
eventNotifyForBucketListeners(eventType, objectName, event.Bucket,
|
||||||
if targetLog != nil {
|
notificationEvent)
|
||||||
targetLog.WithFields(logrus.Fields{
|
|
||||||
"Key": path.Join(event.Bucket, objectName),
|
|
||||||
"EventType": eventType,
|
|
||||||
"Records": notificationEvent,
|
|
||||||
}).Info()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Validate if the event and object match the sns configs.
|
|
||||||
for _, topicConfig := range nConfig.TopicConfigs {
|
|
||||||
ruleMatch := filterRuleMatch(objectName, topicConfig.Filter.Key.FilterRules)
|
|
||||||
eventMatch := eventMatch(eventType, topicConfig.Events)
|
|
||||||
if eventMatch && ruleMatch {
|
|
||||||
targetListeners := globalEventNotifier.GetSNSTarget(topicConfig.TopicARN)
|
|
||||||
for _, listener := range targetListeners {
|
|
||||||
listener <- notificationEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loads notifcation config if any for a given bucket, returns back structured notification config.
|
// loads notification config if any for a given bucket, returns
|
||||||
|
// structured notification config.
|
||||||
func loadNotificationConfig(bucket string, objAPI ObjectLayer) (*notificationConfig, error) {
|
func loadNotificationConfig(bucket string, objAPI ObjectLayer) (*notificationConfig, error) {
|
||||||
// Construct the notification config path.
|
// Construct the notification config path.
|
||||||
notificationConfigPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
notificationConfigPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
||||||
objInfo, err := objAPI.GetObjectInfo(minioMetaBucket, notificationConfigPath)
|
objInfo, err := objAPI.GetObjectInfo(minioMetaBucket, notificationConfigPath)
|
||||||
err = errorCause(err)
|
err = errorCause(err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 'notification.xml' not found return 'errNoSuchNotifications'.
|
// 'notification.xml' not found return
|
||||||
// This is default when no bucket notifications are found on the bucket.
|
// 'errNoSuchNotifications'. This is default when no
|
||||||
|
// bucket notifications are found on the bucket.
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case ObjectNotFound:
|
case ObjectNotFound:
|
||||||
return nil, errNoSuchNotifications
|
return nil, errNoSuchNotifications
|
||||||
@ -251,8 +326,9 @@ func loadNotificationConfig(bucket string, objAPI ObjectLayer) (*notificationCon
|
|||||||
err = objAPI.GetObject(minioMetaBucket, notificationConfigPath, 0, objInfo.Size, &buffer)
|
err = objAPI.GetObject(minioMetaBucket, notificationConfigPath, 0, objInfo.Size, &buffer)
|
||||||
err = errorCause(err)
|
err = errorCause(err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 'notification.xml' not found return 'errNoSuchNotifications'.
|
// 'notification.xml' not found return
|
||||||
// This is default when no bucket notifications are found on the bucket.
|
// 'errNoSuchNotifications'. This is default when no
|
||||||
|
// bucket notifications are found on the bucket.
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case ObjectNotFound:
|
case ObjectNotFound:
|
||||||
return nil, errNoSuchNotifications
|
return nil, errNoSuchNotifications
|
||||||
@ -267,36 +343,144 @@ func loadNotificationConfig(bucket string, objAPI ObjectLayer) (*notificationCon
|
|||||||
notificationCfg := ¬ificationConfig{}
|
notificationCfg := ¬ificationConfig{}
|
||||||
if err = xml.Unmarshal(notificationConfigBytes, ¬ificationCfg); err != nil {
|
if err = xml.Unmarshal(notificationConfigBytes, ¬ificationCfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} // Successfully marshalled notification configuration.
|
}
|
||||||
|
|
||||||
// Return success.
|
// Return success.
|
||||||
return notificationCfg, nil
|
return notificationCfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loads all bucket notifications if present.
|
// loads notification config if any for a given bucket, returns
|
||||||
func loadAllBucketNotifications(objAPI ObjectLayer) (map[string]*notificationConfig, error) {
|
// structured notification config.
|
||||||
// List buckets to proceed loading all notification configuration.
|
func loadListenerConfig(bucket string, objAPI ObjectLayer) ([]listenerConfig, error) {
|
||||||
buckets, err := objAPI.ListBuckets()
|
// Construct the notification config path.
|
||||||
|
listenerConfigPath := path.Join(bucketConfigPrefix, bucket, bucketListenerConfig)
|
||||||
|
objInfo, err := objAPI.GetObjectInfo(minioMetaBucket, listenerConfigPath)
|
||||||
|
err = errorCause(err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// 'listener.json' not found return
|
||||||
|
// 'errNoSuchNotifications'. This is default when no
|
||||||
|
// bucket notifications are found on the bucket.
|
||||||
|
switch err.(type) {
|
||||||
|
case ObjectNotFound:
|
||||||
|
return nil, errNoSuchNotifications
|
||||||
|
}
|
||||||
|
errorIf(err, "Unable to load bucket-listeners for bucket %s", bucket)
|
||||||
|
// Returns error for other errors.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
err = objAPI.GetObject(minioMetaBucket, listenerConfigPath, 0, objInfo.Size, &buffer)
|
||||||
|
err = errorCause(err)
|
||||||
|
if err != nil {
|
||||||
|
// 'notification.xml' not found return
|
||||||
|
// 'errNoSuchNotifications'. This is default when no
|
||||||
|
// bucket listners are found on the bucket.
|
||||||
|
switch err.(type) {
|
||||||
|
case ObjectNotFound:
|
||||||
|
return nil, errNoSuchNotifications
|
||||||
|
}
|
||||||
|
errorIf(err, "Unable to load bucket-listeners for bucket %s", bucket)
|
||||||
|
// Returns error for other errors.
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
configs := make(map[string]*notificationConfig)
|
// Unmarshal notification bytes.
|
||||||
|
var lCfg []listenerConfig
|
||||||
|
lConfigBytes := buffer.Bytes()
|
||||||
|
if err = json.Unmarshal(lConfigBytes, &lCfg); err != nil {
|
||||||
|
errorIf(err, "Unable to unmarshal listener config from JSON.")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return success.
|
||||||
|
return lCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func persistNotificationConfig(bucket string, ncfg *notificationConfig, obj ObjectLayer) error {
|
||||||
|
// marshal to xml
|
||||||
|
buf, err := xml.Marshal(ncfg)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "Unable to marshal notification configuration into XML")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify bucket exists
|
||||||
|
// FIXME: There is a race between this check and PutObject
|
||||||
|
if err = isBucketExist(bucket, obj); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// build path
|
||||||
|
ncPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
||||||
|
// write object to path
|
||||||
|
_, err = obj.PutObject(minioMetaBucket, ncPath, int64(len(buf)),
|
||||||
|
bytes.NewReader(buf), nil, "")
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "Unable to write bucket notification configuration.")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persists validated listener config to object layer.
|
||||||
|
func persistListenerConfig(bucket string, lcfg []listenerConfig, obj ObjectLayer) error {
|
||||||
|
buf, err := json.Marshal(lcfg)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "Unable to marshal listener config to JSON.")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify bucket exists
|
||||||
|
// FIXME: There is a race between this check and PutObject
|
||||||
|
if err = isBucketExist(bucket, obj); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// build path
|
||||||
|
lcPath := path.Join(bucketConfigPrefix, bucket, bucketListenerConfig)
|
||||||
|
// write object to path
|
||||||
|
_, err = obj.PutObject(minioMetaBucket, lcPath, int64(len(buf)),
|
||||||
|
bytes.NewReader(buf), nil, "")
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "Unable to write bucket listener configuration to object layer.")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads all bucket notifications if present.
|
||||||
|
func loadAllBucketNotifications(objAPI ObjectLayer) (map[string]*notificationConfig, map[string][]listenerConfig, error) {
|
||||||
|
// List buckets to proceed loading all notification configuration.
|
||||||
|
buckets, err := objAPI.ListBuckets()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nConfigs := make(map[string]*notificationConfig)
|
||||||
|
lConfigs := make(map[string][]listenerConfig)
|
||||||
|
|
||||||
// Loads all bucket notifications.
|
// Loads all bucket notifications.
|
||||||
for _, bucket := range buckets {
|
for _, bucket := range buckets {
|
||||||
nCfg, nErr := loadNotificationConfig(bucket.Name, objAPI)
|
nCfg, nErr := loadNotificationConfig(bucket.Name, objAPI)
|
||||||
if nErr != nil {
|
if nErr != nil {
|
||||||
if nErr == errNoSuchNotifications {
|
if nErr != errNoSuchNotifications {
|
||||||
continue
|
return nil, nil, nErr
|
||||||
}
|
}
|
||||||
return nil, nErr
|
} else {
|
||||||
|
nConfigs[bucket.Name] = nCfg
|
||||||
|
}
|
||||||
|
|
||||||
|
lCfg, lErr := loadListenerConfig(bucket.Name, objAPI)
|
||||||
|
if lErr != nil {
|
||||||
|
if lErr != errNoSuchNotifications {
|
||||||
|
return nil, nil, lErr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lConfigs[bucket.Name] = lCfg
|
||||||
}
|
}
|
||||||
configs[bucket.Name] = nCfg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
return configs, nil
|
return nConfigs, lConfigs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads all queue targets, initializes each queueARNs depending on their config.
|
// Loads all queue targets, initializes each queueARNs depending on their config.
|
||||||
@ -452,8 +636,9 @@ func initEventNotifier(objAPI ObjectLayer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read all saved bucket notifications.
|
// Read all saved bucket notifications.
|
||||||
configs, err := loadAllBucketNotifications(objAPI)
|
nConfigs, lConfigs, err := loadAllBucketNotifications(objAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
errorIf(err, "Error loading bucket notifications - %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -463,12 +648,36 @@ func initEventNotifier(objAPI ObjectLayer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inititalize event notifier queue.
|
// Initialize internal listener targets
|
||||||
|
listenTargets := make(map[string]*listenerLogger)
|
||||||
|
for _, listeners := range lConfigs {
|
||||||
|
for _, listener := range listeners {
|
||||||
|
ln, err := newListenerLogger(
|
||||||
|
listener.TopicConfig.TopicARN,
|
||||||
|
listener.TargetServer,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "Unable to initialize listener target logger.")
|
||||||
|
//TODO: improve error
|
||||||
|
return fmt.Errorf("Error initializing listner target logger - %v", err)
|
||||||
|
}
|
||||||
|
listenTargets[listener.TopicConfig.TopicARN] = ln
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize event notifier queue.
|
||||||
globalEventNotifier = &eventNotifier{
|
globalEventNotifier = &eventNotifier{
|
||||||
rwMutex: &sync.RWMutex{},
|
external: externalNotifier{
|
||||||
notificationConfigs: configs,
|
notificationConfigs: nConfigs,
|
||||||
queueTargets: queueTargets,
|
targets: queueTargets,
|
||||||
snsTargets: make(map[string][]chan []NotificationEvent),
|
rwMutex: &sync.RWMutex{},
|
||||||
|
},
|
||||||
|
internal: internalNotifier{
|
||||||
|
rwMutex: &sync.RWMutex{},
|
||||||
|
targets: listenTargets,
|
||||||
|
listenerConfigs: lConfigs,
|
||||||
|
connectedListeners: make(map[string]chan []NotificationEvent),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -18,130 +18,13 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tests event notify.
|
|
||||||
func TestEventNotify(t *testing.T) {
|
|
||||||
ExecObjectLayerTest(t, testEventNotify)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testEventNotify(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
|
||||||
bucketName := getRandomBucketName()
|
|
||||||
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig("us-east-1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root folder after the test ends.
|
|
||||||
defer removeAll(rootPath)
|
|
||||||
|
|
||||||
if err := initEventNotifier(obj); err != nil {
|
|
||||||
t.Fatal("Unexpected error:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify object created event.
|
|
||||||
eventNotify(eventData{
|
|
||||||
Type: ObjectCreatedPost,
|
|
||||||
Bucket: bucketName,
|
|
||||||
ObjInfo: ObjectInfo{
|
|
||||||
Bucket: bucketName,
|
|
||||||
Name: "object1",
|
|
||||||
},
|
|
||||||
ReqParams: map[string]string{
|
|
||||||
"sourceIPAddress": "localhost:1337",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := globalEventNotifier.SetBucketNotificationConfig(bucketName, nil); err != errInvalidArgument {
|
|
||||||
t.Errorf("Expected error %s, got %s", errInvalidArgument, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := globalEventNotifier.SetBucketNotificationConfig(bucketName, ¬ificationConfig{}); err != nil {
|
|
||||||
t.Errorf("Expected error to be nil, got %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nConfig := globalEventNotifier.GetBucketNotificationConfig(bucketName)
|
|
||||||
if nConfig == nil {
|
|
||||||
t.Errorf("Notification expected to be set, but notification not set.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(nConfig, ¬ificationConfig{}) {
|
|
||||||
t.Errorf("Mismatching notification configs.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify object created event.
|
|
||||||
eventNotify(eventData{
|
|
||||||
Type: ObjectRemovedDelete,
|
|
||||||
Bucket: bucketName,
|
|
||||||
ObjInfo: ObjectInfo{
|
|
||||||
Bucket: bucketName,
|
|
||||||
Name: "object1",
|
|
||||||
},
|
|
||||||
ReqParams: map[string]string{
|
|
||||||
"sourceIPAddress": "localhost:1337",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests various forms of inititalization of event notifier.
|
|
||||||
func TestInitEventNotifier(t *testing.T) {
|
|
||||||
disks, err := getRandomDisks(1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
|
||||||
}
|
|
||||||
defer removeRoots(disks)
|
|
||||||
fs, _, err := initObjectLayer(disks, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to initialize FS backend.", err)
|
|
||||||
}
|
|
||||||
nDisks := 16
|
|
||||||
disks, err = getRandomDisks(nDisks)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to create directories for XL backend. ", err)
|
|
||||||
}
|
|
||||||
defer removeRoots(disks)
|
|
||||||
xl, _, err := initObjectLayer(disks, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to initialize XL backend.", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collection of test cases for inititalizing event notifier.
|
|
||||||
testCases := []struct {
|
|
||||||
objAPI ObjectLayer
|
|
||||||
configs map[string]*notificationConfig
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
// Test 1 - invalid arguments.
|
|
||||||
{
|
|
||||||
objAPI: nil,
|
|
||||||
err: errInvalidArgument,
|
|
||||||
},
|
|
||||||
// Test 2 - valid FS object layer but no bucket notifications.
|
|
||||||
{
|
|
||||||
objAPI: fs,
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
// Test 3 - valid XL object layer but no bucket notifications.
|
|
||||||
{
|
|
||||||
objAPI: xl,
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate if event notifier is properly initialized.
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
err = initEventNotifier(testCase.objAPI)
|
|
||||||
if err != testCase.err {
|
|
||||||
t.Errorf("Test %d: Expected %s, but got: %s", i+1, testCase.err, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test InitEventNotifier with faulty disks
|
// Test InitEventNotifier with faulty disks
|
||||||
func TestInitEventNotifierFaultyDisks(t *testing.T) {
|
func TestInitEventNotifierFaultyDisks(t *testing.T) {
|
||||||
// Prepare for tests
|
// Prepare for tests
|
||||||
@ -272,78 +155,231 @@ func TestInitEventNotifierWithRedis(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestListenBucketNotification - test Listen Bucket Notification process
|
type TestPeerRPCServerData struct {
|
||||||
|
serverType string
|
||||||
|
testServer TestServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestPeerRPCServerData) Setup(t *testing.T) {
|
||||||
|
s.testServer = StartTestPeersRPCServer(t, s.serverType)
|
||||||
|
|
||||||
|
// setup port and minio addr
|
||||||
|
_, portStr, err := net.SplitHostPort(s.testServer.Server.Listener.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Initialisation error: %v", err)
|
||||||
|
}
|
||||||
|
globalMinioPort, err = strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Initialisation error: %v", err)
|
||||||
|
}
|
||||||
|
globalMinioAddr = getLocalAddress(
|
||||||
|
s.testServer.SrvCmdCfg,
|
||||||
|
)
|
||||||
|
|
||||||
|
// initialize the peer client(s)
|
||||||
|
initGlobalS3Peers(s.testServer.Disks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestPeerRPCServerData) TearDown() {
|
||||||
|
s.testServer.Stop()
|
||||||
|
_ = removeAll(s.testServer.Root)
|
||||||
|
for _, d := range s.testServer.Disks {
|
||||||
|
_ = removeAll(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetNGetBucketNotification(t *testing.T) {
|
||||||
|
s := TestPeerRPCServerData{serverType: "XL"}
|
||||||
|
|
||||||
|
// setup and teardown
|
||||||
|
s.Setup(t)
|
||||||
|
defer s.TearDown()
|
||||||
|
|
||||||
|
bucketName := getRandomBucketName()
|
||||||
|
|
||||||
|
obj := s.testServer.Obj
|
||||||
|
if err := initEventNotifier(obj); err != nil {
|
||||||
|
t.Fatal("Unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
globalEventNotifier.SetBucketNotificationConfig(bucketName, ¬ificationConfig{})
|
||||||
|
nConfig := globalEventNotifier.GetBucketNotificationConfig(bucketName)
|
||||||
|
if nConfig == nil {
|
||||||
|
t.Errorf("Notification expected to be set, but notification not set.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(nConfig, ¬ificationConfig{}) {
|
||||||
|
t.Errorf("Mismatching notification configs.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitEventNotifier(t *testing.T) {
|
||||||
|
s := TestPeerRPCServerData{serverType: "XL"}
|
||||||
|
|
||||||
|
// setup and teardown
|
||||||
|
s.Setup(t)
|
||||||
|
defer s.TearDown()
|
||||||
|
|
||||||
|
// test if empty object layer arg. returns expected error.
|
||||||
|
if err := initEventNotifier(nil); err == nil || err != errInvalidArgument {
|
||||||
|
t.Fatalf("initEventNotifier returned unexpected error value - %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := s.testServer.Obj
|
||||||
|
bucketName := getRandomBucketName()
|
||||||
|
// declare sample configs
|
||||||
|
filterRules := []filterRule{
|
||||||
|
{
|
||||||
|
Name: "prefix",
|
||||||
|
Value: "minio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "suffix",
|
||||||
|
Value: "*.jpg",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sampleSvcCfg := ServiceConfig{
|
||||||
|
[]string{"s3:ObjectRemoved:*", "s3:ObjectCreated:*"},
|
||||||
|
filterStruct{
|
||||||
|
keyFilter{filterRules},
|
||||||
|
},
|
||||||
|
"1",
|
||||||
|
}
|
||||||
|
sampleNotifCfg := notificationConfig{
|
||||||
|
QueueConfigs: []queueConfig{
|
||||||
|
{
|
||||||
|
ServiceConfig: sampleSvcCfg,
|
||||||
|
QueueARN: "testqARN",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sampleListenCfg := []listenerConfig{
|
||||||
|
{
|
||||||
|
TopicConfig: topicConfig{ServiceConfig: sampleSvcCfg,
|
||||||
|
TopicARN: "testlARN"},
|
||||||
|
TargetServer: globalMinioAddr,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// write without an existing bucket and check
|
||||||
|
if err := persistNotificationConfig(bucketName, ¬ificationConfig{}, obj); err == nil {
|
||||||
|
t.Fatalf("Did not get an error though bucket does not exist!")
|
||||||
|
}
|
||||||
|
// no bucket write check for listener
|
||||||
|
if err := persistListenerConfig(bucketName, []listenerConfig{}, obj); err == nil {
|
||||||
|
t.Fatalf("Did not get an error though bucket does not exist!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// create bucket
|
||||||
|
if err := obj.MakeBucket(bucketName); err != nil {
|
||||||
|
t.Fatal("Unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bucket is created, now writing should not give errors.
|
||||||
|
if err := persistNotificationConfig(bucketName, &sampleNotifCfg, obj); err != nil {
|
||||||
|
t.Fatal("Unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := persistListenerConfig(bucketName, sampleListenCfg, obj); err != nil {
|
||||||
|
t.Fatal("Unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test event notifier init
|
||||||
|
if err := initEventNotifier(obj); err != nil {
|
||||||
|
t.Fatal("Unexpected error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch bucket configs and verify
|
||||||
|
ncfg := globalEventNotifier.GetBucketNotificationConfig(bucketName)
|
||||||
|
if ncfg == nil {
|
||||||
|
t.Error("Bucket notification was not present for ", bucketName)
|
||||||
|
}
|
||||||
|
if len(ncfg.QueueConfigs) != 1 || ncfg.QueueConfigs[0].QueueARN != "testqARN" {
|
||||||
|
t.Error("Unexpected bucket notification found - ", *ncfg)
|
||||||
|
}
|
||||||
|
if globalEventNotifier.GetExternalTarget("testqARN") != nil {
|
||||||
|
t.Error("A logger was not expected to be found as it was not enabled in the config.")
|
||||||
|
}
|
||||||
|
|
||||||
|
lcfg := globalEventNotifier.GetBucketListenerConfig(bucketName)
|
||||||
|
if lcfg == nil {
|
||||||
|
t.Error("Bucket listener was not present for ", bucketName)
|
||||||
|
}
|
||||||
|
if len(lcfg) != 1 || lcfg[0].TargetServer != globalMinioAddr || lcfg[0].TopicConfig.TopicARN != "testlARN" {
|
||||||
|
t.Error("Unexpected listener config found - ", lcfg[0])
|
||||||
|
}
|
||||||
|
if globalEventNotifier.GetInternalTarget("testlARN") == nil {
|
||||||
|
t.Error("A listen logger was not found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestListenBucketNotification(t *testing.T) {
|
func TestListenBucketNotification(t *testing.T) {
|
||||||
|
s := TestPeerRPCServerData{serverType: "XL"}
|
||||||
|
|
||||||
|
// setup and teardown
|
||||||
|
s.Setup(t)
|
||||||
|
defer s.TearDown()
|
||||||
|
|
||||||
|
// test initialisation
|
||||||
|
obj := s.testServer.Obj
|
||||||
|
|
||||||
bucketName := "bucket"
|
bucketName := "bucket"
|
||||||
objectName := "object"
|
objectName := "object"
|
||||||
|
|
||||||
// Prepare for tests
|
|
||||||
// Create fs backend
|
|
||||||
rootPath, err := newTestConfig("us-east-1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root folder after the test ends.
|
|
||||||
defer removeAll(rootPath)
|
|
||||||
|
|
||||||
disk, err := getRandomDisks(1)
|
|
||||||
defer removeAll(disk[0])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
|
||||||
}
|
|
||||||
obj, _, err := initObjectLayer(disk, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to initialize FS backend.", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the bucket to listen on
|
// Create the bucket to listen on
|
||||||
if err := obj.MakeBucket(bucketName); err != nil {
|
if err := obj.MakeBucket(bucketName); err != nil {
|
||||||
t.Fatal("Unexpected error:", err)
|
t.Fatal("Unexpected error:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
listenARN := "arn:minio:sns:us-east-1:1:listen"
|
listenARN := "arn:minio:sns:us-east-1:1:listen-" + globalMinioAddr
|
||||||
queueARN := "arn:minio:sqs:us-east-1:1:redis"
|
lcfg := listenerConfig{
|
||||||
|
topicConfig{
|
||||||
|
ServiceConfig{
|
||||||
|
[]string{"s3:ObjectRemoved:*", "s3:ObjectCreated:*"},
|
||||||
|
filterStruct{},
|
||||||
|
"0",
|
||||||
|
},
|
||||||
|
listenARN,
|
||||||
|
},
|
||||||
|
globalMinioAddr,
|
||||||
|
}
|
||||||
|
|
||||||
fs := obj.(fsObjects)
|
// write listener config to storage layer
|
||||||
storage := fs.storage.(*posix)
|
lcfgs := []listenerConfig{lcfg}
|
||||||
|
if err := persistListenerConfig(bucketName, lcfgs, obj); err != nil {
|
||||||
// Create and store notification.xml with listen and queue notification configured
|
t.Fatalf("Test Setup error: %v", err)
|
||||||
notificationXML := "<NotificationConfiguration>"
|
|
||||||
notificationXML += "<TopicConfiguration><Event>s3:ObjectRemoved:*</Event><Event>s3:ObjectRemoved:*</Event><Topic>" + listenARN + "</Topic></TopicConfiguration>"
|
|
||||||
notificationXML += "<QueueConfiguration><Event>s3:ObjectRemoved:*</Event><Event>s3:ObjectRemoved:*</Event><Queue>" + queueARN + "</Queue></QueueConfiguration>"
|
|
||||||
notificationXML += "</NotificationConfiguration>"
|
|
||||||
if err := storage.AppendFile(minioMetaBucket, bucketConfigPrefix+"/"+bucketName+"/"+bucketNotificationConfig, []byte(notificationXML)); err != nil {
|
|
||||||
t.Fatal("Unexpected error:", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init event notifier
|
// Init event notifier
|
||||||
if err := initEventNotifier(fs); err != nil {
|
if err := initEventNotifier(obj); err != nil {
|
||||||
t.Fatal("Unexpected error:", err)
|
t.Fatal("Unexpected error:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the config is loaded
|
// Check if the config is loaded
|
||||||
notificationCfg := globalEventNotifier.GetBucketNotificationConfig(bucketName)
|
listenerCfg := globalEventNotifier.GetBucketListenerConfig(bucketName)
|
||||||
if notificationCfg == nil {
|
if listenerCfg == nil {
|
||||||
t.Fatal("Cannot load bucket notification config")
|
t.Fatal("Cannot load bucket listener config")
|
||||||
}
|
}
|
||||||
if len(notificationCfg.TopicConfigs) != 1 || len(notificationCfg.QueueConfigs) != 1 {
|
if len(listenerCfg) != 1 {
|
||||||
t.Fatal("Notification config is not correctly loaded. Exactly one topic and one queue config are expected")
|
t.Fatal("Listener config is not correctly loaded. Exactly one listener config is expected")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if topic ARN is enabled
|
// Check if topic ARN is correct
|
||||||
if notificationCfg.TopicConfigs[0].TopicARN != listenARN {
|
if listenerCfg[0].TopicConfig.TopicARN != listenARN {
|
||||||
t.Fatal("SNS listen is not configured.")
|
t.Fatal("Configured topic ARN is incorrect.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new notification event channel.
|
// Create a new notification event channel.
|
||||||
nEventCh := make(chan []NotificationEvent)
|
nEventCh := make(chan []NotificationEvent)
|
||||||
// Close the listener channel.
|
// Close the listener channel.
|
||||||
defer close(nEventCh)
|
defer close(nEventCh)
|
||||||
// Set sns target.
|
// Add events channel for listener.
|
||||||
globalEventNotifier.SetSNSTarget(listenARN, nEventCh)
|
if err := globalEventNotifier.AddListenerChan(listenARN, nEventCh); err != nil {
|
||||||
// Remove sns listener after the writer has closed or the client disconnected.
|
t.Fatalf("Test Setup error: %v", err)
|
||||||
defer globalEventNotifier.RemoveSNSTarget(listenARN, nEventCh)
|
}
|
||||||
|
// Remove listen channel after the writer has closed or the
|
||||||
|
// client disconnected.
|
||||||
|
defer globalEventNotifier.RemoveListenerChan(listenARN)
|
||||||
|
|
||||||
// Fire an event notification
|
// Fire an event notification
|
||||||
go eventNotify(eventData{
|
go eventNotify(eventData{
|
||||||
@ -370,73 +406,90 @@ func TestListenBucketNotification(t *testing.T) {
|
|||||||
t.Fatalf("Received wrong object name in notification, expected %s, received %s", n[0].S3.Object.Key, objectName)
|
t.Fatalf("Received wrong object name in notification, expected %s, received %s", n[0].S3.Object.Key, objectName)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case <-time.After(30 * time.Second):
|
case <-time.After(3 * time.Second):
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAddTopicConfig(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
func TestAddRemoveBucketListenerConfig(t *testing.T) {
|
||||||
root, cErr := newTestConfig("us-east-1")
|
s := TestPeerRPCServerData{serverType: "XL"}
|
||||||
if cErr != nil {
|
|
||||||
t.Fatalf("[%s] Failed to initialize test config: %v", instanceType, cErr)
|
|
||||||
}
|
|
||||||
defer removeAll(root)
|
|
||||||
|
|
||||||
|
// setup and teardown
|
||||||
|
s.Setup(t)
|
||||||
|
defer s.TearDown()
|
||||||
|
|
||||||
|
// test code
|
||||||
|
obj := s.testServer.Obj
|
||||||
if err := initEventNotifier(obj); err != nil {
|
if err := initEventNotifier(obj); err != nil {
|
||||||
t.Fatalf("[%s] : Failed to initialize event notifier: %v", instanceType, err)
|
t.Fatalf("Failed to initialize event notifier: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a bucket to store topicConfigs.
|
// Make a bucket to store topicConfigs.
|
||||||
randBucket := getRandomBucketName()
|
randBucket := getRandomBucketName()
|
||||||
if err := obj.MakeBucket(randBucket); err != nil {
|
if err := obj.MakeBucket(randBucket); err != nil {
|
||||||
t.Fatalf("[%s] : Failed to make bucket %s", instanceType, randBucket)
|
t.Fatalf("Failed to make bucket %s", randBucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a topicConfig to an empty notificationConfig.
|
// Add a topicConfig to an empty notificationConfig.
|
||||||
accountID := fmt.Sprintf("%d", time.Now().UTC().UnixNano())
|
accountID := fmt.Sprintf("%d", time.Now().UTC().UnixNano())
|
||||||
accountARN := "arn:minio:sns:" + serverConfig.GetRegion() + accountID + ":listen"
|
accountARN := fmt.Sprintf(
|
||||||
var filterRules []filterRule
|
"arn:minio:sqs:%s:%s:listen-%s",
|
||||||
filterRules = append(filterRules, filterRule{
|
serverConfig.GetRegion(),
|
||||||
Name: "prefix",
|
accountID,
|
||||||
Value: "minio",
|
globalMinioAddr,
|
||||||
})
|
)
|
||||||
filterRules = append(filterRules, filterRule{
|
|
||||||
Name: "suffix",
|
|
||||||
Value: "*.jpg",
|
|
||||||
})
|
|
||||||
|
|
||||||
// Make topic configuration corresponding to this ListenBucketNotification request.
|
// Make topic configuration
|
||||||
sampleTopicCfg := &topicConfig{
|
filterRules := []filterRule{
|
||||||
TopicARN: accountARN,
|
{
|
||||||
serviceConfig: serviceConfig{
|
Name: "prefix",
|
||||||
Filter: struct {
|
Value: "minio",
|
||||||
Key keyFilter `xml:"S3Key,omitempty"`
|
},
|
||||||
}{
|
{
|
||||||
Key: keyFilter{
|
Name: "suffix",
|
||||||
FilterRules: filterRules,
|
Value: "*.jpg",
|
||||||
},
|
|
||||||
},
|
|
||||||
ID: "sns-" + accountID,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
sampleTopicCfg := topicConfig{
|
||||||
|
TopicARN: accountARN,
|
||||||
|
ServiceConfig: ServiceConfig{
|
||||||
|
[]string{"s3:ObjectRemoved:*", "s3:ObjectCreated:*"},
|
||||||
|
filterStruct{
|
||||||
|
keyFilter{filterRules},
|
||||||
|
},
|
||||||
|
"sns-" + accountID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sampleListenerCfg := &listenerConfig{
|
||||||
|
TopicConfig: sampleTopicCfg,
|
||||||
|
TargetServer: globalMinioAddr,
|
||||||
|
}
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
topicCfg *topicConfig
|
lCfg *listenerConfig
|
||||||
expectedErr error
|
expectedErr error
|
||||||
}{
|
}{
|
||||||
{sampleTopicCfg, nil},
|
{sampleListenerCfg, nil},
|
||||||
{nil, errInvalidArgument},
|
{nil, errInvalidArgument},
|
||||||
{sampleTopicCfg, nil},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range testCases {
|
for i, test := range testCases {
|
||||||
err := globalEventNotifier.AddTopicConfig(randBucket, test.topicCfg)
|
err := AddBucketListenerConfig(randBucket, test.lCfg, obj)
|
||||||
if err != test.expectedErr {
|
if err != test.expectedErr {
|
||||||
t.Errorf("Test %d: %s failed with error %v, expected to fail with %v",
|
t.Errorf(
|
||||||
i+1, instanceType, err, test.expectedErr)
|
"Test %d: Failed with error %v, expected to fail with %v",
|
||||||
|
i+1, err, test.expectedErr,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddTopicConfig(t *testing.T) {
|
// test remove listener actually removes a listener
|
||||||
ExecObjectLayerTest(t, testAddTopicConfig)
|
RemoveBucketListenerConfig(randBucket, sampleListenerCfg, obj)
|
||||||
|
// since it does not return errors we fetch the config and
|
||||||
|
// check
|
||||||
|
lcSlice := globalEventNotifier.GetBucketListenerConfig(randBucket)
|
||||||
|
if len(lcSlice) != 0 {
|
||||||
|
t.Errorf("Remove Listener Config Test: did not remove listener config - %v",
|
||||||
|
lcSlice)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,8 +52,13 @@ var (
|
|||||||
globalMaxCacheSize = uint64(maxCacheSize)
|
globalMaxCacheSize = uint64(maxCacheSize)
|
||||||
// Cache expiry.
|
// Cache expiry.
|
||||||
globalCacheExpiry = objcache.DefaultExpiry
|
globalCacheExpiry = objcache.DefaultExpiry
|
||||||
|
// Minio local server address (in `host:port` format)
|
||||||
|
globalMinioAddr = ""
|
||||||
// Minio default port, can be changed through command line.
|
// Minio default port, can be changed through command line.
|
||||||
globalMinioPort = 9000
|
globalMinioPort = 9000
|
||||||
|
// Peer communication struct
|
||||||
|
globalS3Peers = s3Peers{}
|
||||||
|
|
||||||
// Add new variable global values here.
|
// Add new variable global values here.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
@ -50,10 +51,10 @@ type logger struct {
|
|||||||
func funcFromPC(pc uintptr, file string, line int, shortFile bool) string {
|
func funcFromPC(pc uintptr, file string, line int, shortFile bool) string {
|
||||||
var fn, name string
|
var fn, name string
|
||||||
if shortFile {
|
if shortFile {
|
||||||
fn = strings.Replace(file, filepath.ToSlash(GOPATH)+"/src/github.com/minio/minio/cmd/", "", -1)
|
fn = strings.Replace(file, path.Join(filepath.ToSlash(GOPATH)+"/src/github.com/minio/minio/cmd/")+"/", "", -1)
|
||||||
name = strings.Replace(runtime.FuncForPC(pc).Name(), "github.com/minio/minio/cmd.", "", -1)
|
name = strings.Replace(runtime.FuncForPC(pc).Name(), "github.com/minio/minio/cmd.", "", -1)
|
||||||
} else {
|
} else {
|
||||||
fn = strings.Replace(file, filepath.ToSlash(GOPATH)+"/src/", "", -1)
|
fn = strings.Replace(file, path.Join(filepath.ToSlash(GOPATH)+"/src/")+"/", "", -1)
|
||||||
name = strings.Replace(runtime.FuncForPC(pc).Name(), "github.com/minio/minio/cmd.", "", -1)
|
name = strings.Replace(runtime.FuncForPC(pc).Name(), "github.com/minio/minio/cmd.", "", -1)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s [%s:%d]", name, fn, line)
|
return fmt.Sprintf("%s [%s:%d]", name, fn, line)
|
||||||
|
82
cmd/notify-listener.go
Normal file
82
cmd/notify-listener.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* 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 (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listenerConn struct {
|
||||||
|
Client *AuthRPCClient
|
||||||
|
ListenerARN string
|
||||||
|
}
|
||||||
|
|
||||||
|
type listenerLogger struct {
|
||||||
|
log *logrus.Logger
|
||||||
|
lconn listenerConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListenerLogger(listenerArn, targetAddr string) (*listenerLogger, error) {
|
||||||
|
client := globalS3Peers.GetPeerClient(targetAddr)
|
||||||
|
if client == nil {
|
||||||
|
return nil, fmt.Errorf("Peer %s was not initialized - bug!",
|
||||||
|
targetAddr)
|
||||||
|
}
|
||||||
|
lc := listenerConn{
|
||||||
|
Client: client,
|
||||||
|
ListenerARN: listenerArn,
|
||||||
|
}
|
||||||
|
|
||||||
|
lcLog := logrus.New()
|
||||||
|
|
||||||
|
lcLog.Out = ioutil.Discard
|
||||||
|
|
||||||
|
lcLog.Formatter = new(logrus.JSONFormatter)
|
||||||
|
|
||||||
|
lcLog.Hooks.Add(lc)
|
||||||
|
|
||||||
|
return &listenerLogger{lcLog, lc}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc listenerConn) Close() {
|
||||||
|
// ignore closing errors
|
||||||
|
_ = lc.Client.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// send event to target server via rpc client calls.
|
||||||
|
func (lc listenerConn) Fire(entry *logrus.Entry) error {
|
||||||
|
notificationEvent, ok := entry.Data["Records"].([]NotificationEvent)
|
||||||
|
if !ok {
|
||||||
|
// If the record is not of the expected type, silently
|
||||||
|
// discard.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
evArgs := EventArgs{Event: notificationEvent, Arn: lc.ListenerARN}
|
||||||
|
reply := GenericReply{}
|
||||||
|
err := lc.Client.Call("S3.Event", &evArgs, &reply)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc listenerConn) Levels() []logrus.Level {
|
||||||
|
return []logrus.Level{
|
||||||
|
logrus.InfoLevel,
|
||||||
|
}
|
||||||
|
}
|
@ -80,6 +80,9 @@ func configureServerHandler(srvCmdConfig serverCmdConfig) http.Handler {
|
|||||||
registerDistNSLockRouter(mux, srvCmdConfig)
|
registerDistNSLockRouter(mux, srvCmdConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register S3 peer communication router.
|
||||||
|
registerS3PeerRPCRouter(mux)
|
||||||
|
|
||||||
// Register controller rpc router.
|
// Register controller rpc router.
|
||||||
registerControlRPCRouter(mux, srvCmdConfig)
|
registerControlRPCRouter(mux, srvCmdConfig)
|
||||||
|
|
||||||
|
176
cmd/s3-peer-client.go
Normal file
176
cmd/s3-peer-client.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2014-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 (
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/pkg/set"
|
||||||
|
)
|
||||||
|
|
||||||
|
type s3Peers struct {
|
||||||
|
// A map of peer server address (in `host:port` format) to RPC
|
||||||
|
// client connections
|
||||||
|
rpcClient map[string]*AuthRPCClient
|
||||||
|
|
||||||
|
// slice of all peer addresses (in `host:port` format)
|
||||||
|
peers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func initGlobalS3Peers(disks []string) {
|
||||||
|
// get list of de-duplicated peers
|
||||||
|
peers := getAllPeers(disks)
|
||||||
|
globalS3Peers = s3Peers{make(map[string]*AuthRPCClient), nil}
|
||||||
|
for _, peer := range peers {
|
||||||
|
globalS3Peers.InitS3PeerClient(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additionally setup a local peer if one does not exist
|
||||||
|
if globalS3Peers.GetPeerClient(globalMinioAddr) == nil {
|
||||||
|
globalS3Peers.InitS3PeerClient(globalMinioAddr)
|
||||||
|
peers = append(peers, globalMinioAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
globalS3Peers.peers = peers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s3p *s3Peers) GetPeers() []string {
|
||||||
|
return s3p.peers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s3p *s3Peers) GetPeerClient(peer string) *AuthRPCClient {
|
||||||
|
return s3p.rpcClient[peer]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes a new RPC connection (or closes and re-opens if it
|
||||||
|
// already exists) to a peer. Note that peer address is in `host:port`
|
||||||
|
// format.
|
||||||
|
func (s3p *s3Peers) InitS3PeerClient(peer string) {
|
||||||
|
if s3p.rpcClient[peer] != nil {
|
||||||
|
s3p.rpcClient[peer].Close()
|
||||||
|
delete(s3p.rpcClient, peer)
|
||||||
|
}
|
||||||
|
authCfg := &authConfig{
|
||||||
|
accessKey: serverConfig.GetCredential().AccessKeyID,
|
||||||
|
secretKey: serverConfig.GetCredential().SecretAccessKey,
|
||||||
|
address: peer,
|
||||||
|
path: path.Join(reservedBucket, s3Path),
|
||||||
|
loginMethod: "S3.LoginHandler",
|
||||||
|
}
|
||||||
|
s3p.rpcClient[peer] = newAuthClient(authCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s3p *s3Peers) Close() error {
|
||||||
|
for _, v := range s3p.rpcClient {
|
||||||
|
if err := v.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s3p.rpcClient = nil
|
||||||
|
s3p.peers = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the network addresses of all Minio servers in the cluster
|
||||||
|
// in `host:port` format.
|
||||||
|
func getAllPeers(disks []string) []string {
|
||||||
|
res := []string{}
|
||||||
|
// use set to de-duplicate
|
||||||
|
sset := set.NewStringSet()
|
||||||
|
for _, disk := range disks {
|
||||||
|
netAddr, _, err := splitNetPath(disk)
|
||||||
|
if err != nil || netAddr == "" {
|
||||||
|
errorIf(err, "Unexpected error - most likely a bug.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !sset.Contains(netAddr) {
|
||||||
|
res = append(
|
||||||
|
res,
|
||||||
|
fmt.Sprintf("%s:%d", netAddr, globalMinioPort),
|
||||||
|
)
|
||||||
|
sset.Add(netAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make RPC calls with the given method and arguments to all the given
|
||||||
|
// peers (in parallel), and collects the results. Since the methods
|
||||||
|
// intended for use here, have only a success or failure response, we
|
||||||
|
// do not return/inspect the `reply` parameter in the RPC call. The
|
||||||
|
// function attempts to connect to a peer only once, and returns a map
|
||||||
|
// of peer address to error response. If the error is nil, it means
|
||||||
|
// the RPC succeeded.
|
||||||
|
func (s3p *s3Peers) SendRPC(peers []string, method string, args interface {
|
||||||
|
SetToken(token string)
|
||||||
|
SetTimestamp(tstamp time.Time)
|
||||||
|
}) map[string]error {
|
||||||
|
// result type
|
||||||
|
type callResult struct {
|
||||||
|
target string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
// channel to collect results from goroutines
|
||||||
|
resChan := make(chan callResult)
|
||||||
|
// closure to make a single request.
|
||||||
|
callTarget := func(target string) {
|
||||||
|
reply := &GenericReply{}
|
||||||
|
err := s3p.rpcClient[target].Call(method, args, reply)
|
||||||
|
resChan <- callResult{target, err}
|
||||||
|
}
|
||||||
|
// map of errors
|
||||||
|
errsMap := make(map[string]error)
|
||||||
|
// make network calls in parallel
|
||||||
|
for _, target := range peers {
|
||||||
|
go callTarget(target)
|
||||||
|
}
|
||||||
|
// wait on channel and collect all results
|
||||||
|
for range peers {
|
||||||
|
res := <-resChan
|
||||||
|
if res.err != nil {
|
||||||
|
errsMap[res.target] = res.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return errors map
|
||||||
|
return errsMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3PeersUpdateBucketNotification - Sends Update Bucket notification
|
||||||
|
// request to all peers. Currently we log an error and continue.
|
||||||
|
func S3PeersUpdateBucketNotification(bucket string, ncfg *notificationConfig) {
|
||||||
|
setBNPArgs := &SetBNPArgs{Bucket: bucket, NCfg: ncfg}
|
||||||
|
peers := globalS3Peers.GetPeers()
|
||||||
|
errsMap := globalS3Peers.SendRPC(peers, "S3.SetBucketNotificationPeer",
|
||||||
|
setBNPArgs)
|
||||||
|
for peer, err := range errsMap {
|
||||||
|
errorIf(err, "Error sending peer update bucket notification to %s - %v", peer, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3PeersUpdateBucketListener - Sends Update Bucket listeners request
|
||||||
|
// to all peers. Currently we log an error and continue.
|
||||||
|
func S3PeersUpdateBucketListener(bucket string, lcfg []listenerConfig) {
|
||||||
|
setBLPArgs := &SetBLPArgs{Bucket: bucket, LCfg: lcfg}
|
||||||
|
peers := globalS3Peers.GetPeers()
|
||||||
|
errsMap := globalS3Peers.SendRPC(peers, "S3.SetBucketListenerPeer",
|
||||||
|
setBLPArgs)
|
||||||
|
for peer, err := range errsMap {
|
||||||
|
errorIf(err, "Error sending peer update bucket listener to %s - %v", peer, err)
|
||||||
|
}
|
||||||
|
}
|
43
cmd/s3-peer-router.go
Normal file
43
cmd/s3-peer-router.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2014-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 (
|
||||||
|
"net/rpc"
|
||||||
|
|
||||||
|
router "github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
s3Path = "/s3/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
type s3PeerAPIHandlers struct {
|
||||||
|
ObjectAPI func() ObjectLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerS3PeerRPCRouter(mux *router.Router) {
|
||||||
|
s3PeerHandlers := &s3PeerAPIHandlers{
|
||||||
|
ObjectAPI: newObjectLayerFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
s3PeerRPCServer := rpc.NewServer()
|
||||||
|
s3PeerRPCServer.RegisterName("S3", s3PeerHandlers)
|
||||||
|
|
||||||
|
s3PeerRouter := mux.NewRoute().PathPrefix(reservedBucket).Subrouter()
|
||||||
|
s3PeerRouter.Path(s3Path).Handler(s3PeerRPCServer)
|
||||||
|
}
|
123
cmd/s3-peer-rpc-handlers.go
Normal file
123
cmd/s3-peer-rpc-handlers.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2014-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 "time"
|
||||||
|
|
||||||
|
func (s3 *s3PeerAPIHandlers) LoginHandler(args *RPCLoginArgs, reply *RPCLoginReply) error {
|
||||||
|
jwt, err := newJWT(defaultInterNodeJWTExpiry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = jwt.Authenticate(args.Username, args.Password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
token, err := jwt.GenerateToken(args.Username)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reply.Token = token
|
||||||
|
reply.ServerVersion = Version
|
||||||
|
reply.Timestamp = time.Now().UTC()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBNPArgs - Arguments collection to SetBucketNotificationPeer RPC
|
||||||
|
// call
|
||||||
|
type SetBNPArgs struct {
|
||||||
|
// For Auth
|
||||||
|
GenericArgs
|
||||||
|
|
||||||
|
Bucket string
|
||||||
|
|
||||||
|
// Notification config for the given bucket.
|
||||||
|
NCfg *notificationConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s3 *s3PeerAPIHandlers) SetBucketNotificationPeer(args *SetBNPArgs, reply *GenericReply) error {
|
||||||
|
// check auth
|
||||||
|
if !isRPCTokenValid(args.Token) {
|
||||||
|
return errInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if object layer is available.
|
||||||
|
objAPI := s3.ObjectAPI()
|
||||||
|
if objAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update in-memory notification config.
|
||||||
|
globalEventNotifier.SetBucketNotificationConfig(args.Bucket, args.NCfg)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBLPArgs - Arguments collection to SetBucketListenerPeer RPC call
|
||||||
|
type SetBLPArgs struct {
|
||||||
|
// For Auth
|
||||||
|
GenericArgs
|
||||||
|
|
||||||
|
Bucket string
|
||||||
|
|
||||||
|
// Listener config for a given bucket.
|
||||||
|
LCfg []listenerConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s3 *s3PeerAPIHandlers) SetBucketListenerPeer(args SetBLPArgs, reply *GenericReply) error {
|
||||||
|
// check auth
|
||||||
|
if !isRPCTokenValid(args.Token) {
|
||||||
|
return errInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if object layer is available.
|
||||||
|
objAPI := s3.ObjectAPI()
|
||||||
|
if objAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update in-memory notification config.
|
||||||
|
return globalEventNotifier.SetBucketListenerConfig(args.Bucket, args.LCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventArgs - Arguments collection for Event RPC call
|
||||||
|
type EventArgs struct {
|
||||||
|
// For Auth
|
||||||
|
GenericArgs
|
||||||
|
|
||||||
|
// event being sent
|
||||||
|
Event []NotificationEvent
|
||||||
|
|
||||||
|
// client that it is meant for
|
||||||
|
Arn string
|
||||||
|
}
|
||||||
|
|
||||||
|
// submit an event to the receiving server.
|
||||||
|
func (s3 *s3PeerAPIHandlers) Event(args *EventArgs, reply *GenericReply) error {
|
||||||
|
// check auth
|
||||||
|
if !isRPCTokenValid(args.Token) {
|
||||||
|
return errInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if object layer is available.
|
||||||
|
objAPI := s3.ObjectAPI()
|
||||||
|
if objAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
err := globalEventNotifier.SendListenerEvent(args.Arn, args.Event)
|
||||||
|
return err
|
||||||
|
}
|
@ -386,6 +386,12 @@ func serverMain(c *cli.Context) {
|
|||||||
globalObjectAPI = newObject
|
globalObjectAPI = newObject
|
||||||
globalObjLayerMutex.Unlock()
|
globalObjLayerMutex.Unlock()
|
||||||
|
|
||||||
|
// Initialize local server address
|
||||||
|
globalMinioAddr = getLocalAddress(srvConfig)
|
||||||
|
|
||||||
|
// Initialize S3 Peers inter-node communication
|
||||||
|
initGlobalS3Peers(disks)
|
||||||
|
|
||||||
// Initialize a new event notifier.
|
// Initialize a new event notifier.
|
||||||
err = initEventNotifier(newObjectLayerFn())
|
err = initEventNotifier(newObjectLayerFn())
|
||||||
fatalIf(err, "Unable to initialize event notification.")
|
fatalIf(err, "Unable to initialize event notification.")
|
||||||
|
@ -83,10 +83,10 @@ func printEventNotifiers() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
arnMsg := colorBlue("SQS ARNs: ")
|
arnMsg := colorBlue("SQS ARNs: ")
|
||||||
if len(globalEventNotifier.queueTargets) == 0 {
|
if len(globalEventNotifier.external.targets) == 0 {
|
||||||
arnMsg += colorBold(fmt.Sprintf(getFormatStr(len("<none>"), 1), "<none>"))
|
arnMsg += colorBold(fmt.Sprintf(getFormatStr(len("<none>"), 1), "<none>"))
|
||||||
}
|
}
|
||||||
for queueArn := range globalEventNotifier.queueTargets {
|
for queueArn := range globalEventNotifier.external.targets {
|
||||||
arnMsg += colorBold(fmt.Sprintf(getFormatStr(len(queueArn), 1), queueArn))
|
arnMsg += colorBold(fmt.Sprintf(getFormatStr(len(queueArn), 1), queueArn))
|
||||||
}
|
}
|
||||||
console.Println(arnMsg)
|
console.Println(arnMsg)
|
||||||
|
@ -105,125 +105,6 @@ func (s *TestSuiteCommon) TestBucketSQSNotification(c *C) {
|
|||||||
verifyError(c, response, "InvalidArgument", "A specified destination ARN does not exist or is not well-formed. Verify the destination ARN.", http.StatusBadRequest)
|
verifyError(c, response, "InvalidArgument", "A specified destination ARN does not exist or is not well-formed. Verify the destination ARN.", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBucketNotification - Inserts the bucket notification and verifies it by fetching the notification back.
|
|
||||||
func (s *TestSuiteCommon) TestBucketSNSNotification(c *C) {
|
|
||||||
// Sample bucket notification.
|
|
||||||
bucketNotificationBuf := `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:444455556666:listen</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
|
|
||||||
// generate a random bucket Name.
|
|
||||||
bucketName := getRandomBucketName()
|
|
||||||
// HTTP request to create the bucket.
|
|
||||||
request, err := newTestSignedRequestV4("PUT", getMakeBucketURL(s.endPoint, bucketName),
|
|
||||||
0, nil, s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client := http.Client{}
|
|
||||||
// execute the request.
|
|
||||||
response, err := client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
// assert the http response status code.
|
|
||||||
c.Assert(response.StatusCode, Equals, http.StatusOK)
|
|
||||||
|
|
||||||
request, err = newTestSignedRequestV4("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(bucketNotificationBuf)), bytes.NewReader([]byte(bucketNotificationBuf)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
c.Assert(response.StatusCode, Equals, http.StatusOK)
|
|
||||||
|
|
||||||
// Fetch the uploaded policy.
|
|
||||||
request, err = newTestSignedRequestV4("GET", getGetNotificationURL(s.endPoint, bucketName), 0, nil,
|
|
||||||
s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
c.Assert(response.StatusCode, Equals, http.StatusOK)
|
|
||||||
|
|
||||||
bucketNotificationReadBuf, err := ioutil.ReadAll(response.Body)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
// Verify if downloaded policy matches with previousy uploaded.
|
|
||||||
c.Assert(bytes.Equal([]byte(bucketNotificationBuf), bucketNotificationReadBuf), Equals, true)
|
|
||||||
|
|
||||||
invalidBucketNotificationBuf := `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>invalid</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:444455556666:minio</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
|
|
||||||
request, err = newTestSignedRequestV4("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(invalidBucketNotificationBuf)), bytes.NewReader([]byte(invalidBucketNotificationBuf)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
verifyError(c, response, "InvalidArgument", "A specified destination ARN does not exist or is not well-formed. Verify the destination ARN.", http.StatusBadRequest)
|
|
||||||
|
|
||||||
invalidBucketNotificationBuf = `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>invalid</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:1:listen</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
|
|
||||||
request, err = newTestSignedRequestV4("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(invalidBucketNotificationBuf)), bytes.NewReader([]byte(invalidBucketNotificationBuf)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
verifyError(c, response, "InvalidArgument", "filter rule name must be either prefix or suffix", http.StatusBadRequest)
|
|
||||||
|
|
||||||
invalidBucketNotificationBuf = `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>hello\</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:1:listen</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
|
|
||||||
request, err = newTestSignedRequestV4("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(invalidBucketNotificationBuf)), bytes.NewReader([]byte(invalidBucketNotificationBuf)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
verifyError(c, response, "InvalidArgument", "Size of filter rule value cannot exceed 1024 bytes in UTF-8 representation", http.StatusBadRequest)
|
|
||||||
|
|
||||||
invalidBucketNotificationBuf = `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-west-1:444455556666:listen</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
request, err = newTestSignedRequestV4("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(invalidBucketNotificationBuf)), bytes.NewReader([]byte(invalidBucketNotificationBuf)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
verifyError(c, response, "InvalidArgument", "A specified destination is in a different region than the bucket. You must use a destination that resides in the same region as the bucket.", http.StatusBadRequest)
|
|
||||||
|
|
||||||
invalidBucketNotificationBuf = `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Invalid</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:444455556666:listen</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
request, err = newTestSignedRequestV4("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(invalidBucketNotificationBuf)), bytes.NewReader([]byte(invalidBucketNotificationBuf)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
verifyError(c, response, "InvalidArgument", "A specified event is not supported for notifications.", http.StatusBadRequest)
|
|
||||||
|
|
||||||
bucketNotificationDuplicates := `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:444455556666:listen</Topic></TopicConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:444455556666:listen</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
request, err = newTestSignedRequestV4("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(bucketNotificationDuplicates)), bytes.NewReader([]byte(bucketNotificationDuplicates)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
verifyError(c, response, "InvalidArgument", "Configurations overlap. Configurations on the same bucket cannot share a common event type.", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestBucketPolicy - Inserts the bucket policy and verifies it by fetching the policy back.
|
// TestBucketPolicy - Inserts the bucket policy and verifies it by fetching the policy back.
|
||||||
// Deletes the policy and verifies the deletion by fetching it back.
|
// Deletes the policy and verifies the deletion by fetching it back.
|
||||||
func (s *TestSuiteCommon) TestBucketPolicy(c *C) {
|
func (s *TestSuiteCommon) TestBucketPolicy(c *C) {
|
||||||
|
@ -102,125 +102,6 @@ func (s *TestSuiteCommonV2) TestBucketSQSNotification(c *C) {
|
|||||||
verifyError(c, response, "InvalidArgument", "A specified destination ARN does not exist or is not well-formed. Verify the destination ARN.", http.StatusBadRequest)
|
verifyError(c, response, "InvalidArgument", "A specified destination ARN does not exist or is not well-formed. Verify the destination ARN.", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBucketNotification - Inserts the bucket notification and verifies it by fetching the notification back.
|
|
||||||
func (s *TestSuiteCommonV2) TestBucketSNSNotification(c *C) {
|
|
||||||
// Sample bucket notification.
|
|
||||||
bucketNotificationBuf := `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:444455556666:listen</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
|
|
||||||
// generate a random bucket Name.
|
|
||||||
bucketName := getRandomBucketName()
|
|
||||||
// HTTP request to create the bucket.
|
|
||||||
request, err := newTestSignedRequestV2("PUT", getMakeBucketURL(s.endPoint, bucketName),
|
|
||||||
0, nil, s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client := http.Client{}
|
|
||||||
// execute the request.
|
|
||||||
response, err := client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
// assert the http response status code.
|
|
||||||
c.Assert(response.StatusCode, Equals, http.StatusOK)
|
|
||||||
|
|
||||||
request, err = newTestSignedRequestV2("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(bucketNotificationBuf)), bytes.NewReader([]byte(bucketNotificationBuf)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
c.Assert(response.StatusCode, Equals, http.StatusOK)
|
|
||||||
|
|
||||||
// Fetch the uploaded policy.
|
|
||||||
request, err = newTestSignedRequestV2("GET", getGetNotificationURL(s.endPoint, bucketName), 0, nil,
|
|
||||||
s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
c.Assert(response.StatusCode, Equals, http.StatusOK)
|
|
||||||
|
|
||||||
bucketNotificationReadBuf, err := ioutil.ReadAll(response.Body)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
// Verify if downloaded policy matches with previousy uploaded.
|
|
||||||
c.Assert(bytes.Equal([]byte(bucketNotificationBuf), bucketNotificationReadBuf), Equals, true)
|
|
||||||
|
|
||||||
invalidBucketNotificationBuf := `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>invalid</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:444455556666:minio</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
|
|
||||||
request, err = newTestSignedRequestV2("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(invalidBucketNotificationBuf)), bytes.NewReader([]byte(invalidBucketNotificationBuf)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
verifyError(c, response, "InvalidArgument", "A specified destination ARN does not exist or is not well-formed. Verify the destination ARN.", http.StatusBadRequest)
|
|
||||||
|
|
||||||
invalidBucketNotificationBuf = `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>invalid</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:1:listen</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
|
|
||||||
request, err = newTestSignedRequestV2("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(invalidBucketNotificationBuf)), bytes.NewReader([]byte(invalidBucketNotificationBuf)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
verifyError(c, response, "InvalidArgument", "filter rule name must be either prefix or suffix", http.StatusBadRequest)
|
|
||||||
|
|
||||||
invalidBucketNotificationBuf = `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>hello\</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:1:listen</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
|
|
||||||
request, err = newTestSignedRequestV2("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(invalidBucketNotificationBuf)), bytes.NewReader([]byte(invalidBucketNotificationBuf)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
verifyError(c, response, "InvalidArgument", "Size of filter rule value cannot exceed 1024 bytes in UTF-8 representation", http.StatusBadRequest)
|
|
||||||
|
|
||||||
invalidBucketNotificationBuf = `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-west-1:444455556666:listen</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
request, err = newTestSignedRequestV2("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(invalidBucketNotificationBuf)), bytes.NewReader([]byte(invalidBucketNotificationBuf)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
verifyError(c, response, "InvalidArgument", "A specified destination is in a different region than the bucket. You must use a destination that resides in the same region as the bucket.", http.StatusBadRequest)
|
|
||||||
|
|
||||||
invalidBucketNotificationBuf = `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Invalid</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:444455556666:listen</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
request, err = newTestSignedRequestV2("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(invalidBucketNotificationBuf)), bytes.NewReader([]byte(invalidBucketNotificationBuf)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
verifyError(c, response, "InvalidArgument", "A specified event is not supported for notifications.", http.StatusBadRequest)
|
|
||||||
|
|
||||||
bucketNotificationDuplicates := `<NotificationConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:444455556666:listen</Topic></TopicConfiguration><TopicConfiguration><Event>s3:ObjectCreated:Put</Event><Filter><S3Key><FilterRule><Name>prefix</Name><Value>images/</Value></FilterRule></S3Key></Filter><Id>1</Id><Topic>arn:minio:sns:us-east-1:444455556666:listen</Topic></TopicConfiguration></NotificationConfiguration>`
|
|
||||||
request, err = newTestSignedRequestV2("PUT", getPutNotificationURL(s.endPoint, bucketName),
|
|
||||||
int64(len(bucketNotificationDuplicates)), bytes.NewReader([]byte(bucketNotificationDuplicates)), s.accessKey, s.secretKey)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
|
|
||||||
client = http.Client{}
|
|
||||||
// execute the HTTP request.
|
|
||||||
response, err = client.Do(request)
|
|
||||||
c.Assert(err, IsNil)
|
|
||||||
verifyError(c, response, "InvalidArgument", "Configurations overlap. Configurations on the same bucket cannot share a common event type.", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestBucketPolicy - Inserts the bucket policy and verifies it by fetching the policy back.
|
// TestBucketPolicy - Inserts the bucket policy and verifies it by fetching the policy back.
|
||||||
// Deletes the policy and verifies the deletion by fetching it back.
|
// Deletes the policy and verifies the deletion by fetching it back.
|
||||||
func (s *TestSuiteCommonV2) TestBucketPolicy(c *C) {
|
func (s *TestSuiteCommonV2) TestBucketPolicy(c *C) {
|
||||||
|
@ -147,7 +147,7 @@ func getSignature(signingKey []byte, stringToSign string) string {
|
|||||||
|
|
||||||
// doesPolicySignatureMatch - Verify query headers with post policy
|
// doesPolicySignatureMatch - Verify query headers with post policy
|
||||||
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
|
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
|
||||||
// returns true if matches, false otherwise. if error is not nil then it is always false
|
// returns ErrNone if the signature matches.
|
||||||
func doesPolicySignatureMatch(formValues map[string]string) APIErrorCode {
|
func doesPolicySignatureMatch(formValues map[string]string) APIErrorCode {
|
||||||
// Access credentials.
|
// Access credentials.
|
||||||
cred := serverConfig.GetCredential()
|
cred := serverConfig.GetCredential()
|
||||||
@ -193,7 +193,7 @@ func doesPolicySignatureMatch(formValues map[string]string) APIErrorCode {
|
|||||||
|
|
||||||
// doesPresignedSignatureMatch - Verify query headers with presigned signature
|
// doesPresignedSignatureMatch - Verify query headers with presigned signature
|
||||||
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
|
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
|
||||||
// returns true if matches, false otherwise. if error is not nil then it is always false
|
// returns ErrNone if the signature matches.
|
||||||
func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region string) APIErrorCode {
|
func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region string) APIErrorCode {
|
||||||
// Access credentials.
|
// Access credentials.
|
||||||
cred := serverConfig.GetCredential()
|
cred := serverConfig.GetCredential()
|
||||||
@ -316,7 +316,7 @@ func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region s
|
|||||||
|
|
||||||
// doesSignatureMatch - Verify authorization header with calculated header in accordance with
|
// doesSignatureMatch - Verify authorization header with calculated header in accordance with
|
||||||
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
|
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
|
||||||
// returns true if matches, false otherwise. if error is not nil then it is always false
|
// returns ErrNone if signature matches.
|
||||||
func doesSignatureMatch(hashedPayload string, r *http.Request, region string) APIErrorCode {
|
func doesSignatureMatch(hashedPayload string, r *http.Request, region string) APIErrorCode {
|
||||||
// Access credentials.
|
// Access credentials.
|
||||||
cred := serverConfig.GetCredential()
|
cred := serverConfig.GetCredential()
|
||||||
|
@ -149,6 +149,7 @@ type TestServer struct {
|
|||||||
SecretKey string
|
SecretKey string
|
||||||
Server *httptest.Server
|
Server *httptest.Server
|
||||||
Obj ObjectLayer
|
Obj ObjectLayer
|
||||||
|
SrvCmdCfg serverCmdConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts the test server and returns the TestServer instance.
|
// Starts the test server and returns the TestServer instance.
|
||||||
@ -236,6 +237,64 @@ func StartTestStorageRPCServer(t TestErrHandler, instanceType string, diskN int)
|
|||||||
return testRPCServer
|
return testRPCServer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets up a Peers RPC test server.
|
||||||
|
func StartTestPeersRPCServer(t TestErrHandler, instanceType string) TestServer {
|
||||||
|
// create temporary backend for the test server.
|
||||||
|
nDisks := 16
|
||||||
|
disks, err := getRandomDisks(nDisks)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to create disks for the backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
root, err := newTestConfig("us-east-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create an instance of TestServer.
|
||||||
|
testRPCServer := TestServer{}
|
||||||
|
// Get credential.
|
||||||
|
credentials := serverConfig.GetCredential()
|
||||||
|
|
||||||
|
testRPCServer.Root = root
|
||||||
|
testRPCServer.Disks = disks
|
||||||
|
testRPCServer.AccessKey = credentials.AccessKeyID
|
||||||
|
testRPCServer.SecretKey = credentials.SecretAccessKey
|
||||||
|
|
||||||
|
// create temporary backend for the test server.
|
||||||
|
objLayer, storageDisks, err := initObjectLayer(disks, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
globalObjLayerMutex.Lock()
|
||||||
|
globalObjectAPI = objLayer
|
||||||
|
testRPCServer.Obj = objLayer
|
||||||
|
globalObjLayerMutex.Unlock()
|
||||||
|
|
||||||
|
srvCfg := serverCmdConfig{
|
||||||
|
disks: disks,
|
||||||
|
storageDisks: storageDisks,
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := router.NewRouter()
|
||||||
|
// need storage layer for bucket config storage.
|
||||||
|
registerStorageRPCRouters(mux, srvCfg)
|
||||||
|
// need API layer to send requests, etc.
|
||||||
|
registerAPIRouter(mux)
|
||||||
|
// module being tested is Peer RPCs router.
|
||||||
|
registerS3PeerRPCRouter(mux)
|
||||||
|
|
||||||
|
// Run TestServer.
|
||||||
|
testRPCServer.Server = httptest.NewServer(mux)
|
||||||
|
|
||||||
|
// initialize remainder of serverCmdConfig
|
||||||
|
srvCfg.isDistXL = false
|
||||||
|
testRPCServer.SrvCmdCfg = srvCfg
|
||||||
|
|
||||||
|
return testRPCServer
|
||||||
|
}
|
||||||
|
|
||||||
// Initializes control RPC endpoints.
|
// Initializes control RPC endpoints.
|
||||||
// The object Layer will be a temp back used for testing purpose.
|
// The object Layer will be a temp back used for testing purpose.
|
||||||
func initTestControlRPCEndPoint(srvCmdConfig serverCmdConfig) http.Handler {
|
func initTestControlRPCEndPoint(srvCmdConfig serverCmdConfig) http.Handler {
|
||||||
@ -595,7 +654,6 @@ func newTestStreamingSignedBadChunkDateRequest(method, urlStr string, contentLen
|
|||||||
}
|
}
|
||||||
|
|
||||||
currTime := time.Now().UTC()
|
currTime := time.Now().UTC()
|
||||||
fmt.Println("now: ", currTime)
|
|
||||||
signature, err := signStreamingRequest(req, accessKey, secretKey, currTime)
|
signature, err := signStreamingRequest(req, accessKey, secretKey, currTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -603,7 +661,6 @@ func newTestStreamingSignedBadChunkDateRequest(method, urlStr string, contentLen
|
|||||||
|
|
||||||
// skew the time between the chunk signature calculation and seed signature.
|
// skew the time between the chunk signature calculation and seed signature.
|
||||||
currTime = currTime.Add(1 * time.Second)
|
currTime = currTime.Add(1 * time.Second)
|
||||||
fmt.Println("later: ", currTime)
|
|
||||||
req, err = assembleStreamingChunks(req, body, chunkSize, secretKey, signature, currTime)
|
req, err = assembleStreamingChunks(req, body, chunkSize, secretKey, signature, currTime)
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
@ -625,14 +682,15 @@ func newTestStreamingSignedRequest(method, urlStr string, contentLength, chunkSi
|
|||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replaces any occurring '/' in string, into its encoded representation.
|
// Replaces any occurring '/' in string, into its encoded
|
||||||
|
// representation.
|
||||||
func percentEncodeSlash(s string) string {
|
func percentEncodeSlash(s string) string {
|
||||||
return strings.Replace(s, "/", "%2F", -1)
|
return strings.Replace(s, "/", "%2F", -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// queryEncode - encodes query values in their URL encoded form. In
|
// queryEncode - encodes query values in their URL encoded form. In
|
||||||
// addition to the percent encoding performed by getURLEncodedName() used
|
// addition to the percent encoding performed by getURLEncodedName()
|
||||||
// here, it also percent encodes '/' (forward slash)
|
// used here, it also percent encodes '/' (forward slash)
|
||||||
func queryEncode(v url.Values) string {
|
func queryEncode(v url.Values) string {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return ""
|
return ""
|
||||||
|
29
cmd/utils.go
29
cmd/utils.go
@ -80,19 +80,38 @@ func splitNetPath(networkPath string) (netAddr, netPath string, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
networkParts := strings.SplitN(networkPath, ":", 2)
|
networkParts := strings.SplitN(networkPath, ":", 2)
|
||||||
if len(networkParts) == 1 {
|
switch {
|
||||||
|
case len(networkParts) == 1:
|
||||||
return "", networkPath, nil
|
return "", networkPath, nil
|
||||||
}
|
case networkParts[1] == "":
|
||||||
if networkParts[1] == "" {
|
|
||||||
return "", "", &net.AddrError{Err: "Missing path in network path", Addr: networkPath}
|
return "", "", &net.AddrError{Err: "Missing path in network path", Addr: networkPath}
|
||||||
} else if networkParts[0] == "" {
|
case networkParts[0] == "":
|
||||||
return "", "", &net.AddrError{Err: "Missing address in network path", Addr: networkPath}
|
return "", "", &net.AddrError{Err: "Missing address in network path", Addr: networkPath}
|
||||||
} else if !filepath.IsAbs(networkParts[1]) {
|
case !filepath.IsAbs(networkParts[1]):
|
||||||
return "", "", &net.AddrError{Err: "Network path should be absolute", Addr: networkPath}
|
return "", "", &net.AddrError{Err: "Network path should be absolute", Addr: networkPath}
|
||||||
}
|
}
|
||||||
return networkParts[0], networkParts[1], nil
|
return networkParts[0], networkParts[1], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find local node through the command line arguments. Returns in
|
||||||
|
// `host:port` format.
|
||||||
|
func getLocalAddress(srvCmdConfig serverCmdConfig) string {
|
||||||
|
if !srvCmdConfig.isDistXL {
|
||||||
|
return fmt.Sprintf(":%d", globalMinioPort)
|
||||||
|
}
|
||||||
|
for _, export := range srvCmdConfig.disks {
|
||||||
|
// Validates if remote disk is local.
|
||||||
|
if isLocalStorage(export) {
|
||||||
|
var host string
|
||||||
|
if idx := strings.LastIndex(export, ":"); idx != -1 {
|
||||||
|
host = export[:idx]
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s:%d", host, globalMinioPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// xmlDecoder provide decoded value in xml.
|
// xmlDecoder provide decoded value in xml.
|
||||||
func xmlDecoder(body io.Reader, v interface{}, size int64) error {
|
func xmlDecoder(body io.Reader, v interface{}, size int64) error {
|
||||||
var lbody io.Reader
|
var lbody io.Reader
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -174,3 +175,65 @@ func TestMaxPartID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests fetch local address.
|
||||||
|
func TestLocalAddress(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// need to set this to avoid stale values from other tests.
|
||||||
|
globalMinioPort = 9000
|
||||||
|
testCases := []struct {
|
||||||
|
srvCmdConfig serverCmdConfig
|
||||||
|
localAddr string
|
||||||
|
}{
|
||||||
|
// Test 1 - local address is found.
|
||||||
|
{
|
||||||
|
srvCmdConfig: serverCmdConfig{
|
||||||
|
isDistXL: true,
|
||||||
|
disks: []string{
|
||||||
|
"localhost:/mnt/disk1",
|
||||||
|
"1.1.1.2:/mnt/disk2",
|
||||||
|
"1.1.2.1:/mnt/disk3",
|
||||||
|
"1.1.2.2:/mnt/disk4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
localAddr: fmt.Sprintf("localhost:%d", globalMinioPort),
|
||||||
|
},
|
||||||
|
// Test 2 - local address is everything.
|
||||||
|
{
|
||||||
|
srvCmdConfig: serverCmdConfig{
|
||||||
|
isDistXL: false,
|
||||||
|
disks: []string{
|
||||||
|
"/mnt/disk1",
|
||||||
|
"/mnt/disk2",
|
||||||
|
"/mnt/disk3",
|
||||||
|
"/mnt/disk4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
localAddr: fmt.Sprintf(":%d", globalMinioPort),
|
||||||
|
},
|
||||||
|
// Test 3 - local address is not found.
|
||||||
|
{
|
||||||
|
srvCmdConfig: serverCmdConfig{
|
||||||
|
isDistXL: true,
|
||||||
|
disks: []string{
|
||||||
|
"1.1.1.1:/mnt/disk1",
|
||||||
|
"1.1.1.2:/mnt/disk2",
|
||||||
|
"1.1.2.1:/mnt/disk3",
|
||||||
|
"1.1.2.2:/mnt/disk4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
localAddr: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates fetching local address.
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
localAddr := getLocalAddress(testCase.srvCmdConfig)
|
||||||
|
if localAddr != testCase.localAddr {
|
||||||
|
t.Fatalf("Test %d: Expected %s, got %s", i+1, testCase.localAddr, localAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user