mirror of https://github.com/minio/minio.git
make notification as separate package (#5294)
* Remove old notification files * Add net package * Add event package * Modify minio to take new notification system
This commit is contained in:
parent
abffa00b76
commit
0e4431725c
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -176,6 +176,11 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
||||||
// Init global heal state
|
// Init global heal state
|
||||||
initAllHealState(globalIsXL)
|
initAllHealState(globalIsXL)
|
||||||
|
|
||||||
|
globalNotificationSys, err = NewNotificationSys(globalServerConfig, globalEndpoints)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Setup admin mgmt REST API handlers.
|
// Setup admin mgmt REST API handlers.
|
||||||
adminRouter := router.NewRouter()
|
adminRouter := router.NewRouter()
|
||||||
registerAdminRouter(adminRouter)
|
registerAdminRouter(adminRouter)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2014, 2015, 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2014, 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -139,11 +139,6 @@ func (lc localAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
|
||||||
}
|
}
|
||||||
storage := objLayer.StorageInfo()
|
storage := objLayer.StorageInfo()
|
||||||
|
|
||||||
var arns []string
|
|
||||||
for queueArn := range globalEventNotifier.GetAllExternalTargets() {
|
|
||||||
arns = append(arns, queueArn)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ServerInfoData{
|
return ServerInfoData{
|
||||||
StorageInfo: storage,
|
StorageInfo: storage,
|
||||||
ConnStats: globalConnStats.toServerConnStats(),
|
ConnStats: globalConnStats.toServerConnStats(),
|
||||||
|
@ -152,7 +147,7 @@ func (lc localAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
|
||||||
Uptime: UTCNow().Sub(globalBootTime),
|
Uptime: UTCNow().Sub(globalBootTime),
|
||||||
Version: Version,
|
Version: Version,
|
||||||
CommitID: CommitID,
|
CommitID: CommitID,
|
||||||
SQSARN: arns,
|
SQSARN: globalNotificationSys.GetARNList(),
|
||||||
Region: globalServerConfig.GetRegion(),
|
Region: globalServerConfig.GetRegion(),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -120,18 +120,13 @@ func (s *adminCmd) ServerInfoData(args *AuthRPCArgs, reply *ServerInfoDataReply)
|
||||||
}
|
}
|
||||||
storageInfo := objLayer.StorageInfo()
|
storageInfo := objLayer.StorageInfo()
|
||||||
|
|
||||||
var arns []string
|
|
||||||
for queueArn := range globalEventNotifier.GetAllExternalTargets() {
|
|
||||||
arns = append(arns, queueArn)
|
|
||||||
}
|
|
||||||
|
|
||||||
reply.ServerInfoData = ServerInfoData{
|
reply.ServerInfoData = ServerInfoData{
|
||||||
Properties: ServerProperties{
|
Properties: ServerProperties{
|
||||||
Uptime: UTCNow().Sub(globalBootTime),
|
Uptime: UTCNow().Sub(globalBootTime),
|
||||||
Version: Version,
|
Version: Version,
|
||||||
CommitID: CommitID,
|
CommitID: CommitID,
|
||||||
Region: globalServerConfig.GetRegion(),
|
Region: globalServerConfig.GetRegion(),
|
||||||
SQSARN: arns,
|
SQSARN: globalNotificationSys.GetARNList(),
|
||||||
},
|
},
|
||||||
StorageInfo: storageInfo,
|
StorageInfo: storageInfo,
|
||||||
ConnStats: globalConnStats.toServerConnStats(),
|
ConnStats: globalConnStats.toServerConnStats(),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -23,6 +23,7 @@ import (
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/errors"
|
"github.com/minio/minio/pkg/errors"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -950,6 +951,28 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||||
apiErr = ErrPartsSizeUnequal
|
apiErr = ErrPartsSizeUnequal
|
||||||
case BucketPolicyNotFound:
|
case BucketPolicyNotFound:
|
||||||
apiErr = ErrNoSuchBucketPolicy
|
apiErr = ErrNoSuchBucketPolicy
|
||||||
|
case *event.ErrInvalidEventName:
|
||||||
|
apiErr = ErrEventNotification
|
||||||
|
case *event.ErrInvalidARN:
|
||||||
|
apiErr = ErrARNNotification
|
||||||
|
case *event.ErrARNNotFound:
|
||||||
|
apiErr = ErrARNNotification
|
||||||
|
case *event.ErrUnknownRegion:
|
||||||
|
apiErr = ErrRegionNotification
|
||||||
|
case *event.ErrInvalidFilterName:
|
||||||
|
apiErr = ErrFilterNameInvalid
|
||||||
|
case *event.ErrFilterNamePrefix:
|
||||||
|
apiErr = ErrFilterNamePrefix
|
||||||
|
case *event.ErrFilterNameSuffix:
|
||||||
|
apiErr = ErrFilterNameSuffix
|
||||||
|
case *event.ErrInvalidFilterValue:
|
||||||
|
apiErr = ErrFilterValueInvalid
|
||||||
|
case *event.ErrDuplicateEventName:
|
||||||
|
apiErr = ErrOverlappingConfigs
|
||||||
|
case *event.ErrDuplicateQueueConfiguration:
|
||||||
|
apiErr = ErrOverlappingFilterNotification
|
||||||
|
case *event.ErrUnsupportedConfiguration:
|
||||||
|
apiErr = ErrUnsupportedNotification
|
||||||
default:
|
default:
|
||||||
apiErr = ErrInternalError
|
apiErr = ErrInternalError
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -79,14 +79,6 @@ func getObjectResources(values url.Values) (uploadID string, partNumberMarker, m
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse listen bucket notification resources.
|
|
||||||
func getListenBucketNotificationResources(values url.Values) (prefix []string, suffix []string, events []string) {
|
|
||||||
prefix = values["prefix"]
|
|
||||||
suffix = values["suffix"]
|
|
||||||
events = values["events"]
|
|
||||||
return prefix, suffix, events
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validates filter values
|
// Validates filter values
|
||||||
func validateFilterValues(values []string) (err APIErrorCode) {
|
func validateFilterValues(values []string) (err APIErrorCode) {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -27,7 +27,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
timeFormatAMZ = "2006-01-02T15:04:05Z" // Reply date format
|
|
||||||
timeFormatAMZLong = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
|
timeFormatAMZLong = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
|
||||||
maxObjectList = 1000 // Limit number of objects in a listObjectsResponse.
|
maxObjectList = 1000 // Limit number of objects in a listObjectsResponse.
|
||||||
maxUploadsList = 1000 // Limit number of uploads in a listUploadsResponse.
|
maxUploadsList = 1000 // Limit number of uploads in a listUploadsResponse.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -71,11 +71,7 @@ func (br *browserPeerAPIHandlers) SetAuthPeer(args SetAuthPeerArgs, reply *AuthR
|
||||||
|
|
||||||
// Sends SetAuthPeer RPCs to all peers in the Minio cluster
|
// Sends SetAuthPeer RPCs to all peers in the Minio cluster
|
||||||
func updateCredsOnPeers(creds auth.Credentials) map[string]error {
|
func updateCredsOnPeers(creds auth.Credentials) map[string]error {
|
||||||
// Get list of peer addresses (from globalS3Peers)
|
peers := GetRemotePeers(globalEndpoints)
|
||||||
peers := []string{}
|
|
||||||
for _, p := range globalS3Peers {
|
|
||||||
peers = append(peers, p.addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Array of errors for each peer
|
// Array of errors for each peer
|
||||||
errs := make([]error, len(peers))
|
errs := make([]error, len(peers))
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -33,6 +33,7 @@ import (
|
||||||
"github.com/minio/minio-go/pkg/policy"
|
"github.com/minio/minio-go/pkg/policy"
|
||||||
"github.com/minio/minio-go/pkg/set"
|
"github.com/minio/minio-go/pkg/set"
|
||||||
"github.com/minio/minio/pkg/errors"
|
"github.com/minio/minio/pkg/errors"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -356,10 +357,10 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||||
|
|
||||||
// Notify deleted event for objects.
|
// Notify deleted event for objects.
|
||||||
for _, dobj := range deletedObjects {
|
for _, dobj := range deletedObjects {
|
||||||
eventNotify(eventData{
|
sendEvent(eventArgs{
|
||||||
Type: ObjectRemovedDelete,
|
EventName: event.ObjectRemovedDelete,
|
||||||
Bucket: bucket,
|
BucketName: bucket,
|
||||||
ObjInfo: ObjectInfo{
|
Object: ObjectInfo{
|
||||||
Name: dobj.ObjectName,
|
Name: dobj.ObjectName,
|
||||||
},
|
},
|
||||||
ReqParams: extractReqParams(r),
|
ReqParams: extractReqParams(r),
|
||||||
|
@ -606,14 +607,14 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify object created event.
|
// Notify object created event.
|
||||||
defer eventNotify(eventData{
|
defer sendEvent(eventArgs{
|
||||||
Type: ObjectCreatedPost,
|
EventName: event.ObjectCreatedPost,
|
||||||
Bucket: objInfo.Bucket,
|
BucketName: objInfo.Bucket,
|
||||||
ObjInfo: objInfo,
|
Object: objInfo,
|
||||||
ReqParams: extractReqParams(r),
|
ReqParams: extractReqParams(r),
|
||||||
UserAgent: r.UserAgent(),
|
UserAgent: r.UserAgent(),
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
})
|
})
|
||||||
|
|
||||||
if successRedirect != "" {
|
if successRedirect != "" {
|
||||||
|
@ -692,6 +693,16 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify all peers (including self) to update in-memory state
|
||||||
|
for addr, err := range globalNotificationSys.UpdateBucketPolicy(bucket) {
|
||||||
|
errorIf(err, "unable to update policy change in remote peer %v", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
globalNotificationSys.RemoveNotification(bucket)
|
||||||
|
for addr, err := range globalNotificationSys.DeleteBucket(bucket) {
|
||||||
|
errorIf(err, "unable to delete bucket in remote peer %v", addr)
|
||||||
|
}
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -617,11 +617,6 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T) {
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
// register event notifier.
|
|
||||||
err = initEventNotifier(obj)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Notifier initialization failed.")
|
|
||||||
}
|
|
||||||
|
|
||||||
contentBytes := []byte("hello")
|
contentBytes := []byte("hello")
|
||||||
sha256sum := ""
|
sha256sum := ""
|
||||||
|
|
|
@ -1,126 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
|
|
||||||
// BucketMetaState - Interface to update bucket metadata in-memory
|
|
||||||
// state.
|
|
||||||
type BucketMetaState interface {
|
|
||||||
// Updates bucket notification
|
|
||||||
UpdateBucketNotification(args *SetBucketNotificationPeerArgs) error
|
|
||||||
|
|
||||||
// Updates bucket listener
|
|
||||||
UpdateBucketListener(args *SetBucketListenerPeerArgs) error
|
|
||||||
|
|
||||||
// Updates bucket policy
|
|
||||||
UpdateBucketPolicy(args *SetBucketPolicyPeerArgs) error
|
|
||||||
|
|
||||||
// Sends event
|
|
||||||
SendEvent(args *EventArgs) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// BucketUpdater - Interface implementer calls one of BucketMetaState's methods.
|
|
||||||
type BucketUpdater interface {
|
|
||||||
BucketUpdate(client BucketMetaState) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type that implements BucketMetaState for local node.
|
|
||||||
type localBucketMetaState struct {
|
|
||||||
ObjectAPI func() ObjectLayer
|
|
||||||
}
|
|
||||||
|
|
||||||
// localBucketMetaState.UpdateBucketNotification - updates in-memory global bucket
|
|
||||||
// notification info.
|
|
||||||
func (lc *localBucketMetaState) UpdateBucketNotification(args *SetBucketNotificationPeerArgs) error {
|
|
||||||
// check if object layer is available.
|
|
||||||
objAPI := lc.ObjectAPI()
|
|
||||||
if objAPI == nil {
|
|
||||||
return errServerNotInitialized
|
|
||||||
}
|
|
||||||
|
|
||||||
globalEventNotifier.SetBucketNotificationConfig(args.Bucket, args.NCfg)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// localBucketMetaState.UpdateBucketListener - updates in-memory global bucket
|
|
||||||
// listeners info.
|
|
||||||
func (lc *localBucketMetaState) UpdateBucketListener(args *SetBucketListenerPeerArgs) error {
|
|
||||||
// check if object layer is available.
|
|
||||||
objAPI := lc.ObjectAPI()
|
|
||||||
if objAPI == nil {
|
|
||||||
return errServerNotInitialized
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update in-memory notification config.
|
|
||||||
return globalEventNotifier.SetBucketListenerConfig(args.Bucket, args.LCfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// localBucketMetaState.UpdateBucketPolicy - updates in-memory global bucket
|
|
||||||
// policy info.
|
|
||||||
func (lc *localBucketMetaState) UpdateBucketPolicy(args *SetBucketPolicyPeerArgs) error {
|
|
||||||
// check if object layer is available.
|
|
||||||
objAPI := lc.ObjectAPI()
|
|
||||||
if objAPI == nil {
|
|
||||||
return errServerNotInitialized
|
|
||||||
}
|
|
||||||
return objAPI.RefreshBucketPolicy(args.Bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// localBucketMetaState.SendEvent - sends event to local event notifier via
|
|
||||||
// `globalEventNotifier`
|
|
||||||
func (lc *localBucketMetaState) SendEvent(args *EventArgs) error {
|
|
||||||
// check if object layer is available.
|
|
||||||
objAPI := lc.ObjectAPI()
|
|
||||||
if objAPI == nil {
|
|
||||||
return errServerNotInitialized
|
|
||||||
}
|
|
||||||
|
|
||||||
return globalEventNotifier.SendListenerEvent(args.Arn, args.Event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type that implements BucketMetaState for remote node.
|
|
||||||
type remoteBucketMetaState struct {
|
|
||||||
*AuthRPCClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// remoteBucketMetaState.UpdateBucketNotification - sends bucket notification
|
|
||||||
// change to remote peer via RPC call.
|
|
||||||
func (rc *remoteBucketMetaState) UpdateBucketNotification(args *SetBucketNotificationPeerArgs) error {
|
|
||||||
reply := AuthRPCReply{}
|
|
||||||
return rc.Call("S3.SetBucketNotificationPeer", args, &reply)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remoteBucketMetaState.UpdateBucketListener - sends bucket listener change to
|
|
||||||
// remote peer via RPC call.
|
|
||||||
func (rc *remoteBucketMetaState) UpdateBucketListener(args *SetBucketListenerPeerArgs) error {
|
|
||||||
reply := AuthRPCReply{}
|
|
||||||
return rc.Call("S3.SetBucketListenerPeer", args, &reply)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remoteBucketMetaState.UpdateBucketPolicy - sends bucket policy change to remote
|
|
||||||
// peer via RPC call.
|
|
||||||
func (rc *remoteBucketMetaState) UpdateBucketPolicy(args *SetBucketPolicyPeerArgs) error {
|
|
||||||
reply := AuthRPCReply{}
|
|
||||||
return rc.Call("S3.SetBucketPolicyPeer", args, &reply)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remoteBucketMetaState.SendEvent - sends event for bucket listener to remote
|
|
||||||
// peer via RPC call.
|
|
||||||
func (rc *remoteBucketMetaState) SendEvent(args *EventArgs) error {
|
|
||||||
reply := AuthRPCReply{}
|
|
||||||
return rc.Call("S3.Event", args, &reply)
|
|
||||||
}
|
|
|
@ -1,213 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Represents the criteria for the filter rule.
|
|
||||||
type filterRule struct {
|
|
||||||
Name string `xml:"Name"`
|
|
||||||
Value string `xml:"Value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collection of filter rules per service config.
|
|
||||||
type keyFilter struct {
|
|
||||||
FilterRules []filterRule `xml:"FilterRule,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type filterStruct struct {
|
|
||||||
Key keyFilter `xml:"S3Key,omitempty" json:"S3Key,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceConfig - Common elements of service notification.
|
|
||||||
type ServiceConfig struct {
|
|
||||||
Events []string `xml:"Event" json:"Event"`
|
|
||||||
Filter filterStruct `xml:"Filter" json:"Filter"`
|
|
||||||
ID string `xml:"Id" json:"Id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue SQS configuration.
|
|
||||||
type queueConfig struct {
|
|
||||||
ServiceConfig
|
|
||||||
QueueARN string `xml:"Queue"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Topic SNS configuration, this is a compliance field not used by minio yet.
|
|
||||||
type topicConfig struct {
|
|
||||||
ServiceConfig
|
|
||||||
TopicARN string `xml:"Topic" json:"Topic"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lambda function configuration, this is a compliance field not used by minio yet.
|
|
||||||
type lambdaConfig struct {
|
|
||||||
ServiceConfig
|
|
||||||
LambdaARN string `xml:"CloudFunction"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notification configuration structure represents the XML format of
|
|
||||||
// notification configuration of buckets.
|
|
||||||
type notificationConfig struct {
|
|
||||||
XMLName xml.Name `xml:"NotificationConfiguration"`
|
|
||||||
QueueConfigs []queueConfig `xml:"QueueConfiguration"`
|
|
||||||
LambdaConfigs []lambdaConfig `xml:"CloudFunctionConfiguration"`
|
|
||||||
TopicConfigs []topicConfig `xml:"TopicConfiguration"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
var errNoSuchNotifications = errors.New("The specified bucket does not have bucket notifications")
|
|
||||||
|
|
||||||
// EventName is AWS S3 event type:
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html
|
|
||||||
type EventName int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ObjectCreatedPut is s3:ObjectCreated:Put
|
|
||||||
ObjectCreatedPut EventName = iota
|
|
||||||
// ObjectCreatedPost is s3:ObjectCreated:Post
|
|
||||||
ObjectCreatedPost
|
|
||||||
// ObjectCreatedCopy is s3:ObjectCreated:Copy
|
|
||||||
ObjectCreatedCopy
|
|
||||||
// ObjectCreatedCompleteMultipartUpload is s3:ObjectCreated:CompleteMultipartUpload
|
|
||||||
ObjectCreatedCompleteMultipartUpload
|
|
||||||
// ObjectRemovedDelete is s3:ObjectRemoved:Delete
|
|
||||||
ObjectRemovedDelete
|
|
||||||
// ObjectAccessedGet is s3:ObjectAccessed:Get
|
|
||||||
ObjectAccessedGet
|
|
||||||
// ObjectAccessedHead is s3:ObjectAccessed:Head
|
|
||||||
ObjectAccessedHead
|
|
||||||
)
|
|
||||||
|
|
||||||
// Stringer interface for event name.
|
|
||||||
func (eventName EventName) String() string {
|
|
||||||
switch eventName {
|
|
||||||
case ObjectCreatedPut:
|
|
||||||
return "s3:ObjectCreated:Put"
|
|
||||||
case ObjectCreatedPost:
|
|
||||||
return "s3:ObjectCreated:Post"
|
|
||||||
case ObjectCreatedCopy:
|
|
||||||
return "s3:ObjectCreated:Copy"
|
|
||||||
case ObjectCreatedCompleteMultipartUpload:
|
|
||||||
return "s3:ObjectCreated:CompleteMultipartUpload"
|
|
||||||
case ObjectRemovedDelete:
|
|
||||||
return "s3:ObjectRemoved:Delete"
|
|
||||||
case ObjectAccessedGet:
|
|
||||||
return "s3:ObjectAccessed:Get"
|
|
||||||
case ObjectAccessedHead:
|
|
||||||
return "s3:ObjectAccessed:Head"
|
|
||||||
default:
|
|
||||||
return "s3:Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indentity represents the accessKey who caused the event.
|
|
||||||
type identity struct {
|
|
||||||
PrincipalID string `json:"principalId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notification event bucket metadata.
|
|
||||||
type bucketMeta struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
OwnerIdentity identity `json:"ownerIdentity"`
|
|
||||||
ARN string `json:"arn"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notification event object metadata.
|
|
||||||
type objectMeta struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Size int64 `json:"size,omitempty"`
|
|
||||||
ETag string `json:"eTag,omitempty"`
|
|
||||||
ContentType string `json:"contentType,omitempty"`
|
|
||||||
UserMetadata map[string]string `json:"userMetadata,omitempty"`
|
|
||||||
VersionID string `json:"versionId,omitempty"`
|
|
||||||
Sequencer string `json:"sequencer"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Event schema version number defaulting to the value in S3 spec.
|
|
||||||
// ref: http://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html
|
|
||||||
eventSchemaVersion = "1.0"
|
|
||||||
|
|
||||||
// Default ID found in bucket notification configuration.
|
|
||||||
// ref: http://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html
|
|
||||||
eventConfigID = "Config"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Response element origin endpoint key.
|
|
||||||
responseOriginEndpointKey = "x-minio-origin-endpoint"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Notification event server specific metadata.
|
|
||||||
type eventMeta struct {
|
|
||||||
SchemaVersion string `json:"s3SchemaVersion"`
|
|
||||||
ConfigurationID string `json:"configurationId"`
|
|
||||||
Bucket bucketMeta `json:"bucket"`
|
|
||||||
Object objectMeta `json:"object"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Event source static value defaulting to the value in S3 spec.
|
|
||||||
// ref: http://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html
|
|
||||||
eventSource = "aws:s3"
|
|
||||||
|
|
||||||
// Event version number defaulting to the value in S3 spec.
|
|
||||||
// ref: http://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html
|
|
||||||
eventVersion = "2.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
// sourceInfo represents information on the client that triggered the
|
|
||||||
// event notification.
|
|
||||||
type sourceInfo struct {
|
|
||||||
Host string `json:"host"`
|
|
||||||
Port string `json:"port"`
|
|
||||||
UserAgent string `json:"userAgent"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotificationEvent represents an Amazon an S3 bucket notification event.
|
|
||||||
type NotificationEvent struct {
|
|
||||||
EventVersion string `json:"eventVersion"`
|
|
||||||
EventSource string `json:"eventSource"`
|
|
||||||
AwsRegion string `json:"awsRegion"`
|
|
||||||
EventTime string `json:"eventTime"`
|
|
||||||
EventName string `json:"eventName"`
|
|
||||||
UserIdentity identity `json:"userIdentity"`
|
|
||||||
RequestParameters map[string]string `json:"requestParameters"`
|
|
||||||
ResponseElements map[string]string `json:"responseElements"`
|
|
||||||
S3 eventMeta `json:"s3"`
|
|
||||||
Source sourceInfo `json:"source"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Represents the minio sqs type and account id's.
|
|
||||||
type arnSQS struct {
|
|
||||||
Type string
|
|
||||||
AccountID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stringer for constructing AWS ARN compatible string.
|
|
||||||
func (m arnSQS) String() string {
|
|
||||||
return minioSqs + globalServerConfig.GetRegion() + ":" + m.AccountID + ":" + m.Type
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -17,18 +17,16 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/minio/minio/pkg/errors"
|
xerrors "github.com/minio/minio/pkg/errors"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
"github.com/minio/minio/pkg/event/target"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -37,11 +35,11 @@ const (
|
||||||
bucketListenerConfig = "listener.json"
|
bucketListenerConfig = "listener.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetBucketNotificationHandler - This implementation of the GET
|
var errNoSuchNotifications = errors.New("The specified bucket does not have bucket notifications")
|
||||||
// operation uses the notification subresource to return the
|
|
||||||
// notification configuration of a bucket. If notifications are
|
// GetBucketNotificationHandler - This HTTP handler returns event notification configuration
|
||||||
// not enabled on the bucket, the operation returns an empty
|
// as per http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
|
||||||
// NotificationConfiguration element.
|
// It returns empty configuration if its not set.
|
||||||
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
|
@ -60,46 +58,40 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
||||||
}
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucketName := vars["bucket"]
|
||||||
|
|
||||||
_, err := objAPI.GetBucketInfo(bucket)
|
_, err := objAPI.GetBucketInfo(bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
errorIf(err, "Unable to find bucket info.")
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to successfully load notification config.
|
// Attempt to successfully load notification config.
|
||||||
nConfig, err := loadNotificationConfig(bucket, objAPI)
|
nConfig, err := readNotificationConfig(objAPI, bucketName)
|
||||||
if err != nil && errors.Cause(err) != errNoSuchNotifications {
|
if err != nil {
|
||||||
errorIf(err, "Unable to read notification configuration.")
|
// Ignore errNoSuchNotifications to comply with AWS S3.
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
if xerrors.Cause(err) != errNoSuchNotifications {
|
||||||
return
|
errorIf(err, "Unable to read notification configuration.")
|
||||||
}
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
// For no notifications we write a dummy XML.
|
return
|
||||||
if errors.Cause(err) == errNoSuchNotifications {
|
}
|
||||||
// Complies with the s3 behavior in this regard.
|
|
||||||
nConfig = ¬ificationConfig{}
|
nConfig = &event.Config{}
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationBytes, err := xml.Marshal(nConfig)
|
notificationBytes, err := xml.Marshal(nConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// For any marshalling failure.
|
|
||||||
errorIf(err, "Unable to marshal notification configuration into XML.", err)
|
errorIf(err, "Unable to marshal notification configuration into XML.", err)
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success.
|
|
||||||
writeSuccessResponseXML(w, notificationBytes)
|
writeSuccessResponseXML(w, notificationBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutBucketNotificationHandler - Minio notification feature enables
|
// PutBucketNotificationHandler - This HTTP handler stores given notification configuration as per
|
||||||
// you to receive notifications when certain events happen in your bucket.
|
// http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
|
||||||
// Using this API, you can replace an existing notification configuration.
|
|
||||||
// The configuration is an XML file that defines the event types that you
|
|
||||||
// want Minio to publish and the destination where you want Minio to publish
|
|
||||||
// an event notification when it detects an event of the specified type.
|
|
||||||
// By default, your bucket has no event notifications configured. That is,
|
|
||||||
// the notification configuration will be an empty NotificationConfiguration.
|
|
||||||
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
|
@ -118,185 +110,56 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
||||||
}
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucketName := vars["bucket"]
|
||||||
|
|
||||||
_, err := objectAPI.GetBucketInfo(bucket)
|
_, err := objectAPI.GetBucketInfo(bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If Content-Length is unknown or zero, deny the request.
|
|
||||||
// PutBucketNotification always needs a Content-Length.
|
// PutBucketNotification always needs a Content-Length.
|
||||||
if r.ContentLength == -1 || r.ContentLength == 0 {
|
if r.ContentLength <= 0 {
|
||||||
writeErrorResponse(w, ErrMissingContentLength, r.URL)
|
writeErrorResponse(w, ErrMissingContentLength, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads the incoming notification configuration.
|
var config *event.Config
|
||||||
var buffer bytes.Buffer
|
config, err = event.ParseConfig(io.LimitReader(r.Body, r.ContentLength), globalServerConfig.GetRegion(), globalNotificationSys.targetList)
|
||||||
if r.ContentLength >= 0 {
|
|
||||||
_, err = io.CopyN(&buffer, r.Body, r.ContentLength)
|
|
||||||
} else {
|
|
||||||
_, err = io.Copy(&buffer, r.Body)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorIf(err, "Unable to read incoming body.")
|
apiErr := ErrMalformedXML
|
||||||
|
if event.IsEventError(err) {
|
||||||
|
apiErr = toAPIErrorCode(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeErrorResponse(w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acquire a write lock on bucket before modifying its configuration.
|
||||||
|
bucketLock := globalNSMutex.NewNSLock(bucketName, "")
|
||||||
|
if err = bucketLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer bucketLock.Unlock()
|
||||||
|
|
||||||
|
if err = saveNotificationConfig(objectAPI, bucketName, config); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var notificationCfg notificationConfig
|
rulesMap := config.ToRulesMap()
|
||||||
// Unmarshal notification bytes.
|
globalNotificationSys.AddRulesMap(bucketName, rulesMap)
|
||||||
notificationConfigBytes := buffer.Bytes()
|
for addr, err := range globalNotificationSys.PutBucketNotification(bucketName, rulesMap) {
|
||||||
if err = xml.Unmarshal(notificationConfigBytes, ¬ificationCfg); err != nil {
|
errorIf(err, "unable to put bucket notification to remote peer %v", addr)
|
||||||
errorIf(err, "Unable to parse notification configuration XML.")
|
|
||||||
writeErrorResponse(w, ErrMalformedXML, r.URL)
|
|
||||||
return
|
|
||||||
} // Successfully marshalled notification configuration.
|
|
||||||
|
|
||||||
// Validate unmarshalled bucket notification configuration.
|
|
||||||
if s3Error := validateNotificationConfig(notificationCfg); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the incoming ARNs properly to the GetRegion().
|
|
||||||
for i, queueConfig := range notificationCfg.QueueConfigs {
|
|
||||||
queueConfig.QueueARN = unmarshalSqsARN(queueConfig.QueueARN).String()
|
|
||||||
notificationCfg.QueueConfigs[i] = queueConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put bucket notification config.
|
|
||||||
err = PutBucketNotificationConfig(bucket, ¬ificationCfg, objectAPI)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success.
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutBucketNotificationConfig - Put a new notification config for a
|
// ListenBucketNotificationHandler - This HTTP handler sends events to the connected HTTP client.
|
||||||
// bucket (overwrites any previous config) persistently, updates
|
// Client should send prefix/suffix object name to match and events to watch as query parameters.
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Acquire a write lock on bucket before modifying its
|
|
||||||
// configuration.
|
|
||||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
|
||||||
if err := bucketLock.GetLock(globalOperationTimeout); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Release lock after notifying peers
|
|
||||||
defer bucketLock.Unlock()
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
func writeNotification(w http.ResponseWriter, notification map[string][]NotificationEvent) error {
|
|
||||||
// Invalid response writer.
|
|
||||||
if w == nil {
|
|
||||||
return errInvalidArgument
|
|
||||||
}
|
|
||||||
// Invalid notification input.
|
|
||||||
if notification == nil {
|
|
||||||
return errInvalidArgument
|
|
||||||
}
|
|
||||||
// Marshal notification data into JSON and write to client.
|
|
||||||
notificationBytes, err := json.Marshal(¬ification)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add additional CRLF characters for client to
|
|
||||||
// differentiate the individual events properly.
|
|
||||||
_, err = w.Write(append(notificationBytes, crlf...))
|
|
||||||
// Make sure we have flushed, this would set Transfer-Encoding: chunked.
|
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CRLF character used for chunked transfer in accordance with HTTP standards.
|
|
||||||
var crlf = []byte("\r\n")
|
|
||||||
|
|
||||||
// listenChan A `listenChan` provides a data channel to send event
|
|
||||||
// notifications on and `doneCh` to signal that events are no longer
|
|
||||||
// being received. It also sends empty events (whitespace) to keep the
|
|
||||||
// underlying connection alive.
|
|
||||||
type listenChan struct {
|
|
||||||
doneCh chan struct{}
|
|
||||||
dataCh chan []NotificationEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
// newListenChan returns a listenChan with properly initialized
|
|
||||||
// unbuffered channels.
|
|
||||||
func newListenChan() *listenChan {
|
|
||||||
return &listenChan{
|
|
||||||
doneCh: make(chan struct{}),
|
|
||||||
dataCh: make(chan []NotificationEvent),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendNotificationEvent sends notification events on the data channel
|
|
||||||
// unless doneCh is not closed
|
|
||||||
func (l *listenChan) sendNotificationEvent(events []NotificationEvent) {
|
|
||||||
select {
|
|
||||||
// Returns immediately if receiver has quit.
|
|
||||||
case <-l.doneCh:
|
|
||||||
// Blocks until receiver is available.
|
|
||||||
case l.dataCh <- events:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// waitForListener writes event notification OR whitespaces on
|
|
||||||
// ResponseWriter until client closes connection
|
|
||||||
func (l *listenChan) waitForListener(w http.ResponseWriter) {
|
|
||||||
|
|
||||||
// Logs errors other than EPIPE and ECONNRESET.
|
|
||||||
// EPIPE and ECONNRESET indicate that the client stopped
|
|
||||||
// listening to notification events.
|
|
||||||
logClientError := func(err error, msg string) {
|
|
||||||
if oe, ok := err.(*net.OpError); ok && (oe.Err == syscall.EPIPE || oe.Err ==
|
|
||||||
syscall.ECONNRESET) {
|
|
||||||
errorIf(err, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emptyEvent := map[string][]NotificationEvent{"Records": nil}
|
|
||||||
defer close(l.doneCh)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case events := <-l.dataCh:
|
|
||||||
if err := writeNotification(w, map[string][]NotificationEvent{"Records": events}); err != nil {
|
|
||||||
logClientError(err, "Unable to write notification")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-time.After(globalSNSConnAlive):
|
|
||||||
if err := writeNotification(w, emptyEvent); err != nil {
|
|
||||||
logClientError(err, "Unable to write empty notification")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenBucketNotificationHandler - list bucket notifications.
|
|
||||||
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Validate if bucket exists.
|
// Validate if bucket exists.
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
|
@ -314,195 +177,84 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||||
}
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucketName := vars["bucket"]
|
||||||
|
|
||||||
// Parse listen bucket notification resources.
|
values := r.URL.Query()
|
||||||
prefixes, suffixes, events := getListenBucketNotificationResources(r.URL.Query())
|
|
||||||
|
|
||||||
if err := validateFilterValues(prefixes); err != ErrNone {
|
var prefix string
|
||||||
writeErrorResponse(w, err, r.URL)
|
if len(values["prefix"]) > 1 {
|
||||||
return
|
writeErrorResponse(w, ErrFilterNamePrefix, r.URL)
|
||||||
}
|
}
|
||||||
|
if len(values["prefix"]) == 1 {
|
||||||
if err := validateFilterValues(suffixes); err != ErrNone {
|
if err := event.ValidateFilterRuleValue(values["prefix"][0]); err != nil {
|
||||||
writeErrorResponse(w, err, r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all the resource events.
|
|
||||||
for _, event := range events {
|
|
||||||
if errCode := checkEvent(event); errCode != ErrNone {
|
|
||||||
writeErrorResponse(w, errCode, r.URL)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prefix = values["prefix"][0]
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := objAPI.GetBucketInfo(bucket)
|
var suffix string
|
||||||
if err != nil {
|
if len(values["suffix"]) > 1 {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, ErrFilterNameSuffix, r.URL)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
if len(values["suffix"]) == 1 {
|
||||||
targetServer := GetLocalPeer(globalEndpoints)
|
if err := event.ValidateFilterRuleValue(values["suffix"][0]); err != nil {
|
||||||
accountID := fmt.Sprintf("%d", UTCNow().UnixNano())
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
accountARN := fmt.Sprintf(
|
|
||||||
"%s:%s:%s:%s-%s",
|
|
||||||
minioTopic,
|
|
||||||
globalServerConfig.GetRegion(),
|
|
||||||
accountID,
|
|
||||||
snsTypeMinio,
|
|
||||||
targetServer,
|
|
||||||
)
|
|
||||||
|
|
||||||
var filterRules []filterRule
|
|
||||||
|
|
||||||
for _, prefix := range prefixes {
|
|
||||||
filterRules = append(filterRules, filterRule{
|
|
||||||
Name: "prefix",
|
|
||||||
Value: prefix,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, suffix := range suffixes {
|
|
||||||
filterRules = append(filterRules, filterRule{
|
|
||||||
Name: "suffix",
|
|
||||||
Value: suffix,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make topic configuration corresponding to this
|
|
||||||
// ListenBucketNotification request.
|
|
||||||
topicCfg := &topicConfig{
|
|
||||||
TopicARN: accountARN,
|
|
||||||
ServiceConfig: ServiceConfig{
|
|
||||||
Events: events,
|
|
||||||
Filter: struct {
|
|
||||||
Key keyFilter `xml:"S3Key,omitempty" json:"S3Key,omitempty"`
|
|
||||||
}{
|
|
||||||
Key: keyFilter{
|
|
||||||
FilterRules: filterRules,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ID: "sns-" + accountID,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup a listen channel to receive notifications like
|
|
||||||
// s3:ObjectCreated, s3:ObjectDeleted etc.
|
|
||||||
nListenCh := newListenChan()
|
|
||||||
// Add channel for listener events
|
|
||||||
if err = globalEventNotifier.AddListenerChan(accountARN, nListenCh); err != nil {
|
|
||||||
errorIf(err, "Error adding a listener!")
|
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 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 completes, events may start appearing in
|
|
||||||
// nListenCh
|
|
||||||
lc := listenerConfig{
|
|
||||||
TopicConfig: *topicCfg,
|
|
||||||
TargetServer: targetServer,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = AddBucketListenerConfig(bucket, &lc, objAPI)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer RemoveBucketListenerConfig(bucket, &lc, objAPI)
|
|
||||||
|
|
||||||
// Add all common headers.
|
|
||||||
setCommonHeaders(w)
|
|
||||||
|
|
||||||
// https://github.com/containous/traefik/issues/560
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
|
|
||||||
//
|
|
||||||
// Proxies might buffer the connection to avoid this we
|
|
||||||
// need the proper MIME type before writing to client.
|
|
||||||
// This MIME header tells the proxies to avoid buffering
|
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
|
||||||
|
|
||||||
// Start writing bucket notifications to ResponseWriter.
|
|
||||||
nListenCh.waitForListener(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
// Acquire a write lock on bucket before modifying its
|
|
||||||
// configuration.
|
|
||||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
|
||||||
if err := bucketLock.GetLock(globalOperationTimeout); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Release lock after notifying peers
|
|
||||||
defer bucketLock.Unlock()
|
|
||||||
|
|
||||||
// update persistent config if dist XL
|
|
||||||
if globalIsDistXL {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Acquire a write lock on bucket before modifying its
|
|
||||||
// configuration.
|
|
||||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
|
||||||
if bucketLock.GetLock(globalOperationTimeout) != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Release lock after notifying peers
|
|
||||||
defer bucketLock.Unlock()
|
|
||||||
|
|
||||||
// update persistent config if dist XL
|
|
||||||
if globalIsDistXL {
|
|
||||||
err := persistListenerConfig(bucket, updatedLcfgs, objAPI)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Error persisting listener config when removing a listener.")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suffix = values["suffix"][0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// persistence success - now update in-memory globals on all
|
pattern := event.NewPattern(prefix, suffix)
|
||||||
// peers (including local)
|
|
||||||
S3PeersUpdateBucketListener(bucket, updatedLcfgs)
|
eventNames := []event.Name{}
|
||||||
|
for _, s := range values["events"] {
|
||||||
|
eventName, err := event.ParseName(s)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eventNames = append(eventNames, eventName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := objAPI.GetBucketInfo(bucketName); err != nil {
|
||||||
|
errorIf(err, "Unable to get bucket info.")
|
||||||
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
host := xnet.MustParseHost(r.RemoteAddr)
|
||||||
|
target := target.NewHTTPClientTarget(*host, w)
|
||||||
|
rulesMap := event.NewRulesMap(eventNames, pattern, target.ID())
|
||||||
|
|
||||||
|
if err := globalNotificationSys.AddRemoteTarget(bucketName, target, rulesMap); err != nil {
|
||||||
|
errorIf(err, "Unable to add httpclient target %v to globalNotificationSys.targetList.", target)
|
||||||
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer globalNotificationSys.RemoveRemoteTarget(bucketName, target.ID())
|
||||||
|
defer globalNotificationSys.RemoveRulesMap(bucketName, rulesMap)
|
||||||
|
|
||||||
|
thisAddr := xnet.MustParseHost(GetLocalPeer(globalEndpoints))
|
||||||
|
if err := SaveListener(objAPI, bucketName, eventNames, pattern, target.ID(), *thisAddr); err != nil {
|
||||||
|
errorIf(err, "Unable to save HTTP listener %v", target)
|
||||||
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errors := globalNotificationSys.ListenBucketNotification(bucketName, eventNames, pattern, target.ID(), *thisAddr)
|
||||||
|
for addr, err := range errors {
|
||||||
|
errorIf(err, "unable to call listen bucket notification to remote peer %v", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-target.DoneCh
|
||||||
|
|
||||||
|
if err := RemoveListener(objAPI, bucketName, target.ID(), *thisAddr); err != nil {
|
||||||
|
errorIf(err, "Unable to save HTTP listener %v", target)
|
||||||
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,483 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 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 (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/auth"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Implement a dummy flush writer.
|
|
||||||
type flushWriter struct {
|
|
||||||
io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush writer is a dummy writer compatible with http.Flusher and http.ResponseWriter.
|
|
||||||
func (f *flushWriter) Flush() {}
|
|
||||||
func (f *flushWriter) Write(b []byte) (n int, err error) { return f.Writer.Write(b) }
|
|
||||||
func (f *flushWriter) Header() http.Header { return http.Header{} }
|
|
||||||
func (f *flushWriter) WriteHeader(code int) {}
|
|
||||||
|
|
||||||
func newFlushWriter(writer io.Writer) http.ResponseWriter {
|
|
||||||
return &flushWriter{writer}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests write notification code.
|
|
||||||
func TestWriteNotification(t *testing.T) {
|
|
||||||
// Initialize a new test config.
|
|
||||||
root, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unable to initialize test config %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(root)
|
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
// Collection of test cases for each event writer.
|
|
||||||
testCases := []struct {
|
|
||||||
writer http.ResponseWriter
|
|
||||||
event map[string][]NotificationEvent
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
// Invalid input argument with writer `nil` - Test - 1
|
|
||||||
{
|
|
||||||
writer: nil,
|
|
||||||
event: nil,
|
|
||||||
err: errInvalidArgument,
|
|
||||||
},
|
|
||||||
// Invalid input argument with event `nil` - Test - 2
|
|
||||||
{
|
|
||||||
writer: newFlushWriter(ioutil.Discard),
|
|
||||||
event: nil,
|
|
||||||
err: errInvalidArgument,
|
|
||||||
},
|
|
||||||
// Unmarshal and write, validate last 5 bytes. - Test - 3
|
|
||||||
{
|
|
||||||
writer: newFlushWriter(&buffer),
|
|
||||||
event: map[string][]NotificationEvent{
|
|
||||||
"Records": {newNotificationEvent(eventData{
|
|
||||||
Type: ObjectCreatedPut,
|
|
||||||
Bucket: "testbucket",
|
|
||||||
ObjInfo: ObjectInfo{
|
|
||||||
Name: "key",
|
|
||||||
},
|
|
||||||
ReqParams: map[string]string{
|
|
||||||
"ip": "10.1.10.1",
|
|
||||||
}}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Validates all the testcases for writing notification.
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
err := writeNotification(testCase.writer, testCase.event)
|
|
||||||
if err != testCase.err {
|
|
||||||
t.Errorf("Unable to write notification %s", err)
|
|
||||||
}
|
|
||||||
// Validates if the ending string has 'crlf'
|
|
||||||
if err == nil && !bytes.HasSuffix(buffer.Bytes(), crlf) {
|
|
||||||
buf := buffer.Bytes()[buffer.Len()-5 : 0]
|
|
||||||
t.Errorf("Invalid suffix found from the writer last 5 bytes %s, expected `\r\n`", string(buf))
|
|
||||||
}
|
|
||||||
// Not printing 'buf' on purpose, validates look for string '10.1.10.1'.
|
|
||||||
if err == nil && !bytes.Contains(buffer.Bytes(), []byte("10.1.10.1")) {
|
|
||||||
// Enable when debugging)
|
|
||||||
// fmt.Println(string(buffer.Bytes()))
|
|
||||||
t.Errorf("Requested content couldn't be found, expected `10.1.10.1`")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testResponseWriter implements `http.ResponseWriter` that buffers
|
|
||||||
// response body in a `bytes.Buffer` and returns error after `failCount`
|
|
||||||
// calls to `Write` method
|
|
||||||
type testResponseWriter struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
failCount int
|
|
||||||
buf *bytes.Buffer
|
|
||||||
m http.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestResponseWriter(failAt int) *testResponseWriter {
|
|
||||||
return &testResponseWriter{
|
|
||||||
buf: new(bytes.Buffer),
|
|
||||||
m: make(http.Header),
|
|
||||||
failCount: failAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (trw *testResponseWriter) Flush() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (trw *testResponseWriter) Write(p []byte) (int, error) {
|
|
||||||
trw.mu.Lock()
|
|
||||||
defer trw.mu.Unlock()
|
|
||||||
|
|
||||||
if trw.failCount == 0 {
|
|
||||||
return 0, errors.New("Custom error")
|
|
||||||
}
|
|
||||||
trw.failCount--
|
|
||||||
|
|
||||||
return trw.buf.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (trw *testResponseWriter) Header() http.Header {
|
|
||||||
return trw.m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (trw *testResponseWriter) WriteHeader(i int) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListenChan(t *testing.T) {
|
|
||||||
// Initialize a new test config.
|
|
||||||
root, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unable to initialize test config %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(root)
|
|
||||||
|
|
||||||
// Create a listen channel to manage notifications
|
|
||||||
nListenCh := newListenChan()
|
|
||||||
|
|
||||||
// Construct notification events to be passed on the events channel.
|
|
||||||
var events []NotificationEvent
|
|
||||||
evTypes := []EventName{
|
|
||||||
ObjectCreatedPut,
|
|
||||||
ObjectCreatedPost,
|
|
||||||
ObjectCreatedCopy,
|
|
||||||
ObjectCreatedCompleteMultipartUpload,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, evType := range evTypes {
|
|
||||||
events = append(events, newNotificationEvent(eventData{
|
|
||||||
Type: evType,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send notification events one-by-one
|
|
||||||
go func() {
|
|
||||||
for _, event := range events {
|
|
||||||
nListenCh.sendNotificationEvent([]NotificationEvent{event})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Create a http.ResponseWriter that fails after len(events)
|
|
||||||
// number of times
|
|
||||||
trw := newTestResponseWriter(len(events))
|
|
||||||
|
|
||||||
// Wait for all (4) notification events to be received
|
|
||||||
nListenCh.waitForListener(trw)
|
|
||||||
|
|
||||||
// Used to read JSON-formatted event stream line-by-line
|
|
||||||
scanner := bufio.NewScanner(trw.buf)
|
|
||||||
var records map[string][]NotificationEvent
|
|
||||||
for i := 0; scanner.Scan(); i++ {
|
|
||||||
err = json.Unmarshal(scanner.Bytes(), &records)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to unmarshal json %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nEvent := records["Records"][0]
|
|
||||||
if nEvent.EventName != evTypes[i].String() {
|
|
||||||
t.Errorf("notification event name mismatch, expected %s but got %s", evTypes[i], nEvent.EventName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendNotificationEvent(t *testing.T) {
|
|
||||||
// This test verifies that sendNotificationEvent function
|
|
||||||
// returns once listenChan.doneCh is closed
|
|
||||||
|
|
||||||
l := newListenChan()
|
|
||||||
testCh := make(chan struct{})
|
|
||||||
timeout := 5 * time.Second
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// Send one empty notification event on listenChan
|
|
||||||
events := []NotificationEvent{{}}
|
|
||||||
l.sendNotificationEvent(events)
|
|
||||||
testCh <- struct{}{}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// close l.doneCh to signal client exiting from
|
|
||||||
// ListenBucketNotification API call
|
|
||||||
close(l.doneCh)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(timeout):
|
|
||||||
t.Fatalf("sendNotificationEvent didn't return after %v seconds", timeout)
|
|
||||||
case <-testCh:
|
|
||||||
// If we reach this case, sendNotificationEvent
|
|
||||||
// returned on closing l.doneCh
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetBucketNotificationHandler(t *testing.T) {
|
|
||||||
ExecObjectLayerAPITest(t, testGetBucketNotificationHandler, []string{
|
|
||||||
"GetBucketNotification",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGetBucketNotificationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
|
||||||
credentials auth.Credentials, t *testing.T) {
|
|
||||||
// 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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
req, err := newTestSignedRequestV4("GET", getGetBucketNotificationURL("", bucketName),
|
|
||||||
0, nil, credentials.AccessKey, credentials.SecretKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s: Failed to create HTTP testRequest for ListenBucketNotification: <ERROR> %v", instanceType, err)
|
|
||||||
}
|
|
||||||
apiRouter.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("Unexpected http response %d", rec.Code)
|
|
||||||
}
|
|
||||||
if err = persistNotificationConfig(bucketName, &sampleNotifCfg, obj); err != nil {
|
|
||||||
t.Fatalf("Unable to save notification config %s", err)
|
|
||||||
}
|
|
||||||
rec = httptest.NewRecorder()
|
|
||||||
req, err = newTestSignedRequestV4("GET", getGetBucketNotificationURL("", bucketName),
|
|
||||||
0, nil, credentials.AccessKey, credentials.SecretKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s: Failed to create HTTP testRequest for ListenBucketNotification: <ERROR> %v", instanceType, err)
|
|
||||||
}
|
|
||||||
apiRouter.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("Unexpected http response %d", rec.Code)
|
|
||||||
}
|
|
||||||
notificationBytes, err := ioutil.ReadAll(rec.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error %s", err)
|
|
||||||
}
|
|
||||||
nConfig := notificationConfig{}
|
|
||||||
if err = xml.Unmarshal(notificationBytes, &nConfig); err != nil {
|
|
||||||
t.Fatalf("Unexpected XML received %s", err)
|
|
||||||
}
|
|
||||||
if sampleNotifCfg.QueueConfigs[0].QueueARN != nConfig.QueueConfigs[0].QueueARN {
|
|
||||||
t.Fatalf("Uexpected notification configs expected %#v, got %#v", sampleNotifCfg, nConfig)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(sampleNotifCfg.QueueConfigs[0].Events, nConfig.QueueConfigs[0].Events) {
|
|
||||||
t.Fatalf("Uexpected notification configs expected %#v, got %#v", sampleNotifCfg, nConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPutBucketNotificationHandler(t *testing.T) {
|
|
||||||
ExecObjectLayerAPITest(t, testPutBucketNotificationHandler, []string{
|
|
||||||
"PutBucketNotification",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPutBucketNotificationHandler(obj ObjectLayer, instanceType,
|
|
||||||
bucketName string, apiRouter http.Handler, credentials auth.Credentials,
|
|
||||||
t *testing.T) {
|
|
||||||
|
|
||||||
// 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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
sampleNotifCfg.LambdaConfigs = []lambdaConfig{
|
|
||||||
{
|
|
||||||
sampleSvcCfg, "testLARN",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
xmlBytes, err := xml.Marshal(sampleNotifCfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s: Unexpected err: %#v", instanceType, err)
|
|
||||||
}
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
req, err := newTestSignedRequestV4("PUT",
|
|
||||||
getPutBucketNotificationURL("", bucketName),
|
|
||||||
int64(len(xmlBytes)), bytes.NewReader(xmlBytes),
|
|
||||||
credentials.AccessKey, credentials.SecretKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s: Failed to create HTTP testRequest for PutBucketNotification: <ERROR> %v",
|
|
||||||
instanceType, err)
|
|
||||||
}
|
|
||||||
apiRouter.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusBadRequest {
|
|
||||||
t.Fatalf("Unexpected http response %d", rec.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
sampleNotifCfg.LambdaConfigs = nil
|
|
||||||
sampleNotifCfg.TopicConfigs = []topicConfig{
|
|
||||||
{
|
|
||||||
sampleSvcCfg, "testTARN",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
xmlBytes, err := xml.Marshal(sampleNotifCfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s: Unexpected err: %#v", instanceType, err)
|
|
||||||
}
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
req, err := newTestSignedRequestV4("PUT",
|
|
||||||
getPutBucketNotificationURL("", bucketName),
|
|
||||||
int64(len(xmlBytes)), bytes.NewReader(xmlBytes),
|
|
||||||
credentials.AccessKey, credentials.SecretKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s: Failed to create HTTP testRequest for PutBucketNotification: <ERROR> %v",
|
|
||||||
instanceType, err)
|
|
||||||
}
|
|
||||||
apiRouter.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusBadRequest {
|
|
||||||
t.Fatalf("Unexpected http response %d", rec.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListenBucketNotificationNilHandler(t *testing.T) {
|
|
||||||
ExecObjectLayerAPITest(t, testListenBucketNotificationNilHandler, []string{
|
|
||||||
"ListenBucketNotification",
|
|
||||||
"PutObject",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func testListenBucketNotificationNilHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
|
||||||
credentials auth.Credentials, t *testing.T) {
|
|
||||||
// get random bucket name.
|
|
||||||
randBucket := getRandomBucketName()
|
|
||||||
|
|
||||||
// Nil Object layer
|
|
||||||
nilAPIRouter := initTestAPIEndPoints(nil, []string{
|
|
||||||
"ListenBucketNotification",
|
|
||||||
})
|
|
||||||
testRec := httptest.NewRecorder()
|
|
||||||
testReq, tErr := newTestSignedRequestV4("GET",
|
|
||||||
getListenBucketNotificationURL("", randBucket, []string{},
|
|
||||||
[]string{"*.jpg"}, []string{
|
|
||||||
"s3:ObjectCreated:*",
|
|
||||||
"s3:ObjectRemoved:*",
|
|
||||||
"s3:ObjectAccessed:*",
|
|
||||||
}), 0, nil, credentials.AccessKey, credentials.SecretKey)
|
|
||||||
if tErr != nil {
|
|
||||||
t.Fatalf("%s: Failed to create HTTP testRequest for ListenBucketNotification: <ERROR> %v", instanceType, tErr)
|
|
||||||
}
|
|
||||||
nilAPIRouter.ServeHTTP(testRec, testReq)
|
|
||||||
if testRec.Code != http.StatusServiceUnavailable {
|
|
||||||
t.Fatalf("Test 1: %s: expected HTTP code %d, but received %d: <ERROR> %v",
|
|
||||||
instanceType, http.StatusServiceUnavailable, testRec.Code, tErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRemoveNotificationConfig(obj ObjectLayer, instanceType,
|
|
||||||
bucketName string, apiRouter http.Handler, credentials auth.Credentials,
|
|
||||||
t *testing.T) {
|
|
||||||
|
|
||||||
invalidBucket := "Invalid\\Bucket"
|
|
||||||
// get random bucket name.
|
|
||||||
randBucket := bucketName
|
|
||||||
|
|
||||||
nCfg := notificationConfig{
|
|
||||||
QueueConfigs: []queueConfig{
|
|
||||||
{
|
|
||||||
ServiceConfig: ServiceConfig{
|
|
||||||
Events: []string{"s3:ObjectRemoved:*",
|
|
||||||
"s3:ObjectCreated:*"},
|
|
||||||
},
|
|
||||||
QueueARN: "testqARN",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := persistNotificationConfig(randBucket, &nCfg, obj); err != nil {
|
|
||||||
t.Fatalf("Unexpected error: %#v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
bucketName string
|
|
||||||
expectedErr error
|
|
||||||
}{
|
|
||||||
{invalidBucket, BucketNameInvalid{Bucket: invalidBucket}},
|
|
||||||
{randBucket, nil},
|
|
||||||
}
|
|
||||||
for i, test := range testCases {
|
|
||||||
tErr := removeNotificationConfig(test.bucketName, obj)
|
|
||||||
if tErr != test.expectedErr {
|
|
||||||
t.Errorf("Test %d: %s expected error %v, but received %v", i+1, instanceType, test.expectedErr, tErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveNotificationConfig(t *testing.T) {
|
|
||||||
ExecObjectLayerAPITest(t, testRemoveNotificationConfig, []string{
|
|
||||||
"PutBucketNotification",
|
|
||||||
"ListenBucketNotification",
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,317 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/minio/minio-go/pkg/set"
|
|
||||||
)
|
|
||||||
|
|
||||||
// List of valid event types.
|
|
||||||
var suppportedEventTypes = map[string]struct{}{
|
|
||||||
// Object created event types.
|
|
||||||
"s3:ObjectCreated:*": {},
|
|
||||||
"s3:ObjectCreated:Put": {},
|
|
||||||
"s3:ObjectCreated:Post": {},
|
|
||||||
"s3:ObjectCreated:Copy": {},
|
|
||||||
"s3:ObjectCreated:CompleteMultipartUpload": {},
|
|
||||||
// Object removed event types.
|
|
||||||
"s3:ObjectRemoved:*": {},
|
|
||||||
"s3:ObjectRemoved:Delete": {},
|
|
||||||
"s3:ObjectAccessed:Get": {},
|
|
||||||
"s3:ObjectAccessed:Head": {},
|
|
||||||
"s3:ObjectAccessed:*": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkEvent - checks if an event is supported.
|
|
||||||
func checkEvent(event string) APIErrorCode {
|
|
||||||
_, ok := suppportedEventTypes[event]
|
|
||||||
if !ok {
|
|
||||||
return ErrEventNotification
|
|
||||||
}
|
|
||||||
return ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkEvents - checks given list of events if all of them are valid.
|
|
||||||
// given if one of them is invalid, this function returns an error.
|
|
||||||
func checkEvents(events []string) APIErrorCode {
|
|
||||||
for _, event := range events {
|
|
||||||
if s3Error := checkEvent(event); s3Error != ErrNone {
|
|
||||||
return s3Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid if filterName is 'prefix'.
|
|
||||||
func isValidFilterNamePrefix(filterName string) bool {
|
|
||||||
return "prefix" == filterName
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid if filterName is 'suffix'.
|
|
||||||
func isValidFilterNameSuffix(filterName string) bool {
|
|
||||||
return "suffix" == filterName
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is this a valid filterName? - returns true if valid.
|
|
||||||
func isValidFilterName(filterName string) bool {
|
|
||||||
return isValidFilterNamePrefix(filterName) || isValidFilterNameSuffix(filterName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkFilterRules - checks given list of filter rules if all of them are valid.
|
|
||||||
func checkFilterRules(filterRules []filterRule) APIErrorCode {
|
|
||||||
ruleSetMap := make(map[string]string)
|
|
||||||
// Validate all filter rules.
|
|
||||||
for _, filterRule := range filterRules {
|
|
||||||
// Unknown filter rule name found, returns an appropriate error.
|
|
||||||
if !isValidFilterName(filterRule.Name) {
|
|
||||||
return ErrFilterNameInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter names should not be set twice per notification service
|
|
||||||
// configuration, if found return an appropriate error.
|
|
||||||
if _, ok := ruleSetMap[filterRule.Name]; ok {
|
|
||||||
if isValidFilterNamePrefix(filterRule.Name) {
|
|
||||||
return ErrFilterNamePrefix
|
|
||||||
} else if isValidFilterNameSuffix(filterRule.Name) {
|
|
||||||
return ErrFilterNameSuffix
|
|
||||||
} else {
|
|
||||||
return ErrFilterNameInvalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !IsValidObjectPrefix(filterRule.Value) {
|
|
||||||
return ErrFilterValueInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the new rule name to keep track of duplicates.
|
|
||||||
ruleSetMap[filterRule.Name] = filterRule.Value
|
|
||||||
}
|
|
||||||
// Success all prefixes validated.
|
|
||||||
return ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks validity of input ARN for a given arnType.
|
|
||||||
func checkARN(arn, arnType string) APIErrorCode {
|
|
||||||
if !strings.HasPrefix(arn, arnType) {
|
|
||||||
return ErrARNNotification
|
|
||||||
}
|
|
||||||
strs := strings.SplitN(arn, ":", -1)
|
|
||||||
if len(strs) != 6 {
|
|
||||||
return ErrARNNotification
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server region is allowed to be empty by default,
|
|
||||||
// in such a scenario ARN region is not validating
|
|
||||||
// allowing all regions.
|
|
||||||
if sregion := globalServerConfig.GetRegion(); sregion != "" {
|
|
||||||
region := strs[3]
|
|
||||||
if region != sregion {
|
|
||||||
return ErrRegionNotification
|
|
||||||
}
|
|
||||||
}
|
|
||||||
accountID := strs[4]
|
|
||||||
resource := strs[5]
|
|
||||||
if accountID == "" || resource == "" {
|
|
||||||
return ErrARNNotification
|
|
||||||
}
|
|
||||||
return ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkQueueARN - check if the queue arn is valid.
|
|
||||||
func checkQueueARN(queueARN string) APIErrorCode {
|
|
||||||
return checkARN(queueARN, minioSqs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validates account id for input queue ARN.
|
|
||||||
func isValidQueueID(queueARN string) bool {
|
|
||||||
// Unmarshals QueueARN into structured object.
|
|
||||||
sqsARN := unmarshalSqsARN(queueARN)
|
|
||||||
// Is Queue identifier valid?.
|
|
||||||
|
|
||||||
if isAMQPQueue(sqsARN) { // AMQP eueue.
|
|
||||||
amqpN := globalServerConfig.Notify.GetAMQPByID(sqsARN.AccountID)
|
|
||||||
return amqpN.Enable && amqpN.URL != ""
|
|
||||||
} else if isMQTTQueue(sqsARN) {
|
|
||||||
mqttN := globalServerConfig.Notify.GetMQTTByID(sqsARN.AccountID)
|
|
||||||
return mqttN.Enable && mqttN.Broker != ""
|
|
||||||
} else if isNATSQueue(sqsARN) {
|
|
||||||
natsN := globalServerConfig.Notify.GetNATSByID(sqsARN.AccountID)
|
|
||||||
return natsN.Enable && natsN.Address != ""
|
|
||||||
} else if isElasticQueue(sqsARN) { // Elastic queue.
|
|
||||||
elasticN := globalServerConfig.Notify.GetElasticSearchByID(sqsARN.AccountID)
|
|
||||||
return elasticN.Enable && elasticN.URL != ""
|
|
||||||
} else if isRedisQueue(sqsARN) { // Redis queue.
|
|
||||||
redisN := globalServerConfig.Notify.GetRedisByID(sqsARN.AccountID)
|
|
||||||
return redisN.Enable && redisN.Addr != ""
|
|
||||||
} else if isPostgreSQLQueue(sqsARN) {
|
|
||||||
pgN := globalServerConfig.Notify.GetPostgreSQLByID(sqsARN.AccountID)
|
|
||||||
// Postgres can work with only default conn. info.
|
|
||||||
return pgN.Enable
|
|
||||||
} else if isMySQLQueue(sqsARN) {
|
|
||||||
msqlN := globalServerConfig.Notify.GetMySQLByID(sqsARN.AccountID)
|
|
||||||
// Mysql can work with only default conn. info.
|
|
||||||
return msqlN.Enable
|
|
||||||
} else if isKafkaQueue(sqsARN) {
|
|
||||||
kafkaN := globalServerConfig.Notify.GetKafkaByID(sqsARN.AccountID)
|
|
||||||
return (kafkaN.Enable && len(kafkaN.Brokers) > 0 &&
|
|
||||||
kafkaN.Topic != "")
|
|
||||||
} else if isWebhookQueue(sqsARN) {
|
|
||||||
webhookN := globalServerConfig.Notify.GetWebhookByID(sqsARN.AccountID)
|
|
||||||
return webhookN.Enable && webhookN.Endpoint != ""
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check - validates queue configuration and returns error if any.
|
|
||||||
func checkQueueConfig(qConfig queueConfig) APIErrorCode {
|
|
||||||
// Check queue arn is valid.
|
|
||||||
if s3Error := checkQueueARN(qConfig.QueueARN); s3Error != ErrNone {
|
|
||||||
return s3Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate if the account ID is correct.
|
|
||||||
if !isValidQueueID(qConfig.QueueARN) {
|
|
||||||
return ErrARNNotification
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if valid events are set in queue config.
|
|
||||||
if s3Error := checkEvents(qConfig.Events); s3Error != ErrNone {
|
|
||||||
return s3Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if valid filters are set in queue config.
|
|
||||||
if s3Error := checkFilterRules(qConfig.Filter.Key.FilterRules); s3Error != ErrNone {
|
|
||||||
return s3Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success.
|
|
||||||
return ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validates all incoming queue configs, checkQueueConfig validates if the
|
|
||||||
// input fields for each queues is not malformed and has valid configuration
|
|
||||||
// information. If validation fails bucket notifications are not enabled.
|
|
||||||
func validateQueueConfigs(queueConfigs []queueConfig) APIErrorCode {
|
|
||||||
for _, qConfig := range queueConfigs {
|
|
||||||
if s3Error := checkQueueConfig(qConfig); s3Error != ErrNone {
|
|
||||||
return s3Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Success.
|
|
||||||
return ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check all the queue configs for any duplicates.
|
|
||||||
func checkDuplicateQueueConfigs(configs []queueConfig) APIErrorCode {
|
|
||||||
queueConfigARNS := set.NewStringSet()
|
|
||||||
|
|
||||||
// Navigate through each configs and count the entries.
|
|
||||||
for _, config := range configs {
|
|
||||||
queueConfigARNS.Add(config.QueueARN)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(queueConfigARNS) != len(configs) {
|
|
||||||
return ErrOverlappingConfigs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success.
|
|
||||||
return ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validates all the bucket notification configuration for their validity,
|
|
||||||
// if one of the config is malformed or has invalid data it is rejected.
|
|
||||||
// Configuration is never applied partially.
|
|
||||||
func validateNotificationConfig(nConfig notificationConfig) APIErrorCode {
|
|
||||||
// Minio server does not support lambda/topic configurations
|
|
||||||
// currently. Such configuration is rejected.
|
|
||||||
if len(nConfig.LambdaConfigs) > 0 || len(nConfig.TopicConfigs) > 0 {
|
|
||||||
return ErrUnsupportedNotification
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all queue configs.
|
|
||||||
if s3Error := validateQueueConfigs(nConfig.QueueConfigs); s3Error != ErrNone {
|
|
||||||
return s3Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate queue configs.
|
|
||||||
if len(nConfig.QueueConfigs) > 1 {
|
|
||||||
if s3Error := checkDuplicateQueueConfigs(nConfig.QueueConfigs); s3Error != ErrNone {
|
|
||||||
return s3Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add validation for other configurations.
|
|
||||||
return ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshals input value of AWS ARN format into minioSqs object.
|
|
||||||
// Returned value represents minio sqs types, currently supported are
|
|
||||||
// - amqp
|
|
||||||
// - mqtt
|
|
||||||
// - nats
|
|
||||||
// - elasticsearch
|
|
||||||
// - redis
|
|
||||||
// - postgresql
|
|
||||||
// - mysql
|
|
||||||
// - kafka
|
|
||||||
// - webhook
|
|
||||||
func unmarshalSqsARN(queueARN string) (mSqs arnSQS) {
|
|
||||||
strs := strings.SplitN(queueARN, ":", -1)
|
|
||||||
if len(strs) != 6 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server region is allowed to be empty by default,
|
|
||||||
// in such a scenario ARN region is not validating
|
|
||||||
// allowing all regions.
|
|
||||||
if sregion := globalServerConfig.GetRegion(); sregion != "" {
|
|
||||||
region := strs[3]
|
|
||||||
if region != sregion {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sqsType := strs[5]
|
|
||||||
switch sqsType {
|
|
||||||
case queueTypeAMQP:
|
|
||||||
mSqs.Type = queueTypeAMQP
|
|
||||||
case queueTypeMQTT:
|
|
||||||
mSqs.Type = queueTypeMQTT
|
|
||||||
case queueTypeNATS:
|
|
||||||
mSqs.Type = queueTypeNATS
|
|
||||||
case queueTypeElastic:
|
|
||||||
mSqs.Type = queueTypeElastic
|
|
||||||
case queueTypeRedis:
|
|
||||||
mSqs.Type = queueTypeRedis
|
|
||||||
case queueTypePostgreSQL:
|
|
||||||
mSqs.Type = queueTypePostgreSQL
|
|
||||||
case queueTypeMySQL:
|
|
||||||
mSqs.Type = queueTypeMySQL
|
|
||||||
case queueTypeKafka:
|
|
||||||
mSqs.Type = queueTypeKafka
|
|
||||||
case queueTypeWebhook:
|
|
||||||
mSqs.Type = queueTypeWebhook
|
|
||||||
default:
|
|
||||||
errorIf(errors.New("invalid SQS type"), "SQS type: %s", sqsType)
|
|
||||||
} // Add more queues here.
|
|
||||||
|
|
||||||
mSqs.AccountID = strs[4]
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,420 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test validates for duplicate configs.
|
|
||||||
func TestCheckDuplicateConfigs(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
qConfigs []queueConfig
|
|
||||||
expectedErrCode APIErrorCode
|
|
||||||
}{
|
|
||||||
// Error for duplicate queue configs.
|
|
||||||
{
|
|
||||||
qConfigs: []queueConfig{
|
|
||||||
{
|
|
||||||
QueueARN: "arn:minio:sqs:us-east-1:1:redis",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
QueueARN: "arn:minio:sqs:us-east-1:1:redis",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedErrCode: ErrOverlappingConfigs,
|
|
||||||
},
|
|
||||||
// Valid queue configs.
|
|
||||||
{
|
|
||||||
qConfigs: []queueConfig{
|
|
||||||
{
|
|
||||||
QueueARN: "arn:minio:sqs:us-east-1:1:redis",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedErrCode: ErrNone,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... validate for duplicate queue configs.
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
errCode := checkDuplicateQueueConfigs(testCase.qConfigs)
|
|
||||||
if errCode != testCase.expectedErrCode {
|
|
||||||
t.Errorf("Test %d: Expected %d, got %d", i+1, testCase.expectedErrCode, errCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests for validating filter rules.
|
|
||||||
func TestCheckFilterRules(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
rules []filterRule
|
|
||||||
expectedErrCode APIErrorCode
|
|
||||||
}{
|
|
||||||
// Valid prefix and suffix values.
|
|
||||||
{
|
|
||||||
rules: []filterRule{
|
|
||||||
{
|
|
||||||
Name: "prefix",
|
|
||||||
Value: "test/test1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "suffix",
|
|
||||||
Value: ".jpg",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedErrCode: ErrNone,
|
|
||||||
},
|
|
||||||
// Invalid filter name.
|
|
||||||
{
|
|
||||||
rules: []filterRule{
|
|
||||||
{
|
|
||||||
Name: "unknown",
|
|
||||||
Value: "test/test1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedErrCode: ErrFilterNameInvalid,
|
|
||||||
},
|
|
||||||
// Cannot have duplicate prefixes.
|
|
||||||
{
|
|
||||||
rules: []filterRule{
|
|
||||||
{
|
|
||||||
Name: "prefix",
|
|
||||||
Value: "test/test1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "prefix",
|
|
||||||
Value: "test/test1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedErrCode: ErrFilterNamePrefix,
|
|
||||||
},
|
|
||||||
// Cannot have duplicate suffixes.
|
|
||||||
{
|
|
||||||
rules: []filterRule{
|
|
||||||
{
|
|
||||||
Name: "suffix",
|
|
||||||
Value: ".jpg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "suffix",
|
|
||||||
Value: ".txt",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedErrCode: ErrFilterNameSuffix,
|
|
||||||
},
|
|
||||||
// Filter value cannot be bigger than > 1024.
|
|
||||||
{
|
|
||||||
rules: []filterRule{
|
|
||||||
{
|
|
||||||
Name: "prefix",
|
|
||||||
Value: strings.Repeat("a", 1025),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedErrCode: ErrFilterValueInvalid,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
errCode := checkFilterRules(testCase.rules)
|
|
||||||
if errCode != testCase.expectedErrCode {
|
|
||||||
t.Errorf("Test %d: Expected %d, got %d", i+1, testCase.expectedErrCode, errCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests filter name validation.
|
|
||||||
func TestIsValidFilterName(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
filterName string
|
|
||||||
status bool
|
|
||||||
}{
|
|
||||||
// Validate if 'prefix' is correct.
|
|
||||||
{
|
|
||||||
filterName: "prefix",
|
|
||||||
status: true,
|
|
||||||
},
|
|
||||||
// Validate if 'suffix' is correct.
|
|
||||||
{
|
|
||||||
filterName: "suffix",
|
|
||||||
status: true,
|
|
||||||
},
|
|
||||||
// Invalid filter name empty string should return false.
|
|
||||||
{
|
|
||||||
filterName: "",
|
|
||||||
status: false,
|
|
||||||
},
|
|
||||||
// Invalid filter name random character should return false.
|
|
||||||
{
|
|
||||||
filterName: "unknown",
|
|
||||||
status: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
status := isValidFilterName(testCase.filterName)
|
|
||||||
if testCase.status != status {
|
|
||||||
t.Errorf("Test %d: Expected \"%t\", got \"%t\"", i+1, testCase.status, status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests list of valid and invalid events.
|
|
||||||
func TestValidEvents(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
events []string
|
|
||||||
errCode APIErrorCode
|
|
||||||
}{
|
|
||||||
// Return error for unknown event element.
|
|
||||||
{
|
|
||||||
events: []string{
|
|
||||||
"s3:UnknownAPI",
|
|
||||||
},
|
|
||||||
errCode: ErrEventNotification,
|
|
||||||
},
|
|
||||||
// Return success for supported event.
|
|
||||||
{
|
|
||||||
events: []string{
|
|
||||||
"s3:ObjectCreated:Put",
|
|
||||||
},
|
|
||||||
errCode: ErrNone,
|
|
||||||
},
|
|
||||||
// Return success for supported events.
|
|
||||||
{
|
|
||||||
events: []string{
|
|
||||||
"s3:ObjectCreated:*",
|
|
||||||
"s3:ObjectRemoved:*",
|
|
||||||
},
|
|
||||||
errCode: ErrNone,
|
|
||||||
},
|
|
||||||
// Return error for empty event list.
|
|
||||||
{
|
|
||||||
events: []string{""},
|
|
||||||
errCode: ErrEventNotification,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
errCode := checkEvents(testCase.events)
|
|
||||||
if testCase.errCode != errCode {
|
|
||||||
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests queue arn validation.
|
|
||||||
func TestQueueARN(t *testing.T) {
|
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable initialize config file, %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
queueARN string
|
|
||||||
errCode APIErrorCode
|
|
||||||
}{
|
|
||||||
|
|
||||||
// Valid webhook queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:webhook",
|
|
||||||
errCode: ErrNone,
|
|
||||||
},
|
|
||||||
// Valid redis queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:redis",
|
|
||||||
errCode: ErrNone,
|
|
||||||
},
|
|
||||||
// Valid elasticsearch queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:elasticsearch",
|
|
||||||
errCode: ErrNone,
|
|
||||||
},
|
|
||||||
// Valid amqp queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
|
|
||||||
errCode: ErrNone,
|
|
||||||
},
|
|
||||||
// Invalid empty queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "",
|
|
||||||
errCode: ErrARNNotification,
|
|
||||||
},
|
|
||||||
// Invalid notification service type.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sns:us-east-1:1:listen",
|
|
||||||
errCode: ErrARNNotification,
|
|
||||||
},
|
|
||||||
// Invalid queue name empty in queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:",
|
|
||||||
errCode: ErrARNNotification,
|
|
||||||
},
|
|
||||||
// Invalid queue id empty in queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1::redis",
|
|
||||||
errCode: ErrARNNotification,
|
|
||||||
},
|
|
||||||
// Invalid queue id and queue name empty in queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1::",
|
|
||||||
errCode: ErrARNNotification,
|
|
||||||
},
|
|
||||||
// Missing queue id and separator missing at the end in queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:amqp",
|
|
||||||
errCode: ErrARNNotification,
|
|
||||||
},
|
|
||||||
// Missing queue id and empty string at the end in queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:",
|
|
||||||
errCode: ErrARNNotification,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all tests for queue arn.
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
errCode := checkQueueARN(testCase.queueARN)
|
|
||||||
if testCase.errCode != errCode {
|
|
||||||
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test when server region is set.
|
|
||||||
rootPath, err = newTestConfig("us-east-1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable initialize config file, %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
testCases = []struct {
|
|
||||||
queueARN string
|
|
||||||
errCode APIErrorCode
|
|
||||||
}{
|
|
||||||
// Incorrect region should produce error.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-west-1:1:webhook",
|
|
||||||
errCode: ErrRegionNotification,
|
|
||||||
},
|
|
||||||
// Correct region should not produce error.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:webhook",
|
|
||||||
errCode: ErrNone,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all tests for queue arn.
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
errCode := checkQueueARN(testCase.queueARN)
|
|
||||||
if testCase.errCode != errCode {
|
|
||||||
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test unmarshal queue arn.
|
|
||||||
func TestUnmarshalSQSARN(t *testing.T) {
|
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable initialize config file, %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
queueARN string
|
|
||||||
Type string
|
|
||||||
}{
|
|
||||||
// Valid webhook queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:webhook",
|
|
||||||
Type: "webhook",
|
|
||||||
},
|
|
||||||
// Valid redis queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:redis",
|
|
||||||
Type: "redis",
|
|
||||||
},
|
|
||||||
// Valid elasticsearch queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:elasticsearch",
|
|
||||||
Type: "elasticsearch",
|
|
||||||
},
|
|
||||||
// Valid amqp queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
|
|
||||||
Type: "amqp",
|
|
||||||
},
|
|
||||||
// Valid mqtt queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:mqtt",
|
|
||||||
Type: "mqtt",
|
|
||||||
},
|
|
||||||
// Invalid empty queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "",
|
|
||||||
Type: "",
|
|
||||||
},
|
|
||||||
// Partial queue arn.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:",
|
|
||||||
Type: "",
|
|
||||||
},
|
|
||||||
// Invalid queue service value.
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:*",
|
|
||||||
Type: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
mSqs := unmarshalSqsARN(testCase.queueARN)
|
|
||||||
if testCase.Type != mSqs.Type {
|
|
||||||
t.Errorf("Test %d: Expected \"%s\", got \"%s\"", i+1, testCase.Type, mSqs.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test when the server region is set.
|
|
||||||
rootPath, err = newTestConfig("us-east-1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable initialize config file, %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
testCases = []struct {
|
|
||||||
queueARN string
|
|
||||||
Type string
|
|
||||||
}{
|
|
||||||
// Incorrect region in ARN returns empty mSqs.Type
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-west-1:1:webhook",
|
|
||||||
Type: "",
|
|
||||||
},
|
|
||||||
// Correct regionin ARN returns valid mSqs.Type
|
|
||||||
{
|
|
||||||
queueARN: "arn:minio:sqs:us-east-1:1:webhook",
|
|
||||||
Type: "webhook",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
mSqs := unmarshalSqsARN(testCase.queueARN)
|
|
||||||
if testCase.Type != mSqs.Type {
|
|
||||||
t.Errorf("Test %d: Expected \"%s\", got \"%s\"", i+1, testCase.Type, mSqs.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -285,6 +285,10 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for addr, err := range globalNotificationSys.UpdateBucketPolicy(bucket) {
|
||||||
|
errorIf(err, "unable to update policy change in remote peer %v", addr)
|
||||||
|
}
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
}
|
}
|
||||||
|
@ -322,6 +326,10 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for addr, err := range globalNotificationSys.UpdateBucketPolicy(bucket) {
|
||||||
|
errorIf(err, "unable to update policy change in remote peer %v", addr)
|
||||||
|
}
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -31,7 +31,7 @@ import (
|
||||||
const (
|
const (
|
||||||
// Static prefix to be used while constructing bucket ARN.
|
// Static prefix to be used while constructing bucket ARN.
|
||||||
// refer to S3 docs for more info.
|
// refer to S3 docs for more info.
|
||||||
bucketARNPrefix = "arn:" + eventSource + ":::"
|
bucketARNPrefix = "arn:aws:s3:::"
|
||||||
|
|
||||||
// Bucket policy config name.
|
// Bucket policy config name.
|
||||||
bucketPolicyConfig = "policy.json"
|
bucketPolicyConfig = "policy.json"
|
||||||
|
@ -202,7 +202,5 @@ func persistAndNotifyBucketPolicyChange(bucket string, isRemove bool, bktPolicy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify all peers (including self) to update in-memory state
|
|
||||||
S3PeersUpdateBucketPolicy(bucket)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -24,6 +24,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
"github.com/minio/minio/pkg/event/target"
|
||||||
"github.com/minio/minio/pkg/quick"
|
"github.com/minio/minio/pkg/quick"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
@ -129,7 +131,7 @@ func (s *serverConfig) ConfigDiff(t *serverConfig) string {
|
||||||
return "AMQP Notification configuration differs"
|
return "AMQP Notification configuration differs"
|
||||||
case !reflect.DeepEqual(s.Notify.NATS, t.Notify.NATS):
|
case !reflect.DeepEqual(s.Notify.NATS, t.Notify.NATS):
|
||||||
return "NATS Notification configuration differs"
|
return "NATS Notification configuration differs"
|
||||||
case !reflect.DeepEqual(s.Notify.ElasticSearch, t.Notify.ElasticSearch):
|
case !reflect.DeepEqual(s.Notify.Elasticsearch, t.Notify.Elasticsearch):
|
||||||
return "ElasticSearch Notification configuration differs"
|
return "ElasticSearch Notification configuration differs"
|
||||||
case !reflect.DeepEqual(s.Notify.Redis, t.Notify.Redis):
|
case !reflect.DeepEqual(s.Notify.Redis, t.Notify.Redis):
|
||||||
return "Redis Notification configuration differs"
|
return "Redis Notification configuration differs"
|
||||||
|
@ -166,24 +168,24 @@ func newServerConfig() *serverConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure to initialize notification configs.
|
// Make sure to initialize notification configs.
|
||||||
srvCfg.Notify.AMQP = make(map[string]amqpNotify)
|
srvCfg.Notify.AMQP = make(map[string]target.AMQPArgs)
|
||||||
srvCfg.Notify.AMQP["1"] = amqpNotify{}
|
srvCfg.Notify.AMQP["1"] = target.AMQPArgs{}
|
||||||
srvCfg.Notify.MQTT = make(map[string]mqttNotify)
|
srvCfg.Notify.MQTT = make(map[string]target.MQTTArgs)
|
||||||
srvCfg.Notify.MQTT["1"] = mqttNotify{}
|
srvCfg.Notify.MQTT["1"] = target.MQTTArgs{}
|
||||||
srvCfg.Notify.ElasticSearch = make(map[string]elasticSearchNotify)
|
srvCfg.Notify.Elasticsearch = make(map[string]target.ElasticsearchArgs)
|
||||||
srvCfg.Notify.ElasticSearch["1"] = elasticSearchNotify{}
|
srvCfg.Notify.Elasticsearch["1"] = target.ElasticsearchArgs{}
|
||||||
srvCfg.Notify.Redis = make(map[string]redisNotify)
|
srvCfg.Notify.Redis = make(map[string]target.RedisArgs)
|
||||||
srvCfg.Notify.Redis["1"] = redisNotify{}
|
srvCfg.Notify.Redis["1"] = target.RedisArgs{}
|
||||||
srvCfg.Notify.NATS = make(map[string]natsNotify)
|
srvCfg.Notify.NATS = make(map[string]target.NATSArgs)
|
||||||
srvCfg.Notify.NATS["1"] = natsNotify{}
|
srvCfg.Notify.NATS["1"] = target.NATSArgs{}
|
||||||
srvCfg.Notify.PostgreSQL = make(map[string]postgreSQLNotify)
|
srvCfg.Notify.PostgreSQL = make(map[string]target.PostgreSQLArgs)
|
||||||
srvCfg.Notify.PostgreSQL["1"] = postgreSQLNotify{}
|
srvCfg.Notify.PostgreSQL["1"] = target.PostgreSQLArgs{}
|
||||||
srvCfg.Notify.MySQL = make(map[string]mySQLNotify)
|
srvCfg.Notify.MySQL = make(map[string]target.MySQLArgs)
|
||||||
srvCfg.Notify.MySQL["1"] = mySQLNotify{}
|
srvCfg.Notify.MySQL["1"] = target.MySQLArgs{}
|
||||||
srvCfg.Notify.Kafka = make(map[string]kafkaNotify)
|
srvCfg.Notify.Kafka = make(map[string]target.KafkaArgs)
|
||||||
srvCfg.Notify.Kafka["1"] = kafkaNotify{}
|
srvCfg.Notify.Kafka["1"] = target.KafkaArgs{}
|
||||||
srvCfg.Notify.Webhook = make(map[string]webhookNotify)
|
srvCfg.Notify.Webhook = make(map[string]target.WebhookArgs)
|
||||||
srvCfg.Notify.Webhook["1"] = webhookNotify{}
|
srvCfg.Notify.Webhook["1"] = target.WebhookArgs{}
|
||||||
|
|
||||||
return srvCfg
|
return srvCfg
|
||||||
}
|
}
|
||||||
|
@ -310,11 +312,6 @@ func getValidConfig() (*serverConfig, error) {
|
||||||
return nil, errors.New("invalid credential in config file " + configFile)
|
return nil, errors.New("invalid credential in config file " + configFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate notify field
|
|
||||||
if err = srvCfg.Notify.Validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return srvCfg, nil
|
return srvCfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,3 +366,119 @@ func loadConfig() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getNotificationTargets - returns TargetList which contains enabled targets in serverConfig.
|
||||||
|
// A new notification target is added like below
|
||||||
|
// * Add a new target in pkg/event/target package.
|
||||||
|
// * Add newly added target configuration to serverConfig.Notify.<TARGET_NAME>.
|
||||||
|
// * Handle the configuration in this function to create/add into TargetList.
|
||||||
|
func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
||||||
|
targetList := event.NewTargetList()
|
||||||
|
|
||||||
|
for id, args := range config.Notify.AMQP {
|
||||||
|
if args.Enable {
|
||||||
|
newTarget, err := target.NewAMQPTarget(id, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, args := range config.Notify.Elasticsearch {
|
||||||
|
if args.Enable {
|
||||||
|
newTarget, err := target.NewElasticsearchTarget(id, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, args := range config.Notify.Kafka {
|
||||||
|
if args.Enable {
|
||||||
|
newTarget, err := target.NewKafkaTarget(id, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, args := range config.Notify.MQTT {
|
||||||
|
if args.Enable {
|
||||||
|
newTarget, err := target.NewMQTTTarget(id, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, args := range config.Notify.MySQL {
|
||||||
|
if args.Enable {
|
||||||
|
newTarget, err := target.NewMySQLTarget(id, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, args := range config.Notify.NATS {
|
||||||
|
if args.Enable {
|
||||||
|
newTarget, err := target.NewNATSTarget(id, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, args := range config.Notify.PostgreSQL {
|
||||||
|
if args.Enable {
|
||||||
|
newTarget, err := target.NewPostgreSQLTarget(id, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, args := range config.Notify.Redis {
|
||||||
|
if args.Enable {
|
||||||
|
newTarget, err := target.NewRedisTarget(id, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, args := range config.Notify.Webhook {
|
||||||
|
if args.Enable {
|
||||||
|
newTarget := target.NewWebhookTarget(id, args)
|
||||||
|
if err := targetList.Add(newTarget); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetList, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -20,10 +20,10 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
|
"github.com/minio/minio/pkg/event/target"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -45,55 +45,6 @@ func TestServerConfig(t *testing.T) {
|
||||||
t.Errorf("Expecting region `us-west-1` found %s", globalServerConfig.GetRegion())
|
t.Errorf("Expecting region `us-west-1` found %s", globalServerConfig.GetRegion())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set new amqp notification id.
|
|
||||||
globalServerConfig.Notify.SetAMQPByID("2", amqpNotify{})
|
|
||||||
savedNotifyCfg1 := globalServerConfig.Notify.GetAMQPByID("2")
|
|
||||||
if !reflect.DeepEqual(savedNotifyCfg1, amqpNotify{}) {
|
|
||||||
t.Errorf("Expecting AMQP config %#v found %#v", amqpNotify{}, savedNotifyCfg1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set new elastic search notification id.
|
|
||||||
globalServerConfig.Notify.SetElasticSearchByID("2", elasticSearchNotify{})
|
|
||||||
savedNotifyCfg2 := globalServerConfig.Notify.GetElasticSearchByID("2")
|
|
||||||
if !reflect.DeepEqual(savedNotifyCfg2, elasticSearchNotify{}) {
|
|
||||||
t.Errorf("Expecting Elasticsearch config %#v found %#v", elasticSearchNotify{}, savedNotifyCfg2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set new redis notification id.
|
|
||||||
globalServerConfig.Notify.SetRedisByID("2", redisNotify{})
|
|
||||||
savedNotifyCfg3 := globalServerConfig.Notify.GetRedisByID("2")
|
|
||||||
if !reflect.DeepEqual(savedNotifyCfg3, redisNotify{}) {
|
|
||||||
t.Errorf("Expecting Redis config %#v found %#v", redisNotify{}, savedNotifyCfg3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set new kafka notification id.
|
|
||||||
globalServerConfig.Notify.SetKafkaByID("2", kafkaNotify{})
|
|
||||||
savedNotifyCfg4 := globalServerConfig.Notify.GetKafkaByID("2")
|
|
||||||
if !reflect.DeepEqual(savedNotifyCfg4, kafkaNotify{}) {
|
|
||||||
t.Errorf("Expecting Kafka config %#v found %#v", kafkaNotify{}, savedNotifyCfg4)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set new Webhook notification id.
|
|
||||||
globalServerConfig.Notify.SetWebhookByID("2", webhookNotify{})
|
|
||||||
savedNotifyCfg5 := globalServerConfig.Notify.GetWebhookByID("2")
|
|
||||||
if !reflect.DeepEqual(savedNotifyCfg5, webhookNotify{}) {
|
|
||||||
t.Errorf("Expecting Webhook config %#v found %#v", webhookNotify{}, savedNotifyCfg5)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set new MySQL notification id.
|
|
||||||
globalServerConfig.Notify.SetMySQLByID("2", mySQLNotify{})
|
|
||||||
savedNotifyCfg6 := globalServerConfig.Notify.GetMySQLByID("2")
|
|
||||||
if !reflect.DeepEqual(savedNotifyCfg6, mySQLNotify{}) {
|
|
||||||
t.Errorf("Expecting Webhook config %#v found %#v", mySQLNotify{}, savedNotifyCfg6)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set new MQTT notification id.
|
|
||||||
globalServerConfig.Notify.SetMQTTByID("2", mqttNotify{})
|
|
||||||
savedNotifyCfg7 := globalServerConfig.Notify.GetMQTTByID("2")
|
|
||||||
if !reflect.DeepEqual(savedNotifyCfg7, mqttNotify{}) {
|
|
||||||
t.Errorf("Expecting Webhook config %#v found %#v", mqttNotify{}, savedNotifyCfg7)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match version.
|
// Match version.
|
||||||
if globalServerConfig.GetVersion() != serverConfigVersion {
|
if globalServerConfig.GetVersion() != serverConfigVersion {
|
||||||
t.Errorf("Expecting version %s found %s", globalServerConfig.GetVersion(), serverConfigVersion)
|
t.Errorf("Expecting version %s found %s", globalServerConfig.GetVersion(), serverConfigVersion)
|
||||||
|
@ -252,55 +203,55 @@ func TestValidateConfig(t *testing.T) {
|
||||||
{`{"version": "` + v + `", "browser": "on", "browser": "on", "region":"us-east-1", "credential" : {"accessKey":"minio", "secretKey":"minio123"}}`, false},
|
{`{"version": "` + v + `", "browser": "on", "browser": "on", "region":"us-east-1", "credential" : {"accessKey":"minio", "secretKey":"minio123"}}`, false},
|
||||||
|
|
||||||
// Test 11 - Test AMQP
|
// Test 11 - Test AMQP
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "amqp": { "1": { "enable": true, "url": "", "exchange": "", "routingKey": "", "exchangeType": "", "mandatory": false, "immediate": false, "durable": false, "internal": false, "noWait": false, "autoDeleted": false }}}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "amqp": { "1": { "enable": true, "url": "", "exchange": "", "routingKey": "", "exchangeType": "", "mandatory": false, "immediate": false, "durable": false, "internal": false, "noWait": false, "autoDeleted": false }}}}`, true},
|
||||||
|
|
||||||
// Test 12 - Test NATS
|
// Test 12 - Test NATS
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nats": { "1": { "enable": true, "address": "", "subject": "", "username": "", "password": "", "token": "", "secure": false, "pingInterval": 0, "streaming": { "enable": false, "clusterID": "", "clientID": "", "async": false, "maxPubAcksInflight": 0 } } }}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nats": { "1": { "enable": true, "address": "", "subject": "", "username": "", "password": "", "token": "", "secure": false, "pingInterval": 0, "streaming": { "enable": false, "clusterID": "", "clientID": "", "async": false, "maxPubAcksInflight": 0 } } }}}`, true},
|
||||||
|
|
||||||
// Test 13 - Test ElasticSearch
|
// Test 13 - Test ElasticSearch
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "url": "", "index": "" } }}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "url": "", "index": "" } }}}`, true},
|
||||||
|
|
||||||
// Test 14 - Test Redis
|
// Test 14 - Test Redis
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "address": "", "password": "", "key": "" } }}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "address": "", "password": "", "key": "" } }}}`, true},
|
||||||
|
|
||||||
// Test 15 - Test PostgreSQL
|
// Test 15 - Test PostgreSQL
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, true},
|
||||||
|
|
||||||
// Test 16 - Test Kafka
|
// Test 16 - Test Kafka
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "kafka": { "1": { "enable": true, "brokers": null, "topic": "" } }}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "kafka": { "1": { "enable": true, "brokers": null, "topic": "" } }}}`, true},
|
||||||
|
|
||||||
// Test 17 - Test Webhook
|
// Test 17 - Test Webhook
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "webhook": { "1": { "enable": true, "endpoint": "" } }}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "webhook": { "1": { "enable": true, "endpoint": "" } }}}`, true},
|
||||||
|
|
||||||
// Test 18 - Test MySQL
|
// Test 18 - Test MySQL
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, true},
|
||||||
|
|
||||||
// Test 19 - Test Format for MySQL
|
// Test 19 - Test Format for MySQL
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "invalid", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "invalid", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
|
||||||
|
|
||||||
// Test 20 - Test valid Format for MySQL
|
// Test 20 - Test valid Format for MySQL
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "namespace", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "namespace", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
|
||||||
|
|
||||||
// Test 21 - Test Format for PostgreSQL
|
// Test 21 - Test Format for PostgreSQL
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "format": "invalid", "table": "xxx", "host": "myhost", "port": "5432", "user": "abc", "password": "pqr", "database": "test1" }}}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "format": "invalid", "table": "xxx", "host": "myhost", "port": "5432", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
|
||||||
|
|
||||||
// Test 22 - Test valid Format for PostgreSQL
|
// Test 22 - Test valid Format for PostgreSQL
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "format": "namespace", "table": "xxx", "host": "myhost", "port": "5432", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "format": "namespace", "table": "xxx", "host": "myhost", "port": "5432", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
|
||||||
|
|
||||||
// Test 23 - Test Format for ElasticSearch
|
// Test 23 - Test Format for ElasticSearch
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "invalid", "url": "example.com", "index": "myindex" } }}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "invalid", "url": "example.com", "index": "myindex" } }}}`, true},
|
||||||
|
|
||||||
// Test 24 - Test valid Format for ElasticSearch
|
// Test 24 - Test valid Format for ElasticSearch
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "namespace", "url": "example.com", "index": "myindex" } }}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "namespace", "url": "example.com", "index": "myindex" } }}}`, true},
|
||||||
|
|
||||||
// Test 25 - Test Format for Redis
|
// Test 25 - Test Format for Redis
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "invalid", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "invalid", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
|
||||||
|
|
||||||
// Test 26 - Test valid Format for Redis
|
// Test 26 - Test valid Format for Redis
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
|
||||||
|
|
||||||
// Test 27 - Test MQTT
|
// Test 27 - Test MQTT
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "clientId": "", "username": "", "password": ""}}}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "clientId": "", "username": "", "password": ""}}}}`, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
|
@ -345,56 +296,56 @@ func TestConfigDiff(t *testing.T) {
|
||||||
},
|
},
|
||||||
// 7
|
// 7
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{AMQP: map[string]amqpNotify{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{AMQP: map[string]target.AMQPArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{AMQP: map[string]amqpNotify{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{AMQP: map[string]target.AMQPArgs{"1": {Enable: false}}}},
|
||||||
"AMQP Notification configuration differs",
|
"AMQP Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 8
|
// 8
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{NATS: map[string]natsNotify{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{NATS: map[string]target.NATSArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{NATS: map[string]natsNotify{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{NATS: map[string]target.NATSArgs{"1": {Enable: false}}}},
|
||||||
"NATS Notification configuration differs",
|
"NATS Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 9
|
// 9
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{ElasticSearch: map[string]elasticSearchNotify{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{Elasticsearch: map[string]target.ElasticsearchArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{ElasticSearch: map[string]elasticSearchNotify{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{Elasticsearch: map[string]target.ElasticsearchArgs{"1": {Enable: false}}}},
|
||||||
"ElasticSearch Notification configuration differs",
|
"ElasticSearch Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 10
|
// 10
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{Redis: map[string]redisNotify{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{Redis: map[string]target.RedisArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{Redis: map[string]redisNotify{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{Redis: map[string]target.RedisArgs{"1": {Enable: false}}}},
|
||||||
"Redis Notification configuration differs",
|
"Redis Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 11
|
// 11
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{PostgreSQL: map[string]postgreSQLNotify{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{PostgreSQL: map[string]target.PostgreSQLArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{PostgreSQL: map[string]postgreSQLNotify{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{PostgreSQL: map[string]target.PostgreSQLArgs{"1": {Enable: false}}}},
|
||||||
"PostgreSQL Notification configuration differs",
|
"PostgreSQL Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 12
|
// 12
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{Kafka: map[string]kafkaNotify{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{Kafka: map[string]target.KafkaArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{Kafka: map[string]kafkaNotify{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{Kafka: map[string]target.KafkaArgs{"1": {Enable: false}}}},
|
||||||
"Kafka Notification configuration differs",
|
"Kafka Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 13
|
// 13
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{Webhook: map[string]webhookNotify{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{Webhook: map[string]target.WebhookArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{Webhook: map[string]webhookNotify{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{Webhook: map[string]target.WebhookArgs{"1": {Enable: false}}}},
|
||||||
"Webhook Notification configuration differs",
|
"Webhook Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 14
|
// 14
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{MySQL: map[string]mySQLNotify{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{MySQL: map[string]target.MySQLArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{MySQL: map[string]mySQLNotify{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{MySQL: map[string]target.MySQLArgs{"1": {Enable: false}}}},
|
||||||
"MySQL Notification configuration differs",
|
"MySQL Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 15
|
// 15
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{MQTT: map[string]mqttNotify{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{MQTT: map[string]mqttNotify{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: false}}}},
|
||||||
"MQTT Notification configuration differs",
|
"MQTT Notification configuration differs",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -20,6 +20,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
|
"github.com/minio/minio/pkg/event/target"
|
||||||
)
|
)
|
||||||
|
|
||||||
/////////////////// Config V1 ///////////////////
|
/////////////////// Config V1 ///////////////////
|
||||||
|
@ -226,23 +227,23 @@ type configV6 struct {
|
||||||
// Notifier represents collection of supported notification queues in version
|
// Notifier represents collection of supported notification queues in version
|
||||||
// 1 without NATS streaming.
|
// 1 without NATS streaming.
|
||||||
type notifierV1 struct {
|
type notifierV1 struct {
|
||||||
AMQP map[string]amqpNotify `json:"amqp"`
|
AMQP map[string]target.AMQPArgs `json:"amqp"`
|
||||||
NATS map[string]natsNotifyV1 `json:"nats"`
|
NATS map[string]natsNotifyV1 `json:"nats"`
|
||||||
ElasticSearch map[string]elasticSearchNotify `json:"elasticsearch"`
|
ElasticSearch map[string]target.ElasticsearchArgs `json:"elasticsearch"`
|
||||||
Redis map[string]redisNotify `json:"redis"`
|
Redis map[string]target.RedisArgs `json:"redis"`
|
||||||
PostgreSQL map[string]postgreSQLNotify `json:"postgresql"`
|
PostgreSQL map[string]target.PostgreSQLArgs `json:"postgresql"`
|
||||||
Kafka map[string]kafkaNotify `json:"kafka"`
|
Kafka map[string]target.KafkaArgs `json:"kafka"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifier represents collection of supported notification queues in version 2
|
// Notifier represents collection of supported notification queues in version 2
|
||||||
// with NATS streaming but without webhook.
|
// with NATS streaming but without webhook.
|
||||||
type notifierV2 struct {
|
type notifierV2 struct {
|
||||||
AMQP map[string]amqpNotify `json:"amqp"`
|
AMQP map[string]target.AMQPArgs `json:"amqp"`
|
||||||
NATS map[string]natsNotify `json:"nats"`
|
NATS map[string]target.NATSArgs `json:"nats"`
|
||||||
ElasticSearch map[string]elasticSearchNotify `json:"elasticsearch"`
|
ElasticSearch map[string]target.ElasticsearchArgs `json:"elasticsearch"`
|
||||||
Redis map[string]redisNotify `json:"redis"`
|
Redis map[string]target.RedisArgs `json:"redis"`
|
||||||
PostgreSQL map[string]postgreSQLNotify `json:"postgresql"`
|
PostgreSQL map[string]target.PostgreSQLArgs `json:"postgresql"`
|
||||||
Kafka map[string]kafkaNotify `json:"kafka"`
|
Kafka map[string]target.KafkaArgs `json:"kafka"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// configV7 server configuration version '7'.
|
// configV7 server configuration version '7'.
|
||||||
|
@ -368,6 +369,18 @@ type serverConfigV12 struct {
|
||||||
Notify notifierV2 `json:"notify"`
|
Notify notifierV2 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type notifier struct {
|
||||||
|
AMQP map[string]target.AMQPArgs `json:"amqp"`
|
||||||
|
Elasticsearch map[string]target.ElasticsearchArgs `json:"elasticsearch"`
|
||||||
|
Kafka map[string]target.KafkaArgs `json:"kafka"`
|
||||||
|
MQTT map[string]target.MQTTArgs `json:"mqtt"`
|
||||||
|
MySQL map[string]target.MySQLArgs `json:"mysql"`
|
||||||
|
NATS map[string]target.NATSArgs `json:"nats"`
|
||||||
|
PostgreSQL map[string]target.PostgreSQLArgs `json:"postgresql"`
|
||||||
|
Redis map[string]target.RedisArgs `json:"redis"`
|
||||||
|
Webhook map[string]target.WebhookArgs `json:"webhook"`
|
||||||
|
}
|
||||||
|
|
||||||
// serverConfigV13 server configuration version '13' which is like
|
// serverConfigV13 server configuration version '13' which is like
|
||||||
// version '12' except it adds support for webhook notification.
|
// version '12' except it adds support for webhook notification.
|
||||||
type serverConfigV13 struct {
|
type serverConfigV13 struct {
|
||||||
|
|
|
@ -462,9 +462,13 @@ func GetLocalPeer(endpoints EndpointList) (localPeer string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if peerSet.IsEmpty() {
|
if peerSet.IsEmpty() {
|
||||||
// If local peer is empty can happen in FS or Erasure coded mode.
|
// Local peer can be empty in FS or Erasure coded mode.
|
||||||
// then set the value to globalMinioAddr instead.
|
// If so, return globalMinioHost + globalMinioPort value.
|
||||||
return globalMinioAddr
|
if globalMinioHost != "" {
|
||||||
|
return globalMinioHost + ":" + globalMinioPort
|
||||||
|
}
|
||||||
|
|
||||||
|
return "127.0.0.1:" + globalMinioPort
|
||||||
}
|
}
|
||||||
return peerSet.ToSlice()[0]
|
return peerSet.ToSlice()[0]
|
||||||
}
|
}
|
||||||
|
|
|
@ -334,16 +334,19 @@ func TestCreateEndpoints(t *testing.T) {
|
||||||
// is considered a remote service from localhost:9000 perspective.
|
// is considered a remote service from localhost:9000 perspective.
|
||||||
func TestGetLocalPeer(t *testing.T) {
|
func TestGetLocalPeer(t *testing.T) {
|
||||||
tempGlobalMinioAddr := globalMinioAddr
|
tempGlobalMinioAddr := globalMinioAddr
|
||||||
|
tempGlobalMinioPort := globalMinioPort
|
||||||
defer func() {
|
defer func() {
|
||||||
globalMinioAddr = tempGlobalMinioAddr
|
globalMinioAddr = tempGlobalMinioAddr
|
||||||
|
globalMinioPort = tempGlobalMinioPort
|
||||||
}()
|
}()
|
||||||
globalMinioAddr = ":9000"
|
globalMinioAddr = ":9000"
|
||||||
|
globalMinioPort = "9000"
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
endpointArgs []string
|
endpointArgs []string
|
||||||
expectedResult string
|
expectedResult string
|
||||||
}{
|
}{
|
||||||
{[]string{"/d1", "/d2", "d3", "d4"}, ":9000"},
|
{[]string{"/d1", "/d2", "d3", "d4"}, "127.0.0.1:9000"},
|
||||||
{[]string{"http://localhost:9000/d1", "http://localhost:9000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"},
|
{[]string{"http://localhost:9000/d1", "http://localhost:9000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"},
|
||||||
"localhost:9000"},
|
"localhost:9000"},
|
||||||
{[]string{"http://localhost:9000/d1", "http://example.org:9000/d2", "http://example.com:9000/d3", "http://example.net:9000/d4"},
|
{[]string{"http://localhost:9000/d1", "http://example.org:9000/d2", "http://example.com:9000/d3", "http://example.net:9000/d4"},
|
||||||
|
|
|
@ -1,804 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/minio/minio/pkg/errors"
|
|
||||||
"github.com/minio/minio/pkg/hash"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
minioEventSource = "minio:s3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type externalNotifier struct {
|
|
||||||
// Per-bucket notification config. This is updated via
|
|
||||||
// PutBucketNotification API.
|
|
||||||
notificationConfigs map[string]*notificationConfig
|
|
||||||
|
|
||||||
// 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]*listenChan
|
|
||||||
|
|
||||||
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.
|
|
||||||
type eventData struct {
|
|
||||||
Type EventName
|
|
||||||
Bucket string
|
|
||||||
ObjInfo ObjectInfo
|
|
||||||
ReqParams map[string]string
|
|
||||||
Host string
|
|
||||||
Port string
|
|
||||||
UserAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
// New notification event constructs a new notification event message from
|
|
||||||
// input request metadata which completed successfully.
|
|
||||||
func newNotificationEvent(event eventData) NotificationEvent {
|
|
||||||
getResponseOriginEndpointKey := func() string {
|
|
||||||
host := globalMinioHost
|
|
||||||
if host == "" {
|
|
||||||
// FIXME: Send FQDN or hostname of this machine than sending IP address.
|
|
||||||
host = localIP4.ToSlice()[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s://%s:%s", getURLScheme(globalIsSSL), host, globalMinioPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the region.
|
|
||||||
region := globalServerConfig.GetRegion()
|
|
||||||
|
|
||||||
// Fetch the credentials.
|
|
||||||
creds := globalServerConfig.GetCredential()
|
|
||||||
|
|
||||||
// Time when Minio finished processing the request.
|
|
||||||
eventTime := UTCNow()
|
|
||||||
|
|
||||||
// Fetch a hexadecimal representation of event time in nano seconds.
|
|
||||||
uniqueID := mustGetRequestID(eventTime)
|
|
||||||
|
|
||||||
/// Construct a new object created event.
|
|
||||||
|
|
||||||
// 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
|
|
||||||
nEvent := NotificationEvent{
|
|
||||||
EventVersion: eventVersion,
|
|
||||||
EventSource: minioEventSource,
|
|
||||||
AwsRegion: region,
|
|
||||||
EventTime: eventTime.Format(timeFormatAMZ),
|
|
||||||
EventName: event.Type.String(),
|
|
||||||
UserIdentity: identity{creds.AccessKey},
|
|
||||||
RequestParameters: event.ReqParams,
|
|
||||||
ResponseElements: map[string]string{
|
|
||||||
responseRequestIDKey: uniqueID,
|
|
||||||
// Following is a custom response element to indicate
|
|
||||||
// event origin server endpoint.
|
|
||||||
responseOriginEndpointKey: getResponseOriginEndpointKey(),
|
|
||||||
},
|
|
||||||
S3: eventMeta{
|
|
||||||
SchemaVersion: eventSchemaVersion,
|
|
||||||
ConfigurationID: eventConfigID,
|
|
||||||
Bucket: bucketMeta{
|
|
||||||
Name: event.Bucket,
|
|
||||||
OwnerIdentity: identity{creds.AccessKey},
|
|
||||||
ARN: bucketARNPrefix + event.Bucket,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Source: sourceInfo{
|
|
||||||
Host: event.Host,
|
|
||||||
Port: event.Port,
|
|
||||||
UserAgent: event.UserAgent,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape the object name. For example "red flower.jpg" becomes "red+flower.jpg".
|
|
||||||
escapedObj := url.QueryEscape(event.ObjInfo.Name)
|
|
||||||
|
|
||||||
// For delete object event type, we do not need to set ETag and Size.
|
|
||||||
if event.Type == ObjectRemovedDelete {
|
|
||||||
nEvent.S3.Object = objectMeta{
|
|
||||||
Key: escapedObj,
|
|
||||||
VersionID: "1",
|
|
||||||
Sequencer: uniqueID,
|
|
||||||
}
|
|
||||||
return nEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
// For all other events we should set ETag and Size.
|
|
||||||
nEvent.S3.Object = objectMeta{
|
|
||||||
Key: escapedObj,
|
|
||||||
ETag: event.ObjInfo.ETag,
|
|
||||||
Size: event.ObjInfo.Size,
|
|
||||||
ContentType: event.ObjInfo.ContentType,
|
|
||||||
UserMetadata: event.ObjInfo.UserDefined,
|
|
||||||
VersionID: "1",
|
|
||||||
Sequencer: uniqueID,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success.
|
|
||||||
return nEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch all external targets. This returns a copy of the current map of
|
|
||||||
// external notification targets.
|
|
||||||
func (en eventNotifier) GetAllExternalTargets() map[string]*logrus.Logger {
|
|
||||||
en.external.rwMutex.RLock()
|
|
||||||
defer en.external.rwMutex.RUnlock()
|
|
||||||
targetsCopy := make(map[string]*logrus.Logger)
|
|
||||||
for k, v := range en.external.targets {
|
|
||||||
targetsCopy[k] = v
|
|
||||||
}
|
|
||||||
return targetsCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the external target.
|
|
||||||
func (en eventNotifier) GetExternalTarget(queueARN string) *logrus.Logger {
|
|
||||||
en.external.rwMutex.RLock()
|
|
||||||
defer en.external.rwMutex.RUnlock()
|
|
||||||
return en.external.targets[queueARN]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (en eventNotifier) GetInternalTarget(arn string) *listenerLogger {
|
|
||||||
en.internal.rwMutex.RLock()
|
|
||||||
defer en.internal.rwMutex.RUnlock()
|
|
||||||
return en.internal.targets[arn]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a new sns target for an input sns ARN.
|
|
||||||
func (en *eventNotifier) AddListenerChan(snsARN string, listenerCh *listenChan) error {
|
|
||||||
if listenerCh == nil {
|
|
||||||
return errInvalidArgument
|
|
||||||
}
|
|
||||||
en.internal.rwMutex.Lock()
|
|
||||||
defer en.internal.rwMutex.Unlock()
|
|
||||||
en.internal.connectedListeners[snsARN] = listenerCh
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove sns target for an input sns ARN.
|
|
||||||
func (en *eventNotifier) RemoveListenerChan(snsARN string) {
|
|
||||||
en.internal.rwMutex.Lock()
|
|
||||||
defer en.internal.rwMutex.Unlock()
|
|
||||||
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()
|
|
||||||
|
|
||||||
listenChan, ok := en.internal.connectedListeners[arn]
|
|
||||||
if ok {
|
|
||||||
listenChan.sendNotificationEvent(event)
|
|
||||||
}
|
|
||||||
// If the channel is not present we ignore the event.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch bucket notification config for an input bucket.
|
|
||||||
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 len(lcfg) == 0 {
|
|
||||||
delete(en.internal.listenerConfigs, bucket)
|
|
||||||
} else {
|
|
||||||
en.internal.listenerConfigs[bucket] = lcfg
|
|
||||||
}
|
|
||||||
for _, elcArr := range en.internal.listenerConfigs {
|
|
||||||
for _, elcElem := range elcArr {
|
|
||||||
currArn := elcElem.TopicConfig.TopicARN
|
|
||||||
logger, err := newListenerLogger(currArn, elcElem.TargetServer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
en.internal.targets[currArn] = 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func eventNotifyForBucketListeners(eventType, objectName, bucketName string,
|
|
||||||
nEvent []NotificationEvent) {
|
|
||||||
lCfgs := globalEventNotifier.GetBucketListenerConfig(bucketName)
|
|
||||||
if lCfgs == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Validate if the event and object match listener configs
|
|
||||||
for _, lcfg := range lCfgs {
|
|
||||||
ruleMatch := filterRuleMatch(objectName, lcfg.TopicConfig.Filter.Key.FilterRules)
|
|
||||||
eventMatch := eventMatch(eventType, lcfg.TopicConfig.Events)
|
|
||||||
if eventMatch && ruleMatch {
|
|
||||||
targetLog := globalEventNotifier.GetInternalTarget(
|
|
||||||
lcfg.TopicConfig.TopicARN)
|
|
||||||
if targetLog != nil && targetLog.log != nil {
|
|
||||||
targetLog.log.WithFields(logrus.Fields{
|
|
||||||
"Key": path.Join(bucketName, objectName),
|
|
||||||
"EventType": eventType,
|
|
||||||
"Records": nEvent,
|
|
||||||
}).Info()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// eventNotify notifies an event to relevant targets based on their
|
|
||||||
// bucket configuration (notifications and listeners).
|
|
||||||
func eventNotify(event eventData) {
|
|
||||||
if globalEventNotifier == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Notifies a new event.
|
|
||||||
// List of events reported through this function are
|
|
||||||
// - s3:ObjectCreated:Put
|
|
||||||
// - s3:ObjectCreated:Post
|
|
||||||
// - s3:ObjectCreated:Copy
|
|
||||||
// - s3:ObjectCreated:CompleteMultipartUpload
|
|
||||||
// - s3:ObjectRemoved:Delete
|
|
||||||
|
|
||||||
// Event type.
|
|
||||||
eventType := event.Type.String()
|
|
||||||
|
|
||||||
// Object name.
|
|
||||||
objectName := event.ObjInfo.Name
|
|
||||||
|
|
||||||
// Save the notification event to be sent.
|
|
||||||
notificationEvent := []NotificationEvent{newNotificationEvent(event)}
|
|
||||||
|
|
||||||
// Notify external targets.
|
|
||||||
eventNotifyForBucketNotifications(eventType, objectName, event.Bucket, notificationEvent)
|
|
||||||
|
|
||||||
// Notify internal targets.
|
|
||||||
eventNotifyForBucketListeners(eventType, objectName, event.Bucket, notificationEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// loads notification config if any for a given bucket, returns
|
|
||||||
// structured notification config.
|
|
||||||
func loadNotificationConfig(bucket string, objAPI ObjectLayer) (*notificationConfig, error) {
|
|
||||||
// Construct the notification config path.
|
|
||||||
ncPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
err := objAPI.GetObject(minioMetaBucket, ncPath, 0, -1, &buffer, "") // Read everything.
|
|
||||||
if err != nil {
|
|
||||||
// 'notification.xml' not found return
|
|
||||||
// 'errNoSuchNotifications'. This is default when no
|
|
||||||
// bucket notifications are found on the bucket.
|
|
||||||
if isErrObjectNotFound(err) || isErrIncompleteBody(err) {
|
|
||||||
return nil, errors.Trace(errNoSuchNotifications)
|
|
||||||
}
|
|
||||||
errorIf(err, "Unable to load bucket-notification for bucket %s", bucket)
|
|
||||||
// Returns error for other errors.
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if `notifications.xml` is empty we should return NoSuchNotifications.
|
|
||||||
if buffer.Len() == 0 {
|
|
||||||
return nil, errors.Trace(errNoSuchNotifications)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal notification bytes.
|
|
||||||
notificationConfigBytes := buffer.Bytes()
|
|
||||||
notificationCfg := ¬ificationConfig{}
|
|
||||||
// Unmarshal notification bytes only if we read data.
|
|
||||||
if err = xml.Unmarshal(notificationConfigBytes, notificationCfg); err != nil {
|
|
||||||
return nil, errors.Trace(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return success.
|
|
||||||
return notificationCfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loads notification config if any for a given bucket, returns
|
|
||||||
// structured notification config.
|
|
||||||
func loadListenerConfig(bucket string, objAPI ObjectLayer) ([]listenerConfig, error) {
|
|
||||||
// in single node mode, there are no peers, so in this case
|
|
||||||
// there is no configuration to load, as any previously
|
|
||||||
// connected listen clients have been disconnected
|
|
||||||
if !globalIsDistXL {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the notification config path.
|
|
||||||
lcPath := path.Join(bucketConfigPrefix, bucket, bucketListenerConfig)
|
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
err := objAPI.GetObject(minioMetaBucket, lcPath, 0, -1, &buffer, "")
|
|
||||||
if err != nil {
|
|
||||||
// 'listener.json' not found return
|
|
||||||
// 'errNoSuchNotifications'. This is default when no
|
|
||||||
// bucket listeners are found on the bucket
|
|
||||||
if isErrObjectNotFound(err) || isErrIncompleteBody(err) {
|
|
||||||
return nil, errors.Trace(errNoSuchNotifications)
|
|
||||||
}
|
|
||||||
errorIf(err, "Unable to load bucket-listeners for bucket %s", bucket)
|
|
||||||
// Returns error for other errors.
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if `listener.json` is empty we should return NoSuchNotifications.
|
|
||||||
if buffer.Len() == 0 {
|
|
||||||
return nil, errors.Trace(errNoSuchNotifications)
|
|
||||||
}
|
|
||||||
|
|
||||||
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, errors.Trace(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
|
|
||||||
}
|
|
||||||
|
|
||||||
// build path
|
|
||||||
ncPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
|
||||||
|
|
||||||
// write object to path
|
|
||||||
hashReader, err := hash.NewReader(bytes.NewReader(buf), int64(len(buf)), "", getSHA256Hash(buf))
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to write bucket notification configuration.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = obj.PutObject(minioMetaBucket, ncPath, hashReader, 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// build path
|
|
||||||
lcPath := path.Join(bucketConfigPrefix, bucket, bucketListenerConfig)
|
|
||||||
|
|
||||||
// write object to path
|
|
||||||
hashReader, err := hash.NewReader(bytes.NewReader(buf), int64(len(buf)), "", getSHA256Hash(buf))
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to write bucket listener configuration to object layer.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// write object to path
|
|
||||||
_, err = obj.PutObject(minioMetaBucket, lcPath, hashReader, nil)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to write bucket listener configuration to object layer.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes notification.xml for a given bucket, only used during DeleteBucket.
|
|
||||||
func removeNotificationConfig(bucket string, objAPI ObjectLayer) error {
|
|
||||||
// Verify bucket is valid.
|
|
||||||
if !IsValidBucketName(bucket) {
|
|
||||||
return BucketNameInvalid{Bucket: bucket}
|
|
||||||
}
|
|
||||||
|
|
||||||
ncPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
|
||||||
|
|
||||||
return objAPI.DeleteObject(minioMetaBucket, ncPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove listener configuration from storage layer. Used when a bucket is deleted.
|
|
||||||
func removeListenerConfig(bucket string, objAPI ObjectLayer) error {
|
|
||||||
// make the path
|
|
||||||
lcPath := path.Join(bucketConfigPrefix, bucket, bucketListenerConfig)
|
|
||||||
|
|
||||||
return objAPI.DeleteObject(minioMetaBucket, lcPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads both notification and listener config.
|
|
||||||
func loadNotificationAndListenerConfig(bucketName string, objAPI ObjectLayer) (nCfg *notificationConfig, lCfg []listenerConfig, err error) {
|
|
||||||
// Loads notification config if any.
|
|
||||||
nCfg, err = loadNotificationConfig(bucketName, objAPI)
|
|
||||||
if err != nil && !errors.IsErrIgnored(err, errDiskNotFound, errNoSuchNotifications) {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads listener config if any.
|
|
||||||
lCfg, err = loadListenerConfig(bucketName, objAPI)
|
|
||||||
if err != nil && !errors.IsErrIgnored(err, errDiskNotFound, errNoSuchNotifications) {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return nCfg, lCfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
for _, bucket := range buckets {
|
|
||||||
// Load persistent notification and listener configurations
|
|
||||||
// a given bucket name.
|
|
||||||
nConfigs[bucket.Name], lConfigs[bucket.Name], err = loadNotificationAndListenerConfig(bucket.Name, objAPI)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success.
|
|
||||||
return nConfigs, lConfigs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// addQueueTarget - calls newTargetFunc function and adds its returned value to queueTargets
|
|
||||||
func addQueueTarget(queueTargets map[string]*logrus.Logger,
|
|
||||||
accountID, queueType string,
|
|
||||||
newTargetFunc func(string) (*logrus.Logger, error)) (string, error) {
|
|
||||||
|
|
||||||
// Construct the queue ARN for AMQP.
|
|
||||||
queueARN := minioSqs + globalServerConfig.GetRegion() + ":" + accountID + ":" + queueType
|
|
||||||
|
|
||||||
// Queue target if already initialized we move to the next ARN.
|
|
||||||
if _, ok := queueTargets[queueARN]; ok {
|
|
||||||
return queueARN, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using accountID we can now initialize a new AMQP logrus instance.
|
|
||||||
logger, err := newTargetFunc(accountID)
|
|
||||||
if err == nil {
|
|
||||||
queueTargets[queueARN] = logger
|
|
||||||
}
|
|
||||||
|
|
||||||
return queueARN, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads all queue targets, initializes each queueARNs depending on their config.
|
|
||||||
// Each instance of queueARN registers its own logrus to communicate with the
|
|
||||||
// queue service. QueueARN once initialized is not initialized again for the
|
|
||||||
// same queueARN, instead previous connection is used.
|
|
||||||
func loadAllQueueTargets() (map[string]*logrus.Logger, error) {
|
|
||||||
queueTargets := make(map[string]*logrus.Logger)
|
|
||||||
// Load all amqp targets, initialize their respective loggers.
|
|
||||||
for accountID, amqpN := range globalServerConfig.Notify.GetAMQP() {
|
|
||||||
if !amqpN.Enable {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if queueARN, err := addQueueTarget(queueTargets, accountID, queueTypeAMQP, newAMQPNotify); err != nil {
|
|
||||||
if _, ok := err.(net.Error); ok {
|
|
||||||
err = &net.OpError{
|
|
||||||
Op: "Connecting to " + queueARN,
|
|
||||||
Net: "tcp",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load all mqtt targets, initialize their respective loggers.
|
|
||||||
for accountID, mqttN := range globalServerConfig.Notify.GetMQTT() {
|
|
||||||
if !mqttN.Enable {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if queueARN, err := addQueueTarget(queueTargets, accountID, queueTypeMQTT, newMQTTNotify); err != nil {
|
|
||||||
if _, ok := err.(net.Error); ok {
|
|
||||||
err = &net.OpError{
|
|
||||||
Op: "Connecting to " + queueARN,
|
|
||||||
Net: "tcp",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load all nats targets, initialize their respective loggers.
|
|
||||||
for accountID, natsN := range globalServerConfig.Notify.GetNATS() {
|
|
||||||
if !natsN.Enable {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if queueARN, err := addQueueTarget(queueTargets, accountID, queueTypeNATS, newNATSNotify); err != nil {
|
|
||||||
if _, ok := err.(net.Error); ok {
|
|
||||||
err = &net.OpError{
|
|
||||||
Op: "Connecting to " + queueARN,
|
|
||||||
Net: "tcp",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load redis targets, initialize their respective loggers.
|
|
||||||
for accountID, redisN := range globalServerConfig.Notify.GetRedis() {
|
|
||||||
if !redisN.Enable {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if queueARN, err := addQueueTarget(queueTargets, accountID, queueTypeRedis, newRedisNotify); err != nil {
|
|
||||||
if _, ok := err.(net.Error); ok {
|
|
||||||
err = &net.OpError{
|
|
||||||
Op: "Connecting to " + queueARN,
|
|
||||||
Net: "tcp",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load Webhook targets, initialize their respective loggers.
|
|
||||||
for accountID, webhookN := range globalServerConfig.Notify.GetWebhook() {
|
|
||||||
if !webhookN.Enable {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, err := addQueueTarget(queueTargets, accountID, queueTypeWebhook, newWebhookNotify); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load elastic targets, initialize their respective loggers.
|
|
||||||
for accountID, elasticN := range globalServerConfig.Notify.GetElasticSearch() {
|
|
||||||
if !elasticN.Enable {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if queueARN, err := addQueueTarget(queueTargets, accountID, queueTypeElastic, newElasticNotify); err != nil {
|
|
||||||
if _, ok := err.(net.Error); ok {
|
|
||||||
err = &net.OpError{
|
|
||||||
Op: "Connecting to " + queueARN,
|
|
||||||
Net: "tcp",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load PostgreSQL targets, initialize their respective loggers.
|
|
||||||
for accountID, pgN := range globalServerConfig.Notify.GetPostgreSQL() {
|
|
||||||
if !pgN.Enable {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if queueARN, err := addQueueTarget(queueTargets, accountID, queueTypePostgreSQL, newPostgreSQLNotify); err != nil {
|
|
||||||
if _, ok := err.(net.Error); ok {
|
|
||||||
err = &net.OpError{
|
|
||||||
Op: "Connecting to " + queueARN,
|
|
||||||
Net: "tcp",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load MySQL targets, initialize their respective loggers.
|
|
||||||
for accountID, msqlN := range globalServerConfig.Notify.GetMySQL() {
|
|
||||||
if !msqlN.Enable {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if queueARN, err := addQueueTarget(queueTargets, accountID, queueTypeMySQL, newMySQLNotify); err != nil {
|
|
||||||
if _, ok := err.(net.Error); ok {
|
|
||||||
err = &net.OpError{
|
|
||||||
Op: "Connecting to " + queueARN,
|
|
||||||
Net: "tcp",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load Kafka targets, initialize their respective loggers.
|
|
||||||
for accountID, kafkaN := range globalServerConfig.Notify.GetKafka() {
|
|
||||||
if !kafkaN.Enable {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if queueARN, err := addQueueTarget(queueTargets, accountID, queueTypeKafka, newKafkaNotify); err != nil {
|
|
||||||
if _, ok := err.(net.Error); ok {
|
|
||||||
err = &net.OpError{
|
|
||||||
Op: "Connecting to " + queueARN,
|
|
||||||
Net: "tcp",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Successfully initialized queue targets.
|
|
||||||
return queueTargets, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global instance of event notification queue.
|
|
||||||
var globalEventNotifier *eventNotifier
|
|
||||||
|
|
||||||
// Initialize event notifier.
|
|
||||||
func initEventNotifier(objAPI ObjectLayer) error {
|
|
||||||
if objAPI == nil {
|
|
||||||
return errInvalidArgument
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read all saved bucket notifications.
|
|
||||||
nConfigs, lConfigs, err := loadAllBucketNotifications(objAPI)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Error loading bucket notifications - %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializes all queue targets.
|
|
||||||
queueTargets, err := loadAllQueueTargets()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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{
|
|
||||||
external: externalNotifier{
|
|
||||||
notificationConfigs: nConfigs,
|
|
||||||
targets: queueTargets,
|
|
||||||
rwMutex: &sync.RWMutex{},
|
|
||||||
},
|
|
||||||
internal: internalNotifier{
|
|
||||||
rwMutex: &sync.RWMutex{},
|
|
||||||
targets: listenTargets,
|
|
||||||
listenerConfigs: lConfigs,
|
|
||||||
connectedListeners: make(map[string]*listenChan),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,586 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test InitEventNotifier with faulty disks
|
|
||||||
func TestInitEventNotifierFaultyDisks(t *testing.T) {
|
|
||||||
// Prepare for tests
|
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
disks, err := getRandomDisks(16)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
|
||||||
}
|
|
||||||
defer removeRoots(disks)
|
|
||||||
obj, _, err := initObjectLayer(mustGetNewEndpointList(disks...))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to initialize FS backend.", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bucketName := "bucket"
|
|
||||||
if err := obj.MakeBucketWithLocation(bucketName, ""); err != nil {
|
|
||||||
t.Fatal("Unexpected error:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
xl := obj.(*xlObjects)
|
|
||||||
|
|
||||||
listenARN := "arn:minio:sns:us-east-1:1:listen"
|
|
||||||
queueARN := "arn:minio:sqs:us-east-1:1:redis"
|
|
||||||
|
|
||||||
// Write a notification.xml in the disk
|
|
||||||
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>"
|
|
||||||
size := int64(len([]byte(notificationXML)))
|
|
||||||
reader := bytes.NewReader([]byte(notificationXML))
|
|
||||||
bucketConfigPath := bucketConfigPrefix + "/" + bucketName + "/" + bucketNotificationConfig
|
|
||||||
if _, err := xl.PutObject(minioMetaBucket, bucketConfigPath, mustGetHashReader(t, reader, size, "", ""), nil); err != nil {
|
|
||||||
t.Fatal("Unexpected error:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, d := range xl.storageDisks {
|
|
||||||
xl.storageDisks[i] = newNaughtyDisk(d, nil, errFaultyDisk)
|
|
||||||
}
|
|
||||||
// Test initEventNotifier() with faulty disks
|
|
||||||
for i := 1; i <= 3; i++ {
|
|
||||||
if err := initEventNotifier(xl); errors.Cause(err) != errFaultyDisk {
|
|
||||||
t.Fatal("Unexpected error:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitEventNotifierWithPostgreSQL - tests InitEventNotifier when PostgreSQL is not prepared
|
|
||||||
func TestInitEventNotifierWithPostgreSQL(t *testing.T) {
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
disks, err := getRandomDisks(1)
|
|
||||||
defer os.RemoveAll(disks[0])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
|
||||||
}
|
|
||||||
fs, _, err := initObjectLayer(mustGetNewEndpointList(disks...))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to initialize FS backend.", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
globalServerConfig.Notify.SetPostgreSQLByID("1", postgreSQLNotify{Enable: true})
|
|
||||||
if err := initEventNotifier(fs); err == nil {
|
|
||||||
t.Fatal("PostgreSQL config didn't fail.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitEventNotifierWithNATS - tests InitEventNotifier when NATS is not prepared
|
|
||||||
func TestInitEventNotifierWithNATS(t *testing.T) {
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
disks, err := getRandomDisks(1)
|
|
||||||
defer os.RemoveAll(disks[0])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
|
||||||
}
|
|
||||||
fs, _, err := initObjectLayer(mustGetNewEndpointList(disks...))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to initialize FS backend.", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
globalServerConfig.Notify.SetNATSByID("1", natsNotify{Enable: true})
|
|
||||||
if err := initEventNotifier(fs); err == nil {
|
|
||||||
t.Fatal("NATS config didn't fail.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitEventNotifierWithWebHook - tests InitEventNotifier when WebHook is not prepared
|
|
||||||
func TestInitEventNotifierWithWebHook(t *testing.T) {
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
disks, err := getRandomDisks(1)
|
|
||||||
defer os.RemoveAll(disks[0])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
|
||||||
}
|
|
||||||
fs, _, err := initObjectLayer(mustGetNewEndpointList(disks...))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to initialize FS backend.", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
globalServerConfig.Notify.SetWebhookByID("1", webhookNotify{Enable: true})
|
|
||||||
if err := initEventNotifier(fs); err == nil {
|
|
||||||
t.Fatal("WebHook config didn't fail.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitEventNotifierWithAMQP - tests InitEventNotifier when AMQP is not prepared
|
|
||||||
func TestInitEventNotifierWithAMQP(t *testing.T) {
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
disks, err := getRandomDisks(1)
|
|
||||||
defer os.RemoveAll(disks[0])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
|
||||||
}
|
|
||||||
fs, _, err := initObjectLayer(mustGetNewEndpointList(disks...))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to initialize FS backend.", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
globalServerConfig.Notify.SetAMQPByID("1", amqpNotify{Enable: true})
|
|
||||||
if err := initEventNotifier(fs); err == nil {
|
|
||||||
t.Fatal("AMQP config didn't fail.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitEventNotifierWithElasticSearch - test InitEventNotifier when ElasticSearch is not ready
|
|
||||||
func TestInitEventNotifierWithElasticSearch(t *testing.T) {
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
disks, err := getRandomDisks(1)
|
|
||||||
defer os.RemoveAll(disks[0])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
|
||||||
}
|
|
||||||
fs, _, err := initObjectLayer(mustGetNewEndpointList(disks...))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to initialize FS backend.", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
globalServerConfig.Notify.SetElasticSearchByID("1", elasticSearchNotify{Enable: true})
|
|
||||||
if err := initEventNotifier(fs); err == nil {
|
|
||||||
t.Fatal("ElasticSearch config didn't fail.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitEventNotifierWithRedis - test InitEventNotifier when Redis is not ready
|
|
||||||
func TestInitEventNotifierWithRedis(t *testing.T) {
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
disks, err := getRandomDisks(1)
|
|
||||||
defer os.RemoveAll(disks[0])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
|
||||||
}
|
|
||||||
fs, _, err := initObjectLayer(mustGetNewEndpointList(disks...))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to initialize FS backend.", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
globalServerConfig.Notify.SetRedisByID("1", redisNotify{Enable: true})
|
|
||||||
if err := initEventNotifier(fs); err == nil {
|
|
||||||
t.Fatal("Redis config didn't fail.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
host, port := mustSplitHostPort(s.testServer.Server.Listener.Addr().String())
|
|
||||||
globalMinioHost = host
|
|
||||||
globalMinioPort = port
|
|
||||||
globalMinioAddr = getEndpointsLocalAddr(s.testServer.endpoints)
|
|
||||||
|
|
||||||
// initialize the peer client(s)
|
|
||||||
initGlobalS3Peers(s.testServer.Disks)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TestPeerRPCServerData) TearDown() {
|
|
||||||
s.testServer.Stop()
|
|
||||||
_ = os.RemoveAll(s.testServer.Root)
|
|
||||||
for _, d := range s.testServer.Disks {
|
|
||||||
_ = os.RemoveAll(d.Path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
currentIsDistXL := globalIsDistXL
|
|
||||||
defer func() {
|
|
||||||
globalIsDistXL = currentIsDistXL
|
|
||||||
}()
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// create bucket
|
|
||||||
if err := obj.MakeBucketWithLocation(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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// needed to load listener config from disk for testing (in
|
|
||||||
// single peer mode, the listener config is ignored, but here
|
|
||||||
// we want to test the loading from disk too.)
|
|
||||||
globalIsDistXL = true
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
currentIsDistXL := globalIsDistXL
|
|
||||||
defer func() {
|
|
||||||
globalIsDistXL = currentIsDistXL
|
|
||||||
}()
|
|
||||||
|
|
||||||
s := TestPeerRPCServerData{serverType: "XL"}
|
|
||||||
// setup and teardown
|
|
||||||
s.Setup(t)
|
|
||||||
defer s.TearDown()
|
|
||||||
|
|
||||||
// test initialisation
|
|
||||||
obj := s.testServer.Obj
|
|
||||||
|
|
||||||
bucketName := "bucket"
|
|
||||||
objectName := "object"
|
|
||||||
|
|
||||||
// Create the bucket to listen on
|
|
||||||
if err := obj.MakeBucketWithLocation(bucketName, ""); err != nil {
|
|
||||||
t.Fatal("Unexpected error:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
listenARN := fmt.Sprintf("%s:%s:1:%s-%s",
|
|
||||||
minioTopic,
|
|
||||||
globalServerConfig.GetRegion(),
|
|
||||||
snsTypeMinio,
|
|
||||||
s.testServer.Server.Listener.Addr(),
|
|
||||||
)
|
|
||||||
lcfg := listenerConfig{
|
|
||||||
TopicConfig: topicConfig{
|
|
||||||
ServiceConfig{
|
|
||||||
[]string{"s3:ObjectRemoved:*", "s3:ObjectCreated:*"},
|
|
||||||
filterStruct{},
|
|
||||||
"0",
|
|
||||||
},
|
|
||||||
listenARN,
|
|
||||||
},
|
|
||||||
TargetServer: globalMinioAddr,
|
|
||||||
}
|
|
||||||
|
|
||||||
// write listener config to storage layer
|
|
||||||
lcfgs := []listenerConfig{lcfg}
|
|
||||||
if err := persistListenerConfig(bucketName, lcfgs, obj); err != nil {
|
|
||||||
t.Fatalf("Test Setup error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// needed to load listener config from disk for testing (in
|
|
||||||
// single peer mode, the listener config is ingored, but here
|
|
||||||
// we want to test the loading from disk too.)
|
|
||||||
globalIsDistXL = true
|
|
||||||
|
|
||||||
// Init event notifier
|
|
||||||
if err := initEventNotifier(obj); err != nil {
|
|
||||||
t.Fatal("Unexpected error:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the config is loaded
|
|
||||||
listenerCfg := globalEventNotifier.GetBucketListenerConfig(bucketName)
|
|
||||||
if listenerCfg == nil {
|
|
||||||
t.Fatal("Cannot load bucket listener config")
|
|
||||||
}
|
|
||||||
if len(listenerCfg) != 1 {
|
|
||||||
t.Fatal("Listener config is not correctly loaded. Exactly one listener config is expected")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if topic ARN is correct
|
|
||||||
if listenerCfg[0].TopicConfig.TopicARN != listenARN {
|
|
||||||
t.Fatal("Configured topic ARN is incorrect.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new notification event channel.
|
|
||||||
nListenCh := newListenChan()
|
|
||||||
// Add events channel for listener.
|
|
||||||
if err := globalEventNotifier.AddListenerChan(listenARN, nListenCh); err != nil {
|
|
||||||
t.Fatalf("Test Setup error: %v", err)
|
|
||||||
}
|
|
||||||
// Remove listen channel after the writer has closed or the
|
|
||||||
// client disconnected.
|
|
||||||
defer globalEventNotifier.RemoveListenerChan(listenARN)
|
|
||||||
|
|
||||||
// Fire an event notification
|
|
||||||
go eventNotify(eventData{
|
|
||||||
Type: ObjectRemovedDelete,
|
|
||||||
Bucket: bucketName,
|
|
||||||
ObjInfo: ObjectInfo{
|
|
||||||
Bucket: bucketName,
|
|
||||||
Name: objectName,
|
|
||||||
},
|
|
||||||
ReqParams: map[string]string{
|
|
||||||
"sourceIPAddress": "localhost:1337",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for the event notification here, if nothing is received within 30 seconds,
|
|
||||||
// test error will be fired
|
|
||||||
select {
|
|
||||||
case n := <-nListenCh.dataCh:
|
|
||||||
// Check that received event
|
|
||||||
if len(n) == 0 {
|
|
||||||
t.Fatal("Unexpected error occurred")
|
|
||||||
}
|
|
||||||
if n[0].S3.Object.Key != objectName {
|
|
||||||
t.Fatalf("Received wrong object name in notification, expected %s, received %s", n[0].S3.Object.Key, objectName)
|
|
||||||
}
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddRemoveBucketListenerConfig(t *testing.T) {
|
|
||||||
s := TestPeerRPCServerData{serverType: "XL"}
|
|
||||||
|
|
||||||
// setup and teardown
|
|
||||||
s.Setup(t)
|
|
||||||
defer s.TearDown()
|
|
||||||
|
|
||||||
// test code
|
|
||||||
obj := s.testServer.Obj
|
|
||||||
if err := initEventNotifier(obj); err != nil {
|
|
||||||
t.Fatalf("Failed to initialize event notifier: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a bucket to store topicConfigs.
|
|
||||||
randBucket := getRandomBucketName()
|
|
||||||
if err := obj.MakeBucketWithLocation(randBucket, ""); err != nil {
|
|
||||||
t.Fatalf("Failed to make bucket %s", randBucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a topicConfig to an empty notificationConfig.
|
|
||||||
accountID := fmt.Sprintf("%d", UTCNow().UnixNano())
|
|
||||||
accountARN := fmt.Sprintf(
|
|
||||||
"arn:minio:sqs:%s:%s:listen-%s",
|
|
||||||
globalServerConfig.GetRegion(),
|
|
||||||
accountID,
|
|
||||||
globalMinioAddr,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Make topic configuration
|
|
||||||
filterRules := []filterRule{
|
|
||||||
{
|
|
||||||
Name: "prefix",
|
|
||||||
Value: "minio",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "suffix",
|
|
||||||
Value: "*.jpg",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
sampleTopicCfg := topicConfig{
|
|
||||||
TopicARN: accountARN,
|
|
||||||
ServiceConfig: ServiceConfig{
|
|
||||||
[]string{"s3:ObjectRemoved:*", "s3:ObjectCreated:*"},
|
|
||||||
filterStruct{
|
|
||||||
keyFilter{filterRules},
|
|
||||||
},
|
|
||||||
"sns-" + accountID,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
sampleListenerCfg := &listenerConfig{
|
|
||||||
TopicConfig: sampleTopicCfg,
|
|
||||||
TargetServer: globalMinioAddr,
|
|
||||||
}
|
|
||||||
testCases := []struct {
|
|
||||||
lCfg *listenerConfig
|
|
||||||
expectedErr error
|
|
||||||
}{
|
|
||||||
{sampleListenerCfg, nil},
|
|
||||||
{nil, errInvalidArgument},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range testCases {
|
|
||||||
err := AddBucketListenerConfig(randBucket, test.lCfg, obj)
|
|
||||||
if err != test.expectedErr {
|
|
||||||
t.Errorf(
|
|
||||||
"Test %d: Failed with error %v, expected to fail with %v",
|
|
||||||
i+1, err, test.expectedErr,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// test remove listener actually removes a listener
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -169,8 +169,8 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||||
return nil, fmt.Errorf("Unable to load all bucket policies. %s", err)
|
return nil, fmt.Errorf("Unable to load all bucket policies. %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a new event notifier.
|
// Initialize notification system.
|
||||||
if err = initEventNotifier(fs); err != nil {
|
if err = globalNotificationSys.Init(fs); err != nil {
|
||||||
return nil, fmt.Errorf("Unable to initialize event notification. %s", err)
|
return nil, fmt.Errorf("Unable to initialize event notification. %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -162,6 +162,10 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||||
|
|
||||||
initNSLock(false) // Enable local namespace lock.
|
initNSLock(false) // Enable local namespace lock.
|
||||||
|
|
||||||
|
// Initialize notification system.
|
||||||
|
globalNotificationSys, err = NewNotificationSys(globalServerConfig, EndpointList{})
|
||||||
|
fatalIf(err, "Unable to initialize notification system.")
|
||||||
|
|
||||||
newObject, err := gw.NewGatewayLayer(globalServerConfig.GetCredential())
|
newObject, err := gw.NewGatewayLayer(globalServerConfig.GetCredential())
|
||||||
fatalIf(err, "Unable to initialize gateway layer")
|
fatalIf(err, "Unable to initialize gateway layer")
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -114,8 +114,7 @@ var (
|
||||||
// Holds the host that was passed using --address
|
// Holds the host that was passed using --address
|
||||||
globalMinioHost = ""
|
globalMinioHost = ""
|
||||||
|
|
||||||
// Peer communication struct
|
globalNotificationSys *NotificationSys
|
||||||
globalS3Peers = s3Peers{}
|
|
||||||
|
|
||||||
// CA root certificates, a nil value means system certs pool will be used
|
// CA root certificates, a nil value means system certs pool will be used
|
||||||
globalRootCAs *x509.CertPool
|
globalRootCAs *x509.CertPool
|
||||||
|
@ -160,9 +159,6 @@ var (
|
||||||
globalOperationTimeout = newDynamicTimeout(10*time.Minute /*30*/, 600*time.Second) // default timeout for general ops
|
globalOperationTimeout = newDynamicTimeout(10*time.Minute /*30*/, 600*time.Second) // default timeout for general ops
|
||||||
globalHealingTimeout = newDynamicTimeout(30*time.Minute /*1*/, 30*time.Minute) // timeout for healing related ops
|
globalHealingTimeout = newDynamicTimeout(30*time.Minute /*1*/, 30*time.Minute) // timeout for healing related ops
|
||||||
|
|
||||||
// Keep connection active for clients actively using ListenBucketNotification.
|
|
||||||
globalSNSConnAlive = 5 * time.Second // Send a whitespace every 5 seconds.
|
|
||||||
|
|
||||||
// Storage classes
|
// Storage classes
|
||||||
// Set to indicate if storage class is set up
|
// Set to indicate if storage class is set up
|
||||||
globalIsStorageClass bool
|
globalIsStorageClass bool
|
||||||
|
|
|
@ -0,0 +1,634 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
xerrors "github.com/minio/minio/pkg/errors"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
"github.com/minio/minio/pkg/hash"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotificationSys - notification system.
|
||||||
|
type NotificationSys struct {
|
||||||
|
sync.RWMutex
|
||||||
|
targetList *event.TargetList
|
||||||
|
bucketRulesMap map[string]event.RulesMap
|
||||||
|
bucketRemoteTargetRulesMap map[string]map[event.TargetID]event.RulesMap
|
||||||
|
peerRPCClientMap map[xnet.Host]*PeerRPCClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetARNList - returns available ARNs.
|
||||||
|
func (sys *NotificationSys) GetARNList() []string {
|
||||||
|
arns := []string{}
|
||||||
|
region := globalServerConfig.GetRegion()
|
||||||
|
for _, targetID := range sys.targetList.List() {
|
||||||
|
arns = append(arns, targetID.ToARN(region).String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return arns
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeerRPCClient - returns PeerRPCClient of addr.
|
||||||
|
func (sys *NotificationSys) GetPeerRPCClient(addr xnet.Host) *PeerRPCClient {
|
||||||
|
return sys.peerRPCClientMap[addr]
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBucket - calls DeleteBucket RPC call on all peers.
|
||||||
|
func (sys *NotificationSys) DeleteBucket(bucketName string) map[xnet.Host]error {
|
||||||
|
errors := make(map[xnet.Host]error)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for addr, client := range sys.peerRPCClientMap {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := client.DeleteBucket(bucketName); err != nil {
|
||||||
|
errors[addr] = err
|
||||||
|
}
|
||||||
|
}(addr, client)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBucketPolicy - calls UpdateBucketPolicy RPC call on all peers.
|
||||||
|
func (sys *NotificationSys) UpdateBucketPolicy(bucketName string) map[xnet.Host]error {
|
||||||
|
errors := make(map[xnet.Host]error)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for addr, client := range sys.peerRPCClientMap {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := client.UpdateBucketPolicy(bucketName); err != nil {
|
||||||
|
errors[addr] = err
|
||||||
|
}
|
||||||
|
}(addr, client)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutBucketNotification - calls PutBucketNotification RPC call on all peers.
|
||||||
|
func (sys *NotificationSys) PutBucketNotification(bucketName string, rulesMap event.RulesMap) map[xnet.Host]error {
|
||||||
|
errors := make(map[xnet.Host]error)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for addr, client := range sys.peerRPCClientMap {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(addr xnet.Host, client *PeerRPCClient, rulesMap event.RulesMap) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := client.PutBucketNotification(bucketName, rulesMap); err != nil {
|
||||||
|
errors[addr] = err
|
||||||
|
}
|
||||||
|
}(addr, client, rulesMap.Clone())
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenBucketNotification - calls ListenBucketNotification RPC call on all peers.
|
||||||
|
func (sys *NotificationSys) ListenBucketNotification(bucketName string, eventNames []event.Name, pattern string, targetID event.TargetID, localPeer xnet.Host) map[xnet.Host]error {
|
||||||
|
errors := make(map[xnet.Host]error)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for addr, client := range sys.peerRPCClientMap {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := client.ListenBucketNotification(bucketName, eventNames, pattern, targetID, localPeer); err != nil {
|
||||||
|
errors[addr] = err
|
||||||
|
}
|
||||||
|
}(addr, client)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRemoteTarget - adds event rules map, HTTP/PeerRPC client target to bucket name.
|
||||||
|
func (sys *NotificationSys) AddRemoteTarget(bucketName string, target event.Target, rulesMap event.RulesMap) error {
|
||||||
|
if err := sys.targetList.Add(target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Lock()
|
||||||
|
targetMap := sys.bucketRemoteTargetRulesMap[bucketName]
|
||||||
|
if targetMap == nil {
|
||||||
|
targetMap = make(map[event.TargetID]event.RulesMap)
|
||||||
|
}
|
||||||
|
targetMap[target.ID()] = rulesMap.Clone()
|
||||||
|
sys.bucketRemoteTargetRulesMap[bucketName] = targetMap
|
||||||
|
sys.Unlock()
|
||||||
|
|
||||||
|
sys.AddRulesMap(bucketName, rulesMap)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteTargetExist - checks whether given target ID is a HTTP/PeerRPC client target or not.
|
||||||
|
func (sys *NotificationSys) RemoteTargetExist(bucketName string, targetID event.TargetID) bool {
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
targetMap, ok := sys.bucketRemoteTargetRulesMap[bucketName]
|
||||||
|
if ok {
|
||||||
|
_, ok = targetMap[targetID]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// initListeners - initializes PeerRPC clients available in listener.json.
|
||||||
|
func (sys *NotificationSys) initListeners(objAPI ObjectLayer, bucketName string) error {
|
||||||
|
// listener.json is available/applicable only in DistXL mode.
|
||||||
|
if !globalIsDistXL {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct path to listener.json for the given bucket.
|
||||||
|
configFile := path.Join(bucketConfigPrefix, bucketName, bucketListenerConfig)
|
||||||
|
transactionConfigFile := configFile + ".transaction"
|
||||||
|
|
||||||
|
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
|
||||||
|
// and configFile, take a transaction lock to avoid data race between readConfig()
|
||||||
|
// and saveConfig().
|
||||||
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
|
||||||
|
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer objLock.Unlock()
|
||||||
|
|
||||||
|
reader, err := readConfig(objAPI, configFile)
|
||||||
|
if err != nil && !xerrors.IsErrIgnored(err, errDiskNotFound, errNoSuchNotifications) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
listenerList := []ListenBucketNotificationArgs{}
|
||||||
|
if reader != nil {
|
||||||
|
if err = json.NewDecoder(reader).Decode(&listenerList); err != nil {
|
||||||
|
errorIf(err, "Unable to parse listener.json.")
|
||||||
|
return xerrors.Trace(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(listenerList) == 0 {
|
||||||
|
// Nothing to initialize for empty listener list.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
activeListenerList := []ListenBucketNotificationArgs{}
|
||||||
|
for _, args := range listenerList {
|
||||||
|
var found bool
|
||||||
|
if found, err = isLocalHost(args.Addr.Name); err != nil {
|
||||||
|
errorIf(err, "unable to check address %v is local host", args.Addr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
// As this function is called at startup, skip HTTP listener to this host.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcClient := sys.GetPeerRPCClient(args.Addr)
|
||||||
|
if rpcClient == nil {
|
||||||
|
return fmt.Errorf("unable to find PeerRPCClient by address %v in listener.json for bucket %v", args.Addr, bucketName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var exist bool
|
||||||
|
if exist, err = rpcClient.RemoteTargetExist(bucketName, args.TargetID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !exist {
|
||||||
|
// Skip previously connected HTTP listener which is not found in remote peer.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
target := NewPeerRPCClientTarget(bucketName, args.TargetID, rpcClient)
|
||||||
|
rulesMap := event.NewRulesMap(args.EventNames, args.Pattern, target.ID())
|
||||||
|
if err = sys.AddRemoteTarget(bucketName, target, rulesMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
activeListenerList = append(activeListenerList, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(activeListenerList)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveConfig(objAPI, configFile, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init - initializes notification system from notification.xml and listener.json of all buckets.
|
||||||
|
func (sys *NotificationSys) Init(objAPI ObjectLayer) error {
|
||||||
|
if objAPI == nil {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
|
||||||
|
buckets, err := objAPI.ListBuckets()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bucket := range buckets {
|
||||||
|
config, err := readNotificationConfig(objAPI, bucket.Name)
|
||||||
|
if err != nil {
|
||||||
|
if !xerrors.IsErrIgnored(err, errDiskNotFound, errNoSuchNotifications) {
|
||||||
|
errorIf(err, "Unable to load notification configuration of bucket %v", bucket.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sys.AddRulesMap(bucket.Name, config.ToRulesMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = sys.initListeners(objAPI, bucket.Name); err != nil {
|
||||||
|
errorIf(err, "Unable to initialize HTTP listener for bucket %v", bucket.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRulesMap - adds rules map for bucket name.
|
||||||
|
func (sys *NotificationSys) AddRulesMap(bucketName string, rulesMap event.RulesMap) {
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
rulesMap = rulesMap.Clone()
|
||||||
|
|
||||||
|
for _, targetRulesMap := range sys.bucketRemoteTargetRulesMap[bucketName] {
|
||||||
|
rulesMap.Add(targetRulesMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesMap.Add(sys.bucketRulesMap[bucketName])
|
||||||
|
sys.bucketRulesMap[bucketName] = rulesMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRulesMap - removes rules map for bucket name.
|
||||||
|
func (sys *NotificationSys) RemoveRulesMap(bucketName string, rulesMap event.RulesMap) {
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
sys.bucketRulesMap[bucketName].Remove(rulesMap)
|
||||||
|
if len(sys.bucketRulesMap[bucketName]) == 0 {
|
||||||
|
delete(sys.bucketRulesMap, bucketName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveNotification - removes all notification configuration for bucket name.
|
||||||
|
func (sys *NotificationSys) RemoveNotification(bucketName string) {
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
delete(sys.bucketRulesMap, bucketName)
|
||||||
|
|
||||||
|
for targetID := range sys.bucketRemoteTargetRulesMap[bucketName] {
|
||||||
|
sys.targetList.Remove(targetID)
|
||||||
|
delete(sys.bucketRemoteTargetRulesMap[bucketName], targetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(sys.bucketRemoteTargetRulesMap, bucketName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAllRemoteTargets - closes and removes all HTTP/PeerRPC client targets.
|
||||||
|
func (sys *NotificationSys) RemoveAllRemoteTargets() {
|
||||||
|
for _, targetMap := range sys.bucketRemoteTargetRulesMap {
|
||||||
|
for targetID := range targetMap {
|
||||||
|
sys.targetList.Remove(targetID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRemoteTarget - closes and removes target by target ID.
|
||||||
|
func (sys *NotificationSys) RemoveRemoteTarget(bucketName string, targetID event.TargetID) {
|
||||||
|
for id, err := range sys.targetList.Remove(targetID) {
|
||||||
|
errorIf(err, "unable to close target ID %v", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Lock()
|
||||||
|
defer sys.Unlock()
|
||||||
|
|
||||||
|
if _, ok := sys.bucketRemoteTargetRulesMap[bucketName]; ok {
|
||||||
|
delete(sys.bucketRemoteTargetRulesMap[bucketName], targetID)
|
||||||
|
if len(sys.bucketRemoteTargetRulesMap[bucketName]) == 0 {
|
||||||
|
delete(sys.bucketRemoteTargetRulesMap, bucketName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sys *NotificationSys) send(bucketName string, eventData event.Event, targetIDs ...event.TargetID) map[event.TargetID]error {
|
||||||
|
errMap := sys.targetList.Send(eventData, targetIDs...)
|
||||||
|
for targetID := range errMap {
|
||||||
|
if sys.RemoteTargetExist(bucketName, targetID) {
|
||||||
|
sys.RemoveRemoteTarget(bucketName, targetID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends event data to all matching targets.
|
||||||
|
func (sys *NotificationSys) Send(args eventArgs) map[event.TargetID]error {
|
||||||
|
sys.RLock()
|
||||||
|
targetIDSet := sys.bucketRulesMap[args.BucketName].Match(args.EventName, args.Object.Name)
|
||||||
|
sys.RUnlock()
|
||||||
|
if len(targetIDSet) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
targetIDs := targetIDSet.ToSlice()
|
||||||
|
return sys.send(args.BucketName, args.ToEvent(), targetIDs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNotificationSys - creates new notification system object.
|
||||||
|
func NewNotificationSys(config *serverConfig, endpoints EndpointList) (*NotificationSys, error) {
|
||||||
|
targetList, err := getNotificationTargets(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerRPCClientMap := makeRemoteRPCClients(endpoints)
|
||||||
|
|
||||||
|
// bucketRulesMap/bucketRemoteTargetRulesMap are initialized by NotificationSys.Init()
|
||||||
|
return &NotificationSys{
|
||||||
|
targetList: targetList,
|
||||||
|
bucketRulesMap: make(map[string]event.RulesMap),
|
||||||
|
bucketRemoteTargetRulesMap: make(map[string]map[event.TargetID]event.RulesMap),
|
||||||
|
peerRPCClientMap: peerRPCClientMap,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type eventArgs struct {
|
||||||
|
EventName event.Name
|
||||||
|
BucketName string
|
||||||
|
Object ObjectInfo
|
||||||
|
ReqParams map[string]string
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
UserAgent string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToEvent - converts to notification event.
|
||||||
|
func (args eventArgs) ToEvent() event.Event {
|
||||||
|
getOriginEndpoint := func() string {
|
||||||
|
host := globalMinioHost
|
||||||
|
if host == "" {
|
||||||
|
// FIXME: Send FQDN or hostname of this machine than sending IP address.
|
||||||
|
host = localIP4.ToSlice()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s://%s:%s", getURLScheme(globalIsSSL), host, globalMinioPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
creds := globalServerConfig.GetCredential()
|
||||||
|
eventTime := UTCNow()
|
||||||
|
uniqueID := fmt.Sprintf("%X", eventTime.UnixNano())
|
||||||
|
|
||||||
|
newEvent := event.Event{
|
||||||
|
EventVersion: "2.0",
|
||||||
|
EventSource: "minio:s3",
|
||||||
|
AwsRegion: globalServerConfig.GetRegion(),
|
||||||
|
EventTime: eventTime.Format(event.AMZTimeFormat),
|
||||||
|
EventName: args.EventName,
|
||||||
|
UserIdentity: event.Identity{creds.AccessKey},
|
||||||
|
RequestParameters: args.ReqParams,
|
||||||
|
ResponseElements: map[string]string{
|
||||||
|
"x-amz-request-id": uniqueID,
|
||||||
|
"x-minio-origin-endpoint": getOriginEndpoint(), // Minio specific custom elements.
|
||||||
|
},
|
||||||
|
S3: event.Metadata{
|
||||||
|
SchemaVersion: "1.0",
|
||||||
|
ConfigurationID: "Config",
|
||||||
|
Bucket: event.Bucket{
|
||||||
|
Name: args.BucketName,
|
||||||
|
OwnerIdentity: event.Identity{creds.AccessKey},
|
||||||
|
ARN: bucketARNPrefix + args.BucketName,
|
||||||
|
},
|
||||||
|
Object: event.Object{
|
||||||
|
Key: url.QueryEscape(args.Object.Name),
|
||||||
|
VersionID: "1",
|
||||||
|
Sequencer: uniqueID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Source: event.Source{
|
||||||
|
Host: args.Host,
|
||||||
|
Port: args.Port,
|
||||||
|
UserAgent: args.UserAgent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.EventName != event.ObjectRemovedDelete {
|
||||||
|
newEvent.S3.Object.ETag = args.Object.ETag
|
||||||
|
newEvent.S3.Object.Size = args.Object.Size
|
||||||
|
newEvent.S3.Object.ContentType = args.Object.ContentType
|
||||||
|
newEvent.S3.Object.UserMetadata = args.Object.UserDefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return newEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendEvent(args eventArgs) {
|
||||||
|
// globalNotificationSys is not initialized in gateway mode.
|
||||||
|
if globalNotificationSys == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for targetID, err := range globalNotificationSys.Send(args) {
|
||||||
|
errorIf(err, "unable to send event %v of bucket: %v, object: %v to target %v",
|
||||||
|
args.EventName, args.BucketName, args.Object.Name, targetID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveConfig(objAPI ObjectLayer, configFile string, data []byte) error {
|
||||||
|
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = objAPI.PutObject(minioMetaBucket, configFile, hashReader, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfig(objAPI ObjectLayer, configFile string) (*bytes.Buffer, error) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
// Read entire content by setting size to -1
|
||||||
|
err := objAPI.GetObject(minioMetaBucket, configFile, 0, -1, &buffer, "")
|
||||||
|
if err != nil {
|
||||||
|
// Ignore if err is ObjectNotFound or IncompleteBody when bucket is not configured with notification
|
||||||
|
if isErrObjectNotFound(err) || isErrIncompleteBody(err) {
|
||||||
|
return nil, xerrors.Trace(errNoSuchNotifications)
|
||||||
|
}
|
||||||
|
errorIf(err, "Unable to read file %v", configFile)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return NoSuchNotifications on empty content.
|
||||||
|
if buffer.Len() == 0 {
|
||||||
|
return nil, xerrors.Trace(errNoSuchNotifications)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &buffer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNotificationConfig(objAPI ObjectLayer, bucketName string) (*event.Config, error) {
|
||||||
|
// Construct path to notification.xml for the given bucket.
|
||||||
|
configFile := path.Join(bucketConfigPrefix, bucketName, bucketNotificationConfig)
|
||||||
|
|
||||||
|
// Get read lock.
|
||||||
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, configFile)
|
||||||
|
if err := objLock.GetRLock(globalOperationTimeout); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer objLock.RUnlock()
|
||||||
|
|
||||||
|
reader, err := readConfig(objAPI, configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return event.ParseConfig(reader, globalServerConfig.GetRegion(), globalNotificationSys.targetList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveNotificationConfig(objAPI ObjectLayer, bucketName string, config *event.Config) error {
|
||||||
|
data, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile := path.Join(bucketConfigPrefix, bucketName, bucketNotificationConfig)
|
||||||
|
|
||||||
|
// Get write lock.
|
||||||
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, configFile)
|
||||||
|
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer objLock.Unlock()
|
||||||
|
|
||||||
|
return saveConfig(objAPI, configFile, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveListener - saves HTTP client currently listening for events to listener.json.
|
||||||
|
func SaveListener(objAPI ObjectLayer, bucketName string, eventNames []event.Name, pattern string, targetID event.TargetID, addr xnet.Host) error {
|
||||||
|
// listener.json is available/applicable only in DistXL mode.
|
||||||
|
if !globalIsDistXL {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct path to listener.json for the given bucket.
|
||||||
|
configFile := path.Join(bucketConfigPrefix, bucketName, bucketListenerConfig)
|
||||||
|
transactionConfigFile := configFile + ".transaction"
|
||||||
|
|
||||||
|
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
|
||||||
|
// and configFile, take a transaction lock to avoid data race between readConfig()
|
||||||
|
// and saveConfig().
|
||||||
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
|
||||||
|
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer objLock.Unlock()
|
||||||
|
|
||||||
|
reader, err := readConfig(objAPI, configFile)
|
||||||
|
if err != nil && !xerrors.IsErrIgnored(err, errDiskNotFound, errNoSuchNotifications) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
listenerList := []ListenBucketNotificationArgs{}
|
||||||
|
if reader != nil {
|
||||||
|
if err = json.NewDecoder(reader).Decode(&listenerList); err != nil {
|
||||||
|
errorIf(err, "Unable to parse listener.json.")
|
||||||
|
return xerrors.Trace(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listenerList = append(listenerList, ListenBucketNotificationArgs{
|
||||||
|
EventNames: eventNames,
|
||||||
|
Pattern: pattern,
|
||||||
|
TargetID: targetID,
|
||||||
|
Addr: addr,
|
||||||
|
})
|
||||||
|
|
||||||
|
data, err := json.Marshal(listenerList)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveConfig(objAPI, configFile, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveListener - removes HTTP client currently listening for events from listener.json.
|
||||||
|
func RemoveListener(objAPI ObjectLayer, bucketName string, targetID event.TargetID, addr xnet.Host) error {
|
||||||
|
// listener.json is available/applicable only in DistXL mode.
|
||||||
|
if !globalIsDistXL {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct path to listener.json for the given bucket.
|
||||||
|
configFile := path.Join(bucketConfigPrefix, bucketName, bucketListenerConfig)
|
||||||
|
transactionConfigFile := configFile + ".transaction"
|
||||||
|
|
||||||
|
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
|
||||||
|
// and configFile, take a transaction lock to avoid data race between readConfig()
|
||||||
|
// and saveConfig().
|
||||||
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
|
||||||
|
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer objLock.Unlock()
|
||||||
|
|
||||||
|
reader, err := readConfig(objAPI, configFile)
|
||||||
|
if err != nil && !xerrors.IsErrIgnored(err, errDiskNotFound, errNoSuchNotifications) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
listenerList := []ListenBucketNotificationArgs{}
|
||||||
|
if reader != nil {
|
||||||
|
if err = json.NewDecoder(reader).Decode(&listenerList); err != nil {
|
||||||
|
errorIf(err, "Unable to parse listener.json.")
|
||||||
|
return xerrors.Trace(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(listenerList) == 0 {
|
||||||
|
// Nothing to remove.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
activeListenerList := []ListenBucketNotificationArgs{}
|
||||||
|
for _, args := range listenerList {
|
||||||
|
if args.TargetID == targetID && args.Addr.Equal(addr) {
|
||||||
|
// Skip if matches
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
activeListenerList = append(activeListenerList, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(activeListenerList)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveConfig(objAPI, configFile, data)
|
||||||
|
}
|
|
@ -1,347 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2017 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Notifier represents collection of supported notification queues.
|
|
||||||
type notifier struct {
|
|
||||||
AMQP amqpConfigs `json:"amqp"`
|
|
||||||
NATS natsConfigs `json:"nats"`
|
|
||||||
ElasticSearch elasticSearchConfigs `json:"elasticsearch"`
|
|
||||||
Redis redisConfigs `json:"redis"`
|
|
||||||
PostgreSQL postgreSQLConfigs `json:"postgresql"`
|
|
||||||
Kafka kafkaConfigs `json:"kafka"`
|
|
||||||
Webhook webhookConfigs `json:"webhook"`
|
|
||||||
MySQL mySQLConfigs `json:"mysql"`
|
|
||||||
MQTT mqttConfigs `json:"mqtt"`
|
|
||||||
// Add new notification queues. IMPORTANT: When new queues are
|
|
||||||
// added, update `serverConfig.ConfigDiff()` to reflect the
|
|
||||||
// change.
|
|
||||||
}
|
|
||||||
|
|
||||||
type amqpConfigs map[string]amqpNotify
|
|
||||||
|
|
||||||
func (a amqpConfigs) Clone() amqpConfigs {
|
|
||||||
a2 := make(amqpConfigs, len(a))
|
|
||||||
for k, v := range a {
|
|
||||||
a2[k] = v
|
|
||||||
}
|
|
||||||
return a2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a amqpConfigs) Validate() error {
|
|
||||||
for k, v := range a {
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("AMQP [%s] configuration invalid: %s", k, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mqttConfigs map[string]mqttNotify
|
|
||||||
|
|
||||||
func (a mqttConfigs) Clone() mqttConfigs {
|
|
||||||
a2 := make(mqttConfigs, len(a))
|
|
||||||
for k, v := range a {
|
|
||||||
a2[k] = v
|
|
||||||
}
|
|
||||||
return a2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a mqttConfigs) Validate() error {
|
|
||||||
for k, v := range a {
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("MQTT [%s] configuration invalid: %s", k, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type natsConfigs map[string]natsNotify
|
|
||||||
|
|
||||||
func (a natsConfigs) Clone() natsConfigs {
|
|
||||||
a2 := make(natsConfigs, len(a))
|
|
||||||
for k, v := range a {
|
|
||||||
a2[k] = v
|
|
||||||
}
|
|
||||||
return a2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a natsConfigs) Validate() error {
|
|
||||||
for k, v := range a {
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("NATS [%s] configuration invalid: %s", k, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type elasticSearchConfigs map[string]elasticSearchNotify
|
|
||||||
|
|
||||||
func (a elasticSearchConfigs) Clone() elasticSearchConfigs {
|
|
||||||
a2 := make(elasticSearchConfigs, len(a))
|
|
||||||
for k, v := range a {
|
|
||||||
a2[k] = v
|
|
||||||
}
|
|
||||||
return a2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a elasticSearchConfigs) Validate() error {
|
|
||||||
for k, v := range a {
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("ElasticSearch [%s] configuration invalid: %s", k, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type redisConfigs map[string]redisNotify
|
|
||||||
|
|
||||||
func (a redisConfigs) Clone() redisConfigs {
|
|
||||||
a2 := make(redisConfigs, len(a))
|
|
||||||
for k, v := range a {
|
|
||||||
a2[k] = v
|
|
||||||
}
|
|
||||||
return a2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a redisConfigs) Validate() error {
|
|
||||||
for k, v := range a {
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("Redis [%s] configuration invalid: %s", k, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type postgreSQLConfigs map[string]postgreSQLNotify
|
|
||||||
|
|
||||||
func (a postgreSQLConfigs) Clone() postgreSQLConfigs {
|
|
||||||
a2 := make(postgreSQLConfigs, len(a))
|
|
||||||
for k, v := range a {
|
|
||||||
a2[k] = v
|
|
||||||
}
|
|
||||||
return a2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a postgreSQLConfigs) Validate() error {
|
|
||||||
for k, v := range a {
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("PostgreSQL [%s] configuration invalid: %s", k, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type kafkaConfigs map[string]kafkaNotify
|
|
||||||
|
|
||||||
func (a kafkaConfigs) Clone() kafkaConfigs {
|
|
||||||
a2 := make(kafkaConfigs, len(a))
|
|
||||||
for k, v := range a {
|
|
||||||
a2[k] = v
|
|
||||||
}
|
|
||||||
return a2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a kafkaConfigs) Validate() error {
|
|
||||||
for k, v := range a {
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("Kafka [%s] configuration invalid: %s", k, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type webhookConfigs map[string]webhookNotify
|
|
||||||
|
|
||||||
func (a webhookConfigs) Clone() webhookConfigs {
|
|
||||||
a2 := make(webhookConfigs, len(a))
|
|
||||||
for k, v := range a {
|
|
||||||
a2[k] = v
|
|
||||||
}
|
|
||||||
return a2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a webhookConfigs) Validate() error {
|
|
||||||
for k, v := range a {
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("Webhook [%s] configuration invalid: %s", k, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mySQLConfigs map[string]mySQLNotify
|
|
||||||
|
|
||||||
func (a mySQLConfigs) Clone() mySQLConfigs {
|
|
||||||
a2 := make(mySQLConfigs, len(a))
|
|
||||||
for k, v := range a {
|
|
||||||
a2[k] = v
|
|
||||||
}
|
|
||||||
return a2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a mySQLConfigs) Validate() error {
|
|
||||||
for k, v := range a {
|
|
||||||
if err := v.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("MySQL [%s] configuration invalid: %s", k, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) Validate() error {
|
|
||||||
if n == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := n.AMQP.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := n.NATS.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := n.ElasticSearch.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := n.Redis.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := n.PostgreSQL.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := n.Kafka.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := n.Webhook.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := n.MySQL.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return n.MQTT.Validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) SetAMQPByID(accountID string, amqpn amqpNotify) {
|
|
||||||
n.AMQP[accountID] = amqpn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetAMQP() map[string]amqpNotify {
|
|
||||||
return n.AMQP.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetAMQPByID(accountID string) amqpNotify {
|
|
||||||
return n.AMQP[accountID]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) SetMQTTByID(accountID string, mqttn mqttNotify) {
|
|
||||||
n.MQTT[accountID] = mqttn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetMQTT() map[string]mqttNotify {
|
|
||||||
return n.MQTT.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetMQTTByID(accountID string) mqttNotify {
|
|
||||||
return n.MQTT[accountID]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) SetNATSByID(accountID string, natsn natsNotify) {
|
|
||||||
n.NATS[accountID] = natsn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetNATS() map[string]natsNotify {
|
|
||||||
return n.NATS.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetNATSByID(accountID string) natsNotify {
|
|
||||||
return n.NATS[accountID]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) SetElasticSearchByID(accountID string, es elasticSearchNotify) {
|
|
||||||
n.ElasticSearch[accountID] = es
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetElasticSearchByID(accountID string) elasticSearchNotify {
|
|
||||||
return n.ElasticSearch[accountID]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetElasticSearch() map[string]elasticSearchNotify {
|
|
||||||
return n.ElasticSearch.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) SetRedisByID(accountID string, r redisNotify) {
|
|
||||||
n.Redis[accountID] = r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetRedis() map[string]redisNotify {
|
|
||||||
return n.Redis.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetRedisByID(accountID string) redisNotify {
|
|
||||||
return n.Redis[accountID]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetWebhook() map[string]webhookNotify {
|
|
||||||
return n.Webhook.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetWebhookByID(accountID string) webhookNotify {
|
|
||||||
return n.Webhook[accountID]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) SetWebhookByID(accountID string, pgn webhookNotify) {
|
|
||||||
n.Webhook[accountID] = pgn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) SetPostgreSQLByID(accountID string, pgn postgreSQLNotify) {
|
|
||||||
n.PostgreSQL[accountID] = pgn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetPostgreSQL() map[string]postgreSQLNotify {
|
|
||||||
return n.PostgreSQL.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetPostgreSQLByID(accountID string) postgreSQLNotify {
|
|
||||||
return n.PostgreSQL[accountID]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) SetMySQLByID(accountID string, pgn mySQLNotify) {
|
|
||||||
n.MySQL[accountID] = pgn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetMySQL() map[string]mySQLNotify {
|
|
||||||
return n.MySQL.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetMySQLByID(accountID string) mySQLNotify {
|
|
||||||
return n.MySQL[accountID]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) SetKafkaByID(accountID string, kn kafkaNotify) {
|
|
||||||
n.Kafka[accountID] = kn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetKafka() map[string]kafkaNotify {
|
|
||||||
return n.Kafka.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *notifier) GetKafkaByID(accountID string) kafkaNotify {
|
|
||||||
return n.Kafka[accountID]
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2017 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
|
|
258
cmd/notifiers.go
258
cmd/notifiers.go
|
@ -1,258 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/wildcard"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SQS type.
|
|
||||||
const (
|
|
||||||
// Minio sqs ARN prefix.
|
|
||||||
minioSqs = "arn:minio:sqs:"
|
|
||||||
|
|
||||||
// Static string indicating queue type 'amqp'.
|
|
||||||
queueTypeAMQP = "amqp"
|
|
||||||
// Static string indicating queue type 'mqtt'.
|
|
||||||
queueTypeMQTT = "mqtt"
|
|
||||||
// Static string indicating queue type 'nats'.
|
|
||||||
queueTypeNATS = "nats"
|
|
||||||
// Static string indicating queue type 'elasticsearch'.
|
|
||||||
queueTypeElastic = "elasticsearch"
|
|
||||||
// Static string indicating queue type 'redis'.
|
|
||||||
queueTypeRedis = "redis"
|
|
||||||
// Static string indicating queue type 'postgresql'.
|
|
||||||
queueTypePostgreSQL = "postgresql"
|
|
||||||
// Static string indicating queue type 'mysql'.
|
|
||||||
queueTypeMySQL = "mysql"
|
|
||||||
// Static string indicating queue type 'kafka'.
|
|
||||||
queueTypeKafka = "kafka"
|
|
||||||
// Static string for Webhooks
|
|
||||||
queueTypeWebhook = "webhook"
|
|
||||||
|
|
||||||
// Notifier format value constants
|
|
||||||
formatNamespace = "namespace"
|
|
||||||
formatAccess = "access"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Topic type.
|
|
||||||
const (
|
|
||||||
// Minio topic ARN prefix.
|
|
||||||
minioTopic = "arn:minio:sns:"
|
|
||||||
|
|
||||||
// Static string indicating sns type 'listen'.
|
|
||||||
snsTypeMinio = "listen"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errNotifyNotEnabled = errors.New("requested notifier not enabled")
|
|
||||||
|
|
||||||
// Returns true if queueArn is for an AMQP queue.
|
|
||||||
func isAMQPQueue(sqsArn arnSQS) bool {
|
|
||||||
if sqsArn.Type != queueTypeAMQP {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
amqpL := globalServerConfig.Notify.GetAMQPByID(sqsArn.AccountID)
|
|
||||||
if !amqpL.Enable {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Connect to amqp server to validate.
|
|
||||||
amqpC, err := dialAMQP(amqpL)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to connect to amqp service. %#v", amqpL)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer amqpC.conn.Close()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if mqttARN is for an MQTT queue.
|
|
||||||
func isMQTTQueue(sqsArn arnSQS) bool {
|
|
||||||
if sqsArn.Type != queueTypeMQTT {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
mqttL := globalServerConfig.Notify.GetMQTTByID(sqsArn.AccountID)
|
|
||||||
if !mqttL.Enable {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Connect to mqtt server to validate.
|
|
||||||
mqttC, err := dialMQTT(mqttL)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to connect to mqtt service. %#v", mqttL)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer mqttC.Client.Disconnect(250)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if natsArn is for an NATS queue.
|
|
||||||
func isNATSQueue(sqsArn arnSQS) bool {
|
|
||||||
if sqsArn.Type != queueTypeNATS {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
natsL := globalServerConfig.Notify.GetNATSByID(sqsArn.AccountID)
|
|
||||||
if !natsL.Enable {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Connect to nats server to validate.
|
|
||||||
natsC, err := dialNATS(natsL, true)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to connect to nats service. %#v", natsL)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
closeNATS(natsC)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if queueArn is for an Webhook queue
|
|
||||||
func isWebhookQueue(sqsArn arnSQS) bool {
|
|
||||||
if sqsArn.Type != queueTypeWebhook {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
rNotify := globalServerConfig.Notify.GetWebhookByID(sqsArn.AccountID)
|
|
||||||
return rNotify.Enable
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if queueArn is for an Redis queue.
|
|
||||||
func isRedisQueue(sqsArn arnSQS) bool {
|
|
||||||
if sqsArn.Type != queueTypeRedis {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
rNotify := globalServerConfig.Notify.GetRedisByID(sqsArn.AccountID)
|
|
||||||
if !rNotify.Enable {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Connect to redis server to validate.
|
|
||||||
rPool, err := dialRedis(rNotify)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to connect to redis service. %#v", rNotify)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer rPool.Close()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if queueArn is for an ElasticSearch queue.
|
|
||||||
func isElasticQueue(sqsArn arnSQS) bool {
|
|
||||||
if sqsArn.Type != queueTypeElastic {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
esNotify := globalServerConfig.Notify.GetElasticSearchByID(sqsArn.AccountID)
|
|
||||||
if !esNotify.Enable {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
elasticC, err := dialElastic(esNotify)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to connect to elasticsearch service %#v", esNotify)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer elasticC.Stop()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if queueArn is for PostgreSQL.
|
|
||||||
func isPostgreSQLQueue(sqsArn arnSQS) bool {
|
|
||||||
if sqsArn.Type != queueTypePostgreSQL {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
pgNotify := globalServerConfig.Notify.GetPostgreSQLByID(sqsArn.AccountID)
|
|
||||||
if !pgNotify.Enable {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
pgC, err := dialPostgreSQL(pgNotify)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to connect to PostgreSQL server %#v", pgNotify)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer pgC.Close()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if queueArn is for MySQL.
|
|
||||||
func isMySQLQueue(sqsArn arnSQS) bool {
|
|
||||||
if sqsArn.Type != queueTypeMySQL {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
msqlNotify := globalServerConfig.Notify.GetMySQLByID(sqsArn.AccountID)
|
|
||||||
if !msqlNotify.Enable {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
myC, err := dialMySQL(msqlNotify)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to connect to MySQL server %#v", msqlNotify)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer myC.Close()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if queueArn is for Kafka.
|
|
||||||
func isKafkaQueue(sqsArn arnSQS) bool {
|
|
||||||
if sqsArn.Type != queueTypeKafka {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
kafkaNotifyCfg := globalServerConfig.Notify.GetKafkaByID(sqsArn.AccountID)
|
|
||||||
if !kafkaNotifyCfg.Enable {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
kafkaC, err := dialKafka(kafkaNotifyCfg)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to dial Kafka server %#v", kafkaNotifyCfg)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer kafkaC.Close()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match function matches wild cards in 'pattern' for events.
|
|
||||||
func eventMatch(eventType string, events []string) (ok bool) {
|
|
||||||
for _, event := range events {
|
|
||||||
ok = wildcard.MatchSimple(event, eventType)
|
|
||||||
if ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter rule match, matches an object against the filter rules.
|
|
||||||
func filterRuleMatch(object string, frs []filterRule) bool {
|
|
||||||
var prefixMatch, suffixMatch = true, true
|
|
||||||
for _, fr := range frs {
|
|
||||||
if isValidFilterNamePrefix(fr.Name) {
|
|
||||||
prefixMatch = hasPrefix(object, fr.Value)
|
|
||||||
} else if isValidFilterNameSuffix(fr.Name) {
|
|
||||||
suffixMatch = hasSuffix(object, fr.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return prefixMatch && suffixMatch
|
|
||||||
}
|
|
||||||
|
|
||||||
// A type to represent dynamic error generation functions for
|
|
||||||
// notifications.
|
|
||||||
type notificationErrorFactoryFunc func(string, ...interface{}) error
|
|
||||||
|
|
||||||
// A function to build dynamic error generation functions for
|
|
||||||
// notifications by setting an error prefix string.
|
|
||||||
func newNotificationErrorFactory(prefix string) notificationErrorFactoryFunc {
|
|
||||||
return func(msg string, a ...interface{}) error {
|
|
||||||
s := fmt.Sprintf(msg, a...)
|
|
||||||
return fmt.Errorf("%s: %s", prefix, s)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,214 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
// Tests for event filter rules.
|
|
||||||
func TestFilterMatch(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
objectName string
|
|
||||||
rules []filterRule
|
|
||||||
expectedRuleMatch bool
|
|
||||||
}{
|
|
||||||
// Prefix matches for a parent.
|
|
||||||
{
|
|
||||||
objectName: "test/test1/object.txt",
|
|
||||||
rules: []filterRule{
|
|
||||||
{
|
|
||||||
Name: "prefix",
|
|
||||||
Value: "test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedRuleMatch: true,
|
|
||||||
},
|
|
||||||
// Prefix matches for the object.
|
|
||||||
{
|
|
||||||
objectName: "test/test1/object.txt",
|
|
||||||
rules: []filterRule{
|
|
||||||
{
|
|
||||||
Name: "prefix",
|
|
||||||
Value: "test/test1/object",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedRuleMatch: true,
|
|
||||||
},
|
|
||||||
// Prefix doesn't match.
|
|
||||||
{
|
|
||||||
objectName: "test/test1/object.txt",
|
|
||||||
rules: []filterRule{
|
|
||||||
{
|
|
||||||
Name: "prefix",
|
|
||||||
Value: "test/test1/object/",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedRuleMatch: false,
|
|
||||||
},
|
|
||||||
// Suffix matches.
|
|
||||||
{
|
|
||||||
objectName: "test/test1/object.txt",
|
|
||||||
rules: []filterRule{
|
|
||||||
{
|
|
||||||
Name: "suffix",
|
|
||||||
Value: ".txt",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedRuleMatch: true,
|
|
||||||
},
|
|
||||||
// Suffix doesn't match but prefix matches.
|
|
||||||
{
|
|
||||||
objectName: "test/test1/object.txt",
|
|
||||||
rules: []filterRule{
|
|
||||||
{
|
|
||||||
Name: "suffix",
|
|
||||||
Value: ".jpg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "prefix",
|
|
||||||
Value: "test/test1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedRuleMatch: false,
|
|
||||||
},
|
|
||||||
// Prefix doesn't match but suffix matches.
|
|
||||||
{
|
|
||||||
objectName: "test/test2/object.jpg",
|
|
||||||
rules: []filterRule{
|
|
||||||
{
|
|
||||||
Name: "suffix",
|
|
||||||
Value: ".jpg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "prefix",
|
|
||||||
Value: "test/test1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedRuleMatch: false,
|
|
||||||
},
|
|
||||||
// Suffix and prefix doesn't match.
|
|
||||||
{
|
|
||||||
objectName: "test/test2/object.jpg",
|
|
||||||
rules: []filterRule{
|
|
||||||
{
|
|
||||||
Name: "suffix",
|
|
||||||
Value: ".txt",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "prefix",
|
|
||||||
Value: "test/test1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedRuleMatch: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// .. Validate all cases.
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
ruleMatch := filterRuleMatch(testCase.objectName, testCase.rules)
|
|
||||||
if ruleMatch != testCase.expectedRuleMatch {
|
|
||||||
t.Errorf("Test %d: Expected %t, got %t", i+1, testCase.expectedRuleMatch, ruleMatch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests all event match.
|
|
||||||
func TestEventMatch(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
eventName EventName
|
|
||||||
events []string
|
|
||||||
match bool
|
|
||||||
}{
|
|
||||||
// Valid object created PUT event.
|
|
||||||
{
|
|
||||||
eventName: ObjectCreatedPut,
|
|
||||||
events: []string{
|
|
||||||
"s3:ObjectCreated:Put",
|
|
||||||
},
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
// Valid object removed DELETE event.
|
|
||||||
{
|
|
||||||
eventName: ObjectRemovedDelete,
|
|
||||||
events: []string{
|
|
||||||
"s3:ObjectRemoved:Delete",
|
|
||||||
},
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
// Invalid events fails to match with empty events.
|
|
||||||
{
|
|
||||||
eventName: ObjectRemovedDelete,
|
|
||||||
events: []string{""},
|
|
||||||
match: false,
|
|
||||||
},
|
|
||||||
// Invalid events fails to match with valid events.
|
|
||||||
{
|
|
||||||
eventName: ObjectCreatedCompleteMultipartUpload,
|
|
||||||
events: []string{
|
|
||||||
"s3:ObjectRemoved:*",
|
|
||||||
},
|
|
||||||
match: false,
|
|
||||||
},
|
|
||||||
// Valid events wild card match.
|
|
||||||
{
|
|
||||||
eventName: ObjectCreatedPut,
|
|
||||||
events: []string{
|
|
||||||
"s3:ObjectCreated:*",
|
|
||||||
},
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
// Valid events wild card match.
|
|
||||||
{
|
|
||||||
eventName: ObjectCreatedPost,
|
|
||||||
events: []string{
|
|
||||||
"s3:ObjectCreated:*",
|
|
||||||
},
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
// Valid events wild card match.
|
|
||||||
{
|
|
||||||
eventName: ObjectCreatedCopy,
|
|
||||||
events: []string{
|
|
||||||
"s3:ObjectCreated:*",
|
|
||||||
},
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
// Valid events wild card match.
|
|
||||||
{
|
|
||||||
eventName: ObjectCreatedCompleteMultipartUpload,
|
|
||||||
events: []string{
|
|
||||||
"s3:ObjectCreated:*",
|
|
||||||
},
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
// Valid events wild card match.
|
|
||||||
{
|
|
||||||
eventName: ObjectCreatedPut,
|
|
||||||
events: []string{
|
|
||||||
"s3:ObjectCreated:*",
|
|
||||||
"s3:ObjectRemoved:*",
|
|
||||||
},
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
ok := eventMatch(testCase.eventName.String(), testCase.events)
|
|
||||||
if testCase.match != ok {
|
|
||||||
t.Errorf("Test %d: Expected \"%t\", got \"%t\"", i+1, testCase.match, ok)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,192 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 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 (
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/streadway/amqp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// amqpNotify - represents logrus compatible AMQP hook.
|
|
||||||
// All fields represent AMQP configuration details.
|
|
||||||
type amqpNotify struct {
|
|
||||||
Enable bool `json:"enable"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
Exchange string `json:"exchange"`
|
|
||||||
RoutingKey string `json:"routingKey"`
|
|
||||||
ExchangeType string `json:"exchangeType"`
|
|
||||||
DeliveryMode uint8 `json:"deliveryMode"`
|
|
||||||
Mandatory bool `json:"mandatory"`
|
|
||||||
Immediate bool `json:"immediate"`
|
|
||||||
Durable bool `json:"durable"`
|
|
||||||
Internal bool `json:"internal"`
|
|
||||||
NoWait bool `json:"noWait"`
|
|
||||||
AutoDeleted bool `json:"autoDeleted"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *amqpNotify) Validate() error {
|
|
||||||
if !a.Enable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, err := checkURL(a.URL); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// amqpConn implements a reconnecting amqp conn extending *amqp.Connection,
|
|
||||||
// also provides additional protection for such a mutation.
|
|
||||||
type amqpConn struct {
|
|
||||||
sync.Mutex
|
|
||||||
conn *amqp.Connection
|
|
||||||
params amqpNotify
|
|
||||||
}
|
|
||||||
|
|
||||||
// dialAMQP - dials and returns an amqpConnection instance,
|
|
||||||
// for sending notifications. Returns error if amqp logger
|
|
||||||
// is not enabled.
|
|
||||||
func dialAMQP(amqpL amqpNotify) (*amqpConn, error) {
|
|
||||||
if !amqpL.Enable {
|
|
||||||
return nil, errNotifyNotEnabled
|
|
||||||
}
|
|
||||||
conn, err := amqp.Dial(amqpL.URL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &amqpConn{
|
|
||||||
conn: conn,
|
|
||||||
params: amqpL,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAMQPNotify(accountID string) (*logrus.Logger, error) {
|
|
||||||
amqpL := globalServerConfig.Notify.GetAMQPByID(accountID)
|
|
||||||
|
|
||||||
// Connect to amqp server.
|
|
||||||
amqpC, err := dialAMQP(amqpL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
amqpLog := logrus.New()
|
|
||||||
|
|
||||||
// Disable writing to console.
|
|
||||||
amqpLog.Out = ioutil.Discard
|
|
||||||
|
|
||||||
// Add a amqp hook.
|
|
||||||
amqpLog.Hooks.Add(amqpC)
|
|
||||||
|
|
||||||
// Set default JSON formatter.
|
|
||||||
amqpLog.Formatter = new(logrus.JSONFormatter)
|
|
||||||
|
|
||||||
// Successfully enabled all AMQPs.
|
|
||||||
return amqpLog, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if the error represents a closed
|
|
||||||
// network error.
|
|
||||||
func isAMQPClosedNetworkErr(err error) bool {
|
|
||||||
// Any other error other than connection closed, return.
|
|
||||||
if neterr, ok := err.(*net.OpError); ok &&
|
|
||||||
neterr.Err.Error() == "use of closed network connection" {
|
|
||||||
return true
|
|
||||||
} else if err == amqp.ErrClosed {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channel is a wrapper implementation of amqp.Connection.Channel()
|
|
||||||
// which implements transparent reconnection.
|
|
||||||
func (q *amqpConn) Channel() (*amqp.Channel, error) {
|
|
||||||
q.Lock()
|
|
||||||
ch, err := q.conn.Channel()
|
|
||||||
q.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
if !isAMQPClosedNetworkErr(err) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Attempt to connect again.
|
|
||||||
var conn *amqp.Connection
|
|
||||||
conn, err = amqp.Dial(q.params.URL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ch, err = conn.Channel()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q.Lock()
|
|
||||||
q.conn = conn
|
|
||||||
q.Unlock()
|
|
||||||
}
|
|
||||||
return ch, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire is called when an event should be sent to the message broker.
|
|
||||||
func (q *amqpConn) Fire(entry *logrus.Entry) error {
|
|
||||||
ch, err := q.Channel()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer ch.Close()
|
|
||||||
|
|
||||||
err = ch.ExchangeDeclare(
|
|
||||||
q.params.Exchange,
|
|
||||||
q.params.ExchangeType,
|
|
||||||
q.params.Durable,
|
|
||||||
q.params.AutoDeleted,
|
|
||||||
q.params.Internal,
|
|
||||||
q.params.NoWait,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := entry.String()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ch.Publish(
|
|
||||||
q.params.Exchange,
|
|
||||||
q.params.RoutingKey,
|
|
||||||
q.params.Mandatory,
|
|
||||||
q.params.Immediate,
|
|
||||||
amqp.Publishing{
|
|
||||||
ContentType: "application/json",
|
|
||||||
DeliveryMode: q.params.DeliveryMode,
|
|
||||||
Body: []byte(body),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Levels is available logging levels.
|
|
||||||
func (q *amqpConn) Levels() []logrus.Level {
|
|
||||||
return []logrus.Level{
|
|
||||||
logrus.InfoLevel,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2017 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 (
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/streadway/amqp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tests for is closed network error.
|
|
||||||
func TestIsClosedNetworkErr(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
err error
|
|
||||||
success bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
err: amqp.ErrClosed,
|
|
||||||
success: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
err: &net.OpError{Err: errors.New("use of closed network connection")},
|
|
||||||
success: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
err: nil,
|
|
||||||
success: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
err: errors.New("testing error"),
|
|
||||||
success: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
ok := isAMQPClosedNetworkErr(testCase.err)
|
|
||||||
if ok != testCase.success {
|
|
||||||
t.Errorf("Test %d: Expected %t, got %t", i+1, testCase.success, ok)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,183 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"gopkg.in/olivere/elastic.v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
esErrFunc = newNotificationErrorFactory("Elasticsearch")
|
|
||||||
|
|
||||||
errESFormat = esErrFunc(`"format" value is invalid - it must be one of "%s" or "%s".`, formatNamespace, formatAccess)
|
|
||||||
errESIndex = esErrFunc("Index name was not specified in the configuration.")
|
|
||||||
)
|
|
||||||
|
|
||||||
// elasticQueue is a elasticsearch event notification queue.
|
|
||||||
type elasticSearchNotify struct {
|
|
||||||
Enable bool `json:"enable"`
|
|
||||||
Format string `json:"format"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
Index string `json:"index"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *elasticSearchNotify) Validate() error {
|
|
||||||
if !e.Enable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if e.Format != formatNamespace && e.Format != formatAccess {
|
|
||||||
return errESFormat
|
|
||||||
}
|
|
||||||
if _, err := checkURL(e.URL); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if e.Index == "" {
|
|
||||||
return errESIndex
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type elasticClient struct {
|
|
||||||
*elastic.Client
|
|
||||||
params elasticSearchNotify
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connects to elastic search instance at URL.
|
|
||||||
func dialElastic(esNotify elasticSearchNotify) (*elastic.Client, error) {
|
|
||||||
if !esNotify.Enable {
|
|
||||||
return nil, errNotifyNotEnabled
|
|
||||||
}
|
|
||||||
return elastic.NewClient(
|
|
||||||
elastic.SetURL(esNotify.URL),
|
|
||||||
elastic.SetSniff(false),
|
|
||||||
elastic.SetMaxRetries(10),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newElasticNotify(accountID string) (*logrus.Logger, error) {
|
|
||||||
esNotify := globalServerConfig.Notify.GetElasticSearchByID(accountID)
|
|
||||||
|
|
||||||
// Dial to elastic search.
|
|
||||||
client, err := dialElastic(esNotify)
|
|
||||||
if err != nil {
|
|
||||||
return nil, esErrFunc("Error dialing the server: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the IndexExists service to check if a specified index exists.
|
|
||||||
exists, err := client.IndexExists(esNotify.Index).
|
|
||||||
Do(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, esErrFunc("Error checking if index exists: %v", err)
|
|
||||||
}
|
|
||||||
// Index does not exist, attempt to create it.
|
|
||||||
if !exists {
|
|
||||||
var createIndex *elastic.IndicesCreateResult
|
|
||||||
createIndex, err = client.CreateIndex(esNotify.Index).
|
|
||||||
Do(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, esErrFunc("Error creating index `%s`: %v",
|
|
||||||
esNotify.Index, err)
|
|
||||||
}
|
|
||||||
if !createIndex.Acknowledged {
|
|
||||||
return nil, esErrFunc("Index not created")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
elasticCl := elasticClient{
|
|
||||||
Client: client,
|
|
||||||
params: esNotify,
|
|
||||||
}
|
|
||||||
|
|
||||||
elasticSearchLog := logrus.New()
|
|
||||||
|
|
||||||
// Disable writing to console.
|
|
||||||
elasticSearchLog.Out = ioutil.Discard
|
|
||||||
|
|
||||||
// Add a elasticSearch hook.
|
|
||||||
elasticSearchLog.Hooks.Add(elasticCl)
|
|
||||||
|
|
||||||
// Set default JSON formatter.
|
|
||||||
elasticSearchLog.Formatter = new(logrus.JSONFormatter)
|
|
||||||
|
|
||||||
// Success, elastic search successfully initialized.
|
|
||||||
return elasticSearchLog, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire is required to implement logrus hook
|
|
||||||
func (q elasticClient) Fire(entry *logrus.Entry) (err error) {
|
|
||||||
// Reflect on eventType and Key on their native type.
|
|
||||||
entryStr, ok := entry.Data["EventType"].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
keyStr, ok := entry.Data["Key"].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch q.params.Format {
|
|
||||||
case formatNamespace:
|
|
||||||
// If event matches as delete, we purge the previous index.
|
|
||||||
if eventMatch(entryStr, []string{"s3:ObjectRemoved:*"}) {
|
|
||||||
_, err = q.Client.Delete().Index(q.params.Index).
|
|
||||||
Type("event").Id(keyStr).Do(context.Background())
|
|
||||||
break
|
|
||||||
} // else we update elastic index or create a new one.
|
|
||||||
_, err = q.Client.Index().Index(q.params.Index).
|
|
||||||
Type("event").
|
|
||||||
BodyJson(map[string]interface{}{
|
|
||||||
"Records": entry.Data["Records"],
|
|
||||||
}).Id(keyStr).Do(context.Background())
|
|
||||||
case formatAccess:
|
|
||||||
// eventTime is taken from the first entry in the
|
|
||||||
// records.
|
|
||||||
events, ok := entry.Data["Records"].([]NotificationEvent)
|
|
||||||
if !ok {
|
|
||||||
return esErrFunc("Unable to extract event time due to conversion error of entry.Data[\"Records\"]=%v", entry.Data["Records"])
|
|
||||||
}
|
|
||||||
var eventTime time.Time
|
|
||||||
eventTime, err = time.Parse(timeFormatAMZ, events[0].EventTime)
|
|
||||||
if err != nil {
|
|
||||||
return esErrFunc("Unable to parse event time \"%s\": %v",
|
|
||||||
events[0].EventTime, err)
|
|
||||||
}
|
|
||||||
// Extract event time in milliseconds for Elasticsearch.
|
|
||||||
eventTimeStr := fmt.Sprintf("%d", eventTime.UnixNano()/1000000)
|
|
||||||
_, err = q.Client.Index().Index(q.params.Index).Type("event").
|
|
||||||
Timestamp(eventTimeStr).
|
|
||||||
BodyJson(map[string]interface{}{
|
|
||||||
"Records": entry.Data["Records"],
|
|
||||||
}).Do(context.Background())
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return esErrFunc("Error inserting/deleting entry: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Required for logrus hook implementation
|
|
||||||
func (q elasticClient) Levels() []logrus.Level {
|
|
||||||
return []logrus.Level{
|
|
||||||
logrus.InfoLevel,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,150 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 (
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
|
|
||||||
sarama "gopkg.in/Shopify/sarama.v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
kkErrFunc = newNotificationErrorFactory("Kafka")
|
|
||||||
)
|
|
||||||
|
|
||||||
// kafkaNotify holds the configuration of the Kafka server/cluster to
|
|
||||||
// send notifications to.
|
|
||||||
type kafkaNotify struct {
|
|
||||||
// Flag to enable/disable this notification from configuration
|
|
||||||
// file.
|
|
||||||
Enable bool `json:"enable"`
|
|
||||||
|
|
||||||
// List of Kafka brokers in `addr:host` format.
|
|
||||||
Brokers []string `json:"brokers"`
|
|
||||||
|
|
||||||
// Topic to which event notifications should be sent.
|
|
||||||
Topic string `json:"topic"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *kafkaNotify) Validate() error {
|
|
||||||
if !k.Enable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(k.Brokers) == 0 {
|
|
||||||
return kkErrFunc("No broker(s) specified.")
|
|
||||||
}
|
|
||||||
// Validate all specified brokers.
|
|
||||||
for _, brokerAddr := range k.Brokers {
|
|
||||||
if _, _, err := net.SplitHostPort(brokerAddr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// kafkaConn contains the active connection to the Kafka cluster and
|
|
||||||
// the topic to send event notifications to.
|
|
||||||
type kafkaConn struct {
|
|
||||||
producer sarama.SyncProducer
|
|
||||||
topic string
|
|
||||||
}
|
|
||||||
|
|
||||||
func dialKafka(kn kafkaNotify) (kc kafkaConn, e error) {
|
|
||||||
if !kn.Enable {
|
|
||||||
return kc, errNotifyNotEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
if kn.Topic == "" {
|
|
||||||
return kc, kkErrFunc(
|
|
||||||
"Topic was not specified in configuration")
|
|
||||||
}
|
|
||||||
|
|
||||||
config := sarama.NewConfig()
|
|
||||||
// Wait for all in-sync replicas to ack the message
|
|
||||||
config.Producer.RequiredAcks = sarama.WaitForAll
|
|
||||||
// Retry up to 10 times to produce the message
|
|
||||||
config.Producer.Retry.Max = 10
|
|
||||||
config.Producer.Return.Successes = true
|
|
||||||
|
|
||||||
p, err := sarama.NewSyncProducer(kn.Brokers, config)
|
|
||||||
if err != nil {
|
|
||||||
return kc, kkErrFunc("Failed to start producer: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return kafkaConn{p, kn.Topic}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newKafkaNotify(accountID string) (*logrus.Logger, error) {
|
|
||||||
kafkaNotifyCfg := globalServerConfig.Notify.GetKafkaByID(accountID)
|
|
||||||
|
|
||||||
// Try connecting to the configured Kafka broker(s).
|
|
||||||
kc, err := dialKafka(kafkaNotifyCfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure kafkaConn object as a Hook in logrus.
|
|
||||||
kafkaLog := logrus.New()
|
|
||||||
kafkaLog.Out = ioutil.Discard
|
|
||||||
kafkaLog.Formatter = new(logrus.JSONFormatter)
|
|
||||||
kafkaLog.Hooks.Add(kc)
|
|
||||||
|
|
||||||
return kafkaLog, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kC kafkaConn) Close() {
|
|
||||||
_ = kC.producer.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire - to implement logrus.Hook interface
|
|
||||||
func (kC kafkaConn) Fire(entry *logrus.Entry) error {
|
|
||||||
body, err := entry.Reader()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the key of the event as a string
|
|
||||||
keyStr, ok := entry.Data["Key"].(string)
|
|
||||||
if !ok {
|
|
||||||
return kkErrFunc("Unable to convert event key %v to string.",
|
|
||||||
entry.Data["Key"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct message to send to Kafka
|
|
||||||
msg := sarama.ProducerMessage{
|
|
||||||
Topic: kC.topic,
|
|
||||||
Key: sarama.StringEncoder(keyStr),
|
|
||||||
Value: sarama.ByteEncoder(body.Bytes()),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt sending the message to Kafka
|
|
||||||
_, _, err = kC.producer.SendMessage(&msg)
|
|
||||||
if err != nil {
|
|
||||||
return kkErrFunc("Error sending event to Kafka - %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Levels - to implement logrus.Hook interface
|
|
||||||
func (kC kafkaConn) Levels() []logrus.Level {
|
|
||||||
return []logrus.Level{
|
|
||||||
logrus.InfoLevel,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type listenerConn struct {
|
|
||||||
TargetAddr string
|
|
||||||
ListenerARN string
|
|
||||||
BMSClient BucketMetaState
|
|
||||||
}
|
|
||||||
|
|
||||||
type listenerLogger struct {
|
|
||||||
log *logrus.Logger
|
|
||||||
lconn listenerConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func newListenerLogger(listenerArn, targetAddr string) (*listenerLogger, error) {
|
|
||||||
bmsClient := globalS3Peers.GetPeerClient(targetAddr)
|
|
||||||
if bmsClient == nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"Peer %s was not initialized, unexpected error",
|
|
||||||
targetAddr,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
lc := listenerConn{
|
|
||||||
TargetAddr: targetAddr,
|
|
||||||
ListenerARN: listenerArn,
|
|
||||||
BMSClient: bmsClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
lcLog := logrus.New()
|
|
||||||
|
|
||||||
lcLog.Out = ioutil.Discard
|
|
||||||
|
|
||||||
lcLog.Formatter = new(logrus.JSONFormatter)
|
|
||||||
|
|
||||||
lcLog.Hooks.Add(lc)
|
|
||||||
|
|
||||||
return &listenerLogger{lcLog, lc}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send Event RPC call and return error
|
|
||||||
arg := EventArgs{Event: notificationEvent, Arn: lc.ListenerARN}
|
|
||||||
return lc.BMSClient.SendEvent(&arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lc listenerConn) Levels() []logrus.Level {
|
|
||||||
return []logrus.Level{
|
|
||||||
logrus.InfoLevel,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"io/ioutil"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
MQTT "github.com/eclipse/paho.mqtt.golang"
|
|
||||||
)
|
|
||||||
|
|
||||||
type mqttNotify struct {
|
|
||||||
Enable bool `json:"enable"`
|
|
||||||
Broker string `json:"broker"`
|
|
||||||
Topic string `json:"topic"`
|
|
||||||
QoS int `json:"qos"`
|
|
||||||
ClientID string `json:"clientId"`
|
|
||||||
User string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mqttNotify) Validate() error {
|
|
||||||
if !m.Enable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, err := checkURL(m.Broker); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mqttConn struct {
|
|
||||||
params mqttNotify
|
|
||||||
Client MQTT.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func dialMQTT(mqttL mqttNotify) (mc mqttConn, e error) {
|
|
||||||
if !mqttL.Enable {
|
|
||||||
return mc, errNotifyNotEnabled
|
|
||||||
}
|
|
||||||
connOpts := &MQTT.ClientOptions{
|
|
||||||
ClientID: mqttL.ClientID,
|
|
||||||
CleanSession: true,
|
|
||||||
Username: mqttL.User,
|
|
||||||
Password: mqttL.Password,
|
|
||||||
MaxReconnectInterval: 1 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
TLSConfig: tls.Config{RootCAs: globalRootCAs},
|
|
||||||
}
|
|
||||||
connOpts.AddBroker(mqttL.Broker)
|
|
||||||
client := MQTT.NewClient(connOpts)
|
|
||||||
if token := client.Connect(); token.Wait() && token.Error() != nil {
|
|
||||||
return mc, token.Error()
|
|
||||||
}
|
|
||||||
return mqttConn{Client: client, params: mqttL}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMQTTNotify(accountID string) (*logrus.Logger, error) {
|
|
||||||
mqttL := globalServerConfig.Notify.GetMQTTByID(accountID)
|
|
||||||
|
|
||||||
//connect to MQTT Server
|
|
||||||
mqttC, err := dialMQTT(mqttL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mqttLog := logrus.New()
|
|
||||||
|
|
||||||
// Disable writing to console.
|
|
||||||
mqttLog.Out = ioutil.Discard
|
|
||||||
|
|
||||||
// Add a mqtt hook.
|
|
||||||
mqttLog.Hooks.Add(mqttC)
|
|
||||||
|
|
||||||
// Set default JSON formatter
|
|
||||||
mqttLog.Formatter = new(logrus.JSONFormatter)
|
|
||||||
|
|
||||||
// successfully enabled all MQTTs
|
|
||||||
return mqttLog, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire if called when an event should be sent to the message broker.
|
|
||||||
func (q mqttConn) Fire(entry *logrus.Entry) error {
|
|
||||||
body, err := entry.String()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !q.Client.IsConnected() {
|
|
||||||
if token := q.Client.Connect(); token.Wait() && token.Error() != nil {
|
|
||||||
return token.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
token := q.Client.Publish(q.params.Topic, byte(q.params.QoS), false, body)
|
|
||||||
if token.Wait() && token.Error() != nil {
|
|
||||||
return token.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Levels is available logging levels.
|
|
||||||
func (q mqttConn) Levels() []logrus.Level {
|
|
||||||
return []logrus.Level{
|
|
||||||
logrus.InfoLevel,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,337 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2017 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// MySQL Notifier implementation. Two formats, "namespace" and
|
|
||||||
// "access" are supported.
|
|
||||||
//
|
|
||||||
// * Namespace format
|
|
||||||
//
|
|
||||||
// On each create or update object event in Minio Object storage
|
|
||||||
// server, a row is created or updated in the table in MySQL. On each
|
|
||||||
// object removal, the corresponding row is deleted from the table.
|
|
||||||
//
|
|
||||||
// A table with a specific structure (column names, column types, and
|
|
||||||
// primary key/uniqueness constraint) is used. The user may set the
|
|
||||||
// table name in the configuration. A sample SQL command that creates
|
|
||||||
// a command with the required structure is:
|
|
||||||
//
|
|
||||||
// CREATE TABLE myminio (
|
|
||||||
// key_name VARCHAR(2048),
|
|
||||||
// value JSONB,
|
|
||||||
// PRIMARY KEY (key_name),
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// MySQL's "INSERT ... ON DUPLICATE ..." feature (UPSERT) is used
|
|
||||||
// here. The implementation has been tested with MySQL Ver 14.14
|
|
||||||
// Distrib 5.7.17.
|
|
||||||
//
|
|
||||||
// * Access format
|
|
||||||
//
|
|
||||||
// On each event, a row is appended to the configured table. There is
|
|
||||||
// no deletion or modification of existing rows.
|
|
||||||
//
|
|
||||||
// A different table schema is used for this format. A sample SQL
|
|
||||||
// commant that creates a table with the required structure is:
|
|
||||||
//
|
|
||||||
// CREATE TABLE myminio (
|
|
||||||
// event_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
||||||
// event_data JSONB
|
|
||||||
// );
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/go-sql-driver/mysql"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Queries for format=namespace mode.
|
|
||||||
upsertRowForNSMySQL = `INSERT INTO %s (key_name, value)
|
|
||||||
VALUES (?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE value=VALUES(value);
|
|
||||||
`
|
|
||||||
deleteRowForNSMySQL = ` DELETE FROM %s
|
|
||||||
WHERE key_name = ?;`
|
|
||||||
createTableForNSMySQL = `CREATE TABLE %s (
|
|
||||||
key_name VARCHAR(2048),
|
|
||||||
value JSON,
|
|
||||||
PRIMARY KEY (key_name)
|
|
||||||
);`
|
|
||||||
|
|
||||||
// Queries for format=access mode.
|
|
||||||
insertRowForAccessMySQL = `INSERT INTO %s (event_time, event_data)
|
|
||||||
VALUES (?, ?);`
|
|
||||||
createTableForAccessMySQL = `CREATE TABLE %s (
|
|
||||||
event_time DATETIME NOT NULL,
|
|
||||||
event_data JSON
|
|
||||||
);`
|
|
||||||
|
|
||||||
// Query to check if a table already exists.
|
|
||||||
tableExistsMySQL = `SELECT 1 FROM %s;`
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
mysqlErrFunc = newNotificationErrorFactory("MySQL")
|
|
||||||
|
|
||||||
errMysqlFormat = mysqlErrFunc(`"format" value is invalid - it must be one of "%s" or "%s".`, formatNamespace, formatAccess)
|
|
||||||
errMysqlTable = mysqlErrFunc("Table was not specified in the configuration.")
|
|
||||||
)
|
|
||||||
|
|
||||||
type mySQLNotify struct {
|
|
||||||
Enable bool `json:"enable"`
|
|
||||||
|
|
||||||
Format string `json:"format"`
|
|
||||||
|
|
||||||
// pass data-source-name connection string in config
|
|
||||||
// directly. This string is formatted according to
|
|
||||||
// https://github.com/go-sql-driver/mysql#dsn-data-source-name
|
|
||||||
DsnString string `json:"dsnString"`
|
|
||||||
// specifying a table name is required.
|
|
||||||
Table string `json:"table"`
|
|
||||||
|
|
||||||
// uses the values below if no connection string is specified
|
|
||||||
// - however the connection string method offers more
|
|
||||||
// flexibility.
|
|
||||||
Host string `json:"host"`
|
|
||||||
Port string `json:"port"`
|
|
||||||
User string `json:"user"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Database string `json:"database"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mySQLNotify) Validate() error {
|
|
||||||
if !m.Enable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if m.Format != formatNamespace && m.Format != formatAccess {
|
|
||||||
return errMysqlFormat
|
|
||||||
}
|
|
||||||
if m.DsnString == "" {
|
|
||||||
if _, err := checkURL(m.Host); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if m.Table == "" {
|
|
||||||
return errMysqlTable
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mySQLConn struct {
|
|
||||||
dsnStr string
|
|
||||||
table string
|
|
||||||
format string
|
|
||||||
preparedStmts map[string]*sql.Stmt
|
|
||||||
*sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func dialMySQL(msql mySQLNotify) (mc mySQLConn, e error) {
|
|
||||||
if !msql.Enable {
|
|
||||||
return mc, errNotifyNotEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
dsnStr := msql.DsnString
|
|
||||||
// check if connection string is specified
|
|
||||||
if dsnStr == "" {
|
|
||||||
// build from other parameters
|
|
||||||
config := mysql.Config{
|
|
||||||
User: msql.User,
|
|
||||||
Passwd: msql.Password,
|
|
||||||
Net: "tcp",
|
|
||||||
Addr: msql.Host + ":" + msql.Port,
|
|
||||||
DBName: msql.Database,
|
|
||||||
}
|
|
||||||
dsnStr = config.FormatDSN()
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := sql.Open("mysql", dsnStr)
|
|
||||||
if err != nil {
|
|
||||||
return mc, mysqlErrFunc(
|
|
||||||
"Connection opening failure (dsnStr=%s): %v",
|
|
||||||
dsnStr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ping to check that server is actually reachable.
|
|
||||||
err = db.Ping()
|
|
||||||
if err != nil {
|
|
||||||
return mc, mysqlErrFunc(
|
|
||||||
"Ping to server failed with: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that table exists - if not, create it.
|
|
||||||
_, err = db.Exec(fmt.Sprintf(tableExistsMySQL, msql.Table))
|
|
||||||
if err != nil {
|
|
||||||
createStmt := createTableForNSMySQL
|
|
||||||
if msql.Format == formatAccess {
|
|
||||||
createStmt = createTableForAccessMySQL
|
|
||||||
}
|
|
||||||
|
|
||||||
// most likely, table does not exist. try to create it:
|
|
||||||
_, errCreate := db.Exec(fmt.Sprintf(createStmt, msql.Table))
|
|
||||||
if errCreate != nil {
|
|
||||||
// failed to create the table. error out.
|
|
||||||
return mc, mysqlErrFunc(
|
|
||||||
"'Select' failed with %v, then 'Create Table' failed with %v",
|
|
||||||
err, errCreate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create prepared statements
|
|
||||||
stmts := make(map[string]*sql.Stmt)
|
|
||||||
switch msql.Format {
|
|
||||||
case formatNamespace:
|
|
||||||
// insert or update statement
|
|
||||||
stmts["upsertRow"], err = db.Prepare(fmt.Sprintf(upsertRowForNSMySQL,
|
|
||||||
msql.Table))
|
|
||||||
if err != nil {
|
|
||||||
return mc, mysqlErrFunc("create UPSERT prepared statement failed with: %v", err)
|
|
||||||
}
|
|
||||||
// delete statement
|
|
||||||
stmts["deleteRow"], err = db.Prepare(fmt.Sprintf(deleteRowForNSMySQL,
|
|
||||||
msql.Table))
|
|
||||||
if err != nil {
|
|
||||||
return mc, mysqlErrFunc("create DELETE prepared statement failed with: %v", err)
|
|
||||||
}
|
|
||||||
case formatAccess:
|
|
||||||
// insert statement
|
|
||||||
stmts["insertRow"], err = db.Prepare(fmt.Sprintf(insertRowForAccessMySQL,
|
|
||||||
msql.Table))
|
|
||||||
if err != nil {
|
|
||||||
return mc, mysqlErrFunc(
|
|
||||||
"create INSERT prepared statement failed with: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return mySQLConn{dsnStr, msql.Table, msql.Format, stmts, db}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMySQLNotify(accountID string) (*logrus.Logger, error) {
|
|
||||||
mysqlNotify := globalServerConfig.Notify.GetMySQLByID(accountID)
|
|
||||||
|
|
||||||
// Dial mysql
|
|
||||||
myC, err := dialMySQL(mysqlNotify)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mySQLLog := logrus.New()
|
|
||||||
|
|
||||||
mySQLLog.Out = ioutil.Discard
|
|
||||||
|
|
||||||
mySQLLog.Formatter = new(logrus.JSONFormatter)
|
|
||||||
|
|
||||||
mySQLLog.Hooks.Add(myC)
|
|
||||||
|
|
||||||
return mySQLLog, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (myC mySQLConn) Close() {
|
|
||||||
// first close all prepared statements
|
|
||||||
for _, v := range myC.preparedStmts {
|
|
||||||
_ = v.Close()
|
|
||||||
}
|
|
||||||
// close db connection
|
|
||||||
_ = myC.DB.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (myC mySQLConn) Fire(entry *logrus.Entry) error {
|
|
||||||
// get event type by trying to convert to string
|
|
||||||
entryEventType, ok := entry.Data["EventType"].(string)
|
|
||||||
if !ok {
|
|
||||||
// ignore event if converting EventType to string
|
|
||||||
// fails.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonEncoder := func(d interface{}) ([]byte, error) {
|
|
||||||
value, err := json.Marshal(map[string]interface{}{
|
|
||||||
"Records": d,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, mysqlErrFunc(
|
|
||||||
"Unable to encode event %v to JSON: %v", d, err)
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch myC.format {
|
|
||||||
case formatNamespace:
|
|
||||||
// Check for event delete
|
|
||||||
if eventMatch(entryEventType, []string{"s3:ObjectRemoved:*"}) {
|
|
||||||
// delete row from the table
|
|
||||||
_, err := myC.preparedStmts["deleteRow"].Exec(entry.Data["Key"])
|
|
||||||
if err != nil {
|
|
||||||
return mysqlErrFunc(
|
|
||||||
"Error deleting event with key = %v - got mysql error - %v",
|
|
||||||
entry.Data["Key"], err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value, err := jsonEncoder(entry.Data["Records"])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// upsert row into the table
|
|
||||||
_, err = myC.preparedStmts["upsertRow"].Exec(entry.Data["Key"], value)
|
|
||||||
if err != nil {
|
|
||||||
return mysqlErrFunc(
|
|
||||||
"Unable to upsert event with Key=%v and Value=%v - got mysql error - %v",
|
|
||||||
entry.Data["Key"], entry.Data["Records"], err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case formatAccess:
|
|
||||||
// eventTime is taken from the first entry in the
|
|
||||||
// records.
|
|
||||||
events, ok := entry.Data["Records"].([]NotificationEvent)
|
|
||||||
if !ok {
|
|
||||||
return mysqlErrFunc("unable to extract event time due to conversion error of entry.Data[\"Records\"]=%v", entry.Data["Records"])
|
|
||||||
}
|
|
||||||
eventTime, err := time.Parse(timeFormatAMZ, events[0].EventTime)
|
|
||||||
if err != nil {
|
|
||||||
return mysqlErrFunc("unable to parse event time \"%s\": %v",
|
|
||||||
events[0].EventTime, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
value, err := jsonEncodeEventData(entry.Data["Records"])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = myC.preparedStmts["insertRow"].Exec(eventTime, value)
|
|
||||||
if err != nil {
|
|
||||||
return mysqlErrFunc("Unable to insert event with value=%v: %v",
|
|
||||||
value, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (myC mySQLConn) Levels() []logrus.Level {
|
|
||||||
return []logrus.Level{
|
|
||||||
logrus.InfoLevel,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,196 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/nats-io/go-nats-streaming"
|
|
||||||
"github.com/nats-io/nats"
|
|
||||||
)
|
|
||||||
|
|
||||||
// natsNotifyStreaming contains specific options related to connection
|
|
||||||
// to a NATS streaming server
|
|
||||||
type natsNotifyStreaming struct {
|
|
||||||
Enable bool `json:"enable"`
|
|
||||||
ClusterID string `json:"clusterID"`
|
|
||||||
ClientID string `json:"clientID"`
|
|
||||||
Async bool `json:"async"`
|
|
||||||
MaxPubAcksInflight int `json:"maxPubAcksInflight"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// natsNotify - represents logrus compatible NATS hook.
|
|
||||||
// All fields represent NATS configuration details.
|
|
||||||
type natsNotify struct {
|
|
||||||
Enable bool `json:"enable"`
|
|
||||||
Address string `json:"address"`
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Secure bool `json:"secure"`
|
|
||||||
PingInterval int64 `json:"pingInterval"`
|
|
||||||
Streaming natsNotifyStreaming `json:"streaming"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *natsNotify) Validate() error {
|
|
||||||
if !n.Enable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, _, err := net.SplitHostPort(n.Address); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// natsIOConn abstracts connection to any type of NATS server
|
|
||||||
type natsIOConn struct {
|
|
||||||
params natsNotify
|
|
||||||
natsConn *nats.Conn
|
|
||||||
stanConn stan.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// dialNATS - dials and returns an natsIOConn instance,
|
|
||||||
// for sending notifications. Returns error if nats logger
|
|
||||||
// is not enabled.
|
|
||||||
func dialNATS(natsL natsNotify, testDial bool) (nioc natsIOConn, e error) {
|
|
||||||
if !natsL.Enable {
|
|
||||||
return nioc, errNotifyNotEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct natsIOConn which holds all NATS connection information
|
|
||||||
conn := natsIOConn{params: natsL}
|
|
||||||
|
|
||||||
if natsL.Streaming.Enable {
|
|
||||||
// Construct scheme to differentiate between clear and TLS connections
|
|
||||||
scheme := "nats"
|
|
||||||
if natsL.Secure {
|
|
||||||
scheme = "tls"
|
|
||||||
}
|
|
||||||
// Construct address URL
|
|
||||||
addressURL := scheme + "://" + natsL.Username + ":" + natsL.Password + "@" + natsL.Address
|
|
||||||
// Fetch the user-supplied client ID and provide a random one if not provided
|
|
||||||
clientID := natsL.Streaming.ClientID
|
|
||||||
if clientID == "" {
|
|
||||||
clientID = mustGetUUID()
|
|
||||||
}
|
|
||||||
// Add test suffix to clientID to avoid clientID already registered error
|
|
||||||
if testDial {
|
|
||||||
clientID += "-test"
|
|
||||||
}
|
|
||||||
connOpts := []stan.Option{
|
|
||||||
stan.NatsURL(addressURL),
|
|
||||||
}
|
|
||||||
// Setup MaxPubAcksInflight parameter
|
|
||||||
if natsL.Streaming.MaxPubAcksInflight > 0 {
|
|
||||||
connOpts = append(connOpts,
|
|
||||||
stan.MaxPubAcksInflight(natsL.Streaming.MaxPubAcksInflight))
|
|
||||||
}
|
|
||||||
// Do the real connection to the NATS server
|
|
||||||
sc, err := stan.Connect(natsL.Streaming.ClusterID, clientID, connOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nioc, err
|
|
||||||
}
|
|
||||||
// Save the created connection
|
|
||||||
conn.stanConn = sc
|
|
||||||
} else {
|
|
||||||
// Configure and connect to NATS server
|
|
||||||
natsC := nats.DefaultOptions
|
|
||||||
natsC.Url = "nats://" + natsL.Address
|
|
||||||
natsC.User = natsL.Username
|
|
||||||
natsC.Password = natsL.Password
|
|
||||||
natsC.Token = natsL.Token
|
|
||||||
natsC.Secure = natsL.Secure
|
|
||||||
// Do the real connection
|
|
||||||
nc, err := natsC.Connect()
|
|
||||||
if err != nil {
|
|
||||||
return nioc, err
|
|
||||||
}
|
|
||||||
// Save the created connection
|
|
||||||
conn.natsConn = nc
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// closeNATS - close the underlying NATS connection
|
|
||||||
func closeNATS(conn natsIOConn) {
|
|
||||||
if conn.params.Streaming.Enable {
|
|
||||||
conn.stanConn.Close()
|
|
||||||
} else {
|
|
||||||
conn.natsConn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newNATSNotify(accountID string) (*logrus.Logger, error) {
|
|
||||||
natsL := globalServerConfig.Notify.GetNATSByID(accountID)
|
|
||||||
|
|
||||||
// Connect to nats server.
|
|
||||||
natsC, err := dialNATS(natsL, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
natsLog := logrus.New()
|
|
||||||
|
|
||||||
// Disable writing to console.
|
|
||||||
natsLog.Out = ioutil.Discard
|
|
||||||
|
|
||||||
// Add a nats hook.
|
|
||||||
natsLog.Hooks.Add(natsC)
|
|
||||||
|
|
||||||
// Set default JSON formatter.
|
|
||||||
natsLog.Formatter = new(logrus.JSONFormatter)
|
|
||||||
|
|
||||||
// Successfully enabled all NATSs.
|
|
||||||
return natsLog, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire is called when an event should be sent to the message broker
|
|
||||||
func (n natsIOConn) Fire(entry *logrus.Entry) error {
|
|
||||||
body, err := entry.Reader()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if n.params.Streaming.Enable {
|
|
||||||
// Streaming flag is enabled, publish the log synchronously or asynchronously
|
|
||||||
// depending on the user supplied parameter
|
|
||||||
if n.params.Streaming.Async {
|
|
||||||
_, err = n.stanConn.PublishAsync(n.params.Subject, body.Bytes(), nil)
|
|
||||||
} else {
|
|
||||||
err = n.stanConn.Publish(n.params.Subject, body.Bytes())
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Publish the log
|
|
||||||
err = n.natsConn.Publish(n.params.Subject, body.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Levels is available logging levels.
|
|
||||||
func (n natsIOConn) Levels() []logrus.Level {
|
|
||||||
return []logrus.Level{
|
|
||||||
logrus.InfoLevel,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,353 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// PostgreSQL Notifier implementation. Two formats, "namespace" and
|
|
||||||
// "access" are supported.
|
|
||||||
//
|
|
||||||
// * Namespace format
|
|
||||||
//
|
|
||||||
// On each create or update object event in Minio Object storage
|
|
||||||
// server, a row is created or updated in the table in Postgres. On
|
|
||||||
// each object removal, the corresponding row is deleted from the
|
|
||||||
// table.
|
|
||||||
//
|
|
||||||
// A table with a specific structure (column names, column types, and
|
|
||||||
// primary key/uniqueness constraint) is used. The user may set the
|
|
||||||
// table name in the configuration. A sample SQL command that creates
|
|
||||||
// a table with the required structure is:
|
|
||||||
//
|
|
||||||
// CREATE TABLE myminio (
|
|
||||||
// key VARCHAR PRIMARY KEY,
|
|
||||||
// value JSONB
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// PostgreSQL's "INSERT ... ON CONFLICT ... DO UPDATE ..." feature
|
|
||||||
// (UPSERT) is used here, so the minimum version of PostgreSQL
|
|
||||||
// required is 9.5.
|
|
||||||
//
|
|
||||||
// * Access format
|
|
||||||
//
|
|
||||||
// On each event, a row is appended to the configured table. There is
|
|
||||||
// no deletion or modification of existing rows.
|
|
||||||
//
|
|
||||||
// A different table schema is used for this format. A sample SQL
|
|
||||||
// commant that creates a table with the required structure is:
|
|
||||||
//
|
|
||||||
// CREATE TABLE myminio (
|
|
||||||
// event_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
||||||
// event_data JSONB
|
|
||||||
// );
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
|
|
||||||
// Register postgres driver
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Queries for format=namespace mode. Here the `key` column is
|
|
||||||
// the bucket and object of the event. When objects are
|
|
||||||
// deleted, the corresponding row is deleted in the
|
|
||||||
// table. When objects are created or over-written, rows are
|
|
||||||
// inserted or updated respectively in the table.
|
|
||||||
upsertRowForNS = `INSERT INTO %s (key, value)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
ON CONFLICT (key)
|
|
||||||
DO UPDATE SET value = EXCLUDED.value;`
|
|
||||||
deleteRowForNS = ` DELETE FROM %s
|
|
||||||
WHERE key = $1;`
|
|
||||||
createTableForNS = `CREATE TABLE %s (
|
|
||||||
key VARCHAR PRIMARY KEY,
|
|
||||||
value JSONB
|
|
||||||
);`
|
|
||||||
|
|
||||||
// Queries for format=access mode. Here the `event_time`
|
|
||||||
// column of the table, stores the time at which the event
|
|
||||||
// occurred in the Minio server.
|
|
||||||
insertRowForAccess = `INSERT INTO %s (event_time, event_data)
|
|
||||||
VALUES ($1, $2);`
|
|
||||||
createTableForAccess = `CREATE TABLE %s (
|
|
||||||
event_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
||||||
event_data JSONB
|
|
||||||
);`
|
|
||||||
|
|
||||||
// Query to check if a table already exists.
|
|
||||||
tableExists = `SELECT 1 FROM %s;`
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
pgErrFunc = newNotificationErrorFactory("PostgreSQL")
|
|
||||||
|
|
||||||
errPGFormatError = pgErrFunc(`"format" value is invalid - it must be one of "%s" or "%s".`, formatNamespace, formatAccess)
|
|
||||||
errPGTableError = pgErrFunc("Table was not specified in the configuration.")
|
|
||||||
)
|
|
||||||
|
|
||||||
type postgreSQLNotify struct {
|
|
||||||
Enable bool `json:"enable"`
|
|
||||||
|
|
||||||
Format string `json:"format"`
|
|
||||||
|
|
||||||
// Pass connection string in config directly. This string is
|
|
||||||
// formatted according to
|
|
||||||
// https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters
|
|
||||||
ConnectionString string `json:"connectionString"`
|
|
||||||
// specifying a table name is required.
|
|
||||||
Table string `json:"table"`
|
|
||||||
|
|
||||||
// The values below, if non-empty are appended to
|
|
||||||
// ConnectionString above. Default values are shown in
|
|
||||||
// comments below (implicitly used by the library).
|
|
||||||
Host string `json:"host"` // default: localhost
|
|
||||||
Port string `json:"port"` // default: 5432
|
|
||||||
User string `json:"user"` // default: user running minio
|
|
||||||
Password string `json:"password"` // default: no password
|
|
||||||
Database string `json:"database"` // default: same as user
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *postgreSQLNotify) Validate() error {
|
|
||||||
if !p.Enable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if p.Format != formatNamespace && p.Format != formatAccess {
|
|
||||||
return errPGFormatError
|
|
||||||
}
|
|
||||||
if p.ConnectionString == "" {
|
|
||||||
if _, err := checkURL(p.Host); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if p.Table == "" {
|
|
||||||
return errPGTableError
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type pgConn struct {
|
|
||||||
connStr string
|
|
||||||
table string
|
|
||||||
format string
|
|
||||||
preparedStmts map[string]*sql.Stmt
|
|
||||||
*sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func dialPostgreSQL(pgN postgreSQLNotify) (pc pgConn, e error) {
|
|
||||||
if !pgN.Enable {
|
|
||||||
return pc, errNotifyNotEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// collect connection params
|
|
||||||
params := []string{pgN.ConnectionString}
|
|
||||||
if pgN.Host != "" {
|
|
||||||
params = append(params, "host="+pgN.Host)
|
|
||||||
}
|
|
||||||
if pgN.Port != "" {
|
|
||||||
params = append(params, "port="+pgN.Port)
|
|
||||||
}
|
|
||||||
if pgN.User != "" {
|
|
||||||
params = append(params, "user="+pgN.User)
|
|
||||||
}
|
|
||||||
if pgN.Password != "" {
|
|
||||||
params = append(params, "password="+pgN.Password)
|
|
||||||
}
|
|
||||||
if pgN.Database != "" {
|
|
||||||
params = append(params, "dbname="+pgN.Database)
|
|
||||||
}
|
|
||||||
connStr := strings.Join(params, " ")
|
|
||||||
|
|
||||||
db, err := sql.Open("postgres", connStr)
|
|
||||||
if err != nil {
|
|
||||||
return pc, pgErrFunc(
|
|
||||||
"Connection opening failure (connectionString=%s): %v",
|
|
||||||
connStr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ping to check that server is actually reachable.
|
|
||||||
err = db.Ping()
|
|
||||||
if err != nil {
|
|
||||||
return pc, pgErrFunc("Ping to server failed with: %v",
|
|
||||||
err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that table exists - if not, create it.
|
|
||||||
_, err = db.Exec(fmt.Sprintf(tableExists, pgN.Table))
|
|
||||||
if err != nil {
|
|
||||||
createStmt := createTableForNS
|
|
||||||
if pgN.Format == formatAccess {
|
|
||||||
createStmt = createTableForAccess
|
|
||||||
}
|
|
||||||
|
|
||||||
// most likely, table does not exist. try to create it:
|
|
||||||
_, errCreate := db.Exec(fmt.Sprintf(createStmt, pgN.Table))
|
|
||||||
if errCreate != nil {
|
|
||||||
// failed to create the table. error out.
|
|
||||||
return pc, pgErrFunc(
|
|
||||||
"'Select' failed with %v, then 'Create Table' failed with %v",
|
|
||||||
err, errCreate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create prepared statements
|
|
||||||
stmts := make(map[string]*sql.Stmt)
|
|
||||||
switch pgN.Format {
|
|
||||||
case formatNamespace:
|
|
||||||
// insert or update statement
|
|
||||||
stmts["upsertRow"], err = db.Prepare(fmt.Sprintf(upsertRowForNS,
|
|
||||||
pgN.Table))
|
|
||||||
if err != nil {
|
|
||||||
return pc, pgErrFunc(
|
|
||||||
"create UPSERT prepared statement failed with: %v", err)
|
|
||||||
}
|
|
||||||
// delete statement
|
|
||||||
stmts["deleteRow"], err = db.Prepare(fmt.Sprintf(deleteRowForNS,
|
|
||||||
pgN.Table))
|
|
||||||
if err != nil {
|
|
||||||
return pc, pgErrFunc(
|
|
||||||
"create DELETE prepared statement failed with: %v", err)
|
|
||||||
}
|
|
||||||
case formatAccess:
|
|
||||||
// insert statement
|
|
||||||
stmts["insertRow"], err = db.Prepare(fmt.Sprintf(insertRowForAccess,
|
|
||||||
pgN.Table))
|
|
||||||
if err != nil {
|
|
||||||
return pc, pgErrFunc(
|
|
||||||
"create INSERT prepared statement failed with: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pgConn{connStr, pgN.Table, pgN.Format, stmts, db}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPostgreSQLNotify(accountID string) (*logrus.Logger, error) {
|
|
||||||
pgNotify := globalServerConfig.Notify.GetPostgreSQLByID(accountID)
|
|
||||||
|
|
||||||
// Dial postgres
|
|
||||||
pgC, err := dialPostgreSQL(pgNotify)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pgLog := logrus.New()
|
|
||||||
|
|
||||||
pgLog.Out = ioutil.Discard
|
|
||||||
|
|
||||||
pgLog.Formatter = new(logrus.JSONFormatter)
|
|
||||||
|
|
||||||
pgLog.Hooks.Add(pgC)
|
|
||||||
|
|
||||||
return pgLog, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pgC pgConn) Close() {
|
|
||||||
// first close all prepared statements
|
|
||||||
for _, v := range pgC.preparedStmts {
|
|
||||||
_ = v.Close()
|
|
||||||
}
|
|
||||||
// close db connection
|
|
||||||
_ = pgC.DB.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonEncodeEventData(d interface{}) ([]byte, error) {
|
|
||||||
// json encode the value for the row
|
|
||||||
value, err := json.Marshal(map[string]interface{}{
|
|
||||||
"Records": d,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, pgErrFunc(
|
|
||||||
"Unable to encode event %v to JSON: %v", d, err)
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pgC pgConn) Fire(entry *logrus.Entry) error {
|
|
||||||
// get event type by trying to convert to string
|
|
||||||
entryEventType, ok := entry.Data["EventType"].(string)
|
|
||||||
if !ok {
|
|
||||||
// ignore event if converting EventType to string
|
|
||||||
// fails.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch pgC.format {
|
|
||||||
case formatNamespace:
|
|
||||||
// Check for event delete
|
|
||||||
if eventMatch(entryEventType, []string{"s3:ObjectRemoved:*"}) {
|
|
||||||
// delete row from the table
|
|
||||||
_, err := pgC.preparedStmts["deleteRow"].Exec(entry.Data["Key"])
|
|
||||||
if err != nil {
|
|
||||||
return pgErrFunc(
|
|
||||||
"Error deleting event with key=%v: %v",
|
|
||||||
entry.Data["Key"], err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value, err := jsonEncodeEventData(entry.Data["Records"])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// upsert row into the table
|
|
||||||
_, err = pgC.preparedStmts["upsertRow"].Exec(entry.Data["Key"], value)
|
|
||||||
if err != nil {
|
|
||||||
return pgErrFunc(
|
|
||||||
"Unable to upsert event with key=%v and value=%v: %v",
|
|
||||||
entry.Data["Key"], entry.Data["Records"], err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case formatAccess:
|
|
||||||
// eventTime is taken from the first entry in the
|
|
||||||
// records.
|
|
||||||
events, ok := entry.Data["Records"].([]NotificationEvent)
|
|
||||||
if !ok {
|
|
||||||
return pgErrFunc("unable to extract event time due to conversion error of entry.Data[\"Records\"]=%v", entry.Data["Records"])
|
|
||||||
}
|
|
||||||
eventTime, err := time.Parse(timeFormatAMZ, events[0].EventTime)
|
|
||||||
if err != nil {
|
|
||||||
return pgErrFunc("unable to parse event time \"%s\": %v",
|
|
||||||
events[0].EventTime, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
value, err := jsonEncodeEventData(entry.Data["Records"])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = pgC.preparedStmts["insertRow"].Exec(eventTime, value)
|
|
||||||
if err != nil {
|
|
||||||
return pgErrFunc("Unable to insert event with value=%v: %v",
|
|
||||||
value, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pgC pgConn) Levels() []logrus.Level {
|
|
||||||
return []logrus.Level{
|
|
||||||
logrus.InfoLevel,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,222 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/garyburd/redigo/redis"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
redisErrFunc = newNotificationErrorFactory("Redis")
|
|
||||||
|
|
||||||
errRedisFormat = redisErrFunc(`"format" value is invalid - it must be one of "access" or "namespace".`)
|
|
||||||
errRedisKeyError = redisErrFunc("Key was not specified in the configuration.")
|
|
||||||
)
|
|
||||||
|
|
||||||
// redisNotify to send logs to Redis server
|
|
||||||
type redisNotify struct {
|
|
||||||
Enable bool `json:"enable"`
|
|
||||||
Format string `json:"format"`
|
|
||||||
Addr string `json:"address"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *redisNotify) Validate() error {
|
|
||||||
if !r.Enable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if r.Format != formatNamespace && r.Format != formatAccess {
|
|
||||||
return errRedisFormat
|
|
||||||
}
|
|
||||||
if _, _, err := net.SplitHostPort(r.Addr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if r.Key == "" {
|
|
||||||
return errRedisKeyError
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type redisConn struct {
|
|
||||||
*redis.Pool
|
|
||||||
params redisNotify
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial a new connection to redis instance at addr, optionally with a
|
|
||||||
// password if any.
|
|
||||||
func dialRedis(rNotify redisNotify) (*redis.Pool, error) {
|
|
||||||
// Return error if redis not enabled.
|
|
||||||
if !rNotify.Enable {
|
|
||||||
return nil, errNotifyNotEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
addr := rNotify.Addr
|
|
||||||
password := rNotify.Password
|
|
||||||
rPool := &redis.Pool{
|
|
||||||
MaxIdle: 3,
|
|
||||||
IdleTimeout: 240 * time.Second, // Time 2minutes.
|
|
||||||
Dial: func() (redis.Conn, error) {
|
|
||||||
c, err := redis.Dial("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if password != "" {
|
|
||||||
if _, derr := c.Do("AUTH", password); derr != nil {
|
|
||||||
c.Close()
|
|
||||||
return nil, derr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c, err
|
|
||||||
},
|
|
||||||
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
|
||||||
_, err := c.Do("PING")
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test if connection with REDIS can be established.
|
|
||||||
rConn := rPool.Get()
|
|
||||||
defer rConn.Close()
|
|
||||||
|
|
||||||
// Check connection.
|
|
||||||
_, err := rConn.Do("PING")
|
|
||||||
if err != nil {
|
|
||||||
return nil, redisErrFunc("Error connecting to server: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that Key is of desired type
|
|
||||||
reply, err := redis.String(rConn.Do("TYPE", rNotify.Key))
|
|
||||||
if err != nil {
|
|
||||||
return nil, redisErrFunc("Error getting type of Key=%s: %v",
|
|
||||||
rNotify.Key, err)
|
|
||||||
}
|
|
||||||
if reply != "none" {
|
|
||||||
expectedType := "hash"
|
|
||||||
if rNotify.Format == formatAccess {
|
|
||||||
expectedType = "list"
|
|
||||||
}
|
|
||||||
if reply != expectedType {
|
|
||||||
return nil, redisErrFunc(
|
|
||||||
"Key=%s has type %s, but we expect it to be a %s",
|
|
||||||
rNotify.Key, reply, expectedType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return pool.
|
|
||||||
return rPool, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRedisNotify(accountID string) (*logrus.Logger, error) {
|
|
||||||
rNotify := globalServerConfig.Notify.GetRedisByID(accountID)
|
|
||||||
|
|
||||||
// Dial redis.
|
|
||||||
rPool, err := dialRedis(rNotify)
|
|
||||||
if err != nil {
|
|
||||||
return nil, redisErrFunc("Error dialing server: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rrConn := redisConn{
|
|
||||||
Pool: rPool,
|
|
||||||
params: rNotify,
|
|
||||||
}
|
|
||||||
|
|
||||||
redisLog := logrus.New()
|
|
||||||
|
|
||||||
redisLog.Out = ioutil.Discard
|
|
||||||
|
|
||||||
// Set default JSON formatter.
|
|
||||||
redisLog.Formatter = new(logrus.JSONFormatter)
|
|
||||||
|
|
||||||
redisLog.Hooks.Add(rrConn)
|
|
||||||
|
|
||||||
// Success, redis enabled.
|
|
||||||
return redisLog, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire is called when an event should be sent to the message broker.
|
|
||||||
func (r redisConn) Fire(entry *logrus.Entry) error {
|
|
||||||
rConn := r.Pool.Get()
|
|
||||||
defer rConn.Close()
|
|
||||||
|
|
||||||
// Fetch event type upon reflecting on its original type.
|
|
||||||
entryStr, ok := entry.Data["EventType"].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.params.Format {
|
|
||||||
case formatNamespace:
|
|
||||||
// Match the event if its a delete request, attempt to delete the key
|
|
||||||
if eventMatch(entryStr, []string{"s3:ObjectRemoved:*"}) {
|
|
||||||
_, err := rConn.Do("HDEL", r.params.Key, entry.Data["Key"])
|
|
||||||
if err != nil {
|
|
||||||
return redisErrFunc("Error deleting entry: %v",
|
|
||||||
err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
} // else save this as new entry or update any existing ones.
|
|
||||||
|
|
||||||
value, err := json.Marshal(map[string]interface{}{
|
|
||||||
"Records": entry.Data["Records"],
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return redisErrFunc(
|
|
||||||
"Unable to encode event %v to JSON: %v",
|
|
||||||
entry.Data["Records"], err)
|
|
||||||
}
|
|
||||||
_, err = rConn.Do("HSET", r.params.Key, entry.Data["Key"],
|
|
||||||
value)
|
|
||||||
if err != nil {
|
|
||||||
return redisErrFunc("Error updating hash entry: %v",
|
|
||||||
err)
|
|
||||||
}
|
|
||||||
case formatAccess:
|
|
||||||
// eventTime is taken from the first entry in the
|
|
||||||
// records.
|
|
||||||
events, ok := entry.Data["Records"].([]NotificationEvent)
|
|
||||||
if !ok {
|
|
||||||
return redisErrFunc("unable to extract event time due to conversion error of entry.Data[\"Records\"]=%v", entry.Data["Records"])
|
|
||||||
}
|
|
||||||
eventTime := events[0].EventTime
|
|
||||||
|
|
||||||
listEntry := []interface{}{eventTime, entry.Data["Records"]}
|
|
||||||
jsonValue, err := json.Marshal(listEntry)
|
|
||||||
if err != nil {
|
|
||||||
return redisErrFunc("JSON encoding error: %v", err)
|
|
||||||
}
|
|
||||||
_, err = rConn.Do("RPUSH", r.params.Key, jsonValue)
|
|
||||||
if err != nil {
|
|
||||||
return redisErrFunc("Error appending to Redis list: %v",
|
|
||||||
err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Required for logrus hook implementation
|
|
||||||
func (r redisConn) Levels() []logrus.Level {
|
|
||||||
return []logrus.Level{
|
|
||||||
logrus.InfoLevel,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,212 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type webhookNotify struct {
|
|
||||||
Enable bool `json:"enable"`
|
|
||||||
Endpoint string `json:"endpoint"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *webhookNotify) Validate() error {
|
|
||||||
if !w.Enable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, err := checkURL(w.Endpoint); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type httpConn struct {
|
|
||||||
*http.Client
|
|
||||||
Endpoint string
|
|
||||||
}
|
|
||||||
|
|
||||||
// isNetErrorIgnored - is network error ignored.
|
|
||||||
func isNetErrorIgnored(err error) bool {
|
|
||||||
if err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "Client.Timeout exceeded while awaiting headers") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
switch err.(type) {
|
|
||||||
case net.Error:
|
|
||||||
switch e := err.(type) {
|
|
||||||
case *net.DNSError, *net.OpError, net.UnknownNetworkError:
|
|
||||||
return true
|
|
||||||
case *url.Error:
|
|
||||||
// Fixes https://github.com/minio/minio/issues/4050
|
|
||||||
switch e.Err.(type) {
|
|
||||||
case *net.DNSError, *net.OpError, net.UnknownNetworkError:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// For a URL error, where it replies back "connection closed"
|
|
||||||
// retry again.
|
|
||||||
if strings.Contains(err.Error(), "Connection closed by foreign host") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if strings.Contains(err.Error(), "net/http: TLS handshake timeout") {
|
|
||||||
// If error is - tlsHandshakeTimeoutError, retry.
|
|
||||||
return true
|
|
||||||
} else if strings.Contains(err.Error(), "i/o timeout") {
|
|
||||||
// If error is - tcp timeoutError, retry.
|
|
||||||
return true
|
|
||||||
} else if strings.Contains(err.Error(), "connection timed out") {
|
|
||||||
// If err is a net.Dial timeout, retry.
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lookup endpoint address by successfully POSTting
|
|
||||||
// empty body.
|
|
||||||
func lookupEndpoint(urlStr string) error {
|
|
||||||
req, err := http.NewRequest("POST", urlStr, bytes.NewReader([]byte("")))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 1 * time.Second,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
// Need to close connection after usage.
|
|
||||||
DisableKeepAlives: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set content-length to zero as there is no payload.
|
|
||||||
req.ContentLength = 0
|
|
||||||
|
|
||||||
// Set proper server user-agent.
|
|
||||||
req.Header.Set("User-Agent", globalServerUserAgent)
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
if isNetErrorIgnored(err) {
|
|
||||||
errorIf(err, "Unable to lookup webhook endpoint %s", urlStr)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
// HTTP status OK/NoContent.
|
|
||||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
|
||||||
return fmt.Errorf("Unable to lookup webhook endpoint %s response(%s)", urlStr, resp.Status)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializes new webhook logrus notifier.
|
|
||||||
func newWebhookNotify(accountID string) (*logrus.Logger, error) {
|
|
||||||
rNotify := globalServerConfig.Notify.GetWebhookByID(accountID)
|
|
||||||
if rNotify.Endpoint == "" {
|
|
||||||
return nil, errInvalidArgument
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := lookupEndpoint(rNotify.Endpoint); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conn := httpConn{
|
|
||||||
// Configure aggressive timeouts for client posts.
|
|
||||||
Client: &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{RootCAs: globalRootCAs},
|
|
||||||
DialContext: (&net.Dialer{
|
|
||||||
Timeout: 5 * time.Second,
|
|
||||||
KeepAlive: 5 * time.Second,
|
|
||||||
}).DialContext,
|
|
||||||
TLSHandshakeTimeout: 3 * time.Second,
|
|
||||||
ResponseHeaderTimeout: 3 * time.Second,
|
|
||||||
ExpectContinueTimeout: 2 * time.Second,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Endpoint: rNotify.Endpoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyLog := logrus.New()
|
|
||||||
notifyLog.Out = ioutil.Discard
|
|
||||||
|
|
||||||
// Set default JSON formatter.
|
|
||||||
notifyLog.Formatter = new(logrus.JSONFormatter)
|
|
||||||
|
|
||||||
notifyLog.Hooks.Add(conn)
|
|
||||||
|
|
||||||
// Success
|
|
||||||
return notifyLog, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire is called when an event should be sent to the message broker.
|
|
||||||
func (n httpConn) Fire(entry *logrus.Entry) error {
|
|
||||||
body, err := entry.Reader()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", n.Endpoint, body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set content-type.
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
// Set proper server user-agent.
|
|
||||||
req.Header.Set("User-Agent", globalServerUserAgent)
|
|
||||||
|
|
||||||
// Initiate the http request.
|
|
||||||
resp, err := n.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure to close the response body so the connection can be re-used.
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK &&
|
|
||||||
resp.StatusCode != http.StatusAccepted &&
|
|
||||||
resp.StatusCode != http.StatusContinue {
|
|
||||||
return fmt.Errorf("Unable to send event %s", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Levels are Required for logrus hook implementation
|
|
||||||
func (httpConn) Levels() []logrus.Level {
|
|
||||||
return []logrus.Level{
|
|
||||||
logrus.InfoLevel,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2017 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 (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Custom post handler to handle POST requests.
|
|
||||||
type postHandler struct{}
|
|
||||||
|
|
||||||
func (p postHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "POST" {
|
|
||||||
http.Error(w, fmt.Sprintf("Unexpected method %s", r.Method), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
io.Copy(w, r.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
type errorHandler struct{}
|
|
||||||
|
|
||||||
func (e errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Error(w, fmt.Sprintf("Unexpected method %s", r.Method), http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests web hook initialization.
|
|
||||||
func TestNewWebHookNotify(t *testing.T) {
|
|
||||||
root, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(root)
|
|
||||||
|
|
||||||
server := httptest.NewServer(postHandler{})
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
_, err = newWebhookNotify("1")
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Unexpected should fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
globalServerConfig.Notify.SetWebhookByID("10", webhookNotify{Enable: true, Endpoint: server.URL})
|
|
||||||
_, err = newWebhookNotify("10")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unexpected should not fail with lookupEndpoint", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
globalServerConfig.Notify.SetWebhookByID("15", webhookNotify{Enable: true, Endpoint: "http://%"})
|
|
||||||
_, err = newWebhookNotify("15")
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Unexpected should fail with invalid URL escape")
|
|
||||||
}
|
|
||||||
|
|
||||||
globalServerConfig.Notify.SetWebhookByID("20", webhookNotify{Enable: true, Endpoint: server.URL})
|
|
||||||
webhook, err := newWebhookNotify("20")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unexpected shouldn't fail", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
webhook.WithFields(logrus.Fields{
|
|
||||||
"Key": path.Join("bucket", "object"),
|
|
||||||
"EventType": "s3:ObjectCreated:Put",
|
|
||||||
}).Info()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add tests for lookup endpoint.
|
|
||||||
func TestLookupEndpoint(t *testing.T) {
|
|
||||||
server := httptest.NewServer(errorHandler{})
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
endpoint string
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
// Ignore endpoints which don't exist.
|
|
||||||
{
|
|
||||||
endpoint: "http://unknown",
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: "%%%",
|
|
||||||
err: errors.New("parse %%%: invalid URL escape \"%%%\""),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
endpoint: server.URL,
|
|
||||||
err: fmt.Errorf("Unable to lookup webhook endpoint %s response(400 Bad Request)", server.URL),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range testCases {
|
|
||||||
if err := lookupEndpoint(test.endpoint); err != nil {
|
|
||||||
if err.Error() != test.err.Error() {
|
|
||||||
t.Errorf("Expected %s, got %s", test.err, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -89,19 +89,11 @@ func deleteBucketMetadata(bucket string, objAPI ObjectLayer) {
|
||||||
// Delete bucket access policy, if present - ignore any errors.
|
// Delete bucket access policy, if present - ignore any errors.
|
||||||
_ = removeBucketPolicy(bucket, objAPI)
|
_ = removeBucketPolicy(bucket, objAPI)
|
||||||
|
|
||||||
// Notify all peers (including self) to update in-memory state
|
|
||||||
S3PeersUpdateBucketPolicy(bucket)
|
|
||||||
|
|
||||||
// Delete notification config, if present - ignore any errors.
|
// Delete notification config, if present - ignore any errors.
|
||||||
_ = removeNotificationConfig(bucket, objAPI)
|
_ = removeNotificationConfig(objAPI, bucket)
|
||||||
|
|
||||||
// Notify all peers (including self) to update in-memory state
|
|
||||||
S3PeersUpdateBucketNotification(bucket, nil)
|
|
||||||
// Delete listener config, if present - ignore any errors.
|
// Delete listener config, if present - ignore any errors.
|
||||||
_ = removeListenerConfig(bucket, objAPI)
|
_ = removeListenerConfig(objAPI, bucket)
|
||||||
|
|
||||||
// Notify all peers (including self) to update in-memory state
|
|
||||||
S3PeersUpdateBucketListener(bucket, []listenerConfig{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// House keeping code for FS/XL and distributed Minio setup.
|
// House keeping code for FS/XL and distributed Minio setup.
|
||||||
|
@ -194,3 +186,23 @@ func cleanupDir(storage StorageAPI, volume, dirPath string) error {
|
||||||
err := delFunc(retainSlash(pathJoin(dirPath)))
|
err := delFunc(retainSlash(pathJoin(dirPath)))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Removes notification.xml for a given bucket, only used during DeleteBucket.
|
||||||
|
func removeNotificationConfig(objAPI ObjectLayer, bucket string) error {
|
||||||
|
// Verify bucket is valid.
|
||||||
|
if !IsValidBucketName(bucket) {
|
||||||
|
return BucketNameInvalid{Bucket: bucket}
|
||||||
|
}
|
||||||
|
|
||||||
|
ncPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
||||||
|
|
||||||
|
return objAPI.DeleteObject(minioMetaBucket, ncPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove listener configuration from storage layer. Used when a bucket is deleted.
|
||||||
|
func removeListenerConfig(objAPI ObjectLayer, bucket string) error {
|
||||||
|
// make the path
|
||||||
|
lcPath := path.Join(bucketConfigPrefix, bucket, bucketListenerConfig)
|
||||||
|
|
||||||
|
return objAPI.DeleteObject(minioMetaBucket, lcPath)
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -21,6 +21,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Validates the preconditions for CopyObjectPart, returns true if CopyObjectPart
|
// Validates the preconditions for CopyObjectPart, returns true if CopyObjectPart
|
||||||
|
@ -240,10 +242,10 @@ func deleteObject(obj ObjectLayer, bucket, object string, r *http.Request) (err
|
||||||
host, port, _ := net.SplitHostPort(r.RemoteAddr)
|
host, port, _ := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
|
||||||
// Notify object deleted event.
|
// Notify object deleted event.
|
||||||
eventNotify(eventData{
|
sendEvent(eventArgs{
|
||||||
Type: ObjectRemovedDelete,
|
EventName: event.ObjectRemovedDelete,
|
||||||
Bucket: bucket,
|
BucketName: bucket,
|
||||||
ObjInfo: ObjectInfo{
|
Object: ObjectInfo{
|
||||||
Name: object,
|
Name: object,
|
||||||
},
|
},
|
||||||
ReqParams: extractReqParams(r),
|
ReqParams: extractReqParams(r),
|
||||||
|
|
|
@ -32,6 +32,7 @@ import (
|
||||||
|
|
||||||
mux "github.com/gorilla/mux"
|
mux "github.com/gorilla/mux"
|
||||||
"github.com/minio/minio/pkg/errors"
|
"github.com/minio/minio/pkg/errors"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/handlers"
|
"github.com/minio/minio/pkg/handlers"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
"github.com/minio/minio/pkg/ioutil"
|
"github.com/minio/minio/pkg/ioutil"
|
||||||
|
@ -191,14 +192,14 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify object accessed via a GET request.
|
// Notify object accessed via a GET request.
|
||||||
eventNotify(eventData{
|
sendEvent(eventArgs{
|
||||||
Type: ObjectAccessedGet,
|
EventName: event.ObjectAccessedGet,
|
||||||
Bucket: bucket,
|
BucketName: bucket,
|
||||||
ObjInfo: objInfo,
|
Object: objInfo,
|
||||||
ReqParams: extractReqParams(r),
|
ReqParams: extractReqParams(r),
|
||||||
UserAgent: r.UserAgent(),
|
UserAgent: r.UserAgent(),
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,14 +268,14 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify object accessed via a HEAD request.
|
// Notify object accessed via a HEAD request.
|
||||||
eventNotify(eventData{
|
sendEvent(eventArgs{
|
||||||
Type: ObjectAccessedHead,
|
EventName: event.ObjectAccessedHead,
|
||||||
Bucket: bucket,
|
BucketName: bucket,
|
||||||
ObjInfo: objInfo,
|
Object: objInfo,
|
||||||
ReqParams: extractReqParams(r),
|
ReqParams: extractReqParams(r),
|
||||||
UserAgent: r.UserAgent(),
|
UserAgent: r.UserAgent(),
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -523,14 +524,14 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify object created event.
|
// Notify object created event.
|
||||||
eventNotify(eventData{
|
sendEvent(eventArgs{
|
||||||
Type: ObjectCreatedCopy,
|
EventName: event.ObjectCreatedCopy,
|
||||||
Bucket: dstBucket,
|
BucketName: dstBucket,
|
||||||
ObjInfo: objInfo,
|
Object: objInfo,
|
||||||
ReqParams: extractReqParams(r),
|
ReqParams: extractReqParams(r),
|
||||||
UserAgent: r.UserAgent(),
|
UserAgent: r.UserAgent(),
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -706,14 +707,14 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify object created event.
|
// Notify object created event.
|
||||||
eventNotify(eventData{
|
sendEvent(eventArgs{
|
||||||
Type: ObjectCreatedPut,
|
EventName: event.ObjectCreatedPut,
|
||||||
Bucket: bucket,
|
BucketName: bucket,
|
||||||
ObjInfo: objInfo,
|
Object: objInfo,
|
||||||
ReqParams: extractReqParams(r),
|
ReqParams: extractReqParams(r),
|
||||||
UserAgent: r.UserAgent(),
|
UserAgent: r.UserAgent(),
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1303,14 +1304,14 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify object created event.
|
// Notify object created event.
|
||||||
eventNotify(eventData{
|
sendEvent(eventArgs{
|
||||||
Type: ObjectCreatedCompleteMultipartUpload,
|
EventName: event.ObjectCreatedCompleteMultipartUpload,
|
||||||
Bucket: bucket,
|
BucketName: bucket,
|
||||||
ObjInfo: objInfo,
|
Object: objInfo,
|
||||||
ReqParams: extractReqParams(r),
|
ReqParams: extractReqParams(r),
|
||||||
UserAgent: r.UserAgent(),
|
UserAgent: r.UserAgent(),
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -478,11 +478,8 @@ func testAPIPutObjectStreamSigV4Handler(obj ObjectLayer, instanceType, bucketNam
|
||||||
bytesData := bytes.Repeat([]byte{'a'}, bytesDataLen)
|
bytesData := bytes.Repeat([]byte{'a'}, bytesDataLen)
|
||||||
oneKData := bytes.Repeat([]byte("a"), 1*humanize.KiByte)
|
oneKData := bytes.Repeat([]byte("a"), 1*humanize.KiByte)
|
||||||
|
|
||||||
err := initEventNotifier(obj)
|
var err error
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("[%s] - Failed to initialize event notifiers <ERROR> %v", instanceType, err)
|
|
||||||
|
|
||||||
}
|
|
||||||
type streamFault int
|
type streamFault int
|
||||||
const (
|
const (
|
||||||
None streamFault = iota
|
None streamFault = iota
|
||||||
|
@ -787,12 +784,7 @@ func TestAPIPutObjectHandler(t *testing.T) {
|
||||||
func testAPIPutObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPIPutObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T) {
|
||||||
|
|
||||||
// register event notifier.
|
var err error
|
||||||
err := initEventNotifier(obj)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Notifier initialization failed.")
|
|
||||||
}
|
|
||||||
objectName := "test-object"
|
objectName := "test-object"
|
||||||
// byte data for PutObject.
|
// byte data for PutObject.
|
||||||
bytesData := generateBytesData(6 * humanize.KiByte)
|
bytesData := generateBytesData(6 * humanize.KiByte)
|
||||||
|
@ -1041,11 +1033,7 @@ func testAPICopyObjectPartHandlerSanity(obj ObjectLayer, instanceType, bucketNam
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T) {
|
||||||
|
|
||||||
objectName := "test-object"
|
objectName := "test-object"
|
||||||
// register event notifier.
|
var err error
|
||||||
err := initEventNotifier(obj)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Initializing event notifiers failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// set of byte data for PutObject.
|
// set of byte data for PutObject.
|
||||||
// object has to be created before running tests for Copy Object.
|
// object has to be created before running tests for Copy Object.
|
||||||
|
@ -1156,11 +1144,7 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T) {
|
||||||
|
|
||||||
objectName := "test-object"
|
objectName := "test-object"
|
||||||
// register event notifier.
|
var err error
|
||||||
err := initEventNotifier(obj)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Initializing event notifiers failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// set of byte data for PutObject.
|
// set of byte data for PutObject.
|
||||||
// object has to be created before running tests for Copy Object.
|
// object has to be created before running tests for Copy Object.
|
||||||
|
@ -1488,11 +1472,7 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
||||||
objectName := "test-object"
|
objectName := "test-object"
|
||||||
// object used for anonymous HTTP request test.
|
// object used for anonymous HTTP request test.
|
||||||
anonObject := "anon-object"
|
anonObject := "anon-object"
|
||||||
// register event notifier.
|
var err error
|
||||||
err := initEventNotifier(obj)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Initializing event notifiers failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// set of byte data for PutObject.
|
// set of byte data for PutObject.
|
||||||
// object has to be created before running tests for Copy Object.
|
// object has to be created before running tests for Copy Object.
|
||||||
|
@ -2110,12 +2090,6 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T) {
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
// register event notifier.
|
|
||||||
err = initEventNotifier(obj)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Notifier initialization failed.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// object used for the test.
|
// object used for the test.
|
||||||
objectName := "test-object-new-multipart"
|
objectName := "test-object-new-multipart"
|
||||||
|
@ -2465,12 +2439,6 @@ func testAPIAbortMultipartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T) {
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
// register event notifier.
|
|
||||||
err = initEventNotifier(obj)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Notifier initialization failed.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// object used for the test.
|
// object used for the test.
|
||||||
objectName := "test-object-new-multipart"
|
objectName := "test-object-new-multipart"
|
||||||
|
@ -2633,12 +2601,7 @@ func TestAPIDeleteObjectHandler(t *testing.T) {
|
||||||
func testAPIDeleteObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPIDeleteObjectHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T) {
|
||||||
|
|
||||||
// register event notifier.
|
var err error
|
||||||
err := initEventNotifier(obj)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Notifier initialization failed.")
|
|
||||||
}
|
|
||||||
|
|
||||||
objectName := "test-object"
|
objectName := "test-object"
|
||||||
// Object used for anonymous API request test.
|
// Object used for anonymous API request test.
|
||||||
|
|
|
@ -0,0 +1,311 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
xerrors "github.com/minio/minio/pkg/errors"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
const s3Path = "/s3/remote"
|
||||||
|
|
||||||
|
// PeerRPCReceiver - Peer RPC receiver for peer RPC server.
|
||||||
|
type PeerRPCReceiver struct {
|
||||||
|
AuthRPCServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBucketArgs - delete bucket RPC arguments.
|
||||||
|
type DeleteBucketArgs struct {
|
||||||
|
AuthRPCArgs
|
||||||
|
BucketName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBucket - handles delete bucket RPC call which removes all values of given bucket in global NotificationSys object.
|
||||||
|
func (receiver *PeerRPCReceiver) DeleteBucket(args *DeleteBucketArgs, reply *AuthRPCArgs) error {
|
||||||
|
globalNotificationSys.RemoveNotification(args.BucketName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBucketPolicyArgs - update bucket policy RPC arguments.
|
||||||
|
type UpdateBucketPolicyArgs struct {
|
||||||
|
AuthRPCArgs
|
||||||
|
BucketName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBucketPolicy - handles update bucket policy RPC call which sets bucket policies to given bucket in global BucketPolicies object.
|
||||||
|
func (receiver *PeerRPCReceiver) UpdateBucketPolicy(args *UpdateBucketPolicyArgs, reply *AuthRPCArgs) error {
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
// If the object layer is just coming up then it will load the policy from the disk.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return objectAPI.RefreshBucketPolicy(args.BucketName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutBucketNotificationArgs - put bucket notification RPC arguments.
|
||||||
|
type PutBucketNotificationArgs struct {
|
||||||
|
AuthRPCArgs
|
||||||
|
BucketName string
|
||||||
|
RulesMap event.RulesMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutBucketNotification - handles put bucket notification RPC call which adds rules to given bucket to global NotificationSys object.
|
||||||
|
func (receiver *PeerRPCReceiver) PutBucketNotification(args *PutBucketNotificationArgs, reply *AuthRPCReply) error {
|
||||||
|
if err := args.IsAuthenticated(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
globalNotificationSys.AddRulesMap(args.BucketName, args.RulesMap)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenBucketNotificationArgs - listen bucket notification RPC arguments.
|
||||||
|
type ListenBucketNotificationArgs struct {
|
||||||
|
AuthRPCArgs `json:"-"`
|
||||||
|
BucketName string `json:"-"`
|
||||||
|
EventNames []event.Name `json:"eventNames"`
|
||||||
|
Pattern string `json:"pattern"`
|
||||||
|
TargetID event.TargetID `json:"targetId"`
|
||||||
|
Addr xnet.Host `json:"addr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenBucketNotification - handles listen bucket notification RPC call. It creates PeerRPCClient target which pushes requested events to target in remote peer.
|
||||||
|
func (receiver *PeerRPCReceiver) ListenBucketNotification(args *ListenBucketNotificationArgs, reply *AuthRPCReply) error {
|
||||||
|
if err := args.IsAuthenticated(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcClient := globalNotificationSys.GetPeerRPCClient(args.Addr)
|
||||||
|
if rpcClient == nil {
|
||||||
|
return fmt.Errorf("unable to find PeerRPCClient for provided address %v. This happens only if remote and this minio run with different set of endpoints", args.Addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
target := NewPeerRPCClientTarget(args.BucketName, args.TargetID, rpcClient)
|
||||||
|
rulesMap := event.NewRulesMap(args.EventNames, args.Pattern, target.ID())
|
||||||
|
if err := globalNotificationSys.AddRemoteTarget(args.BucketName, target, rulesMap); err != nil {
|
||||||
|
errorIf(err, "Unable to add PeerRPCClientTarget %v to globalNotificationSys.targetList.", target)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteTargetExistArgs - remote target ID exist RPC arguments.
|
||||||
|
type RemoteTargetExistArgs struct {
|
||||||
|
AuthRPCArgs
|
||||||
|
BucketName string
|
||||||
|
TargetID event.TargetID
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteTargetExistReply - remote target ID exist RPC reply.
|
||||||
|
type RemoteTargetExistReply struct {
|
||||||
|
AuthRPCReply
|
||||||
|
Exist bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteTargetExist - handles target ID exist RPC call which checks whether given target ID is a HTTP client target or not.
|
||||||
|
func (receiver *PeerRPCReceiver) RemoteTargetExist(args *RemoteTargetExistArgs, reply *RemoteTargetExistReply) error {
|
||||||
|
reply.Exist = globalNotificationSys.RemoteTargetExist(args.BucketName, args.TargetID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEventArgs - send event RPC arguments.
|
||||||
|
type SendEventArgs struct {
|
||||||
|
AuthRPCArgs
|
||||||
|
Event event.Event
|
||||||
|
TargetID event.TargetID
|
||||||
|
BucketName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEventReply - send event RPC reply.
|
||||||
|
type SendEventReply struct {
|
||||||
|
AuthRPCReply
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEvent - handles send event RPC call which sends given event to target by given target ID.
|
||||||
|
func (receiver *PeerRPCReceiver) SendEvent(args *SendEventArgs, reply *SendEventReply) error {
|
||||||
|
if err := args.IsAuthenticated(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if errMap := globalNotificationSys.send(args.BucketName, args.Event, args.TargetID); len(errMap) != 0 {
|
||||||
|
var found bool
|
||||||
|
if err, found = errMap[args.TargetID]; !found {
|
||||||
|
// errMap must be zero or one element map because we sent to only one target ID.
|
||||||
|
panic(fmt.Errorf("error for target %v not found in error map %+v", args.TargetID, errMap))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "unable to send event %v to target %v", args.Event, args.TargetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.Error = err
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerS3PeerRPCRouter - creates and registers Peer RPC server and its router.
|
||||||
|
func registerS3PeerRPCRouter(router *mux.Router) error {
|
||||||
|
peerRPCServer := newRPCServer()
|
||||||
|
if err := peerRPCServer.RegisterName("Peer", &PeerRPCReceiver{}); err != nil {
|
||||||
|
return xerrors.Trace(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subrouter := router.NewRoute().PathPrefix(minioReservedBucketPath).Subrouter()
|
||||||
|
subrouter.Path(s3Path).Handler(peerRPCServer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerRPCClient - peer RPC client talks to peer RPC server.
|
||||||
|
type PeerRPCClient struct {
|
||||||
|
*AuthRPCClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBucket - calls delete bucket RPC.
|
||||||
|
func (rpcClient *PeerRPCClient) DeleteBucket(bucketName string) error {
|
||||||
|
args := DeleteBucketArgs{BucketName: bucketName}
|
||||||
|
reply := AuthRPCReply{}
|
||||||
|
return rpcClient.Call("Peer.DeleteBucket", &args, &reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBucketPolicy - calls update bucket policy RPC.
|
||||||
|
func (rpcClient *PeerRPCClient) UpdateBucketPolicy(bucketName string) error {
|
||||||
|
args := UpdateBucketPolicyArgs{
|
||||||
|
BucketName: bucketName,
|
||||||
|
}
|
||||||
|
reply := AuthRPCReply{}
|
||||||
|
return rpcClient.Call("Peer.UpdateBucketPolicy", &args, &reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutBucketNotification - calls put bukcet notification RPC.
|
||||||
|
func (rpcClient *PeerRPCClient) PutBucketNotification(bucketName string, rulesMap event.RulesMap) error {
|
||||||
|
args := PutBucketNotificationArgs{
|
||||||
|
BucketName: bucketName,
|
||||||
|
RulesMap: rulesMap,
|
||||||
|
}
|
||||||
|
reply := AuthRPCReply{}
|
||||||
|
return rpcClient.Call("Peer.PutBucketNotification", &args, &reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenBucketNotification - calls listen bucket notification RPC.
|
||||||
|
func (rpcClient *PeerRPCClient) ListenBucketNotification(bucketName string, eventNames []event.Name,
|
||||||
|
pattern string, targetID event.TargetID, addr xnet.Host) error {
|
||||||
|
args := ListenBucketNotificationArgs{
|
||||||
|
BucketName: bucketName,
|
||||||
|
EventNames: eventNames,
|
||||||
|
Pattern: pattern,
|
||||||
|
TargetID: targetID,
|
||||||
|
Addr: addr,
|
||||||
|
}
|
||||||
|
reply := AuthRPCReply{}
|
||||||
|
return rpcClient.Call("Peer.ListenBucketNotification", &args, &reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteTargetExist - calls remote target ID exist RPC.
|
||||||
|
func (rpcClient *PeerRPCClient) RemoteTargetExist(bucketName string, targetID event.TargetID) (bool, error) {
|
||||||
|
args := RemoteTargetExistArgs{
|
||||||
|
BucketName: bucketName,
|
||||||
|
TargetID: targetID,
|
||||||
|
}
|
||||||
|
|
||||||
|
reply := RemoteTargetExistReply{}
|
||||||
|
if err := rpcClient.Call("Peer.RemoteTargetExist", &args, &reply); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.Exist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEvent - calls send event RPC.
|
||||||
|
func (rpcClient *PeerRPCClient) SendEvent(bucketName string, targetID, remoteTargetID event.TargetID, eventData event.Event) error {
|
||||||
|
args := SendEventArgs{
|
||||||
|
BucketName: bucketName,
|
||||||
|
TargetID: remoteTargetID,
|
||||||
|
Event: eventData,
|
||||||
|
}
|
||||||
|
reply := SendEventReply{}
|
||||||
|
if err := rpcClient.Call("Peer.SendEvent", &args, &reply); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if reply.Error != nil {
|
||||||
|
errorIf(reply.Error, "unable to send event %v to rpc target %v of bucket %v", args, targetID, bucketName)
|
||||||
|
globalNotificationSys.RemoveRemoteTarget(bucketName, targetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeRemoteRPCClients - creates Peer RPCClients for given endpoint list.
|
||||||
|
func makeRemoteRPCClients(endpoints EndpointList) map[xnet.Host]*PeerRPCClient {
|
||||||
|
peerRPCClientMap := make(map[xnet.Host]*PeerRPCClient)
|
||||||
|
|
||||||
|
cred := globalServerConfig.GetCredential()
|
||||||
|
serviceEndpoint := path.Join(minioReservedBucketPath, s3Path)
|
||||||
|
for _, hostStr := range GetRemotePeers(endpoints) {
|
||||||
|
host := xnet.MustParseHost(hostStr)
|
||||||
|
peerRPCClientMap[*host] = &PeerRPCClient{newAuthRPCClient(authConfig{
|
||||||
|
accessKey: cred.AccessKey,
|
||||||
|
secretKey: cred.SecretKey,
|
||||||
|
serverAddr: hostStr,
|
||||||
|
serviceEndpoint: serviceEndpoint,
|
||||||
|
secureConn: globalIsSSL,
|
||||||
|
serviceName: "Peer",
|
||||||
|
})}
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerRPCClientMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerRPCClientTarget - RPCClient is an event.Target which sends event to target of remote peer.
|
||||||
|
type PeerRPCClientTarget struct {
|
||||||
|
id event.TargetID
|
||||||
|
remoteTargetID event.TargetID
|
||||||
|
rpcClient *PeerRPCClient
|
||||||
|
bucketName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID - returns target ID.
|
||||||
|
func (target *PeerRPCClientTarget) ID() event.TargetID {
|
||||||
|
return target.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends event to remote peer by making RPC call.
|
||||||
|
func (target *PeerRPCClientTarget) Send(eventData event.Event) error {
|
||||||
|
return target.rpcClient.SendEvent(target.bucketName, target.id, target.remoteTargetID, eventData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close - does nothing and available for interface compatibility.
|
||||||
|
func (target *PeerRPCClientTarget) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPeerRPCClientTarget - creates RPCClient target with given target ID available in remote peer.
|
||||||
|
func NewPeerRPCClientTarget(bucketName string, targetID event.TargetID, rpcClient *PeerRPCClient) *PeerRPCClientTarget {
|
||||||
|
return &PeerRPCClientTarget{
|
||||||
|
id: event.TargetID{targetID.ID, targetID.Name + "+" + mustGetUUID()},
|
||||||
|
remoteTargetID: targetID,
|
||||||
|
bucketName: bucketName,
|
||||||
|
rpcClient: rpcClient,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -123,12 +123,6 @@ func testPostPolicyBucketHandler(obj ObjectLayer, instanceType string, t TestErr
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
|
|
||||||
// Register event notifier.
|
|
||||||
err = initEventNotifier(obj)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Initializing event notifiers failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// get random bucket name.
|
// get random bucket name.
|
||||||
bucketName := getRandomBucketName()
|
bucketName := getRandomBucketName()
|
||||||
|
|
||||||
|
@ -431,12 +425,6 @@ func testPostPolicyBucketHandlerRedirect(obj ObjectLayer, instanceType string, t
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(root)
|
||||||
|
|
||||||
// Register event notifier.
|
|
||||||
err = initEventNotifier(obj)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Initializing event notifiers failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// get random bucket name.
|
// get random bucket name.
|
||||||
bucketName := getRandomBucketName()
|
bucketName := getRandomBucketName()
|
||||||
|
|
||||||
|
|
|
@ -1,182 +0,0 @@
|
||||||
/*
|
|
||||||
* 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"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/minio/minio-go/pkg/set"
|
|
||||||
)
|
|
||||||
|
|
||||||
// s3Peer structs contains the address of a peer in the cluster, and
|
|
||||||
// its BucketMetaState interface objects.
|
|
||||||
type s3Peer struct {
|
|
||||||
// address in `host:port` format
|
|
||||||
addr string
|
|
||||||
// BucketMetaState client interface
|
|
||||||
bmsClient BucketMetaState
|
|
||||||
}
|
|
||||||
|
|
||||||
// type representing all peers in the cluster
|
|
||||||
type s3Peers []s3Peer
|
|
||||||
|
|
||||||
// makeS3Peers makes an s3Peers struct value from the given urls
|
|
||||||
// slice. The urls slice is assumed to be non-empty and free of nil
|
|
||||||
// values.
|
|
||||||
func makeS3Peers(endpoints EndpointList) (s3PeerList s3Peers) {
|
|
||||||
localAddr := GetLocalPeer(endpoints)
|
|
||||||
s3PeerList = append(s3PeerList, s3Peer{
|
|
||||||
localAddr,
|
|
||||||
&localBucketMetaState{ObjectAPI: newObjectLayerFn},
|
|
||||||
})
|
|
||||||
|
|
||||||
hostSet := set.CreateStringSet(localAddr)
|
|
||||||
cred := globalServerConfig.GetCredential()
|
|
||||||
serviceEndpoint := path.Join(minioReservedBucketPath, s3Path)
|
|
||||||
for _, host := range GetRemotePeers(endpoints) {
|
|
||||||
if hostSet.Contains(host) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
hostSet.Add(host)
|
|
||||||
s3PeerList = append(s3PeerList, s3Peer{
|
|
||||||
addr: host,
|
|
||||||
bmsClient: &remoteBucketMetaState{
|
|
||||||
newAuthRPCClient(authConfig{
|
|
||||||
accessKey: cred.AccessKey,
|
|
||||||
secretKey: cred.SecretKey,
|
|
||||||
serverAddr: host,
|
|
||||||
serviceEndpoint: serviceEndpoint,
|
|
||||||
secureConn: globalIsSSL,
|
|
||||||
serviceName: "S3",
|
|
||||||
})},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return s3PeerList
|
|
||||||
}
|
|
||||||
|
|
||||||
// initGlobalS3Peers - initialize globalS3Peers by passing in
|
|
||||||
// endpoints - intended to be called early in program start-up.
|
|
||||||
func initGlobalS3Peers(endpoints EndpointList) {
|
|
||||||
globalS3Peers = makeS3Peers(endpoints)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPeerClient - fetch BucketMetaState interface by peer address
|
|
||||||
func (s3p s3Peers) GetPeerClient(peer string) BucketMetaState {
|
|
||||||
for _, p := range s3p {
|
|
||||||
if p.addr == peer {
|
|
||||||
return p.bmsClient
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendUpdate sends bucket metadata updates to all given peer
|
|
||||||
// indices. The update calls are sent in parallel, and errors are
|
|
||||||
// returned per peer in an array. The returned error arrayslice is
|
|
||||||
// always as long as s3p.peers.addr.
|
|
||||||
//
|
|
||||||
// The input peerIndex slice can be nil if the update is to be sent to
|
|
||||||
// all peers. This is the common case.
|
|
||||||
//
|
|
||||||
// The updates are sent via a type implementing the BucketMetaState
|
|
||||||
// interface. This makes sure that the local node is directly updated,
|
|
||||||
// and remote nodes are updated via RPC calls.
|
|
||||||
func (s3p s3Peers) SendUpdate(peerIndex []int, args BucketUpdater) []error {
|
|
||||||
|
|
||||||
// peer error array
|
|
||||||
errs := make([]error, len(s3p))
|
|
||||||
|
|
||||||
// Start a wait group and make RPC requests to peers.
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
// Function that sends update to peer at `index`
|
|
||||||
sendUpdateToPeer := func(index int) {
|
|
||||||
defer wg.Done()
|
|
||||||
errs[index] = args.BucketUpdate(s3p[index].bmsClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special (but common) case of peerIndex == nil, implies send
|
|
||||||
// update to all peers.
|
|
||||||
if peerIndex == nil {
|
|
||||||
for idx := 0; idx < len(s3p); idx++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go sendUpdateToPeer(idx)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Send update only to given peer indices.
|
|
||||||
for _, idx := range peerIndex {
|
|
||||||
// check idx is in array bounds.
|
|
||||||
if !(idx >= 0 && idx < len(s3p)) {
|
|
||||||
errorIf(
|
|
||||||
fmt.Errorf("Bad peer index %d input to SendUpdate()", idx),
|
|
||||||
"peerIndex out of bounds",
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
wg.Add(1)
|
|
||||||
go sendUpdateToPeer(idx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for requests to complete and return
|
|
||||||
wg.Wait()
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
// S3PeersUpdateBucketNotification - Sends Update Bucket notification
|
|
||||||
// request to all peers. Currently we log an error and continue.
|
|
||||||
func S3PeersUpdateBucketNotification(bucket string, ncfg *notificationConfig) {
|
|
||||||
setBNPArgs := &SetBucketNotificationPeerArgs{Bucket: bucket, NCfg: ncfg}
|
|
||||||
errs := globalS3Peers.SendUpdate(nil, setBNPArgs)
|
|
||||||
for idx, err := range errs {
|
|
||||||
errorIf(
|
|
||||||
err,
|
|
||||||
"Error sending update bucket notification to %s - %v",
|
|
||||||
globalS3Peers[idx].addr, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// S3PeersUpdateBucketListener - Sends Update Bucket listeners request
|
|
||||||
// to all peers. Currently we log an error and continue.
|
|
||||||
func S3PeersUpdateBucketListener(bucket string, lcfg []listenerConfig) {
|
|
||||||
setBLPArgs := &SetBucketListenerPeerArgs{Bucket: bucket, LCfg: lcfg}
|
|
||||||
errs := globalS3Peers.SendUpdate(nil, setBLPArgs)
|
|
||||||
for idx, err := range errs {
|
|
||||||
errorIf(
|
|
||||||
err,
|
|
||||||
"Error sending update bucket listener to %s - %v",
|
|
||||||
globalS3Peers[idx].addr, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// S3PeersUpdateBucketPolicy - Sends update bucket policy request to
|
|
||||||
// all peers. Currently we log an error and continue.
|
|
||||||
func S3PeersUpdateBucketPolicy(bucket string) {
|
|
||||||
setBPPArgs := &SetBucketPolicyPeerArgs{Bucket: bucket}
|
|
||||||
errs := globalS3Peers.SendUpdate(nil, setBPPArgs)
|
|
||||||
for idx, err := range errs {
|
|
||||||
errorIf(
|
|
||||||
err,
|
|
||||||
"Error sending update bucket policy to %s - %v",
|
|
||||||
globalS3Peers[idx].addr, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validates makeS3Peers, fetches all peers based on list of storage
|
|
||||||
// endpoints.
|
|
||||||
func TestMakeS3Peers(t *testing.T) {
|
|
||||||
// Initialize configuration
|
|
||||||
root, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(root)
|
|
||||||
|
|
||||||
// test cases
|
|
||||||
testCases := []struct {
|
|
||||||
gMinioAddr string
|
|
||||||
eps EndpointList
|
|
||||||
peers []string
|
|
||||||
}{
|
|
||||||
{"127.0.0.1:9000", mustGetNewEndpointList("/mnt/disk1"), []string{"127.0.0.1:9000"}},
|
|
||||||
{"example.org:9000", mustGetNewEndpointList("http://example.org:9000/d1", "http://example.com:9000/d1", "http://example.net:9000/d1", "http://example.edu:9000/d1"), []string{"example.org:9000", "example.com:9000", "example.edu:9000", "example.net:9000"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
getPeersHelper := func(s3p s3Peers) []string {
|
|
||||||
r := []string{}
|
|
||||||
for _, p := range s3p {
|
|
||||||
r = append(r, p.addr)
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute tests
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
globalMinioAddr = testCase.gMinioAddr
|
|
||||||
s3peers := makeS3Peers(testCase.eps)
|
|
||||||
referencePeers := getPeersHelper(s3peers)
|
|
||||||
if !reflect.DeepEqual(testCase.peers, referencePeers) {
|
|
||||||
t.Errorf("Test %d: Expected %v, got %v", i+1, testCase.peers, referencePeers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 (
|
|
||||||
router "github.com/gorilla/mux"
|
|
||||||
"github.com/minio/minio/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
s3Path = "/s3/remote"
|
|
||||||
)
|
|
||||||
|
|
||||||
type s3PeerAPIHandlers struct {
|
|
||||||
AuthRPCServer
|
|
||||||
bms BucketMetaState
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerS3PeerRPCRouter(mux *router.Router) error {
|
|
||||||
s3PeerHandlers := &s3PeerAPIHandlers{
|
|
||||||
AuthRPCServer{},
|
|
||||||
&localBucketMetaState{
|
|
||||||
ObjectAPI: newObjectLayerFn,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s3PeerRPCServer := newRPCServer()
|
|
||||||
err := s3PeerRPCServer.RegisterName("S3", s3PeerHandlers)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Trace(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s3PeerRouter := mux.NewRoute().PathPrefix(minioReservedBucketPath).Subrouter()
|
|
||||||
s3PeerRouter.Path(s3Path).Handler(s3PeerRPCServer)
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
|
|
||||||
// SetBucketNotificationPeerArgs - Arguments collection to SetBucketNotificationPeer RPC
|
|
||||||
// call
|
|
||||||
type SetBucketNotificationPeerArgs struct {
|
|
||||||
// For Auth
|
|
||||||
AuthRPCArgs
|
|
||||||
|
|
||||||
Bucket string
|
|
||||||
|
|
||||||
// Notification config for the given bucket.
|
|
||||||
NCfg *notificationConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// BucketUpdate - implements bucket notification updates,
|
|
||||||
// the underlying operation is a network call updates all
|
|
||||||
// the peers participating in bucket notification.
|
|
||||||
func (s *SetBucketNotificationPeerArgs) BucketUpdate(client BucketMetaState) error {
|
|
||||||
return client.UpdateBucketNotification(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s3 *s3PeerAPIHandlers) SetBucketNotificationPeer(args *SetBucketNotificationPeerArgs, reply *AuthRPCReply) error {
|
|
||||||
if err := args.IsAuthenticated(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s3.bms.UpdateBucketNotification(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBucketListenerPeerArgs - Arguments collection to SetBucketListenerPeer RPC call
|
|
||||||
type SetBucketListenerPeerArgs struct {
|
|
||||||
// For Auth
|
|
||||||
AuthRPCArgs
|
|
||||||
|
|
||||||
Bucket string
|
|
||||||
|
|
||||||
// Listener config for a given bucket.
|
|
||||||
LCfg []listenerConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// BucketUpdate - implements bucket listener updates,
|
|
||||||
// the underlying operation is a network call updates all
|
|
||||||
// the peers participating in listen bucket notification.
|
|
||||||
func (s *SetBucketListenerPeerArgs) BucketUpdate(client BucketMetaState) error {
|
|
||||||
return client.UpdateBucketListener(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s3 *s3PeerAPIHandlers) SetBucketListenerPeer(args *SetBucketListenerPeerArgs, reply *AuthRPCReply) error {
|
|
||||||
if err := args.IsAuthenticated(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s3.bms.UpdateBucketListener(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventArgs - Arguments collection for Event RPC call
|
|
||||||
type EventArgs struct {
|
|
||||||
// For Auth
|
|
||||||
AuthRPCArgs
|
|
||||||
|
|
||||||
// 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 *AuthRPCReply) error {
|
|
||||||
if err := args.IsAuthenticated(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s3.bms.SendEvent(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBucketPolicyPeerArgs - Arguments collection for SetBucketPolicyPeer RPC call
|
|
||||||
type SetBucketPolicyPeerArgs struct {
|
|
||||||
// For Auth
|
|
||||||
AuthRPCArgs
|
|
||||||
|
|
||||||
Bucket string
|
|
||||||
}
|
|
||||||
|
|
||||||
// BucketUpdate - implements bucket policy updates,
|
|
||||||
// the underlying operation is a network call updates all
|
|
||||||
// the peers participating for new set/unset policies.
|
|
||||||
func (s *SetBucketPolicyPeerArgs) BucketUpdate(client BucketMetaState) error {
|
|
||||||
return client.UpdateBucketPolicy(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// tell receiving server to update a bucket policy
|
|
||||||
func (s3 *s3PeerAPIHandlers) SetBucketPolicyPeer(args *SetBucketPolicyPeerArgs, reply *AuthRPCReply) error {
|
|
||||||
if err := args.IsAuthenticated(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s3.bms.UpdateBucketPolicy(args)
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 (
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TestRPCS3PeerSuite struct {
|
|
||||||
testServer TestServer
|
|
||||||
testAuthConf authConfig
|
|
||||||
disks []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up the suite and start the test server.
|
|
||||||
func (s *TestRPCS3PeerSuite) SetUpSuite(t *testing.T) {
|
|
||||||
s.testServer, s.disks = StartTestS3PeerRPCServer(t)
|
|
||||||
s.testAuthConf = authConfig{
|
|
||||||
serverAddr: s.testServer.Server.Listener.Addr().String(),
|
|
||||||
accessKey: s.testServer.AccessKey,
|
|
||||||
secretKey: s.testServer.SecretKey,
|
|
||||||
serviceEndpoint: path.Join(minioReservedBucketPath, s3Path),
|
|
||||||
serviceName: "S3",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TestRPCS3PeerSuite) TearDownSuite(t *testing.T) {
|
|
||||||
s.testServer.Stop()
|
|
||||||
removeRoots(s.disks)
|
|
||||||
os.RemoveAll(s.testServer.Root)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestS3PeerRPC(t *testing.T) {
|
|
||||||
// setup
|
|
||||||
s := &TestRPCS3PeerSuite{}
|
|
||||||
s.SetUpSuite(t)
|
|
||||||
|
|
||||||
// run test
|
|
||||||
s.testS3PeerRPC(t)
|
|
||||||
|
|
||||||
// teardown
|
|
||||||
s.TearDownSuite(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test S3 RPC handlers
|
|
||||||
func (s *TestRPCS3PeerSuite) testS3PeerRPC(t *testing.T) {
|
|
||||||
// Validate for invalid token.
|
|
||||||
args := AuthRPCArgs{}
|
|
||||||
rclient := newAuthRPCClient(s.testAuthConf)
|
|
||||||
defer rclient.Close()
|
|
||||||
|
|
||||||
if err := rclient.Login(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rclient.authToken = "garbage"
|
|
||||||
err := rclient.Call("S3.SetBucketNotificationPeer", &args, &AuthRPCReply{})
|
|
||||||
if err != nil {
|
|
||||||
if err.Error() != errInvalidToken.Error() {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check bucket notification call works.
|
|
||||||
BNPArgs := SetBucketNotificationPeerArgs{Bucket: "bucket", NCfg: ¬ificationConfig{}}
|
|
||||||
client := newAuthRPCClient(s.testAuthConf)
|
|
||||||
defer client.Close()
|
|
||||||
err = client.Call("S3.SetBucketNotificationPeer", &BNPArgs, &AuthRPCReply{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check bucket listener update call works.
|
|
||||||
BLPArgs := SetBucketListenerPeerArgs{Bucket: "bucket", LCfg: nil}
|
|
||||||
err = client.Call("S3.SetBucketListenerPeer", &BLPArgs, &AuthRPCReply{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
BPPArgs := SetBucketPolicyPeerArgs{Bucket: "bucket"}
|
|
||||||
err = client.Call("S3.SetBucketPolicyPeer", &BPPArgs, &AuthRPCReply{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check event send event call works.
|
|
||||||
evArgs := EventArgs{Event: nil, Arn: "localhost:9000"}
|
|
||||||
err = client.Call("S3.Event", &evArgs, &AuthRPCReply{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -223,8 +223,9 @@ func serverMain(ctx *cli.Context) {
|
||||||
handler, err = configureServerHandler(globalEndpoints)
|
handler, err = configureServerHandler(globalEndpoints)
|
||||||
fatalIf(err, "Unable to configure one of server's RPC services.")
|
fatalIf(err, "Unable to configure one of server's RPC services.")
|
||||||
|
|
||||||
// Initialize S3 Peers inter-node communication only in distributed setup.
|
// Initialize notification system.
|
||||||
initGlobalS3Peers(globalEndpoints)
|
globalNotificationSys, err = NewNotificationSys(globalServerConfig, globalEndpoints)
|
||||||
|
fatalIf(err, "Unable to initialize notification system.")
|
||||||
|
|
||||||
// Initialize Admin Peers inter-node communication only in distributed setup.
|
// Initialize Admin Peers inter-node communication only in distributed setup.
|
||||||
initGlobalAdminPeers(globalEndpoints)
|
initGlobalAdminPeers(globalEndpoints)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -125,19 +125,16 @@ func printServerCommonMsg(apiEndpoints []string) {
|
||||||
|
|
||||||
// Prints bucket notification configurations.
|
// Prints bucket notification configurations.
|
||||||
func printEventNotifiers() {
|
func printEventNotifiers() {
|
||||||
if globalEventNotifier == nil {
|
arns := globalNotificationSys.GetARNList()
|
||||||
// In case initEventNotifier() was not done or failed.
|
if len(arns) == 0 {
|
||||||
return
|
|
||||||
}
|
|
||||||
// Get all configured external notification targets
|
|
||||||
externalTargets := globalEventNotifier.GetAllExternalTargets()
|
|
||||||
if len(externalTargets) == 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
arnMsg := colorBlue("SQS ARNs: ")
|
arnMsg := colorBlue("SQS ARNs: ")
|
||||||
for queueArn := range externalTargets {
|
for _, arn := range arns {
|
||||||
arnMsg += colorBold(fmt.Sprintf(getFormatStr(len(queueArn), 1), queueArn))
|
arnMsg += colorBold(fmt.Sprintf(getFormatStr(len(arn), 1), arn))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println(arnMsg)
|
log.Println(arnMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -75,13 +75,10 @@ func verifyError(c *check, response *http.Response, code, description string, st
|
||||||
|
|
||||||
func runAllTests(suite *TestSuiteCommon, c *check) {
|
func runAllTests(suite *TestSuiteCommon, c *check) {
|
||||||
suite.SetUpSuite(c)
|
suite.SetUpSuite(c)
|
||||||
suite.TestBucketSQSNotificationWebHook(c)
|
|
||||||
suite.TestObjectDir(c)
|
suite.TestObjectDir(c)
|
||||||
suite.TestBucketSQSNotificationAMQP(c)
|
|
||||||
suite.TestBucketPolicy(c)
|
suite.TestBucketPolicy(c)
|
||||||
suite.TestDeleteBucket(c)
|
suite.TestDeleteBucket(c)
|
||||||
suite.TestDeleteBucketNotEmpty(c)
|
suite.TestDeleteBucketNotEmpty(c)
|
||||||
suite.TestListenBucketNotificationHandler(c)
|
|
||||||
suite.TestDeleteMultipleObjects(c)
|
suite.TestDeleteMultipleObjects(c)
|
||||||
suite.TestDeleteObject(c)
|
suite.TestDeleteObject(c)
|
||||||
suite.TestNonExistentBucket(c)
|
suite.TestNonExistentBucket(c)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -38,6 +38,10 @@ func handleSignals() {
|
||||||
stopProcess := func() bool {
|
stopProcess := func() bool {
|
||||||
var err, oerr error
|
var err, oerr error
|
||||||
|
|
||||||
|
if globalNotificationSys != nil {
|
||||||
|
globalNotificationSys.RemoveAllRemoteTargets()
|
||||||
|
}
|
||||||
|
|
||||||
err = globalHTTPServer.Shutdown()
|
err = globalHTTPServer.Shutdown()
|
||||||
errorIf(err, "Unable to shutdown http server")
|
errorIf(err, "Unable to shutdown http server")
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -351,7 +351,10 @@ func UnstartedTestServer(t TestErrHandler, instanceType string) TestServer {
|
||||||
globalMinioHost = host
|
globalMinioHost = host
|
||||||
globalMinioPort = port
|
globalMinioPort = port
|
||||||
globalMinioAddr = getEndpointsLocalAddr(testServer.Disks)
|
globalMinioAddr = getEndpointsLocalAddr(testServer.Disks)
|
||||||
initGlobalS3Peers(testServer.Disks)
|
globalNotificationSys, err = NewNotificationSys(globalServerConfig, testServer.Disks)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to initialize queue configuration")
|
||||||
|
}
|
||||||
|
|
||||||
return testServer
|
return testServer
|
||||||
}
|
}
|
||||||
|
@ -511,11 +514,6 @@ func resetGlobalNSLock() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset Global event notifier.
|
|
||||||
func resetGlobalEventnotify() {
|
|
||||||
globalEventNotifier = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resetGlobalEndpoints() {
|
func resetGlobalEndpoints() {
|
||||||
globalEndpoints = EndpointList{}
|
globalEndpoints = EndpointList{}
|
||||||
}
|
}
|
||||||
|
@ -558,8 +556,6 @@ func resetTestGlobals() {
|
||||||
resetGlobalConfig()
|
resetGlobalConfig()
|
||||||
// Reset global NSLock.
|
// Reset global NSLock.
|
||||||
resetGlobalNSLock()
|
resetGlobalNSLock()
|
||||||
// Reset global event notifier.
|
|
||||||
resetGlobalEventnotify()
|
|
||||||
// Reset global endpoints.
|
// Reset global endpoints.
|
||||||
resetGlobalEndpoints()
|
resetGlobalEndpoints()
|
||||||
// Reset global isXL flag.
|
// Reset global isXL flag.
|
||||||
|
@ -1637,18 +1633,6 @@ func getCompleteMultipartUploadURL(endPoint, bucketName, objectName, uploadID st
|
||||||
return makeTestTargetURL(endPoint, bucketName, objectName, queryValue)
|
return makeTestTargetURL(endPoint, bucketName, objectName, queryValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// return URL for put bucket notification.
|
|
||||||
func getPutBucketNotificationURL(endPoint, bucketName string) string {
|
|
||||||
return getGetBucketNotificationURL(endPoint, bucketName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// return URL for get bucket notification.
|
|
||||||
func getGetBucketNotificationURL(endPoint, bucketName string) string {
|
|
||||||
queryValue := url.Values{}
|
|
||||||
queryValue.Set("notification", "")
|
|
||||||
return makeTestTargetURL(endPoint, bucketName, "", queryValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// return URL for listen bucket notification.
|
// return URL for listen bucket notification.
|
||||||
func getListenBucketNotificationURL(endPoint, bucketName string, prefixes, suffixes, events []string) string {
|
func getListenBucketNotificationURL(endPoint, bucketName string, prefixes, suffixes, events []string) string {
|
||||||
queryValue := url.Values{}
|
queryValue := url.Values{}
|
||||||
|
@ -1720,7 +1704,7 @@ func newTestObjectLayer(endpoints EndpointList) (newObject ObjectLayer, err erro
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a new event notifier.
|
// Initialize a new event notifier.
|
||||||
if err = initEventNotifier(xl); err != nil {
|
if globalNotificationSys, err = NewNotificationSys(globalServerConfig, endpoints); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -36,6 +36,7 @@ import (
|
||||||
"github.com/minio/minio/browser"
|
"github.com/minio/minio/browser"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/errors"
|
"github.com/minio/minio/pkg/errors"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -568,11 +569,11 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify object created event.
|
// Notify object created event.
|
||||||
eventNotify(eventData{
|
sendEvent(eventArgs{
|
||||||
Type: ObjectCreatedPut,
|
EventName: event.ObjectCreatedPut,
|
||||||
Bucket: bucket,
|
BucketName: bucket,
|
||||||
ObjInfo: objInfo,
|
Object: objInfo,
|
||||||
ReqParams: extractReqParams(r),
|
ReqParams: extractReqParams(r),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -41,6 +41,21 @@ import (
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Implement a dummy flush writer.
|
||||||
|
type flushWriter struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush writer is a dummy writer compatible with http.Flusher and http.ResponseWriter.
|
||||||
|
func (f *flushWriter) Flush() {}
|
||||||
|
func (f *flushWriter) Write(b []byte) (n int, err error) { return f.Writer.Write(b) }
|
||||||
|
func (f *flushWriter) Header() http.Header { return http.Header{} }
|
||||||
|
func (f *flushWriter) WriteHeader(code int) {}
|
||||||
|
|
||||||
|
func newFlushWriter(writer io.Writer) http.ResponseWriter {
|
||||||
|
return &flushWriter{writer}
|
||||||
|
}
|
||||||
|
|
||||||
// Tests private function writeWebErrorResponse.
|
// Tests private function writeWebErrorResponse.
|
||||||
func TestWriteWebErrorResponse(t *testing.T) {
|
func TestWriteWebErrorResponse(t *testing.T) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
|
@ -228,9 +228,9 @@ func newXLSets(endpoints EndpointList, format *formatXLV2, setCount int, drivesP
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a new event notifier.
|
// Initialize notification system.
|
||||||
if err := initEventNotifier(s); err != nil {
|
if err = globalNotificationSys.Init(s); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("Unable to initialize event notification. %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the disk monitoring and connect routine.
|
// Start the disk monitoring and connect routine.
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ARN - SQS resource name representation.
|
||||||
|
type ARN struct {
|
||||||
|
TargetID
|
||||||
|
region string
|
||||||
|
}
|
||||||
|
|
||||||
|
// String - returns string representation.
|
||||||
|
func (arn ARN) String() string {
|
||||||
|
if arn.TargetID.ID == "" && arn.TargetID.Name == "" && arn.region == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return "arn:minio:sqs:" + arn.region + ":" + arn.TargetID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalXML - encodes to XML data.
|
||||||
|
func (arn ARN) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return e.EncodeElement(arn.String(), start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML - decodes XML data.
|
||||||
|
func (arn *ARN) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var s string
|
||||||
|
if err := d.DecodeElement(&s, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedARN, err := parseARN(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*arn = *parsedARN
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseARN - parses string to ARN.
|
||||||
|
func parseARN(s string) (*ARN, error) {
|
||||||
|
// ARN must be in the format of arn:minio:sqs:<REGION>:<ID>:<TYPE>
|
||||||
|
if !strings.HasPrefix(s, "arn:minio:sqs:") {
|
||||||
|
return nil, &ErrInvalidARN{s}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens := strings.Split(s, ":")
|
||||||
|
if len(tokens) != 6 {
|
||||||
|
return nil, &ErrInvalidARN{s}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokens[4] == "" || tokens[5] == "" {
|
||||||
|
return nil, &ErrInvalidARN{s}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ARN{
|
||||||
|
region: tokens[3],
|
||||||
|
TargetID: TargetID{
|
||||||
|
ID: tokens[4],
|
||||||
|
Name: tokens[5],
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestARNString(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
arn ARN
|
||||||
|
expectedResult string
|
||||||
|
}{
|
||||||
|
{ARN{}, ""},
|
||||||
|
{ARN{TargetID{"1", "webhook"}, ""}, "arn:minio:sqs::1:webhook"},
|
||||||
|
{ARN{TargetID{"1", "webhook"}, "us-east-1"}, "arn:minio:sqs:us-east-1:1:webhook"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.arn.String()
|
||||||
|
|
||||||
|
if result != testCase.expectedResult {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestARNMarshalXML(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
arn ARN
|
||||||
|
expectedData []byte
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{ARN{}, []byte("<ARN></ARN>"), false},
|
||||||
|
{ARN{TargetID{"1", "webhook"}, ""}, []byte("<ARN>arn:minio:sqs::1:webhook</ARN>"), false},
|
||||||
|
{ARN{TargetID{"1", "webhook"}, "us-east-1"}, []byte("<ARN>arn:minio:sqs:us-east-1:1:webhook</ARN>"), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
data, err := xml.Marshal(testCase.arn)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(data, testCase.expectedData) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, string(testCase.expectedData), string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestARNUnmarshalXML(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
data []byte
|
||||||
|
expectedARN *ARN
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{[]byte("<ARN></ARN>"), nil, true},
|
||||||
|
{[]byte("<ARN>arn:minio:sqs:::</ARN>"), nil, true},
|
||||||
|
{[]byte("<ARN>arn:minio:sqs::1:webhook</ARN>"), &ARN{TargetID{"1", "webhook"}, ""}, false},
|
||||||
|
{[]byte("<ARN>arn:minio:sqs:us-east-1:1:webhook</ARN>"), &ARN{TargetID{"1", "webhook"}, "us-east-1"}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
arn := &ARN{}
|
||||||
|
err := xml.Unmarshal(testCase.data, &arn)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if *arn != *testCase.expectedARN {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedARN, arn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseARN(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
s string
|
||||||
|
expectedARN *ARN
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{"", nil, true},
|
||||||
|
{"arn:minio:sqs:::", nil, true},
|
||||||
|
{"arn:minio:sqs::1:webhook:remote", nil, true},
|
||||||
|
{"arn:aws:sqs::1:webhook", nil, true},
|
||||||
|
{"arn:minio:sns::1:webhook", nil, true},
|
||||||
|
{"arn:minio:sqs::1:webhook", &ARN{TargetID{"1", "webhook"}, ""}, false},
|
||||||
|
{"arn:minio:sqs:us-east-1:1:webhook", &ARN{TargetID{"1", "webhook"}, "us-east-1"}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
arn, err := parseARN(testCase.s)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if *arn != *testCase.expectedARN {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedARN, arn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,292 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/pkg/set"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateFilterRuleValue - checks if given value is filter rule value or not.
|
||||||
|
func ValidateFilterRuleValue(value string) error {
|
||||||
|
for _, segment := range strings.Split(value, "/") {
|
||||||
|
if segment == "." || segment == ".." {
|
||||||
|
return &ErrInvalidFilterValue{value}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(value) <= 1024 && utf8.ValidString(value) && !strings.Contains(value, `\`) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ErrInvalidFilterValue{value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterRule - represents elements inside <FilterRule>...</FilterRule>
|
||||||
|
type FilterRule struct {
|
||||||
|
Name string `xml:"Name"`
|
||||||
|
Value string `xml:"Value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML - decodes XML data.
|
||||||
|
func (filter *FilterRule) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
// Make subtype to avoid recursive UnmarshalXML().
|
||||||
|
type filterRule FilterRule
|
||||||
|
rule := filterRule{}
|
||||||
|
if err := d.DecodeElement(&rule, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.Name != "prefix" && rule.Name != "suffix" {
|
||||||
|
return &ErrInvalidFilterName{rule.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateFilterRuleValue(filter.Value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*filter = FilterRule(rule)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterRuleList - represents multiple <FilterRule>...</FilterRule>
|
||||||
|
type FilterRuleList struct {
|
||||||
|
Rules []FilterRule `xml:"FilterRule,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML - decodes XML data.
|
||||||
|
func (ruleList *FilterRuleList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
// Make subtype to avoid recursive UnmarshalXML().
|
||||||
|
type filterRuleList FilterRuleList
|
||||||
|
rules := filterRuleList{}
|
||||||
|
if err := d.DecodeElement(&rules, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterRuleList must have only one prefix and/or suffix.
|
||||||
|
nameSet := set.NewStringSet()
|
||||||
|
for _, rule := range rules.Rules {
|
||||||
|
if nameSet.Contains(rule.Name) {
|
||||||
|
if rule.Name == "prefix" {
|
||||||
|
return &ErrFilterNamePrefix{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ErrFilterNameSuffix{}
|
||||||
|
}
|
||||||
|
|
||||||
|
nameSet.Add(rule.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
*ruleList = FilterRuleList(rules)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern - returns pattern using prefix and suffix values.
|
||||||
|
func (ruleList FilterRuleList) Pattern() string {
|
||||||
|
var prefix string
|
||||||
|
var suffix string
|
||||||
|
|
||||||
|
for _, rule := range ruleList.Rules {
|
||||||
|
switch rule.Name {
|
||||||
|
case "prefix":
|
||||||
|
prefix = rule.Value
|
||||||
|
case "suffix":
|
||||||
|
suffix = rule.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewPattern(prefix, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// S3Key - represents elements inside <S3Key>...</S3Key>
|
||||||
|
type S3Key struct {
|
||||||
|
RuleList FilterRuleList `xml:"S3Key,omitempty" json:"S3Key,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// common - represents common elements inside <QueueConfiguration>, <CloudFunctionConfiguration>
|
||||||
|
// and <TopicConfiguration>
|
||||||
|
type common struct {
|
||||||
|
ID string `xml:"Id" json:"Id"`
|
||||||
|
Filter S3Key `xml:"Filter" json:"Filter"`
|
||||||
|
Events []Name `xml:"Event" json:"Event"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue - represents elements inside <QueueConfiguration>
|
||||||
|
type Queue struct {
|
||||||
|
common
|
||||||
|
ARN ARN `xml:"Queue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML - decodes XML data.
|
||||||
|
func (q *Queue) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
// Make subtype to avoid recursive UnmarshalXML().
|
||||||
|
type queue Queue
|
||||||
|
parsedQueue := queue{}
|
||||||
|
if err := d.DecodeElement(&parsedQueue, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parsedQueue.Events) == 0 {
|
||||||
|
return errors.New("missing event name(s)")
|
||||||
|
}
|
||||||
|
|
||||||
|
eventStringSet := set.NewStringSet()
|
||||||
|
for _, eventName := range parsedQueue.Events {
|
||||||
|
if eventStringSet.Contains(eventName.String()) {
|
||||||
|
return &ErrDuplicateEventName{eventName}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventStringSet.Add(eventName.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
*q = Queue(parsedQueue)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate - checks whether queue has valid values or not.
|
||||||
|
func (q Queue) Validate(region string, targetList *TargetList) error {
|
||||||
|
if region != "" && q.ARN.region != region {
|
||||||
|
return &ErrUnknownRegion{q.ARN.region}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !targetList.Exists(q.ARN.TargetID) {
|
||||||
|
return &ErrARNNotFound{q.ARN}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRegion - sets region value to queue's ARN.
|
||||||
|
func (q *Queue) SetRegion(region string) {
|
||||||
|
q.ARN.region = region
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRulesMap - converts Queue to RulesMap
|
||||||
|
func (q Queue) ToRulesMap() RulesMap {
|
||||||
|
pattern := q.Filter.RuleList.Pattern()
|
||||||
|
return NewRulesMap(q.Events, pattern, q.ARN.TargetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unused. Available for completion.
|
||||||
|
type lambda struct {
|
||||||
|
common
|
||||||
|
ARN string `xml:"CloudFunction"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unused. Available for completion.
|
||||||
|
type topic struct {
|
||||||
|
common
|
||||||
|
ARN string `xml:"Topic" json:"Topic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config - notification configuration described in
|
||||||
|
// http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html
|
||||||
|
type Config struct {
|
||||||
|
XMLName xml.Name `xml:"NotificationConfiguration"`
|
||||||
|
QueueList []Queue `xml:"QueueConfiguration,omitempty"`
|
||||||
|
LambdaList []lambda `xml:"CloudFunctionConfiguration,omitempty"`
|
||||||
|
TopicList []topic `xml:"TopicConfiguration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML - decodes XML data.
|
||||||
|
func (conf *Config) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
// Make subtype to avoid recursive UnmarshalXML().
|
||||||
|
type config Config
|
||||||
|
parsedConfig := config{}
|
||||||
|
if err := d.DecodeElement(&parsedConfig, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parsedConfig.QueueList) == 0 {
|
||||||
|
return errors.New("missing queue configuration(s)")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, q1 := range parsedConfig.QueueList[:len(parsedConfig.QueueList)-1] {
|
||||||
|
for _, q2 := range parsedConfig.QueueList[i+1:] {
|
||||||
|
if reflect.DeepEqual(q1, q2) {
|
||||||
|
return &ErrDuplicateQueueConfiguration{q1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parsedConfig.LambdaList) > 0 || len(parsedConfig.TopicList) > 0 {
|
||||||
|
return &ErrUnsupportedConfiguration{}
|
||||||
|
}
|
||||||
|
|
||||||
|
*conf = Config(parsedConfig)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate - checks whether config has valid values or not.
|
||||||
|
func (conf Config) Validate(region string, targetList *TargetList) error {
|
||||||
|
for _, queue := range conf.QueueList {
|
||||||
|
if err := queue.Validate(region, targetList); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Need to discuss/check why same ARN cannot be used in another queue configuration.
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRegion - sets region to all queue configuration.
|
||||||
|
func (conf *Config) SetRegion(region string) {
|
||||||
|
for i := range conf.QueueList {
|
||||||
|
conf.QueueList[i].SetRegion(region)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRulesMap - converts all queue configuration to RulesMap.
|
||||||
|
func (conf *Config) ToRulesMap() RulesMap {
|
||||||
|
rulesMap := make(RulesMap)
|
||||||
|
|
||||||
|
for _, queue := range conf.QueueList {
|
||||||
|
rulesMap.Add(queue.ToRulesMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
return rulesMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseConfig - parses data in reader to notification configuration.
|
||||||
|
func ParseConfig(reader io.Reader, region string, targetList *TargetList) (*Config, error) {
|
||||||
|
var config Config
|
||||||
|
if err := xml.NewDecoder(reader).Decode(&config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.QueueList) == 0 {
|
||||||
|
return nil, errors.New("missing queue configuration(s)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.Validate(region, targetList); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config.SetRegion(region)
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
|
@ -0,0 +1,961 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateFilterRuleValue(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
value string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{"foo/.", true},
|
||||||
|
{"../foo", true},
|
||||||
|
{`foo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/bazfoo/bar/baz`, true},
|
||||||
|
{string([]byte{0xff, 0xfe, 0xfd}), true},
|
||||||
|
{`foo\bar`, true},
|
||||||
|
{"Hello/世界", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
err := ValidateFilterRuleValue(testCase.value)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterRuleUnmarshalXML(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
data []byte
|
||||||
|
expectedResult *FilterRule
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{[]byte(`<FilterRule></FilterRule>`), nil, true},
|
||||||
|
{[]byte(`<FilterRule><Name></Name></FilterRule>`), nil, true},
|
||||||
|
{[]byte(`<FilterRule><Value></Value></FilterRule>`), nil, true},
|
||||||
|
{[]byte(`<FilterRule><Name></Name><Value></Value></FilterRule>`), nil, true},
|
||||||
|
{[]byte(`<FilterRule><Name>Prefix</Name><Value>Hello/世界</Value></FilterRule>`), nil, true},
|
||||||
|
{[]byte(`<FilterRule><Name>ends</Name><Value>foo/bar</Value></FilterRule>`), nil, true},
|
||||||
|
{[]byte(`<FilterRule><Name>prefix</Name><Value>Hello/世界</Value></FilterRule>`), &FilterRule{"prefix", "Hello/世界"}, false},
|
||||||
|
{[]byte(`<FilterRule><Name>suffix</Name><Value>foo/bar</Value></FilterRule>`), &FilterRule{"suffix", "foo/bar"}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := &FilterRule{}
|
||||||
|
err := xml.Unmarshal(testCase.data, result)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterRuleListUnmarshalXML(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
data []byte
|
||||||
|
expectedResult *FilterRuleList
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{[]byte(`<S3Key><FilterRule><Name>suffix</Name><Value>Hello/世界</Value></FilterRule><FilterRule><Name>suffix</Name><Value>foo/bar</Value></FilterRule></S3Key>`), nil, true},
|
||||||
|
{[]byte(`<S3Key><FilterRule><Name>prefix</Name><Value>Hello/世界</Value></FilterRule><FilterRule><Name>prefix</Name><Value>foo/bar</Value></FilterRule></S3Key>`), nil, true},
|
||||||
|
{[]byte(`<S3Key><FilterRule><Name>prefix</Name><Value>Hello/世界</Value></FilterRule></S3Key>`), &FilterRuleList{[]FilterRule{{"prefix", "Hello/世界"}}}, false},
|
||||||
|
{[]byte(`<S3Key><FilterRule><Name>suffix</Name><Value>foo/bar</Value></FilterRule></S3Key>`), &FilterRuleList{[]FilterRule{{"suffix", "foo/bar"}}}, false},
|
||||||
|
{[]byte(`<S3Key><FilterRule><Name>prefix</Name><Value>Hello/世界</Value></FilterRule><FilterRule><Name>suffix</Name><Value>foo/bar</Value></FilterRule></S3Key>`), &FilterRuleList{[]FilterRule{{"prefix", "Hello/世界"}, {"suffix", "foo/bar"}}}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := &FilterRuleList{}
|
||||||
|
err := xml.Unmarshal(testCase.data, result)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterRuleListPattern(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
filterRuleList FilterRuleList
|
||||||
|
expectedResult string
|
||||||
|
}{
|
||||||
|
{FilterRuleList{}, ""},
|
||||||
|
{FilterRuleList{[]FilterRule{{"prefix", "Hello/世界"}}}, "Hello/世界*"},
|
||||||
|
{FilterRuleList{[]FilterRule{{"suffix", "foo/bar"}}}, "*foo/bar"},
|
||||||
|
{FilterRuleList{[]FilterRule{{"prefix", "Hello/世界"}, {"suffix", "foo/bar"}}}, "Hello/世界*foo/bar"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.filterRuleList.Pattern()
|
||||||
|
|
||||||
|
if result != testCase.expectedResult {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueueUnmarshalXML(t *testing.T) {
|
||||||
|
dataCase1 := []byte(`
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>`)
|
||||||
|
|
||||||
|
dataCase2 := []byte(`
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>`)
|
||||||
|
|
||||||
|
dataCase3 := []byte(`
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>`)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
data []byte
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{dataCase1, false},
|
||||||
|
{dataCase2, false},
|
||||||
|
{dataCase3, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
err := xml.Unmarshal(testCase.data, &Queue{})
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueueValidate(t *testing.T) {
|
||||||
|
var data []byte
|
||||||
|
data = []byte(`
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>`)
|
||||||
|
queue1 := &Queue{}
|
||||||
|
if err := xml.Unmarshal(data, queue1); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(`
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>`)
|
||||||
|
queue2 := &Queue{}
|
||||||
|
if err := xml.Unmarshal(data, queue2); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(`
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:eu-west-2:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>`)
|
||||||
|
queue3 := &Queue{}
|
||||||
|
if err := xml.Unmarshal(data, queue3); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetList1 := NewTargetList()
|
||||||
|
|
||||||
|
targetList2 := NewTargetList()
|
||||||
|
if err := targetList2.Add(&ExampleTarget{TargetID{"1", "webhook"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
queue *Queue
|
||||||
|
region string
|
||||||
|
targetList *TargetList
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{queue1, "eu-west-1", nil, true},
|
||||||
|
{queue2, "us-east-1", targetList1, true},
|
||||||
|
{queue3, "", targetList2, false},
|
||||||
|
{queue2, "us-east-1", targetList2, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
err := testCase.queue.Validate(testCase.region, testCase.targetList)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueueSetRegion(t *testing.T) {
|
||||||
|
var data []byte
|
||||||
|
data = []byte(`
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>`)
|
||||||
|
queue1 := &Queue{}
|
||||||
|
if err := xml.Unmarshal(data, queue1); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(`
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs::1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>`)
|
||||||
|
queue2 := &Queue{}
|
||||||
|
if err := xml.Unmarshal(data, queue2); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
queue *Queue
|
||||||
|
region string
|
||||||
|
expectedResult ARN
|
||||||
|
}{
|
||||||
|
{queue1, "eu-west-1", ARN{TargetID{"1", "webhook"}, "eu-west-1"}},
|
||||||
|
{queue1, "", ARN{TargetID{"1", "webhook"}, ""}},
|
||||||
|
{queue2, "us-east-1", ARN{TargetID{"1", "webhook"}, "us-east-1"}},
|
||||||
|
{queue2, "", ARN{TargetID{"1", "webhook"}, ""}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
testCase.queue.SetRegion(testCase.region)
|
||||||
|
result := testCase.queue.ARN
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueueToRulesMap(t *testing.T) {
|
||||||
|
var data []byte
|
||||||
|
data = []byte(`
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>`)
|
||||||
|
queueCase1 := &Queue{}
|
||||||
|
if err := xml.Unmarshal(data, queueCase1); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(`
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>`)
|
||||||
|
queueCase2 := &Queue{}
|
||||||
|
if err := xml.Unmarshal(data, queueCase2); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesMapCase1 := NewRulesMap([]Name{ObjectAccessedAll, ObjectCreatedAll, ObjectRemovedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
rulesMapCase2 := NewRulesMap([]Name{ObjectCreatedPut}, "images/*jpg", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
queue *Queue
|
||||||
|
expectedResult RulesMap
|
||||||
|
}{
|
||||||
|
{queueCase1, rulesMapCase1},
|
||||||
|
{queueCase2, rulesMapCase2},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.queue.ToRulesMap()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigUnmarshalXML(t *testing.T) {
|
||||||
|
dataCase1 := []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
|
||||||
|
dataCase2 := []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
|
||||||
|
dataCase3 := []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>2</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
|
||||||
|
dataCase4 := []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
<CloudFunctionConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>.jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Cloudcode>arn:aws:lambda:us-west-2:444455556666:cloud-function-A</Cloudcode>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</CloudFunctionConfiguration>
|
||||||
|
<TopicConfiguration>
|
||||||
|
<Topic>arn:aws:sns:us-west-2:444455556666:sns-notification-one</Topic>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
</TopicConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
testCases := []struct {
|
||||||
|
data []byte
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{dataCase1, false},
|
||||||
|
{dataCase2, false},
|
||||||
|
{dataCase3, false},
|
||||||
|
{dataCase4, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
err := xml.Unmarshal(testCase.data, &Config{})
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidate(t *testing.T) {
|
||||||
|
var data []byte
|
||||||
|
data = []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
config1 := &Config{}
|
||||||
|
if err := xml.Unmarshal(data, config1); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
config2 := &Config{}
|
||||||
|
if err := xml.Unmarshal(data, config2); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>2</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
config3 := &Config{}
|
||||||
|
if err := xml.Unmarshal(data, config3); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetList1 := NewTargetList()
|
||||||
|
|
||||||
|
targetList2 := NewTargetList()
|
||||||
|
if err := targetList2.Add(&ExampleTarget{TargetID{"1", "webhook"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
config *Config
|
||||||
|
region string
|
||||||
|
targetList *TargetList
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{config1, "eu-west-1", nil, true},
|
||||||
|
{config2, "us-east-1", targetList1, true},
|
||||||
|
{config3, "", targetList2, false},
|
||||||
|
{config2, "us-east-1", targetList2, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
err := testCase.config.Validate(testCase.region, testCase.targetList)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSetRegion(t *testing.T) {
|
||||||
|
var data []byte
|
||||||
|
data = []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
config1 := &Config{}
|
||||||
|
if err := xml.Unmarshal(data, config1); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs::1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
config2 := &Config{}
|
||||||
|
if err := xml.Unmarshal(data, config2); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>2</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:2:amqp</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
config3 := &Config{}
|
||||||
|
if err := xml.Unmarshal(data, config3); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
config *Config
|
||||||
|
region string
|
||||||
|
expectedResult []ARN
|
||||||
|
}{
|
||||||
|
{config1, "eu-west-1", []ARN{{TargetID{"1", "webhook"}, "eu-west-1"}}},
|
||||||
|
{config1, "", []ARN{{TargetID{"1", "webhook"}, ""}}},
|
||||||
|
{config2, "us-east-1", []ARN{{TargetID{"1", "webhook"}, "us-east-1"}}},
|
||||||
|
{config2, "", []ARN{{TargetID{"1", "webhook"}, ""}}},
|
||||||
|
{config3, "us-east-1", []ARN{{TargetID{"1", "webhook"}, "us-east-1"}, {TargetID{"2", "amqp"}, "us-east-1"}}},
|
||||||
|
{config3, "", []ARN{{TargetID{"1", "webhook"}, ""}, {TargetID{"2", "amqp"}, ""}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
testCase.config.SetRegion(testCase.region)
|
||||||
|
result := []ARN{}
|
||||||
|
for _, queue := range testCase.config.QueueList {
|
||||||
|
result = append(result, queue.ARN)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigToRulesMap(t *testing.T) {
|
||||||
|
var data []byte
|
||||||
|
data = []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
config1 := &Config{}
|
||||||
|
if err := xml.Unmarshal(data, config1); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs::1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
config2 := &Config{}
|
||||||
|
if err := xml.Unmarshal(data, config2); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = []byte(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>2</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:2:amqp</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
config3 := &Config{}
|
||||||
|
if err := xml.Unmarshal(data, config3); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesMapCase1 := NewRulesMap([]Name{ObjectAccessedAll, ObjectCreatedAll, ObjectRemovedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesMapCase2 := NewRulesMap([]Name{ObjectCreatedPut}, "images/*jpg", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesMapCase3 := NewRulesMap([]Name{ObjectAccessedAll, ObjectCreatedAll, ObjectRemovedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
rulesMapCase3.add([]Name{ObjectCreatedPut}, "images/*jpg", TargetID{"2", "amqp"})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
config *Config
|
||||||
|
expectedResult RulesMap
|
||||||
|
}{
|
||||||
|
{config1, rulesMapCase1},
|
||||||
|
{config2, rulesMapCase2},
|
||||||
|
{config3, rulesMapCase3},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.config.ToRulesMap()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseConfig(t *testing.T) {
|
||||||
|
reader1 := strings.NewReader(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
|
||||||
|
reader2 := strings.NewReader(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
|
||||||
|
reader3 := strings.NewReader(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>2</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>prefix</Name>
|
||||||
|
<Value>images/</Value>
|
||||||
|
</FilterRule>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
|
||||||
|
reader4 := strings.NewReader(`
|
||||||
|
<NotificationConfiguration>
|
||||||
|
<QueueConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter></Filter>
|
||||||
|
<Queue>arn:minio:sqs:us-east-1:1:webhook</Queue>
|
||||||
|
<Event>s3:ObjectAccessed:*</Event>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
<Event>s3:ObjectRemoved:*</Event>
|
||||||
|
</QueueConfiguration>
|
||||||
|
<CloudFunctionConfiguration>
|
||||||
|
<Id>1</Id>
|
||||||
|
<Filter>
|
||||||
|
<S3Key>
|
||||||
|
<FilterRule>
|
||||||
|
<Name>suffix</Name>
|
||||||
|
<Value>.jpg</Value>
|
||||||
|
</FilterRule>
|
||||||
|
</S3Key>
|
||||||
|
</Filter>
|
||||||
|
<Cloudcode>arn:aws:lambda:us-west-2:444455556666:cloud-function-A</Cloudcode>
|
||||||
|
<Event>s3:ObjectCreated:Put</Event>
|
||||||
|
</CloudFunctionConfiguration>
|
||||||
|
<TopicConfiguration>
|
||||||
|
<Topic>arn:aws:sns:us-west-2:444455556666:sns-notification-one</Topic>
|
||||||
|
<Event>s3:ObjectCreated:*</Event>
|
||||||
|
</TopicConfiguration>
|
||||||
|
</NotificationConfiguration>
|
||||||
|
`)
|
||||||
|
|
||||||
|
targetList1 := NewTargetList()
|
||||||
|
|
||||||
|
targetList2 := NewTargetList()
|
||||||
|
if err := targetList2.Add(&ExampleTarget{TargetID{"1", "webhook"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
reader *strings.Reader
|
||||||
|
region string
|
||||||
|
targetList *TargetList
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{reader1, "eu-west-1", nil, true},
|
||||||
|
{reader2, "us-east-1", targetList1, true},
|
||||||
|
{reader4, "us-east-1", targetList1, true},
|
||||||
|
{reader3, "", targetList2, false},
|
||||||
|
{reader2, "us-east-1", targetList2, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
if _, err := testCase.reader.Seek(0, 0); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err := ParseConfig(testCase.reader, testCase.region, testCase.targetList)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsEventError - checks whether given error is event error or not.
|
||||||
|
func IsEventError(err error) bool {
|
||||||
|
switch err.(type) {
|
||||||
|
case ErrInvalidFilterName, *ErrInvalidFilterName:
|
||||||
|
return true
|
||||||
|
case ErrFilterNamePrefix, *ErrFilterNamePrefix:
|
||||||
|
return true
|
||||||
|
case ErrFilterNameSuffix, *ErrFilterNameSuffix:
|
||||||
|
return true
|
||||||
|
case ErrInvalidFilterValue, *ErrInvalidFilterValue:
|
||||||
|
return true
|
||||||
|
case ErrDuplicateEventName, *ErrDuplicateEventName:
|
||||||
|
return true
|
||||||
|
case ErrUnsupportedConfiguration, *ErrUnsupportedConfiguration:
|
||||||
|
return true
|
||||||
|
case ErrDuplicateQueueConfiguration, *ErrDuplicateQueueConfiguration:
|
||||||
|
return true
|
||||||
|
case ErrUnknownRegion, *ErrUnknownRegion:
|
||||||
|
return true
|
||||||
|
case ErrARNNotFound, *ErrARNNotFound:
|
||||||
|
return true
|
||||||
|
case ErrInvalidARN, *ErrInvalidARN:
|
||||||
|
return true
|
||||||
|
case ErrInvalidEventName, *ErrInvalidEventName:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidFilterName - invalid filter name error.
|
||||||
|
type ErrInvalidFilterName struct {
|
||||||
|
FilterName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrInvalidFilterName) Error() string {
|
||||||
|
return fmt.Sprintf("invalid filter name '%v'", err.FilterName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrFilterNamePrefix - more than one prefix usage error.
|
||||||
|
type ErrFilterNamePrefix struct{}
|
||||||
|
|
||||||
|
func (err ErrFilterNamePrefix) Error() string {
|
||||||
|
return fmt.Sprintf("more than one prefix in filter rule")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrFilterNameSuffix - more than one suffix usage error.
|
||||||
|
type ErrFilterNameSuffix struct{}
|
||||||
|
|
||||||
|
func (err ErrFilterNameSuffix) Error() string {
|
||||||
|
return fmt.Sprintf("more than one suffix in filter rule")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidFilterValue - invalid filter value error.
|
||||||
|
type ErrInvalidFilterValue struct {
|
||||||
|
FilterValue string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrInvalidFilterValue) Error() string {
|
||||||
|
return fmt.Sprintf("invalid filter value '%v'", err.FilterValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrDuplicateEventName - duplicate event name error.
|
||||||
|
type ErrDuplicateEventName struct {
|
||||||
|
EventName Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrDuplicateEventName) Error() string {
|
||||||
|
return fmt.Sprintf("duplicate event name '%v' found", err.EventName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnsupportedConfiguration - unsupported configuration error.
|
||||||
|
type ErrUnsupportedConfiguration struct{}
|
||||||
|
|
||||||
|
func (err ErrUnsupportedConfiguration) Error() string {
|
||||||
|
return "topic or cloud function configuration is not supported"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrDuplicateQueueConfiguration - duplicate queue configuration error.
|
||||||
|
type ErrDuplicateQueueConfiguration struct {
|
||||||
|
Queue Queue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrDuplicateQueueConfiguration) Error() string {
|
||||||
|
var message string
|
||||||
|
if data, xerr := xml.Marshal(err.Queue); xerr != nil {
|
||||||
|
message = fmt.Sprintf("%+v", err.Queue)
|
||||||
|
} else {
|
||||||
|
message = string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("duplicate queue configuration %v", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnknownRegion - unknown region error.
|
||||||
|
type ErrUnknownRegion struct {
|
||||||
|
Region string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrUnknownRegion) Error() string {
|
||||||
|
return fmt.Sprintf("unknown region '%v'", err.Region)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrARNNotFound - ARN not found error.
|
||||||
|
type ErrARNNotFound struct {
|
||||||
|
ARN ARN
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrARNNotFound) Error() string {
|
||||||
|
return fmt.Sprintf("ARN '%v' not found", err.ARN)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidARN - invalid ARN error.
|
||||||
|
type ErrInvalidARN struct {
|
||||||
|
ARN string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrInvalidARN) Error() string {
|
||||||
|
return fmt.Sprintf("invalid ARN '%v'", err.ARN)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidEventName - invalid event name error.
|
||||||
|
type ErrInvalidEventName struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrInvalidEventName) Error() string {
|
||||||
|
return fmt.Sprintf("invalid event name '%v'", err.Name)
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NamespaceFormat - namespace log format used in some event targets.
|
||||||
|
NamespaceFormat = "namespace"
|
||||||
|
|
||||||
|
// AccessFormat - access log format used in some event targets.
|
||||||
|
AccessFormat = "access"
|
||||||
|
|
||||||
|
// AMZTimeFormat - event time format.
|
||||||
|
AMZTimeFormat = "2006-01-02T15:04:05Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Identity represents access key who caused the event.
|
||||||
|
type Identity struct {
|
||||||
|
PrincipalID string `json:"principalId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bucket represents bucket metadata of the event.
|
||||||
|
type Bucket struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
OwnerIdentity Identity `json:"ownerIdentity"`
|
||||||
|
ARN string `json:"arn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object represents object metadata of the event.
|
||||||
|
type Object struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
ETag string `json:"eTag,omitempty"`
|
||||||
|
ContentType string `json:"contentType,omitempty"`
|
||||||
|
UserMetadata map[string]string `json:"userMetadata,omitempty"`
|
||||||
|
VersionID string `json:"versionId,omitempty"`
|
||||||
|
Sequencer string `json:"sequencer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata represents event metadata.
|
||||||
|
type Metadata struct {
|
||||||
|
SchemaVersion string `json:"s3SchemaVersion"`
|
||||||
|
ConfigurationID string `json:"configurationId"`
|
||||||
|
Bucket Bucket `json:"bucket"`
|
||||||
|
Object Object `json:"object"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source represents client information who triggered the event.
|
||||||
|
type Source struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port string `json:"port"`
|
||||||
|
UserAgent string `json:"userAgent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event represents event notification information defined in
|
||||||
|
// http://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html.
|
||||||
|
type Event struct {
|
||||||
|
EventVersion string `json:"eventVersion"`
|
||||||
|
EventSource string `json:"eventSource"`
|
||||||
|
AwsRegion string `json:"awsRegion"`
|
||||||
|
EventTime string `json:"eventTime"`
|
||||||
|
EventName Name `json:"eventName"`
|
||||||
|
UserIdentity Identity `json:"userIdentity"`
|
||||||
|
RequestParameters map[string]string `json:"requestParameters"`
|
||||||
|
ResponseElements map[string]string `json:"responseElements"`
|
||||||
|
S3 Metadata `json:"s3"`
|
||||||
|
Source Source `json:"source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log represents event information for some event targets.
|
||||||
|
type Log struct {
|
||||||
|
EventName Name
|
||||||
|
Key string
|
||||||
|
Records []Event
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Name - event type enum.
|
||||||
|
// Refer http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#notification-how-to-event-types-and-destinations
|
||||||
|
type Name int
|
||||||
|
|
||||||
|
// Values of Name
|
||||||
|
const (
|
||||||
|
ObjectAccessedAll Name = 1 + iota
|
||||||
|
ObjectAccessedGet
|
||||||
|
ObjectAccessedHead
|
||||||
|
ObjectCreatedAll
|
||||||
|
ObjectCreatedCompleteMultipartUpload
|
||||||
|
ObjectCreatedCopy
|
||||||
|
ObjectCreatedPost
|
||||||
|
ObjectCreatedPut
|
||||||
|
ObjectRemovedAll
|
||||||
|
ObjectRemovedDelete
|
||||||
|
)
|
||||||
|
|
||||||
|
// Expand - returns expanded values of abbreviated event type.
|
||||||
|
func (name Name) Expand() []Name {
|
||||||
|
switch name {
|
||||||
|
case ObjectAccessedAll:
|
||||||
|
return []Name{ObjectAccessedGet, ObjectAccessedHead}
|
||||||
|
case ObjectCreatedAll:
|
||||||
|
return []Name{ObjectCreatedCompleteMultipartUpload, ObjectCreatedCopy, ObjectCreatedPost, ObjectCreatedPut}
|
||||||
|
case ObjectRemovedAll:
|
||||||
|
return []Name{ObjectRemovedDelete}
|
||||||
|
default:
|
||||||
|
return []Name{name}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String - returns string representation of event type.
|
||||||
|
func (name Name) String() string {
|
||||||
|
switch name {
|
||||||
|
case ObjectAccessedAll:
|
||||||
|
return "s3:ObjectAccessed:*"
|
||||||
|
case ObjectAccessedGet:
|
||||||
|
return "s3:ObjectAccessed:Get"
|
||||||
|
case ObjectAccessedHead:
|
||||||
|
return "s3:ObjectAccessed:Head"
|
||||||
|
case ObjectCreatedAll:
|
||||||
|
return "s3:ObjectCreated:*"
|
||||||
|
case ObjectCreatedCompleteMultipartUpload:
|
||||||
|
return "s3:ObjectCreated:CompleteMultipartUpload"
|
||||||
|
case ObjectCreatedCopy:
|
||||||
|
return "s3:ObjectCreated:Copy"
|
||||||
|
case ObjectCreatedPost:
|
||||||
|
return "s3:ObjectCreated:Post"
|
||||||
|
case ObjectCreatedPut:
|
||||||
|
return "s3:ObjectCreated:Put"
|
||||||
|
case ObjectRemovedAll:
|
||||||
|
return "s3:ObjectRemoved:*"
|
||||||
|
case ObjectRemovedDelete:
|
||||||
|
return "s3:ObjectRemoved:Delete"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalXML - encodes to XML data.
|
||||||
|
func (name Name) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return e.EncodeElement(name.String(), start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML - decodes XML data.
|
||||||
|
func (name *Name) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var s string
|
||||||
|
if err := d.DecodeElement(&s, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
eventName, err := ParseName(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*name = eventName
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON - encodes to JSON data.
|
||||||
|
func (name Name) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(name.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON - decodes JSON data.
|
||||||
|
func (name *Name) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(data, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
eventName, err := ParseName(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*name = eventName
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseName - parses string to Name.
|
||||||
|
func ParseName(s string) (Name, error) {
|
||||||
|
switch s {
|
||||||
|
case "s3:ObjectAccessed:*":
|
||||||
|
return ObjectAccessedAll, nil
|
||||||
|
case "s3:ObjectAccessed:Get":
|
||||||
|
return ObjectAccessedGet, nil
|
||||||
|
case "s3:ObjectAccessed:Head":
|
||||||
|
return ObjectAccessedHead, nil
|
||||||
|
case "s3:ObjectCreated:*":
|
||||||
|
return ObjectCreatedAll, nil
|
||||||
|
case "s3:ObjectCreated:CompleteMultipartUpload":
|
||||||
|
return ObjectCreatedCompleteMultipartUpload, nil
|
||||||
|
case "s3:ObjectCreated:Copy":
|
||||||
|
return ObjectCreatedCopy, nil
|
||||||
|
case "s3:ObjectCreated:Post":
|
||||||
|
return ObjectCreatedPost, nil
|
||||||
|
case "s3:ObjectCreated:Put":
|
||||||
|
return ObjectCreatedPut, nil
|
||||||
|
case "s3:ObjectRemoved:*":
|
||||||
|
return ObjectRemovedAll, nil
|
||||||
|
case "s3:ObjectRemoved:Delete":
|
||||||
|
return ObjectRemovedDelete, nil
|
||||||
|
default:
|
||||||
|
return 0, &ErrInvalidEventName{s}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,220 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNameExpand(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name Name
|
||||||
|
expectedResult []Name
|
||||||
|
}{
|
||||||
|
{ObjectAccessedAll, []Name{ObjectAccessedGet, ObjectAccessedHead}},
|
||||||
|
{ObjectCreatedAll, []Name{ObjectCreatedCompleteMultipartUpload, ObjectCreatedCopy, ObjectCreatedPost, ObjectCreatedPut}},
|
||||||
|
{ObjectRemovedAll, []Name{ObjectRemovedDelete}},
|
||||||
|
{ObjectAccessedHead, []Name{ObjectAccessedHead}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.name.Expand()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNameString(t *testing.T) {
|
||||||
|
var blankName Name
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name Name
|
||||||
|
expectedResult string
|
||||||
|
}{
|
||||||
|
{ObjectAccessedAll, "s3:ObjectAccessed:*"},
|
||||||
|
{ObjectAccessedGet, "s3:ObjectAccessed:Get"},
|
||||||
|
{ObjectAccessedHead, "s3:ObjectAccessed:Head"},
|
||||||
|
{ObjectCreatedAll, "s3:ObjectCreated:*"},
|
||||||
|
{ObjectCreatedCompleteMultipartUpload, "s3:ObjectCreated:CompleteMultipartUpload"},
|
||||||
|
{ObjectCreatedCopy, "s3:ObjectCreated:Copy"},
|
||||||
|
{ObjectCreatedPost, "s3:ObjectCreated:Post"},
|
||||||
|
{ObjectCreatedPut, "s3:ObjectCreated:Put"},
|
||||||
|
{ObjectRemovedAll, "s3:ObjectRemoved:*"},
|
||||||
|
{ObjectRemovedDelete, "s3:ObjectRemoved:Delete"},
|
||||||
|
{blankName, ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.name.String()
|
||||||
|
|
||||||
|
if result != testCase.expectedResult {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNameMarshalXML(t *testing.T) {
|
||||||
|
var blankName Name
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name Name
|
||||||
|
expectedData []byte
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{ObjectAccessedAll, []byte("<Name>s3:ObjectAccessed:*</Name>"), false},
|
||||||
|
{ObjectRemovedDelete, []byte("<Name>s3:ObjectRemoved:Delete</Name>"), false},
|
||||||
|
{blankName, []byte("<Name></Name>"), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
data, err := xml.Marshal(testCase.name)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(data, testCase.expectedData) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, string(testCase.expectedData), string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNameUnmarshalXML(t *testing.T) {
|
||||||
|
var blankName Name
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
data []byte
|
||||||
|
expectedName Name
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{[]byte("<Name>s3:ObjectAccessed:*</Name>"), ObjectAccessedAll, false},
|
||||||
|
{[]byte("<Name>s3:ObjectRemoved:Delete</Name>"), ObjectRemovedDelete, false},
|
||||||
|
{[]byte("<Name></Name>"), blankName, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
var name Name
|
||||||
|
err := xml.Unmarshal(testCase.data, &name)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(name, testCase.expectedName) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedName, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNameMarshalJSON(t *testing.T) {
|
||||||
|
var blankName Name
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name Name
|
||||||
|
expectedData []byte
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{ObjectAccessedAll, []byte(`"s3:ObjectAccessed:*"`), false},
|
||||||
|
{ObjectRemovedDelete, []byte(`"s3:ObjectRemoved:Delete"`), false},
|
||||||
|
{blankName, []byte(`""`), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
data, err := json.Marshal(testCase.name)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(data, testCase.expectedData) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, string(testCase.expectedData), string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNameUnmarshalJSON(t *testing.T) {
|
||||||
|
var blankName Name
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
data []byte
|
||||||
|
expectedName Name
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{[]byte(`"s3:ObjectAccessed:*"`), ObjectAccessedAll, false},
|
||||||
|
{[]byte(`"s3:ObjectRemoved:Delete"`), ObjectRemovedDelete, false},
|
||||||
|
{[]byte(`""`), blankName, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
var name Name
|
||||||
|
err := json.Unmarshal(testCase.data, &name)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(name, testCase.expectedName) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedName, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseName(t *testing.T) {
|
||||||
|
var blankName Name
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
s string
|
||||||
|
expectedName Name
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{"s3:ObjectAccessed:*", ObjectAccessedAll, false},
|
||||||
|
{"s3:ObjectRemoved:Delete", ObjectRemovedDelete, false},
|
||||||
|
{"", blankName, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
name, err := ParseName(testCase.s)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(name, testCase.expectedName) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedName, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/wildcard"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPattern - create new pattern for prefix/suffix.
|
||||||
|
func NewPattern(prefix, suffix string) (pattern string) {
|
||||||
|
if prefix != "" {
|
||||||
|
if !strings.HasSuffix(prefix, "*") {
|
||||||
|
prefix += "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern = prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
if suffix != "" {
|
||||||
|
if !strings.HasPrefix(suffix, "*") {
|
||||||
|
suffix = "*" + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern += suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern = strings.Replace(pattern, "**", "*", -1)
|
||||||
|
|
||||||
|
return pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rules - event rules
|
||||||
|
type Rules map[string]TargetIDSet
|
||||||
|
|
||||||
|
// Add - adds pattern and target ID.
|
||||||
|
func (rules Rules) Add(pattern string, targetID TargetID) {
|
||||||
|
rules[pattern] = NewTargetIDSet(targetID).Union(rules[pattern])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match - returns TargetIDSet matching object name in rules.
|
||||||
|
func (rules Rules) Match(objectName string) TargetIDSet {
|
||||||
|
targetIDs := NewTargetIDSet()
|
||||||
|
|
||||||
|
for pattern, targetIDSet := range rules {
|
||||||
|
if wildcard.MatchSimple(pattern, objectName) {
|
||||||
|
targetIDs = targetIDs.Union(targetIDSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone - returns copy of this rules.
|
||||||
|
func (rules Rules) Clone() Rules {
|
||||||
|
rulesCopy := make(Rules)
|
||||||
|
|
||||||
|
for pattern, targetIDSet := range rules {
|
||||||
|
rulesCopy[pattern] = targetIDSet.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
return rulesCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union - returns union with given rules as new rules.
|
||||||
|
func (rules Rules) Union(rules2 Rules) Rules {
|
||||||
|
nrules := rules.Clone()
|
||||||
|
|
||||||
|
for pattern, targetIDSet := range rules2 {
|
||||||
|
nrules[pattern] = nrules[pattern].Union(targetIDSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nrules
|
||||||
|
}
|
||||||
|
|
||||||
|
// Difference - returns diffrence with given rules as new rules.
|
||||||
|
func (rules Rules) Difference(rules2 Rules) Rules {
|
||||||
|
nrules := make(Rules)
|
||||||
|
|
||||||
|
for pattern, targetIDSet := range rules {
|
||||||
|
if nv := targetIDSet.Difference(rules2[pattern]); len(nv) > 0 {
|
||||||
|
nrules[pattern] = nv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nrules
|
||||||
|
}
|
|
@ -0,0 +1,275 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewPattern(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
prefix string
|
||||||
|
suffix string
|
||||||
|
expectedResult string
|
||||||
|
}{
|
||||||
|
{"", "", ""},
|
||||||
|
{"*", "", "*"},
|
||||||
|
{"", "*", "*"},
|
||||||
|
{"images/", "", "images/*"},
|
||||||
|
{"images/*", "", "images/*"},
|
||||||
|
{"", "jpg", "*jpg"},
|
||||||
|
{"", "*jpg", "*jpg"},
|
||||||
|
{"images/", "jpg", "images/*jpg"},
|
||||||
|
{"images/*", "jpg", "images/*jpg"},
|
||||||
|
{"images/", "*jpg", "images/*jpg"},
|
||||||
|
{"images/*", "*jpg", "images/*jpg"},
|
||||||
|
{"201*/images/", "jpg", "201*/images/*jpg"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := NewPattern(testCase.prefix, testCase.suffix)
|
||||||
|
|
||||||
|
if result != testCase.expectedResult {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRulesAdd(t *testing.T) {
|
||||||
|
rulesCase1 := make(Rules)
|
||||||
|
|
||||||
|
rulesCase2 := make(Rules)
|
||||||
|
rulesCase2.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesCase3 := make(Rules)
|
||||||
|
rulesCase3.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesCase4 := make(Rules)
|
||||||
|
rulesCase4.Add(NewPattern("", "*.jpg"), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesCase5 := make(Rules)
|
||||||
|
|
||||||
|
rulesCase6 := make(Rules)
|
||||||
|
rulesCase6.Add(NewPattern("", "*.jpg"), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesCase7 := make(Rules)
|
||||||
|
rulesCase7.Add(NewPattern("", "*.jpg"), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesCase8 := make(Rules)
|
||||||
|
rulesCase8.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
rules Rules
|
||||||
|
pattern string
|
||||||
|
targetID TargetID
|
||||||
|
expectedResult int
|
||||||
|
}{
|
||||||
|
{rulesCase1, NewPattern("*", ""), TargetID{"1", "webhook"}, 1},
|
||||||
|
{rulesCase2, NewPattern("*", ""), TargetID{"2", "amqp"}, 2},
|
||||||
|
{rulesCase3, NewPattern("2010*", ""), TargetID{"1", "webhook"}, 1},
|
||||||
|
{rulesCase4, NewPattern("*", ""), TargetID{"1", "webhook"}, 2},
|
||||||
|
{rulesCase5, NewPattern("", "*.jpg"), TargetID{"1", "webhook"}, 1},
|
||||||
|
{rulesCase6, NewPattern("", "*"), TargetID{"2", "amqp"}, 2},
|
||||||
|
{rulesCase7, NewPattern("", "*.jpg"), TargetID{"1", "webhook"}, 1},
|
||||||
|
{rulesCase8, NewPattern("", "*.jpg"), TargetID{"1", "webhook"}, 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
testCase.rules.Add(testCase.pattern, testCase.targetID)
|
||||||
|
result := len(testCase.rules)
|
||||||
|
|
||||||
|
if result != testCase.expectedResult {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRulesMatch(t *testing.T) {
|
||||||
|
rulesCase1 := make(Rules)
|
||||||
|
|
||||||
|
rulesCase2 := make(Rules)
|
||||||
|
rulesCase2.Add(NewPattern("*", "*"), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesCase3 := make(Rules)
|
||||||
|
rulesCase3.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
rulesCase3.Add(NewPattern("", "*.png"), TargetID{"2", "amqp"})
|
||||||
|
|
||||||
|
rulesCase4 := make(Rules)
|
||||||
|
rulesCase4.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
rules Rules
|
||||||
|
objectName string
|
||||||
|
expectedResult TargetIDSet
|
||||||
|
}{
|
||||||
|
{rulesCase1, "photos.jpg", NewTargetIDSet()},
|
||||||
|
{rulesCase2, "photos.jpg", NewTargetIDSet(TargetID{"1", "webhook"})},
|
||||||
|
{rulesCase3, "2010/photos.jpg", NewTargetIDSet(TargetID{"1", "webhook"})},
|
||||||
|
{rulesCase4, "2000/photos.jpg", NewTargetIDSet()},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.rules.Match(testCase.objectName)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(testCase.expectedResult, result) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRulesClone(t *testing.T) {
|
||||||
|
rulesCase1 := make(Rules)
|
||||||
|
|
||||||
|
rulesCase2 := make(Rules)
|
||||||
|
rulesCase2.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesCase3 := make(Rules)
|
||||||
|
rulesCase3.Add(NewPattern("", "*.jpg"), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
rules Rules
|
||||||
|
prefix string
|
||||||
|
targetID TargetID
|
||||||
|
}{
|
||||||
|
{rulesCase1, "2010*", TargetID{"1", "webhook"}},
|
||||||
|
{rulesCase2, "2000*", TargetID{"2", "amqp"}},
|
||||||
|
{rulesCase3, "2010*", TargetID{"1", "webhook"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.rules.Clone()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, testCase.rules) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.rules, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(NewPattern(testCase.prefix, ""), testCase.targetID)
|
||||||
|
if reflect.DeepEqual(result, testCase.rules) {
|
||||||
|
t.Fatalf("test %v: result: expected: not equal, got: equal", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRulesUnion(t *testing.T) {
|
||||||
|
rulesCase1 := make(Rules)
|
||||||
|
rules2Case1 := make(Rules)
|
||||||
|
expectedResultCase1 := make(Rules)
|
||||||
|
|
||||||
|
rulesCase2 := make(Rules)
|
||||||
|
rules2Case2 := make(Rules)
|
||||||
|
rules2Case2.Add(NewPattern("*", ""), TargetID{"1", "webhook"})
|
||||||
|
expectedResultCase2 := make(Rules)
|
||||||
|
expectedResultCase2.Add(NewPattern("*", ""), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesCase3 := make(Rules)
|
||||||
|
rulesCase3.Add(NewPattern("", "*"), TargetID{"1", "webhook"})
|
||||||
|
rules2Case3 := make(Rules)
|
||||||
|
expectedResultCase3 := make(Rules)
|
||||||
|
expectedResultCase3.Add(NewPattern("", "*"), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesCase4 := make(Rules)
|
||||||
|
rulesCase4.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
rules2Case4 := make(Rules)
|
||||||
|
rules2Case4.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
expectedResultCase4 := make(Rules)
|
||||||
|
expectedResultCase4.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesCase5 := make(Rules)
|
||||||
|
rulesCase5.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
rulesCase5.Add(NewPattern("", "*.png"), TargetID{"2", "amqp"})
|
||||||
|
rules2Case5 := make(Rules)
|
||||||
|
rules2Case5.Add(NewPattern("*", ""), TargetID{"1", "webhook"})
|
||||||
|
expectedResultCase5 := make(Rules)
|
||||||
|
expectedResultCase5.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
expectedResultCase5.Add(NewPattern("", "*.png"), TargetID{"2", "amqp"})
|
||||||
|
expectedResultCase5.Add(NewPattern("*", ""), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
rules Rules
|
||||||
|
rules2 Rules
|
||||||
|
expectedResult Rules
|
||||||
|
}{
|
||||||
|
{rulesCase1, rules2Case1, expectedResultCase1},
|
||||||
|
{rulesCase2, rules2Case2, expectedResultCase2},
|
||||||
|
{rulesCase3, rules2Case3, expectedResultCase3},
|
||||||
|
{rulesCase4, rules2Case4, expectedResultCase4},
|
||||||
|
{rulesCase5, rules2Case5, expectedResultCase5},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.rules.Union(testCase.rules2)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(testCase.expectedResult, result) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRulesDifference(t *testing.T) {
|
||||||
|
rulesCase1 := make(Rules)
|
||||||
|
rules2Case1 := make(Rules)
|
||||||
|
expectedResultCase1 := make(Rules)
|
||||||
|
|
||||||
|
rulesCase2 := make(Rules)
|
||||||
|
rules2Case2 := make(Rules)
|
||||||
|
rules2Case2.Add(NewPattern("*", "*"), TargetID{"1", "webhook"})
|
||||||
|
expectedResultCase2 := make(Rules)
|
||||||
|
|
||||||
|
rulesCase3 := make(Rules)
|
||||||
|
rulesCase3.Add(NewPattern("*", "*"), TargetID{"1", "webhook"})
|
||||||
|
rules2Case3 := make(Rules)
|
||||||
|
expectedResultCase3 := make(Rules)
|
||||||
|
expectedResultCase3.Add(NewPattern("*", "*"), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesCase4 := make(Rules)
|
||||||
|
rulesCase4.Add(NewPattern("*", "*"), TargetID{"1", "webhook"})
|
||||||
|
rules2Case4 := make(Rules)
|
||||||
|
rules2Case4.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
rules2Case4.Add(NewPattern("", "*.png"), TargetID{"2", "amqp"})
|
||||||
|
expectedResultCase4 := make(Rules)
|
||||||
|
expectedResultCase4.Add(NewPattern("*", "*"), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesCase5 := make(Rules)
|
||||||
|
rulesCase5.Add(NewPattern("*", ""), TargetID{"1", "webhook"})
|
||||||
|
rulesCase5.Add(NewPattern("", "*"), TargetID{"2", "amqp"})
|
||||||
|
rules2Case5 := make(Rules)
|
||||||
|
rules2Case5.Add(NewPattern("2010*", ""), TargetID{"1", "webhook"})
|
||||||
|
rules2Case5.Add(NewPattern("", "*"), TargetID{"2", "amqp"})
|
||||||
|
expectedResultCase5 := make(Rules)
|
||||||
|
expectedResultCase5.Add(NewPattern("*", ""), TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
rules Rules
|
||||||
|
rules2 Rules
|
||||||
|
expectedResult Rules
|
||||||
|
}{
|
||||||
|
{rulesCase1, rules2Case1, expectedResultCase1},
|
||||||
|
{rulesCase2, rules2Case2, expectedResultCase2},
|
||||||
|
{rulesCase3, rules2Case3, expectedResultCase3},
|
||||||
|
{rulesCase4, rules2Case4, expectedResultCase4},
|
||||||
|
{rulesCase5, rules2Case5, expectedResultCase5},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.rules.Difference(testCase.rules2)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(testCase.expectedResult, result) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
// RulesMap - map of rules for every event name.
|
||||||
|
type RulesMap map[Name]Rules
|
||||||
|
|
||||||
|
// add - adds event names, prefixes, suffixes and target ID to rules map.
|
||||||
|
func (rulesMap RulesMap) add(eventNames []Name, pattern string, targetID TargetID) {
|
||||||
|
rules := make(Rules)
|
||||||
|
rules.Add(pattern, targetID)
|
||||||
|
|
||||||
|
for _, eventName := range eventNames {
|
||||||
|
for _, name := range eventName.Expand() {
|
||||||
|
rulesMap[name] = rulesMap[name].Union(rules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone - returns copy of this rules map.
|
||||||
|
func (rulesMap RulesMap) Clone() RulesMap {
|
||||||
|
rulesMapCopy := make(RulesMap)
|
||||||
|
|
||||||
|
for eventName, rules := range rulesMap {
|
||||||
|
rulesMapCopy[eventName] = rules.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
return rulesMapCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add - adds given rules map.
|
||||||
|
func (rulesMap RulesMap) Add(rulesMap2 RulesMap) {
|
||||||
|
for eventName, rules := range rulesMap2 {
|
||||||
|
rulesMap[eventName] = rules.Union(rulesMap[eventName])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove - removes given rules map.
|
||||||
|
func (rulesMap RulesMap) Remove(rulesMap2 RulesMap) {
|
||||||
|
for eventName, rules := range rulesMap {
|
||||||
|
if nr := rules.Difference(rulesMap2[eventName]); len(nr) != 0 {
|
||||||
|
rulesMap[eventName] = nr
|
||||||
|
} else {
|
||||||
|
delete(rulesMap, eventName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match - returns TargetIDSet matching object name and event name in rules map.
|
||||||
|
func (rulesMap RulesMap) Match(eventName Name, objectName string) TargetIDSet {
|
||||||
|
return rulesMap[eventName].Match(objectName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRulesMap - creates new rules map with given values.
|
||||||
|
func NewRulesMap(eventNames []Name, pattern string, targetID TargetID) RulesMap {
|
||||||
|
// If pattern is empty, add '*' wildcard to match all.
|
||||||
|
if pattern == "" {
|
||||||
|
pattern = "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesMap := make(RulesMap)
|
||||||
|
rulesMap.add(eventNames, pattern, targetID)
|
||||||
|
return rulesMap
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRulesMapClone(t *testing.T) {
|
||||||
|
rulesMapCase1 := make(RulesMap)
|
||||||
|
rulesMapToAddCase1 := NewRulesMap([]Name{ObjectCreatedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesMapCase2 := NewRulesMap([]Name{ObjectCreatedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
rulesMapToAddCase2 := NewRulesMap([]Name{ObjectCreatedAll}, "2010*.jpg", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesMapCase3 := NewRulesMap([]Name{ObjectCreatedAll}, "2010*.jpg", TargetID{"1", "webhook"})
|
||||||
|
rulesMapToAddCase3 := NewRulesMap([]Name{ObjectCreatedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
rulesMap RulesMap
|
||||||
|
rulesMapToAdd RulesMap
|
||||||
|
}{
|
||||||
|
{rulesMapCase1, rulesMapToAddCase1},
|
||||||
|
{rulesMapCase2, rulesMapToAddCase2},
|
||||||
|
{rulesMapCase3, rulesMapToAddCase3},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.rulesMap.Clone()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, testCase.rulesMap) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.rulesMap, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(testCase.rulesMapToAdd)
|
||||||
|
if reflect.DeepEqual(result, testCase.rulesMap) {
|
||||||
|
t.Fatalf("test %v: result: expected: not equal, got: equal", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRulesMapAdd(t *testing.T) {
|
||||||
|
rulesMapCase1 := make(RulesMap)
|
||||||
|
rulesMapToAddCase1 := make(RulesMap)
|
||||||
|
expectedResultCase1 := make(RulesMap)
|
||||||
|
|
||||||
|
rulesMapCase2 := make(RulesMap)
|
||||||
|
rulesMapToAddCase2 := NewRulesMap([]Name{ObjectCreatedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
expectedResultCase2 := NewRulesMap([]Name{ObjectCreatedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesMapCase3 := NewRulesMap([]Name{ObjectCreatedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
rulesMapToAddCase3 := NewRulesMap([]Name{ObjectCreatedAll}, "2010*.jpg", TargetID{"1", "webhook"})
|
||||||
|
expectedResultCase3 := NewRulesMap([]Name{ObjectCreatedAll}, "2010*.jpg", TargetID{"1", "webhook"})
|
||||||
|
expectedResultCase3.add([]Name{ObjectCreatedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
rulesMap RulesMap
|
||||||
|
rulesMapToAdd RulesMap
|
||||||
|
expectedResult RulesMap
|
||||||
|
}{
|
||||||
|
{rulesMapCase1, rulesMapToAddCase1, expectedResultCase1},
|
||||||
|
{rulesMapCase2, rulesMapToAddCase2, expectedResultCase2},
|
||||||
|
{rulesMapCase3, rulesMapToAddCase3, expectedResultCase3},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
testCase.rulesMap.Add(testCase.rulesMapToAdd)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(testCase.rulesMap, testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, testCase.rulesMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRulesMapRemove(t *testing.T) {
|
||||||
|
rulesMapCase1 := make(RulesMap)
|
||||||
|
rulesMapToAddCase1 := make(RulesMap)
|
||||||
|
expectedResultCase1 := make(RulesMap)
|
||||||
|
|
||||||
|
rulesMapCase2 := NewRulesMap([]Name{ObjectCreatedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
rulesMapToAddCase2 := NewRulesMap([]Name{ObjectCreatedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
expectedResultCase2 := make(RulesMap)
|
||||||
|
|
||||||
|
rulesMapCase3 := NewRulesMap([]Name{ObjectCreatedAll}, "2010*.jpg", TargetID{"1", "webhook"})
|
||||||
|
rulesMapCase3.add([]Name{ObjectCreatedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
rulesMapToAddCase3 := NewRulesMap([]Name{ObjectCreatedAll}, "2010*.jpg", TargetID{"1", "webhook"})
|
||||||
|
expectedResultCase3 := NewRulesMap([]Name{ObjectCreatedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
rulesMap RulesMap
|
||||||
|
rulesMapToAdd RulesMap
|
||||||
|
expectedResult RulesMap
|
||||||
|
}{
|
||||||
|
{rulesMapCase1, rulesMapToAddCase1, expectedResultCase1},
|
||||||
|
{rulesMapCase2, rulesMapToAddCase2, expectedResultCase2},
|
||||||
|
{rulesMapCase3, rulesMapToAddCase3, expectedResultCase3},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
testCase.rulesMap.Remove(testCase.rulesMapToAdd)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(testCase.rulesMap, testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, testCase.rulesMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRulesMapMatch(t *testing.T) {
|
||||||
|
rulesMapCase1 := make(RulesMap)
|
||||||
|
|
||||||
|
rulesMapCase2 := NewRulesMap([]Name{ObjectCreatedAll}, "*", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesMapCase3 := NewRulesMap([]Name{ObjectCreatedAll}, "2010*.jpg", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesMapCase4 := NewRulesMap([]Name{ObjectCreatedAll}, "2010*.jpg", TargetID{"1", "webhook"})
|
||||||
|
rulesMapCase4.add([]Name{ObjectCreatedAll}, "*", TargetID{"2", "amqp"})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
rulesMap RulesMap
|
||||||
|
eventName Name
|
||||||
|
objectName string
|
||||||
|
expectedResult TargetIDSet
|
||||||
|
}{
|
||||||
|
{rulesMapCase1, ObjectCreatedPut, "2010/photo.jpg", NewTargetIDSet()},
|
||||||
|
{rulesMapCase2, ObjectCreatedPut, "2010/photo.jpg", NewTargetIDSet(TargetID{"1", "webhook"})},
|
||||||
|
{rulesMapCase3, ObjectCreatedPut, "2000/photo.png", NewTargetIDSet()},
|
||||||
|
{rulesMapCase4, ObjectCreatedPut, "2000/photo.png", NewTargetIDSet(TargetID{"2", "amqp"})},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.rulesMap.Match(testCase.eventName, testCase.objectName)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRulesMap(t *testing.T) {
|
||||||
|
rulesMapCase1 := make(RulesMap)
|
||||||
|
rulesMapCase1.add([]Name{ObjectAccessedGet, ObjectAccessedHead}, "*", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesMapCase2 := make(RulesMap)
|
||||||
|
rulesMapCase2.add([]Name{ObjectAccessedGet, ObjectAccessedHead, ObjectCreatedPut}, "*", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
rulesMapCase3 := make(RulesMap)
|
||||||
|
rulesMapCase3.add([]Name{ObjectRemovedDelete}, "2010*.jpg", TargetID{"1", "webhook"})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
eventNames []Name
|
||||||
|
pattern string
|
||||||
|
targetID TargetID
|
||||||
|
expectedResult RulesMap
|
||||||
|
}{
|
||||||
|
{[]Name{ObjectAccessedAll}, "", TargetID{"1", "webhook"}, rulesMapCase1},
|
||||||
|
{[]Name{ObjectAccessedAll, ObjectCreatedPut}, "", TargetID{"1", "webhook"}, rulesMapCase2},
|
||||||
|
{[]Name{ObjectRemovedDelete}, "2010*.jpg", TargetID{"1", "webhook"}, rulesMapCase3},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := NewRulesMap(testCase.eventNames, testCase.pattern, testCase.targetID)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
"github.com/streadway/amqp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AMQPArgs - AMQP target arguments.
|
||||||
|
type AMQPArgs struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
URL xnet.URL `json:"url"`
|
||||||
|
Exchange string `json:"exchange"`
|
||||||
|
RoutingKey string `json:"routingKey"`
|
||||||
|
ExchangeType string `json:"exchangeType"`
|
||||||
|
DeliveryMode uint8 `json:"deliveryMode"`
|
||||||
|
Mandatory bool `json:"mandatory"`
|
||||||
|
Immediate bool `json:"immediate"`
|
||||||
|
Durable bool `json:"durable"`
|
||||||
|
Internal bool `json:"internal"`
|
||||||
|
NoWait bool `json:"noWait"`
|
||||||
|
AutoDeleted bool `json:"autoDeleted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AMQPTarget - AMQP target
|
||||||
|
type AMQPTarget struct {
|
||||||
|
id event.TargetID
|
||||||
|
args AMQPArgs
|
||||||
|
conn *amqp.Connection
|
||||||
|
connMutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID - returns TargetID.
|
||||||
|
func (target *AMQPTarget) ID() event.TargetID {
|
||||||
|
return target.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (target *AMQPTarget) channel() (*amqp.Channel, error) {
|
||||||
|
isAMQPClosedErr := func(err error) bool {
|
||||||
|
if err == amqp.ErrClosed {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if nerr, ok := err.(*net.OpError); ok {
|
||||||
|
return (nerr.Err.Error() == "use of closed network connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
target.connMutex.Lock()
|
||||||
|
defer target.connMutex.Unlock()
|
||||||
|
|
||||||
|
ch, err := target.conn.Channel()
|
||||||
|
if err == nil {
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAMQPClosedErr(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var conn *amqp.Connection
|
||||||
|
if conn, err = amqp.Dial(target.args.URL.String()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch, err = conn.Channel(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
target.conn = conn
|
||||||
|
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends event to AMQP.
|
||||||
|
func (target *AMQPTarget) Send(eventData event.Event) error {
|
||||||
|
ch, err := target.channel()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
// FIXME: log returned error. ignore time being.
|
||||||
|
_ = ch.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := eventData.S3.Bucket.Name + "/" + objectName
|
||||||
|
|
||||||
|
data, err := json.Marshal(event.Log{eventData.EventName, key, []event.Event{eventData}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ch.ExchangeDeclare(target.args.Exchange, target.args.ExchangeType, target.args.Durable,
|
||||||
|
target.args.AutoDeleted, target.args.Internal, target.args.NoWait, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ch.Publish(target.args.Exchange, target.args.RoutingKey, target.args.Mandatory,
|
||||||
|
target.args.Immediate, amqp.Publishing{
|
||||||
|
ContentType: "application/json",
|
||||||
|
DeliveryMode: target.args.DeliveryMode,
|
||||||
|
Body: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close - does nothing and available for interface compatibility.
|
||||||
|
func (target *AMQPTarget) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAMQPTarget - creates new AMQP target.
|
||||||
|
func NewAMQPTarget(id string, args AMQPArgs) (*AMQPTarget, error) {
|
||||||
|
conn, err := amqp.Dial(args.URL.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AMQPTarget{
|
||||||
|
id: event.TargetID{id, "amqp"},
|
||||||
|
args: args,
|
||||||
|
conn: conn,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
|
||||||
|
"gopkg.in/olivere/elastic.v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ElasticsearchArgs - Elasticsearch target arguments.
|
||||||
|
type ElasticsearchArgs struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
URL xnet.URL `json:"url"`
|
||||||
|
Index string `json:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ElasticsearchTarget - Elasticsearch target.
|
||||||
|
type ElasticsearchTarget struct {
|
||||||
|
id event.TargetID
|
||||||
|
args ElasticsearchArgs
|
||||||
|
client *elastic.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID - returns target ID.
|
||||||
|
func (target *ElasticsearchTarget) ID() event.TargetID {
|
||||||
|
return target.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends event to Elasticsearch.
|
||||||
|
func (target *ElasticsearchTarget) Send(eventData event.Event) (err error) {
|
||||||
|
var key string
|
||||||
|
|
||||||
|
remove := func() error {
|
||||||
|
_, err := target.client.Delete().Index(target.args.Index).Type("event").Id(key).Do(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
update := func() error {
|
||||||
|
_, err := target.client.Index().Index(target.args.Index).Type("event").BodyJson(map[string]interface{}{"Records": []event.Event{eventData}}).Id(key).Do(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
add := func() error {
|
||||||
|
eventTime, err := time.Parse(event.AMZTimeFormat, eventData.EventTime)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
eventTimeMS := fmt.Sprintf("%d", eventTime.UnixNano()/1000000)
|
||||||
|
_, err = target.client.Index().Index(target.args.Index).Type("event").Timestamp(eventTimeMS).BodyJson(map[string]interface{}{"Records": []event.Event{eventData}}).Do(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.args.Format == event.NamespaceFormat {
|
||||||
|
objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
key = eventData.S3.Bucket.Name + "/" + objectName
|
||||||
|
if eventData.EventName == event.ObjectRemovedDelete {
|
||||||
|
err = remove()
|
||||||
|
} else {
|
||||||
|
err = update()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.args.Format == event.AccessFormat {
|
||||||
|
return add()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close - does nothing and available for interface compatibility.
|
||||||
|
func (target *ElasticsearchTarget) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewElasticsearchTarget - creates new Elasticsearch target.
|
||||||
|
func NewElasticsearchTarget(id string, args ElasticsearchArgs) (*ElasticsearchTarget, error) {
|
||||||
|
client, err := elastic.NewClient(elastic.SetURL(args.URL.String()), elastic.SetSniff(false), elastic.SetMaxRetries(10))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := client.IndexExists(args.Index).Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
var createIndex *elastic.IndicesCreateResult
|
||||||
|
if createIndex, err = client.CreateIndex(args.Index).Do(context.Background()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !createIndex.Acknowledged {
|
||||||
|
return nil, fmt.Errorf("index %v not created", args.Index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ElasticsearchTarget{
|
||||||
|
id: event.TargetID{id, "elasticsearch"},
|
||||||
|
args: args,
|
||||||
|
client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
"github.com/skyrings/skyring-common/tools/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPClientTarget - HTTP client target.
|
||||||
|
type HTTPClientTarget struct {
|
||||||
|
id event.TargetID
|
||||||
|
w http.ResponseWriter
|
||||||
|
eventCh chan []byte
|
||||||
|
DoneCh chan struct{}
|
||||||
|
stopCh chan struct{}
|
||||||
|
isStopped uint32
|
||||||
|
isRunning uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID - returns target ID.
|
||||||
|
func (target HTTPClientTarget) ID() event.TargetID {
|
||||||
|
return target.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (target *HTTPClientTarget) start() {
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
atomic.AddUint32(&target.isRunning, 1)
|
||||||
|
|
||||||
|
// Close DoneCh to indicate we are done.
|
||||||
|
close(target.DoneCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
write := func(event []byte) error {
|
||||||
|
if _, err := target.w.Write(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
target.w.(http.Flusher).Flush()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
|
||||||
|
select {
|
||||||
|
case <-target.stopCh:
|
||||||
|
// We are asked to stop.
|
||||||
|
return
|
||||||
|
case event, ok := <-target.eventCh:
|
||||||
|
if !ok {
|
||||||
|
// Got read error. Exit the goroutine.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := write(event); err != nil {
|
||||||
|
// Got write error to the client. Exit the goroutine.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-keepAliveTicker.C:
|
||||||
|
if err := write([]byte(" ")); err != nil {
|
||||||
|
// Got write error to the client. Exit the goroutine.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends event to HTTP client.
|
||||||
|
func (target *HTTPClientTarget) Send(eventData event.Event) error {
|
||||||
|
if atomic.LoadUint32(&target.isRunning) != 0 {
|
||||||
|
return errors.New("closed http connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(struct{ Records []event.Event }{[]event.Event{eventData}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = append(data, byte('\n'))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case target.eventCh <- data:
|
||||||
|
return nil
|
||||||
|
case <-target.DoneCh:
|
||||||
|
return errors.New("error in sending event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close - closes underneath goroutine.
|
||||||
|
func (target *HTTPClientTarget) Close() error {
|
||||||
|
atomic.AddUint32(&target.isStopped, 1)
|
||||||
|
if atomic.LoadUint32(&target.isStopped) == 1 {
|
||||||
|
close(target.stopCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetNewUUID() string {
|
||||||
|
uuid, err := uuid.New()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("%s. Unable to generate random UUID", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuid.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTTPClientTarget - creates new HTTP client target.
|
||||||
|
func NewHTTPClientTarget(host xnet.Host, w http.ResponseWriter) *HTTPClientTarget {
|
||||||
|
c := &HTTPClientTarget{
|
||||||
|
id: event.TargetID{"httpclient" + "+" + mustGetNewUUID() + "+" + host.Name, host.Port.String()},
|
||||||
|
w: w,
|
||||||
|
eventCh: make(chan []byte),
|
||||||
|
DoneCh: make(chan struct{}),
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
c.start()
|
||||||
|
return c
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
|
||||||
|
sarama "gopkg.in/Shopify/sarama.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KafkaArgs - Kafka target arguments.
|
||||||
|
type KafkaArgs struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Brokers []xnet.Host `json:"brokers"`
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KafkaTarget - Kafka target.
|
||||||
|
type KafkaTarget struct {
|
||||||
|
id event.TargetID
|
||||||
|
args KafkaArgs
|
||||||
|
producer sarama.SyncProducer
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID - returns target ID.
|
||||||
|
func (target *KafkaTarget) ID() event.TargetID {
|
||||||
|
return target.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends event to Kafka.
|
||||||
|
func (target *KafkaTarget) Send(eventData event.Event) error {
|
||||||
|
objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := eventData.S3.Bucket.Name + "/" + objectName
|
||||||
|
|
||||||
|
data, err := json.Marshal(event.Log{eventData.EventName, key, []event.Event{eventData}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := sarama.ProducerMessage{
|
||||||
|
Topic: target.args.Topic,
|
||||||
|
Key: sarama.StringEncoder(key),
|
||||||
|
Value: sarama.ByteEncoder(data),
|
||||||
|
}
|
||||||
|
_, _, err = target.producer.SendMessage(&msg)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close - closes underneath kafka connection.
|
||||||
|
func (target *KafkaTarget) Close() error {
|
||||||
|
return target.producer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKafkaTarget - creates new Kafka target.
|
||||||
|
func NewKafkaTarget(id string, args KafkaArgs) (*KafkaTarget, error) {
|
||||||
|
config := sarama.NewConfig()
|
||||||
|
config.Producer.RequiredAcks = sarama.WaitForAll
|
||||||
|
config.Producer.Retry.Max = 10
|
||||||
|
config.Producer.Return.Successes = true
|
||||||
|
|
||||||
|
brokers := []string{}
|
||||||
|
for _, broker := range args.Brokers {
|
||||||
|
brokers = append(brokers, broker.String())
|
||||||
|
}
|
||||||
|
producer, err := sarama.NewSyncProducer(brokers, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &KafkaTarget{
|
||||||
|
id: event.TargetID{id, "kafka"},
|
||||||
|
args: args,
|
||||||
|
producer: producer,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/eclipse/paho.mqtt.golang"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MQTTArgs - MQTT target arguments.
|
||||||
|
type MQTTArgs struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Broker xnet.URL `json:"broker"`
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
QoS byte `json:"qos"`
|
||||||
|
ClientID string `json:"clientId"`
|
||||||
|
User string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
MaxReconnectInterval time.Duration `json:"reconnectInterval"`
|
||||||
|
KeepAlive time.Duration `json:"keepAliveInterval"`
|
||||||
|
RootCAs *x509.CertPool `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MQTTTarget - MQTT target.
|
||||||
|
type MQTTTarget struct {
|
||||||
|
id event.TargetID
|
||||||
|
args MQTTArgs
|
||||||
|
client mqtt.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID - returns target ID.
|
||||||
|
func (target *MQTTTarget) ID() event.TargetID {
|
||||||
|
return target.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends event to MQTT.
|
||||||
|
func (target *MQTTTarget) Send(eventData event.Event) error {
|
||||||
|
if !target.client.IsConnected() {
|
||||||
|
token := target.client.Connect()
|
||||||
|
if token.Wait() {
|
||||||
|
if err := token.Error(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := eventData.S3.Bucket.Name + "/" + objectName
|
||||||
|
|
||||||
|
data, err := json.Marshal(event.Log{eventData.EventName, key, []event.Event{eventData}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
token := target.client.Publish(target.args.Topic, target.args.QoS, false, string(data))
|
||||||
|
|
||||||
|
if token.Wait() {
|
||||||
|
return token.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close - does nothing and available for interface compatibility.
|
||||||
|
func (target *MQTTTarget) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMQTTTarget - creates new MQTT target.
|
||||||
|
func NewMQTTTarget(id string, args MQTTArgs) (*MQTTTarget, error) {
|
||||||
|
options := &mqtt.ClientOptions{
|
||||||
|
ClientID: args.ClientID,
|
||||||
|
CleanSession: true,
|
||||||
|
Username: args.User,
|
||||||
|
Password: args.Password,
|
||||||
|
MaxReconnectInterval: args.MaxReconnectInterval,
|
||||||
|
KeepAlive: args.KeepAlive,
|
||||||
|
TLSConfig: tls.Config{RootCAs: args.RootCAs},
|
||||||
|
}
|
||||||
|
options.AddBroker(args.Broker.String())
|
||||||
|
|
||||||
|
client := mqtt.NewClient(options)
|
||||||
|
token := client.Connect()
|
||||||
|
if token.Wait() && token.Error() != nil {
|
||||||
|
return nil, token.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MQTTTarget{
|
||||||
|
id: event.TargetID{id, "mqtt"},
|
||||||
|
args: args,
|
||||||
|
client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,226 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// MySQL Notifier implementation. Two formats, "namespace" and
|
||||||
|
// "access" are supported.
|
||||||
|
//
|
||||||
|
// * Namespace format
|
||||||
|
//
|
||||||
|
// On each create or update object event in Minio Object storage
|
||||||
|
// server, a row is created or updated in the table in MySQL. On each
|
||||||
|
// object removal, the corresponding row is deleted from the table.
|
||||||
|
//
|
||||||
|
// A table with a specific structure (column names, column types, and
|
||||||
|
// primary key/uniqueness constraint) is used. The user may set the
|
||||||
|
// table name in the configuration. A sample SQL command that creates
|
||||||
|
// a command with the required structure is:
|
||||||
|
//
|
||||||
|
// CREATE TABLE myminio (
|
||||||
|
// key_name VARCHAR(2048),
|
||||||
|
// value JSONB,
|
||||||
|
// PRIMARY KEY (key_name),
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// MySQL's "INSERT ... ON DUPLICATE ..." feature (UPSERT) is used
|
||||||
|
// here. The implementation has been tested with MySQL Ver 14.14
|
||||||
|
// Distrib 5.7.17.
|
||||||
|
//
|
||||||
|
// * Access format
|
||||||
|
//
|
||||||
|
// On each event, a row is appended to the configured table. There is
|
||||||
|
// no deletion or modification of existing rows.
|
||||||
|
//
|
||||||
|
// A different table schema is used for this format. A sample SQL
|
||||||
|
// commant that creates a table with the required structure is:
|
||||||
|
//
|
||||||
|
// CREATE TABLE myminio (
|
||||||
|
// event_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
// event_data JSONB
|
||||||
|
// );
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mysqlTableExists = `SELECT 1 FROM %s;`
|
||||||
|
mysqlCreateNamespaceTable = `CREATE TABLE %s (key_name VARCHAR(2048), value JSON, PRIMARY KEY (key_name));`
|
||||||
|
mysqlCreateAccessTable = `CREATE TABLE %s (event_time DATETIME NOT NULL, event_data JSON);`
|
||||||
|
|
||||||
|
mysqlUpdateRow = `INSERT INTO %s (key_name, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value);`
|
||||||
|
mysqlDeleteRow = `DELETE FROM %s WHERE key_name = ?;`
|
||||||
|
mysqlInsertRow = `INSERT INTO %s (event_time, event_data) VALUES (?, ?);`
|
||||||
|
)
|
||||||
|
|
||||||
|
// MySQLArgs - MySQL target arguments.
|
||||||
|
type MySQLArgs struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
DSN string `json:"dsnString"`
|
||||||
|
Table string `json:"table"`
|
||||||
|
Host xnet.URL `json:"host"`
|
||||||
|
Port string `json:"port"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Database string `json:"database"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MySQLTarget - MySQL target.
|
||||||
|
type MySQLTarget struct {
|
||||||
|
id event.TargetID
|
||||||
|
args MySQLArgs
|
||||||
|
updateStmt *sql.Stmt
|
||||||
|
deleteStmt *sql.Stmt
|
||||||
|
insertStmt *sql.Stmt
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID - returns target ID.
|
||||||
|
func (target *MySQLTarget) ID() event.TargetID {
|
||||||
|
return target.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends event to MySQL.
|
||||||
|
func (target *MySQLTarget) Send(eventData event.Event) error {
|
||||||
|
if target.args.Format == event.NamespaceFormat {
|
||||||
|
objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := eventData.S3.Bucket.Name + "/" + objectName
|
||||||
|
|
||||||
|
if eventData.EventName == event.ObjectRemovedDelete {
|
||||||
|
_, err = target.deleteStmt.Exec(key)
|
||||||
|
} else {
|
||||||
|
var data []byte
|
||||||
|
if data, err = json.Marshal(struct{ Records []event.Event }{[]event.Event{eventData}}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = target.updateStmt.Exec(key, data)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.args.Format == event.AccessFormat {
|
||||||
|
eventTime, err := time.Parse(event.AMZTimeFormat, eventData.EventTime)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(struct{ Records []event.Event }{[]event.Event{eventData}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = target.insertStmt.Exec(eventTime, data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close - closes underneath connections to MySQL database.
|
||||||
|
func (target *MySQLTarget) Close() error {
|
||||||
|
if target.updateStmt != nil {
|
||||||
|
// FIXME: log returned error. ignore time being.
|
||||||
|
_ = target.updateStmt.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.deleteStmt != nil {
|
||||||
|
// FIXME: log returned error. ignore time being.
|
||||||
|
_ = target.deleteStmt.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.insertStmt != nil {
|
||||||
|
// FIXME: log returned error. ignore time being.
|
||||||
|
_ = target.insertStmt.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return target.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMySQLTarget - creates new MySQL target.
|
||||||
|
func NewMySQLTarget(id string, args MySQLArgs) (*MySQLTarget, error) {
|
||||||
|
if args.DSN == "" {
|
||||||
|
config := mysql.Config{
|
||||||
|
User: args.User,
|
||||||
|
Passwd: args.Password,
|
||||||
|
Net: "tcp",
|
||||||
|
Addr: args.Host.String() + ":" + args.Port,
|
||||||
|
DBName: args.Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
args.DSN = config.FormatDSN()
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sql.Open("mysql", args.DSN)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.Ping(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = db.Exec(fmt.Sprintf(mysqlTableExists, args.Table)); err != nil {
|
||||||
|
createStmt := mysqlCreateNamespaceTable
|
||||||
|
if args.Format == event.AccessFormat {
|
||||||
|
createStmt = mysqlCreateAccessTable
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = db.Exec(fmt.Sprintf(createStmt, args.Table)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateStmt, deleteStmt, insertStmt *sql.Stmt
|
||||||
|
switch args.Format {
|
||||||
|
case event.NamespaceFormat:
|
||||||
|
// insert or update statement
|
||||||
|
if updateStmt, err = db.Prepare(fmt.Sprintf(mysqlUpdateRow, args.Table)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// delete statement
|
||||||
|
if deleteStmt, err = db.Prepare(fmt.Sprintf(mysqlDeleteRow, args.Table)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case event.AccessFormat:
|
||||||
|
// insert statement
|
||||||
|
if insertStmt, err = db.Prepare(fmt.Sprintf(mysqlInsertRow, args.Table)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MySQLTarget{
|
||||||
|
id: event.TargetID{id, "mysql"},
|
||||||
|
args: args,
|
||||||
|
updateStmt: updateStmt,
|
||||||
|
deleteStmt: deleteStmt,
|
||||||
|
insertStmt: insertStmt,
|
||||||
|
db: db,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
"github.com/nats-io/go-nats-streaming"
|
||||||
|
"github.com/nats-io/nats"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NATSArgs - NATS target arguments.
|
||||||
|
type NATSArgs struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Address xnet.Host `json:"address"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Secure bool `json:"secure"`
|
||||||
|
PingInterval int64 `json:"pingInterval"`
|
||||||
|
Streaming struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
ClusterID string `json:"clusterID"`
|
||||||
|
ClientID string `json:"clientID"`
|
||||||
|
Async bool `json:"async"`
|
||||||
|
MaxPubAcksInflight int `json:"maxPubAcksInflight"`
|
||||||
|
} `json:"streaming"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NATSTarget - NATS target.
|
||||||
|
type NATSTarget struct {
|
||||||
|
id event.TargetID
|
||||||
|
args NATSArgs
|
||||||
|
natsConn *nats.Conn
|
||||||
|
stanConn stan.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID - returns target ID.
|
||||||
|
func (target *NATSTarget) ID() event.TargetID {
|
||||||
|
return target.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends event to NATS.
|
||||||
|
func (target *NATSTarget) Send(eventData event.Event) (err error) {
|
||||||
|
objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := eventData.S3.Bucket.Name + "/" + objectName
|
||||||
|
|
||||||
|
data, err := json.Marshal(event.Log{eventData.EventName, key, []event.Event{eventData}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.stanConn != nil {
|
||||||
|
if target.args.Streaming.Async {
|
||||||
|
_, err = target.stanConn.PublishAsync(target.args.Subject, data, nil)
|
||||||
|
} else {
|
||||||
|
err = target.stanConn.Publish(target.args.Subject, data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = target.natsConn.Publish(target.args.Subject, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close - closes underneath connections to NATS server.
|
||||||
|
func (target *NATSTarget) Close() (err error) {
|
||||||
|
if target.stanConn != nil {
|
||||||
|
err = target.stanConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.natsConn != nil {
|
||||||
|
target.natsConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNATSTarget - creates new NATS target.
|
||||||
|
func NewNATSTarget(id string, args NATSArgs) (*NATSTarget, error) {
|
||||||
|
var natsConn *nats.Conn
|
||||||
|
var stanConn stan.Conn
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if args.Streaming.Enable {
|
||||||
|
scheme := "nats"
|
||||||
|
if args.Secure {
|
||||||
|
scheme = "tls"
|
||||||
|
}
|
||||||
|
addressURL := scheme + "://" + args.Username + ":" + args.Password + "@" + args.Address.String()
|
||||||
|
|
||||||
|
clientID := args.Streaming.ClientID
|
||||||
|
if clientID == "" {
|
||||||
|
clientID = mustGetNewUUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
connOpts := []stan.Option{stan.NatsURL(addressURL)}
|
||||||
|
if args.Streaming.MaxPubAcksInflight > 0 {
|
||||||
|
connOpts = append(connOpts, stan.MaxPubAcksInflight(args.Streaming.MaxPubAcksInflight))
|
||||||
|
}
|
||||||
|
|
||||||
|
stanConn, err = stan.Connect(args.Streaming.ClusterID, clientID, connOpts...)
|
||||||
|
} else {
|
||||||
|
options := nats.DefaultOptions
|
||||||
|
options.Url = "nats://" + args.Address.String()
|
||||||
|
options.User = args.Username
|
||||||
|
options.Password = args.Password
|
||||||
|
options.Token = args.Token
|
||||||
|
options.Secure = args.Secure
|
||||||
|
natsConn, err = options.Connect()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &NATSTarget{
|
||||||
|
id: event.TargetID{id, "nats"},
|
||||||
|
args: args,
|
||||||
|
stanConn: stanConn,
|
||||||
|
natsConn: natsConn,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,233 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// PostgreSQL Notifier implementation. Two formats, "namespace" and
|
||||||
|
// "access" are supported.
|
||||||
|
//
|
||||||
|
// * Namespace format
|
||||||
|
//
|
||||||
|
// On each create or update object event in Minio Object storage
|
||||||
|
// server, a row is created or updated in the table in Postgres. On
|
||||||
|
// each object removal, the corresponding row is deleted from the
|
||||||
|
// table.
|
||||||
|
//
|
||||||
|
// A table with a specific structure (column names, column types, and
|
||||||
|
// primary key/uniqueness constraint) is used. The user may set the
|
||||||
|
// table name in the configuration. A sample SQL command that creates
|
||||||
|
// a table with the required structure is:
|
||||||
|
//
|
||||||
|
// CREATE TABLE myminio (
|
||||||
|
// key VARCHAR PRIMARY KEY,
|
||||||
|
// value JSONB
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// PostgreSQL's "INSERT ... ON CONFLICT ... DO UPDATE ..." feature
|
||||||
|
// (UPSERT) is used here, so the minimum version of PostgreSQL
|
||||||
|
// required is 9.5.
|
||||||
|
//
|
||||||
|
// * Access format
|
||||||
|
//
|
||||||
|
// On each event, a row is appended to the configured table. There is
|
||||||
|
// no deletion or modification of existing rows.
|
||||||
|
//
|
||||||
|
// A different table schema is used for this format. A sample SQL
|
||||||
|
// commant that creates a table with the required structure is:
|
||||||
|
//
|
||||||
|
// CREATE TABLE myminio (
|
||||||
|
// event_time TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
// event_data JSONB
|
||||||
|
// );
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq" // Register postgres driver
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
psqlTableExists = `SELECT 1 FROM %s;`
|
||||||
|
psqlCreateNamespaceTable = `CREATE TABLE %s (key VARCHAR PRIMARY KEY, value JSONB);`
|
||||||
|
psqlCreateAccessTable = `CREATE TABLE %s (event_time TIMESTAMP WITH TIME ZONE NOT NULL, event_data JSONB);`
|
||||||
|
|
||||||
|
psqlUpdateRow = `INSERT INTO %s (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;`
|
||||||
|
psqlDeleteRow = `DELETE FROM %s WHERE key = $1;`
|
||||||
|
psqlInsertRow = `INSERT INTO %s (event_time, event_data) VALUES ($1, $2);`
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostgreSQLArgs - PostgreSQL target arguments.
|
||||||
|
type PostgreSQLArgs struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
ConnectionString string `json:"connectionString"`
|
||||||
|
Table string `json:"table"`
|
||||||
|
Host xnet.URL `json:"host"` // default: localhost
|
||||||
|
Port string `json:"port"` // default: 5432
|
||||||
|
User string `json:"user"` // default: user running minio
|
||||||
|
Password string `json:"password"` // default: no password
|
||||||
|
Database string `json:"database"` // default: same as user
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQLTarget - PostgreSQL target.
|
||||||
|
type PostgreSQLTarget struct {
|
||||||
|
id event.TargetID
|
||||||
|
args PostgreSQLArgs
|
||||||
|
updateStmt *sql.Stmt
|
||||||
|
deleteStmt *sql.Stmt
|
||||||
|
insertStmt *sql.Stmt
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID - returns target ID.
|
||||||
|
func (target *PostgreSQLTarget) ID() event.TargetID {
|
||||||
|
return target.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends event to PostgreSQL.
|
||||||
|
func (target *PostgreSQLTarget) Send(eventData event.Event) error {
|
||||||
|
if target.args.Format == event.NamespaceFormat {
|
||||||
|
objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := eventData.S3.Bucket.Name + "/" + objectName
|
||||||
|
|
||||||
|
if eventData.EventName == event.ObjectRemovedDelete {
|
||||||
|
_, err = target.deleteStmt.Exec(key)
|
||||||
|
} else {
|
||||||
|
var data []byte
|
||||||
|
if data, err = json.Marshal(struct{ Records []event.Event }{[]event.Event{eventData}}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = target.updateStmt.Exec(key, data)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.args.Format == event.AccessFormat {
|
||||||
|
eventTime, err := time.Parse(event.AMZTimeFormat, eventData.EventTime)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(struct{ Records []event.Event }{[]event.Event{eventData}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = target.insertStmt.Exec(eventTime, data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close - closes underneath connections to PostgreSQL database.
|
||||||
|
func (target *PostgreSQLTarget) Close() error {
|
||||||
|
if target.updateStmt != nil {
|
||||||
|
// FIXME: log returned error. ignore time being.
|
||||||
|
_ = target.updateStmt.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.deleteStmt != nil {
|
||||||
|
// FIXME: log returned error. ignore time being.
|
||||||
|
_ = target.deleteStmt.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.insertStmt != nil {
|
||||||
|
// FIXME: log returned error. ignore time being.
|
||||||
|
_ = target.insertStmt.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return target.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPostgreSQLTarget - creates new PostgreSQL target.
|
||||||
|
func NewPostgreSQLTarget(id string, args PostgreSQLArgs) (*PostgreSQLTarget, error) {
|
||||||
|
params := []string{args.ConnectionString}
|
||||||
|
if !args.Host.IsEmpty() {
|
||||||
|
params = append(params, "host="+args.Host.String())
|
||||||
|
}
|
||||||
|
if args.Port != "" {
|
||||||
|
params = append(params, "port="+args.Port)
|
||||||
|
}
|
||||||
|
if args.User != "" {
|
||||||
|
params = append(params, "user="+args.User)
|
||||||
|
}
|
||||||
|
if args.Password != "" {
|
||||||
|
params = append(params, "password="+args.Password)
|
||||||
|
}
|
||||||
|
if args.Database != "" {
|
||||||
|
params = append(params, "dbname="+args.Database)
|
||||||
|
}
|
||||||
|
connStr := strings.Join(params, " ")
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.Ping(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = db.Exec(fmt.Sprintf(psqlTableExists, args.Table)); err != nil {
|
||||||
|
createStmt := psqlCreateNamespaceTable
|
||||||
|
if args.Format == event.AccessFormat {
|
||||||
|
createStmt = psqlCreateAccessTable
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = db.Exec(fmt.Sprintf(createStmt, args.Table)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateStmt, deleteStmt, insertStmt *sql.Stmt
|
||||||
|
switch args.Format {
|
||||||
|
case event.NamespaceFormat:
|
||||||
|
// insert or update statement
|
||||||
|
if updateStmt, err = db.Prepare(fmt.Sprintf(psqlUpdateRow, args.Table)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// delete statement
|
||||||
|
if deleteStmt, err = db.Prepare(fmt.Sprintf(psqlDeleteRow, args.Table)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case event.AccessFormat:
|
||||||
|
// insert statement
|
||||||
|
if insertStmt, err = db.Prepare(fmt.Sprintf(psqlInsertRow, args.Table)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PostgreSQLTarget{
|
||||||
|
id: event.TargetID{id, "postgresql"},
|
||||||
|
args: args,
|
||||||
|
updateStmt: updateStmt,
|
||||||
|
deleteStmt: deleteStmt,
|
||||||
|
insertStmt: insertStmt,
|
||||||
|
db: db,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RedisArgs - Redis target arguments.
|
||||||
|
type RedisArgs struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
Addr xnet.Host `json:"address"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedisTarget - Redis target.
|
||||||
|
type RedisTarget struct {
|
||||||
|
id event.TargetID
|
||||||
|
args RedisArgs
|
||||||
|
pool *redis.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID - returns target ID.
|
||||||
|
func (target *RedisTarget) ID() event.TargetID {
|
||||||
|
return target.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends event to Redis.
|
||||||
|
func (target *RedisTarget) Send(eventData event.Event) error {
|
||||||
|
conn := target.pool.Get()
|
||||||
|
defer func() {
|
||||||
|
// FIXME: log returned error. ignore time being.
|
||||||
|
_ = conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if target.args.Format == event.NamespaceFormat {
|
||||||
|
objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := eventData.S3.Bucket.Name + "/" + objectName
|
||||||
|
|
||||||
|
if eventData.EventName == event.ObjectRemovedDelete {
|
||||||
|
_, err = conn.Do("HDEL", target.args.Key, key)
|
||||||
|
} else {
|
||||||
|
var data []byte
|
||||||
|
if data, err = json.Marshal(struct{ Records []event.Event }{[]event.Event{eventData}}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.Do("HSET", target.args.Key, key, data)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.args.Format == event.AccessFormat {
|
||||||
|
data, err := json.Marshal([]interface{}{eventData.EventTime, []event.Event{eventData}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = conn.Do("RPUSH", target.args.Key, data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close - does nothing and available for interface compatibility.
|
||||||
|
func (target *RedisTarget) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRedisTarget - creates new Redis target.
|
||||||
|
func NewRedisTarget(id string, args RedisArgs) (*RedisTarget, error) {
|
||||||
|
pool := &redis.Pool{
|
||||||
|
MaxIdle: 3,
|
||||||
|
IdleTimeout: 2 * 60 * time.Second,
|
||||||
|
Dial: func() (redis.Conn, error) {
|
||||||
|
conn, err := redis.Dial("tcp", args.Addr.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Password == "" {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = conn.Do("AUTH", args.Password); err != nil {
|
||||||
|
// FIXME: log returned error. ignore time being.
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
},
|
||||||
|
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||||
|
_, err := c.Do("PING")
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := pool.Get()
|
||||||
|
defer func() {
|
||||||
|
// FIXME: log returned error. ignore time being.
|
||||||
|
_ = conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err := conn.Do("PING"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
typeAvailable, err := redis.String(conn.Do("TYPE", args.Key))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeAvailable != "none" {
|
||||||
|
expectedType := "hash"
|
||||||
|
if args.Format == event.AccessFormat {
|
||||||
|
expectedType = "list"
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeAvailable != expectedType {
|
||||||
|
return nil, fmt.Errorf("expected type %v does not match with available type %v", expectedType, typeAvailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RedisTarget{
|
||||||
|
id: event.TargetID{id, "redis"},
|
||||||
|
args: args,
|
||||||
|
pool: pool,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/event"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebhookArgs - Webhook target arguments.
|
||||||
|
type WebhookArgs struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Endpoint xnet.URL `json:"endpoint"`
|
||||||
|
RootCAs *x509.CertPool `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookTarget - Webhook target.
|
||||||
|
type WebhookTarget struct {
|
||||||
|
id event.TargetID
|
||||||
|
args WebhookArgs
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID - returns target ID.
|
||||||
|
func (target WebhookTarget) ID() event.TargetID {
|
||||||
|
return target.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends event to Webhook.
|
||||||
|
func (target *WebhookTarget) Send(eventData event.Event) error {
|
||||||
|
objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := eventData.S3.Bucket.Name + "/" + objectName
|
||||||
|
|
||||||
|
data, err := json.Marshal(event.Log{eventData.EventName, key, []event.Event{eventData}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", target.args.Endpoint.String(), bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// req.Header.Set("User-Agent", globalServerUserAgent)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := target.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: log returned error. ignore time being.
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK, http.StatusAccepted, http.StatusContinue:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("sending event failed with %v", resp.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close - does nothing and available for interface compatibility.
|
||||||
|
func (target *WebhookTarget) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWebhookTarget - creates new Webhook target.
|
||||||
|
func NewWebhookTarget(id string, args WebhookArgs) *WebhookTarget {
|
||||||
|
return &WebhookTarget{
|
||||||
|
id: event.TargetID{id, "webhook"},
|
||||||
|
args: args,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{RootCAs: args.RootCAs},
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
KeepAlive: 5 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
TLSHandshakeTimeout: 3 * time.Second,
|
||||||
|
ResponseHeaderTimeout: 3 * time.Second,
|
||||||
|
ExpectContinueTimeout: 2 * time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TargetID - holds identification and name strings of notification target.
|
||||||
|
type TargetID struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// String - returns string representation.
|
||||||
|
func (tid TargetID) String() string {
|
||||||
|
return tid.ID + ":" + tid.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToARN - converts to ARN.
|
||||||
|
func (tid TargetID) ToARN(region string) ARN {
|
||||||
|
return ARN{TargetID: tid, region: region}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON - encodes to JSON data.
|
||||||
|
func (tid TargetID) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(tid.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON - decodes JSON data.
|
||||||
|
func (tid *TargetID) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(data, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetID, err := parseTargetID(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*tid = *targetID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTargetID - parses string to TargetID.
|
||||||
|
func parseTargetID(s string) (*TargetID, error) {
|
||||||
|
tokens := strings.Split(s, ":")
|
||||||
|
if len(tokens) != 2 {
|
||||||
|
return nil, fmt.Errorf("invalid TargetID format '%v'", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TargetID{
|
||||||
|
ID: tokens[0],
|
||||||
|
Name: tokens[1],
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTargetDString(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tid TargetID
|
||||||
|
expectedResult string
|
||||||
|
}{
|
||||||
|
{TargetID{}, ":"},
|
||||||
|
{TargetID{"1", "webhook"}, "1:webhook"},
|
||||||
|
{TargetID{"httpclient+2e33cdee-fbec-4bdd-917e-7d8e3c5a2531", "localhost:55638"}, "httpclient+2e33cdee-fbec-4bdd-917e-7d8e3c5a2531:localhost:55638"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.tid.String()
|
||||||
|
|
||||||
|
if result != testCase.expectedResult {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetDToARN(t *testing.T) {
|
||||||
|
tid := TargetID{"1", "webhook"}
|
||||||
|
testCases := []struct {
|
||||||
|
tid TargetID
|
||||||
|
region string
|
||||||
|
expectedARN ARN
|
||||||
|
}{
|
||||||
|
{tid, "", ARN{TargetID: tid, region: ""}},
|
||||||
|
{tid, "us-east-1", ARN{TargetID: tid, region: "us-east-1"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
arn := testCase.tid.ToARN(testCase.region)
|
||||||
|
|
||||||
|
if arn != testCase.expectedARN {
|
||||||
|
t.Fatalf("test %v: ARN: expected: %v, got: %v", i+1, testCase.expectedARN, arn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetDMarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
tid TargetID
|
||||||
|
expectedData []byte
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{TargetID{}, []byte(`":"`), false},
|
||||||
|
{TargetID{"1", "webhook"}, []byte(`"1:webhook"`), false},
|
||||||
|
{TargetID{"httpclient+2e33cdee-fbec-4bdd-917e-7d8e3c5a2531", "localhost:55638"}, []byte(`"httpclient+2e33cdee-fbec-4bdd-917e-7d8e3c5a2531:localhost:55638"`), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
data, err := testCase.tid.MarshalJSON()
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(data, testCase.expectedData) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, string(testCase.expectedData), string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetDUnmarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
data []byte
|
||||||
|
expectedTargetID *TargetID
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{[]byte(`""`), nil, true},
|
||||||
|
{[]byte(`"httpclient+2e33cdee-fbec-4bdd-917e-7d8e3c5a2531:localhost:55638"`), nil, true},
|
||||||
|
{[]byte(`":"`), &TargetID{}, false},
|
||||||
|
{[]byte(`"1:webhook"`), &TargetID{"1", "webhook"}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
targetID := &TargetID{}
|
||||||
|
err := targetID.UnmarshalJSON(testCase.data)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if *targetID != *testCase.expectedTargetID {
|
||||||
|
t.Fatalf("test %v: TargetID: expected: %v, got: %v", i+1, testCase.expectedTargetID, targetID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// TargetIDSet - Set representation of TargetIDs.
|
||||||
|
type TargetIDSet map[TargetID]struct{}
|
||||||
|
|
||||||
|
// ToSlice - returns TargetID slice from TargetIDSet.
|
||||||
|
func (set TargetIDSet) ToSlice() []TargetID {
|
||||||
|
keys := make([]TargetID, 0, len(set))
|
||||||
|
for k := range set {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// String - returns string representation.
|
||||||
|
func (set TargetIDSet) String() string {
|
||||||
|
return fmt.Sprintf("%v", set.ToSlice())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone - returns copy of this set.
|
||||||
|
func (set TargetIDSet) Clone() TargetIDSet {
|
||||||
|
setCopy := NewTargetIDSet()
|
||||||
|
for k, v := range set {
|
||||||
|
setCopy[k] = v
|
||||||
|
}
|
||||||
|
return setCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
// add - adds TargetID to the set.
|
||||||
|
func (set TargetIDSet) add(targetID TargetID) {
|
||||||
|
set[targetID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union - returns union with given set as new set.
|
||||||
|
func (set TargetIDSet) Union(sset TargetIDSet) TargetIDSet {
|
||||||
|
nset := set.Clone()
|
||||||
|
|
||||||
|
for k := range sset {
|
||||||
|
nset.add(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Difference - returns diffrence with given set as new set.
|
||||||
|
func (set TargetIDSet) Difference(sset TargetIDSet) TargetIDSet {
|
||||||
|
nset := NewTargetIDSet()
|
||||||
|
for k := range set {
|
||||||
|
if _, ok := sset[k]; !ok {
|
||||||
|
nset.add(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nset
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTargetIDSet - creates new TargetID set with given TargetIDs.
|
||||||
|
func NewTargetIDSet(targetIDs ...TargetID) TargetIDSet {
|
||||||
|
set := make(TargetIDSet)
|
||||||
|
for _, targetID := range targetIDs {
|
||||||
|
set.add(targetID)
|
||||||
|
}
|
||||||
|
return set
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTargetIDSetToSlice(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
set TargetIDSet
|
||||||
|
expectedResult []TargetID
|
||||||
|
}{
|
||||||
|
{NewTargetIDSet(), []TargetID{}},
|
||||||
|
{NewTargetIDSet(TargetID{"1", "webhook"}), []TargetID{{"1", "webhook"}}},
|
||||||
|
{NewTargetIDSet(TargetID{"1", "webhook"}, TargetID{"2", "amqp"}), []TargetID{{"1", "webhook"}, {"2", "amqp"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.set.ToSlice()
|
||||||
|
|
||||||
|
if len(result) != len(testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, targetID1 := range result {
|
||||||
|
var found bool
|
||||||
|
for _, targetID2 := range testCase.expectedResult {
|
||||||
|
if reflect.DeepEqual(targetID1, targetID2) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetIDSetString(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
set TargetIDSet
|
||||||
|
expectedResult string
|
||||||
|
}{
|
||||||
|
{NewTargetIDSet(), "[]"},
|
||||||
|
{NewTargetIDSet(TargetID{"1", "webhook"}), "[1:webhook]"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.set.String()
|
||||||
|
|
||||||
|
if result != testCase.expectedResult {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetIDSetClone(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
set TargetIDSet
|
||||||
|
targetIDToAdd TargetID
|
||||||
|
}{
|
||||||
|
{NewTargetIDSet(), TargetID{"1", "webhook"}},
|
||||||
|
{NewTargetIDSet(TargetID{"1", "webhook"}), TargetID{"2", "webhook"}},
|
||||||
|
{NewTargetIDSet(TargetID{"1", "webhook"}, TargetID{"2", "amqp"}), TargetID{"2", "webhook"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.set.Clone()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, testCase.set) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.set, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.add(testCase.targetIDToAdd)
|
||||||
|
if reflect.DeepEqual(result, testCase.set) {
|
||||||
|
t.Fatalf("test %v: result: expected: not equal, got: equal", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetIDSetUnion(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
set TargetIDSet
|
||||||
|
setToAdd TargetIDSet
|
||||||
|
expectedResult TargetIDSet
|
||||||
|
}{
|
||||||
|
{NewTargetIDSet(), NewTargetIDSet(), NewTargetIDSet()},
|
||||||
|
{NewTargetIDSet(), NewTargetIDSet(TargetID{"1", "webhook"}), NewTargetIDSet(TargetID{"1", "webhook"})},
|
||||||
|
{NewTargetIDSet(TargetID{"1", "webhook"}), NewTargetIDSet(), NewTargetIDSet(TargetID{"1", "webhook"})},
|
||||||
|
{NewTargetIDSet(TargetID{"1", "webhook"}), NewTargetIDSet(TargetID{"2", "amqp"}), NewTargetIDSet(TargetID{"1", "webhook"}, TargetID{"2", "amqp"})},
|
||||||
|
{NewTargetIDSet(TargetID{"1", "webhook"}), NewTargetIDSet(TargetID{"1", "webhook"}), NewTargetIDSet(TargetID{"1", "webhook"})},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.set.Union(testCase.setToAdd)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(testCase.expectedResult, result) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetIDSetDifference(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
set TargetIDSet
|
||||||
|
setToRemove TargetIDSet
|
||||||
|
expectedResult TargetIDSet
|
||||||
|
}{
|
||||||
|
{NewTargetIDSet(), NewTargetIDSet(), NewTargetIDSet()},
|
||||||
|
{NewTargetIDSet(), NewTargetIDSet(TargetID{"1", "webhook"}), NewTargetIDSet()},
|
||||||
|
{NewTargetIDSet(TargetID{"1", "webhook"}), NewTargetIDSet(), NewTargetIDSet(TargetID{"1", "webhook"})},
|
||||||
|
{NewTargetIDSet(TargetID{"1", "webhook"}), NewTargetIDSet(TargetID{"2", "amqp"}), NewTargetIDSet(TargetID{"1", "webhook"})},
|
||||||
|
{NewTargetIDSet(TargetID{"1", "webhook"}), NewTargetIDSet(TargetID{"1", "webhook"}), NewTargetIDSet()},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.set.Difference(testCase.setToRemove)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(testCase.expectedResult, result) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTargetIDSet(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
targetIDs []TargetID
|
||||||
|
expectedResult TargetIDSet
|
||||||
|
}{
|
||||||
|
{[]TargetID{}, NewTargetIDSet()},
|
||||||
|
{[]TargetID{{"1", "webhook"}}, NewTargetIDSet(TargetID{"1", "webhook"})},
|
||||||
|
{[]TargetID{{"1", "webhook"}, {"2", "amqp"}}, NewTargetIDSet(TargetID{"1", "webhook"}, TargetID{"2", "amqp"})},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := NewTargetIDSet(testCase.targetIDs...)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(testCase.expectedResult, result) {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Target - event target interface
|
||||||
|
type Target interface {
|
||||||
|
ID() TargetID
|
||||||
|
Send(Event) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetList - holds list of targets indexed by target ID.
|
||||||
|
type TargetList struct {
|
||||||
|
sync.RWMutex
|
||||||
|
targets map[TargetID]Target
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add - adds unique target to target list.
|
||||||
|
func (list *TargetList) Add(target Target) error {
|
||||||
|
list.Lock()
|
||||||
|
defer list.Unlock()
|
||||||
|
|
||||||
|
if _, ok := list.targets[target.ID()]; ok {
|
||||||
|
return fmt.Errorf("target %v already exists", target.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
list.targets[target.ID()] = target
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists - checks whether target by target ID exists or not.
|
||||||
|
func (list *TargetList) Exists(id TargetID) bool {
|
||||||
|
list.RLock()
|
||||||
|
defer list.RUnlock()
|
||||||
|
|
||||||
|
_, found := list.targets[id]
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove - closes and removes targets by given target IDs.
|
||||||
|
func (list *TargetList) Remove(ids ...TargetID) map[TargetID]error {
|
||||||
|
list.Lock()
|
||||||
|
defer list.Unlock()
|
||||||
|
|
||||||
|
errors := make(map[TargetID]error)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, id := range ids {
|
||||||
|
if target, ok := list.targets[id]; ok {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id TargetID, target Target) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := target.Close(); err != nil {
|
||||||
|
errors[id] = err
|
||||||
|
}
|
||||||
|
}(id, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
delete(list.targets, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// List - returns available target IDs.
|
||||||
|
func (list *TargetList) List() []TargetID {
|
||||||
|
list.RLock()
|
||||||
|
defer list.RUnlock()
|
||||||
|
|
||||||
|
keys := []TargetID{}
|
||||||
|
for k := range list.targets {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - sends events to targets identified by target IDs.
|
||||||
|
func (list *TargetList) Send(event Event, targetIDs ...TargetID) map[TargetID]error {
|
||||||
|
list.Lock()
|
||||||
|
defer list.Unlock()
|
||||||
|
|
||||||
|
errors := make(map[TargetID]error)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, id := range targetIDs {
|
||||||
|
if target, ok := list.targets[id]; ok {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id TargetID, target Target) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := target.Send(event); err != nil {
|
||||||
|
errors[id] = err
|
||||||
|
}
|
||||||
|
}(id, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTargetList - creates TargetList.
|
||||||
|
func NewTargetList() *TargetList {
|
||||||
|
return &TargetList{targets: make(map[TargetID]Target)}
|
||||||
|
}
|
|
@ -0,0 +1,272 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExampleTarget struct {
|
||||||
|
id TargetID
|
||||||
|
sendErr bool
|
||||||
|
closeErr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (target ExampleTarget) ID() TargetID {
|
||||||
|
return target.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (target ExampleTarget) Send(eventData Event) error {
|
||||||
|
b := make([]byte, 1)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(b[0]) * time.Millisecond)
|
||||||
|
|
||||||
|
if target.sendErr {
|
||||||
|
return errors.New("send error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (target ExampleTarget) Close() error {
|
||||||
|
if target.closeErr {
|
||||||
|
return errors.New("close error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetListAdd(t *testing.T) {
|
||||||
|
targetListCase1 := NewTargetList()
|
||||||
|
|
||||||
|
targetListCase2 := NewTargetList()
|
||||||
|
if err := targetListCase2.Add(&ExampleTarget{TargetID{"2", "testcase"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetListCase3 := NewTargetList()
|
||||||
|
if err := targetListCase3.Add(&ExampleTarget{TargetID{"3", "testcase"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
targetList *TargetList
|
||||||
|
target Target
|
||||||
|
expectedResult []TargetID
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{targetListCase1, &ExampleTarget{TargetID{"1", "webhook"}, false, false}, []TargetID{{"1", "webhook"}}, false},
|
||||||
|
{targetListCase2, &ExampleTarget{TargetID{"1", "webhook"}, false, false}, []TargetID{{"2", "testcase"}, {"1", "webhook"}}, false},
|
||||||
|
{targetListCase3, &ExampleTarget{TargetID{"3", "testcase"}, false, false}, nil, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
err := testCase.targetList.Add(testCase.target)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
result := testCase.targetList.List()
|
||||||
|
|
||||||
|
if len(result) != len(testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, targetID1 := range result {
|
||||||
|
var found bool
|
||||||
|
for _, targetID2 := range testCase.expectedResult {
|
||||||
|
if reflect.DeepEqual(targetID1, targetID2) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetListExists(t *testing.T) {
|
||||||
|
targetListCase1 := NewTargetList()
|
||||||
|
|
||||||
|
targetListCase2 := NewTargetList()
|
||||||
|
if err := targetListCase2.Add(&ExampleTarget{TargetID{"2", "testcase"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetListCase3 := NewTargetList()
|
||||||
|
if err := targetListCase3.Add(&ExampleTarget{TargetID{"3", "testcase"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
targetList *TargetList
|
||||||
|
targetID TargetID
|
||||||
|
expectedResult bool
|
||||||
|
}{
|
||||||
|
{targetListCase1, TargetID{"1", "webhook"}, false},
|
||||||
|
{targetListCase2, TargetID{"1", "webhook"}, false},
|
||||||
|
{targetListCase3, TargetID{"3", "testcase"}, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.targetList.Exists(testCase.targetID)
|
||||||
|
|
||||||
|
if result != testCase.expectedResult {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetListRemove(t *testing.T) {
|
||||||
|
targetListCase1 := NewTargetList()
|
||||||
|
|
||||||
|
targetListCase2 := NewTargetList()
|
||||||
|
if err := targetListCase2.Add(&ExampleTarget{TargetID{"2", "testcase"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetListCase3 := NewTargetList()
|
||||||
|
if err := targetListCase3.Add(&ExampleTarget{TargetID{"3", "testcase"}, false, true}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
targetList *TargetList
|
||||||
|
targetID TargetID
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{targetListCase1, TargetID{"1", "webhook"}, false},
|
||||||
|
{targetListCase2, TargetID{"1", "webhook"}, false},
|
||||||
|
{targetListCase3, TargetID{"3", "testcase"}, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
errors := testCase.targetList.Remove(testCase.targetID)
|
||||||
|
err := errors[testCase.targetID]
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetListList(t *testing.T) {
|
||||||
|
targetListCase1 := NewTargetList()
|
||||||
|
|
||||||
|
targetListCase2 := NewTargetList()
|
||||||
|
if err := targetListCase2.Add(&ExampleTarget{TargetID{"2", "testcase"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetListCase3 := NewTargetList()
|
||||||
|
if err := targetListCase3.Add(&ExampleTarget{TargetID{"3", "testcase"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := targetListCase3.Add(&ExampleTarget{TargetID{"1", "webhook"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
targetList *TargetList
|
||||||
|
expectedResult []TargetID
|
||||||
|
}{
|
||||||
|
{targetListCase1, []TargetID{}},
|
||||||
|
{targetListCase2, []TargetID{{"2", "testcase"}}},
|
||||||
|
{targetListCase3, []TargetID{{"3", "testcase"}, {"1", "webhook"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.targetList.List()
|
||||||
|
|
||||||
|
if len(result) != len(testCase.expectedResult) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, targetID1 := range result {
|
||||||
|
var found bool
|
||||||
|
for _, targetID2 := range testCase.expectedResult {
|
||||||
|
if reflect.DeepEqual(targetID1, targetID2) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetListSend(t *testing.T) {
|
||||||
|
targetListCase1 := NewTargetList()
|
||||||
|
|
||||||
|
targetListCase2 := NewTargetList()
|
||||||
|
if err := targetListCase2.Add(&ExampleTarget{TargetID{"2", "testcase"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetListCase3 := NewTargetList()
|
||||||
|
if err := targetListCase3.Add(&ExampleTarget{TargetID{"3", "testcase"}, false, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetListCase4 := NewTargetList()
|
||||||
|
if err := targetListCase4.Add(&ExampleTarget{TargetID{"4", "testcase"}, true, false}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
targetList *TargetList
|
||||||
|
targetID TargetID
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{targetListCase1, TargetID{"1", "webhook"}, false},
|
||||||
|
{targetListCase2, TargetID{"1", "non-existent"}, false},
|
||||||
|
{targetListCase3, TargetID{"3", "testcase"}, false},
|
||||||
|
{targetListCase4, TargetID{"4", "testcase"}, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
errors := testCase.targetList.Send(Event{}, testCase.targetID)
|
||||||
|
err := errors[testCase.targetID]
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTargetList(t *testing.T) {
|
||||||
|
if result := NewTargetList(); result == nil {
|
||||||
|
t.Fatalf("test: result: expected: <non-nil>, got: <nil>")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var hostLabelRegexp = regexp.MustCompile("^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$")
|
||||||
|
|
||||||
|
// Host - holds network host IP/name and its port.
|
||||||
|
type Host struct {
|
||||||
|
Name string
|
||||||
|
Port Port
|
||||||
|
IsPortSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty - returns whether Host is empty or not
|
||||||
|
func (host Host) IsEmpty() bool {
|
||||||
|
return host.Name == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// String - returns string representation of Host.
|
||||||
|
func (host Host) String() string {
|
||||||
|
if !host.IsPortSet {
|
||||||
|
return host.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return host.Name + ":" + host.Port.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal - checks whether given host is equal or not.
|
||||||
|
func (host Host) Equal(compHost Host) bool {
|
||||||
|
return host.String() == compHost.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON - converts Host into JSON data
|
||||||
|
func (host Host) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(host.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON - parses data into Host.
|
||||||
|
func (host *Host) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
var s string
|
||||||
|
if err = json.Unmarshal(data, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow empty string
|
||||||
|
if s == "" {
|
||||||
|
*host = Host{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var h *Host
|
||||||
|
if h, err = ParseHost(s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*host = *h
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseHost - parses string into Host
|
||||||
|
func ParseHost(s string) (*Host, error) {
|
||||||
|
isValidHost := func(host string) bool {
|
||||||
|
if host == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// host is not a valid IPv4 or IPv6 address
|
||||||
|
// host may be a hostname
|
||||||
|
// refer https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||||
|
// why checks are done like below
|
||||||
|
if len(host) < 1 || len(host) > 253 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range strings.Split(host, ".") {
|
||||||
|
if len(label) < 1 || len(label) > 63 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hostLabelRegexp.MatchString(label) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var port Port
|
||||||
|
var isPortSet bool
|
||||||
|
host, portStr, err := net.SplitHostPort(s)
|
||||||
|
if err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "missing port in address") {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
host = s
|
||||||
|
portStr = ""
|
||||||
|
} else {
|
||||||
|
if port, err = ParsePort(portStr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isPortSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidHost(host) {
|
||||||
|
return nil, errors.New("invalid hostname")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Host{
|
||||||
|
Name: host,
|
||||||
|
Port: port,
|
||||||
|
IsPortSet: isPortSet,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustParseHost - parses given string to Host, else panics.
|
||||||
|
func MustParseHost(s string) *Host {
|
||||||
|
host, err := ParseHost(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
|
@ -0,0 +1,236 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHostIsEmpty(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
host Host
|
||||||
|
expectedResult bool
|
||||||
|
}{
|
||||||
|
{Host{"", 0, false}, true},
|
||||||
|
{Host{"", 0, true}, true},
|
||||||
|
{Host{"play", 9000, false}, false},
|
||||||
|
{Host{"play", 9000, true}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.host.IsEmpty()
|
||||||
|
|
||||||
|
if result != testCase.expectedResult {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostString(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
host Host
|
||||||
|
expectedStr string
|
||||||
|
}{
|
||||||
|
{Host{"", 0, false}, ""},
|
||||||
|
{Host{"", 0, true}, ":0"},
|
||||||
|
{Host{"play", 9000, false}, "play"},
|
||||||
|
{Host{"play", 9000, true}, "play:9000"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
str := testCase.host.String()
|
||||||
|
|
||||||
|
if str != testCase.expectedStr {
|
||||||
|
t.Fatalf("test %v: string: expected: %v, got: %v", i+1, testCase.expectedStr, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostEqual(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
host Host
|
||||||
|
compHost Host
|
||||||
|
expectedResult bool
|
||||||
|
}{
|
||||||
|
{Host{"", 0, false}, Host{"", 0, true}, false},
|
||||||
|
{Host{"play", 9000, true}, Host{"play", 9000, false}, false},
|
||||||
|
{Host{"", 0, true}, Host{"", 0, true}, true},
|
||||||
|
{Host{"play", 9000, false}, Host{"play", 9000, false}, true},
|
||||||
|
{Host{"play", 9000, true}, Host{"play", 9000, true}, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.host.Equal(testCase.compHost)
|
||||||
|
|
||||||
|
if result != testCase.expectedResult {
|
||||||
|
t.Fatalf("test %v: string: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostMarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
host Host
|
||||||
|
expectedData []byte
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{Host{}, []byte(`""`), false},
|
||||||
|
{Host{"play", 0, false}, []byte(`"play"`), false},
|
||||||
|
{Host{"play", 0, true}, []byte(`"play:0"`), false},
|
||||||
|
{Host{"play", 9000, true}, []byte(`"play:9000"`), false},
|
||||||
|
{Host{"play.minio.io", 0, false}, []byte(`"play.minio.io"`), false},
|
||||||
|
{Host{"play.minio.io", 9000, true}, []byte(`"play.minio.io:9000"`), false},
|
||||||
|
{Host{"147.75.201.93", 0, false}, []byte(`"147.75.201.93"`), false},
|
||||||
|
{Host{"147.75.201.93", 9000, true}, []byte(`"147.75.201.93:9000"`), false},
|
||||||
|
{Host{"play12", 0, false}, []byte(`"play12"`), false},
|
||||||
|
{Host{"12play", 0, false}, []byte(`"12play"`), false},
|
||||||
|
{Host{"play-minio-io", 0, false}, []byte(`"play-minio-io"`), false},
|
||||||
|
{Host{"play--minio.io", 0, false}, []byte(`"play--minio.io"`), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
data, err := testCase.host.MarshalJSON()
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(data, testCase.expectedData) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, string(testCase.expectedData), string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostUnmarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
data []byte
|
||||||
|
expectedHost *Host
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{[]byte(`""`), &Host{}, false},
|
||||||
|
{[]byte(`"play"`), &Host{"play", 0, false}, false},
|
||||||
|
{[]byte(`"play:0"`), &Host{"play", 0, true}, false},
|
||||||
|
{[]byte(`"play:9000"`), &Host{"play", 9000, true}, false},
|
||||||
|
{[]byte(`"play.minio.io"`), &Host{"play.minio.io", 0, false}, false},
|
||||||
|
{[]byte(`"play.minio.io:9000"`), &Host{"play.minio.io", 9000, true}, false},
|
||||||
|
{[]byte(`"147.75.201.93"`), &Host{"147.75.201.93", 0, false}, false},
|
||||||
|
{[]byte(`"147.75.201.93:9000"`), &Host{"147.75.201.93", 9000, true}, false},
|
||||||
|
{[]byte(`"play12"`), &Host{"play12", 0, false}, false},
|
||||||
|
{[]byte(`"12play"`), &Host{"12play", 0, false}, false},
|
||||||
|
{[]byte(`"play-minio-io"`), &Host{"play-minio-io", 0, false}, false},
|
||||||
|
{[]byte(`"play--minio.io"`), &Host{"play--minio.io", 0, false}, false},
|
||||||
|
{[]byte(`":9000"`), nil, true},
|
||||||
|
{[]byte(`"play:"`), nil, true},
|
||||||
|
{[]byte(`"play::"`), nil, true},
|
||||||
|
{[]byte(`"play:90000"`), nil, true},
|
||||||
|
{[]byte(`"play:-10"`), nil, true},
|
||||||
|
{[]byte(`"play-"`), nil, true},
|
||||||
|
{[]byte(`"play.minio..io"`), nil, true},
|
||||||
|
{[]byte(`":"`), nil, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
var host Host
|
||||||
|
err := host.UnmarshalJSON(testCase.data)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(&host, testCase.expectedHost) {
|
||||||
|
t.Fatalf("test %v: host: expected: %#v, got: %#v", i+1, testCase.expectedHost, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHost(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
s string
|
||||||
|
expectedHost *Host
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{"play", &Host{"play", 0, false}, false},
|
||||||
|
{"play:0", &Host{"play", 0, true}, false},
|
||||||
|
{"play:9000", &Host{"play", 9000, true}, false},
|
||||||
|
{"play.minio.io", &Host{"play.minio.io", 0, false}, false},
|
||||||
|
{"play.minio.io:9000", &Host{"play.minio.io", 9000, true}, false},
|
||||||
|
{"147.75.201.93", &Host{"147.75.201.93", 0, false}, false},
|
||||||
|
{"147.75.201.93:9000", &Host{"147.75.201.93", 9000, true}, false},
|
||||||
|
{"play12", &Host{"play12", 0, false}, false},
|
||||||
|
{"12play", &Host{"12play", 0, false}, false},
|
||||||
|
{"play-minio-io", &Host{"play-minio-io", 0, false}, false},
|
||||||
|
{"play--minio.io", &Host{"play--minio.io", 0, false}, false},
|
||||||
|
{":9000", nil, true},
|
||||||
|
{"play:", nil, true},
|
||||||
|
{"play::", nil, true},
|
||||||
|
{"play:90000", nil, true},
|
||||||
|
{"play:-10", nil, true},
|
||||||
|
{"play-", nil, true},
|
||||||
|
{"play.minio..io", nil, true},
|
||||||
|
{":", nil, true},
|
||||||
|
{"", nil, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
host, err := ParseHost(testCase.s)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(host, testCase.expectedHost) {
|
||||||
|
t.Fatalf("test %v: host: expected: %#v, got: %#v", i+1, testCase.expectedHost, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMustParseHost(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
s string
|
||||||
|
expectedHost *Host
|
||||||
|
}{
|
||||||
|
{"play", &Host{"play", 0, false}},
|
||||||
|
{"play:0", &Host{"play", 0, true}},
|
||||||
|
{"play:9000", &Host{"play", 9000, true}},
|
||||||
|
{"play.minio.io", &Host{"play.minio.io", 0, false}},
|
||||||
|
{"play.minio.io:9000", &Host{"play.minio.io", 9000, true}},
|
||||||
|
{"147.75.201.93", &Host{"147.75.201.93", 0, false}},
|
||||||
|
{"147.75.201.93:9000", &Host{"147.75.201.93", 9000, true}},
|
||||||
|
{"play12", &Host{"play12", 0, false}},
|
||||||
|
{"12play", &Host{"12play", 0, false}},
|
||||||
|
{"play-minio-io", &Host{"play-minio-io", 0, false}},
|
||||||
|
{"play--minio.io", &Host{"play--minio.io", 0, false}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
host := MustParseHost(testCase.s)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(host, testCase.expectedHost) {
|
||||||
|
t.Fatalf("test %v: host: expected: %#v, got: %#v", i+1, testCase.expectedHost, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Port - network port
|
||||||
|
type Port uint16
|
||||||
|
|
||||||
|
// String - returns string representation of port.
|
||||||
|
func (p Port) String() string {
|
||||||
|
return strconv.Itoa(int(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePort - parses string into Port
|
||||||
|
func ParsePort(s string) (p Port, err error) {
|
||||||
|
var i int
|
||||||
|
if i, err = strconv.Atoi(s); err != nil {
|
||||||
|
return p, errors.New("invalid port number")
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < 0 || i > 65535 {
|
||||||
|
return p, errors.New("port must be between 0 to 65535")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Port(i), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustParsePort - parses string into Port, else panics
|
||||||
|
func MustParsePort(s string) Port {
|
||||||
|
p, err := ParsePort(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPortString(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
port Port
|
||||||
|
expectedStr string
|
||||||
|
}{
|
||||||
|
{Port(0), "0"},
|
||||||
|
{Port(9000), "9000"},
|
||||||
|
{Port(65535), "65535"},
|
||||||
|
{Port(1024), "1024"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
str := testCase.port.String()
|
||||||
|
|
||||||
|
if str != testCase.expectedStr {
|
||||||
|
t.Fatalf("test %v: error: port: %v, got: %v", i+1, testCase.expectedStr, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePort(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
s string
|
||||||
|
expectedPort Port
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{"0", Port(0), false},
|
||||||
|
{"9000", Port(9000), false},
|
||||||
|
{"65535", Port(65535), false},
|
||||||
|
{"90000", Port(0), true},
|
||||||
|
{"-10", Port(0), true},
|
||||||
|
{"", Port(0), true},
|
||||||
|
{"http", Port(0), true},
|
||||||
|
{" 1024", Port(0), true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
port, err := ParsePort(testCase.s)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if port != testCase.expectedPort {
|
||||||
|
t.Fatalf("test %v: error: port: %v, got: %v", i+1, testCase.expectedPort, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMustParsePort(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
s string
|
||||||
|
expectedPort Port
|
||||||
|
}{
|
||||||
|
{"0", Port(0)},
|
||||||
|
{"9000", Port(9000)},
|
||||||
|
{"65535", Port(65535)},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
port := MustParsePort(testCase.s)
|
||||||
|
|
||||||
|
if port != testCase.expectedPort {
|
||||||
|
t.Fatalf("test %v: error: port: %v, got: %v", i+1, testCase.expectedPort, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// URL - improved JSON friendly url.URL.
|
||||||
|
type URL url.URL
|
||||||
|
|
||||||
|
// IsEmpty - checks URL is empty or not.
|
||||||
|
func (u URL) IsEmpty() bool {
|
||||||
|
return u.String() == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// String - returns string representation of URL.
|
||||||
|
func (u URL) String() string {
|
||||||
|
// if port number 80 and 443, remove for http and https scheme respectively
|
||||||
|
if u.Host != "" {
|
||||||
|
host := MustParseHost(u.Host)
|
||||||
|
switch {
|
||||||
|
case u.Scheme == "http" && host.Port == 80:
|
||||||
|
fallthrough
|
||||||
|
case u.Scheme == "https" && host.Port == 443:
|
||||||
|
u.Host = host.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uu := url.URL(u)
|
||||||
|
return uu.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON - converts to JSON string data.
|
||||||
|
func (u URL) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(u.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON - parses given data into URL.
|
||||||
|
func (u *URL) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
var s string
|
||||||
|
if err = json.Unmarshal(data, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow empty string
|
||||||
|
if s == "" {
|
||||||
|
*u = URL{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ru *URL
|
||||||
|
if ru, err = ParseURL(s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*u = *ru
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseURL - parses string into URL.
|
||||||
|
func ParseURL(s string) (u *URL, err error) {
|
||||||
|
var uu *url.URL
|
||||||
|
if uu, err = url.Parse(s); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if uu.Host == "" {
|
||||||
|
if uu.Scheme != "" {
|
||||||
|
return nil, errors.New("scheme appears with empty host")
|
||||||
|
}
|
||||||
|
} else if _, err = ParseHost(uu.Host); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean path in the URL.
|
||||||
|
// Note: path.Clean() is used on purpose because in MS Windows filepath.Clean() converts
|
||||||
|
// `/` into `\` ie `/foo` becomes `\foo`
|
||||||
|
if uu.Path != "" {
|
||||||
|
uu.Path = path.Clean(uu.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
v := URL(*uu)
|
||||||
|
u = &v
|
||||||
|
return u, nil
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestURLIsEmpty(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
url URL
|
||||||
|
expectedResult bool
|
||||||
|
}{
|
||||||
|
{URL{}, true},
|
||||||
|
{URL{Scheme: "http", Host: "play"}, false},
|
||||||
|
{URL{Path: "path/to/play"}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
result := testCase.url.IsEmpty()
|
||||||
|
|
||||||
|
if result != testCase.expectedResult {
|
||||||
|
t.Fatalf("test %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURLString(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
url URL
|
||||||
|
expectedStr string
|
||||||
|
}{
|
||||||
|
{URL{}, ""},
|
||||||
|
{URL{Scheme: "http", Host: "play"}, "http://play"},
|
||||||
|
{URL{Scheme: "https", Host: "play:443"}, "https://play"},
|
||||||
|
{URL{Scheme: "https", Host: "play.minio.io:80"}, "https://play.minio.io:80"},
|
||||||
|
{URL{Scheme: "https", Host: "147.75.201.93:9000", Path: "/"}, "https://147.75.201.93:9000/"},
|
||||||
|
{URL{Scheme: "https", Host: "s3.amazonaws.com", Path: "/", RawQuery: "location"}, "https://s3.amazonaws.com/?location"},
|
||||||
|
{URL{Scheme: "http", Host: "myminio:10000", Path: "/mybucket/myobject"}, "http://myminio:10000/mybucket/myobject"},
|
||||||
|
{URL{Scheme: "ftp", Host: "myftp.server:10000", Path: "/myuser"}, "ftp://myftp.server:10000/myuser"},
|
||||||
|
{URL{Path: "path/to/play"}, "path/to/play"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
str := testCase.url.String()
|
||||||
|
|
||||||
|
if str != testCase.expectedStr {
|
||||||
|
t.Fatalf("test %v: string: expected: %v, got: %v", i+1, testCase.expectedStr, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURLMarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
url URL
|
||||||
|
expectedData []byte
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{URL{}, []byte(`""`), false},
|
||||||
|
{URL{Scheme: "http", Host: "play"}, []byte(`"http://play"`), false},
|
||||||
|
{URL{Scheme: "https", Host: "play.minio.io:0"}, []byte(`"https://play.minio.io:0"`), false},
|
||||||
|
{URL{Scheme: "https", Host: "147.75.201.93:9000", Path: "/"}, []byte(`"https://147.75.201.93:9000/"`), false},
|
||||||
|
{URL{Scheme: "https", Host: "s3.amazonaws.com", Path: "/", RawQuery: "location"}, []byte(`"https://s3.amazonaws.com/?location"`), false},
|
||||||
|
{URL{Scheme: "http", Host: "myminio:10000", Path: "/mybucket/myobject"}, []byte(`"http://myminio:10000/mybucket/myobject"`), false},
|
||||||
|
{URL{Scheme: "ftp", Host: "myftp.server:10000", Path: "/myuser"}, []byte(`"ftp://myftp.server:10000/myuser"`), false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
data, err := testCase.url.MarshalJSON()
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(data, testCase.expectedData) {
|
||||||
|
t.Fatalf("test %v: data: expected: %v, got: %v", i+1, string(testCase.expectedData), string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURLUnmarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
data []byte
|
||||||
|
expectedURL *URL
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{[]byte(`""`), &URL{}, false},
|
||||||
|
{[]byte(`"http://play"`), &URL{Scheme: "http", Host: "play"}, false},
|
||||||
|
{[]byte(`"https://play.minio.io:0"`), &URL{Scheme: "https", Host: "play.minio.io:0"}, false},
|
||||||
|
{[]byte(`"https://147.75.201.93:9000/"`), &URL{Scheme: "https", Host: "147.75.201.93:9000", Path: "/"}, false},
|
||||||
|
{[]byte(`"https://s3.amazonaws.com/?location"`), &URL{Scheme: "https", Host: "s3.amazonaws.com", Path: "/", RawQuery: "location"}, false},
|
||||||
|
{[]byte(`"http://myminio:10000/mybucket//myobject/"`), &URL{Scheme: "http", Host: "myminio:10000", Path: "/mybucket/myobject"}, false},
|
||||||
|
{[]byte(`"ftp://myftp.server:10000/myuser"`), &URL{Scheme: "ftp", Host: "myftp.server:10000", Path: "/myuser"}, false},
|
||||||
|
{[]byte(`"myserver:1000"`), nil, true},
|
||||||
|
{[]byte(`"http://:1000/mybucket"`), nil, true},
|
||||||
|
{[]byte(`"https://147.75.201.93:90000/"`), nil, true},
|
||||||
|
{[]byte(`"http:/play"`), nil, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
var url URL
|
||||||
|
err := url.UnmarshalJSON(testCase.data)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(&url, testCase.expectedURL) {
|
||||||
|
t.Fatalf("test %v: host: expected: %#v, got: %#v", i+1, testCase.expectedURL, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseURL(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
s string
|
||||||
|
expectedURL *URL
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{"http://play", &URL{Scheme: "http", Host: "play"}, false},
|
||||||
|
{"https://play.minio.io:0", &URL{Scheme: "https", Host: "play.minio.io:0"}, false},
|
||||||
|
{"https://147.75.201.93:9000/", &URL{Scheme: "https", Host: "147.75.201.93:9000", Path: "/"}, false},
|
||||||
|
{"https://s3.amazonaws.com/?location", &URL{Scheme: "https", Host: "s3.amazonaws.com", Path: "/", RawQuery: "location"}, false},
|
||||||
|
{"http://myminio:10000/mybucket//myobject/", &URL{Scheme: "http", Host: "myminio:10000", Path: "/mybucket/myobject"}, false},
|
||||||
|
{"ftp://myftp.server:10000/myuser", &URL{Scheme: "ftp", Host: "myftp.server:10000", Path: "/myuser"}, false},
|
||||||
|
{"myserver:1000", nil, true},
|
||||||
|
{"http://:1000/mybucket", nil, true},
|
||||||
|
{"https://147.75.201.93:90000/", nil, true},
|
||||||
|
{"http:/play", nil, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
url, err := ParseURL(testCase.s)
|
||||||
|
expectErr := (err != nil)
|
||||||
|
|
||||||
|
if expectErr != testCase.expectErr {
|
||||||
|
t.Fatalf("test %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.expectErr {
|
||||||
|
if !reflect.DeepEqual(url, testCase.expectedURL) {
|
||||||
|
t.Fatalf("test %v: host: expected: %#v, got: %#v", i+1, testCase.expectedURL, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,66 +0,0 @@
|
||||||
# 0.10.0
|
|
||||||
|
|
||||||
* feature: Add a test hook (#180)
|
|
||||||
* feature: `ParseLevel` is now case-insensitive (#326)
|
|
||||||
* feature: `FieldLogger` interface that generalizes `Logger` and `Entry` (#308)
|
|
||||||
* performance: avoid re-allocations on `WithFields` (#335)
|
|
||||||
|
|
||||||
# 0.9.0
|
|
||||||
|
|
||||||
* logrus/text_formatter: don't emit empty msg
|
|
||||||
* logrus/hooks/airbrake: move out of main repository
|
|
||||||
* logrus/hooks/sentry: move out of main repository
|
|
||||||
* logrus/hooks/papertrail: move out of main repository
|
|
||||||
* logrus/hooks/bugsnag: move out of main repository
|
|
||||||
* logrus/core: run tests with `-race`
|
|
||||||
* logrus/core: detect TTY based on `stderr`
|
|
||||||
* logrus/core: support `WithError` on logger
|
|
||||||
* logrus/core: Solaris support
|
|
||||||
|
|
||||||
# 0.8.7
|
|
||||||
|
|
||||||
* logrus/core: fix possible race (#216)
|
|
||||||
* logrus/doc: small typo fixes and doc improvements
|
|
||||||
|
|
||||||
|
|
||||||
# 0.8.6
|
|
||||||
|
|
||||||
* hooks/raven: allow passing an initialized client
|
|
||||||
|
|
||||||
# 0.8.5
|
|
||||||
|
|
||||||
* logrus/core: revert #208
|
|
||||||
|
|
||||||
# 0.8.4
|
|
||||||
|
|
||||||
* formatter/text: fix data race (#218)
|
|
||||||
|
|
||||||
# 0.8.3
|
|
||||||
|
|
||||||
* logrus/core: fix entry log level (#208)
|
|
||||||
* logrus/core: improve performance of text formatter by 40%
|
|
||||||
* logrus/core: expose `LevelHooks` type
|
|
||||||
* logrus/core: add support for DragonflyBSD and NetBSD
|
|
||||||
* formatter/text: print structs more verbosely
|
|
||||||
|
|
||||||
# 0.8.2
|
|
||||||
|
|
||||||
* logrus: fix more Fatal family functions
|
|
||||||
|
|
||||||
# 0.8.1
|
|
||||||
|
|
||||||
* logrus: fix not exiting on `Fatalf` and `Fatalln`
|
|
||||||
|
|
||||||
# 0.8.0
|
|
||||||
|
|
||||||
* logrus: defaults to stderr instead of stdout
|
|
||||||
* hooks/sentry: add special field for `*http.Request`
|
|
||||||
* formatter/text: ignore Windows for colors
|
|
||||||
|
|
||||||
# 0.7.3
|
|
||||||
|
|
||||||
* formatter/\*: allow configuration of timestamp layout
|
|
||||||
|
|
||||||
# 0.7.2
|
|
||||||
|
|
||||||
* formatter/text: Add configuration option for time format (#158)
|
|
|
@ -1,21 +0,0 @@
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2014 Simon Eskildsen
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue