snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
64
server/pkg/eventbus/README.adoc
Normal file
64
server/pkg/eventbus/README.adoc
Normal file
@@ -0,0 +1,64 @@
|
||||
# pkg/eventbus
|
||||
|
||||
Package handles event dispatching and handler registration
|
||||
|
||||
## Event
|
||||
|
||||
Event (in context of eventbus package) contains of resource type, event type and a matcher.
|
||||
|
||||
### Resource type
|
||||
|
||||
Resource types are identifiers of internal resources, like user, role, request, ...
|
||||
|
||||
### Event type
|
||||
|
||||
Type describes direct or indirect user action or some other event in the system.
|
||||
|
||||
#### Deferred events
|
||||
|
||||
Deferred events (onTimestamp and onInterval) are executed by scheduler package service.
|
||||
|
||||
#### Manual events
|
||||
|
||||
Manual events (onManual) are a special case.
|
||||
These events are not (nor should be) dispatched directly or through event-bus.
|
||||
|
||||
We use it to route requests and pack attributes from the API to the Corredor service
|
||||
|
||||
### Event matcher
|
||||
|
||||
Matcher is a function on event that helps filtering fired events.
|
||||
It decides (returns true or false) if fired event is a match for any of a registered handlers
|
||||
|
||||
Bus has basic internal filtering for resource & event type.
|
||||
Other constraints are passed to event's matcher function.
|
||||
|
||||
.Matcher will receive 2+ string parameters:
|
||||
- name
|
||||
- operator
|
||||
- zero or more values
|
||||
|
||||
It's matcher responsibility to handle contents of name, operator and values parameters.
|
||||
|
||||
Constraint checker procedure will call matcher for each constraint.
|
||||
All constraints must match.
|
||||
First non-match will break constraint checking procedure.
|
||||
|
||||
|
||||
## Handler
|
||||
|
||||
Handler is combination of (event matching) rules/constraints and handler callback function.
|
||||
Function is called when a event compatible with registered rules & constraints is fired.
|
||||
|
||||
Handler can respond to multiple resource/event combinations.
|
||||
|
||||
### Constraints
|
||||
|
||||
Matcher function is called multiple times, once per each handler constraint.
|
||||
If any of the calls result in a non-match, check
|
||||
|
||||
Handler without any constraints is considered a match.
|
||||
|
||||
### Weight
|
||||
|
||||
Weight controls order of execution.
|
||||
178
server/pkg/eventbus/constraints.go
Normal file
178
server/pkg/eventbus/constraints.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
mustBeEqual struct {
|
||||
not bool
|
||||
name string
|
||||
values []string
|
||||
}
|
||||
|
||||
mustBeLike struct {
|
||||
not bool
|
||||
name string
|
||||
values []string
|
||||
}
|
||||
|
||||
mustMatch struct {
|
||||
not bool
|
||||
name string
|
||||
values []*regexp.Regexp
|
||||
}
|
||||
|
||||
ConstraintMatcher interface {
|
||||
Name() string
|
||||
Values() []string
|
||||
Match(value string) bool
|
||||
}
|
||||
|
||||
constraintSet []ConstraintMatcher
|
||||
)
|
||||
|
||||
var ErrUnsupportedOp = fmt.Errorf("operator not supported")
|
||||
var ErrUnsupportedName = fmt.Errorf("constraint name not supported")
|
||||
|
||||
func (c mustBeEqual) Name() string { return c.name }
|
||||
func (c mustBeLike) Name() string { return c.name }
|
||||
func (c mustMatch) Name() string { return c.name }
|
||||
func (c mustBeEqual) Values() []string { return c.values }
|
||||
func (c mustBeLike) Values() []string { return c.values }
|
||||
func (c mustMatch) Values() []string { return nil }
|
||||
|
||||
// Converts raw ConstraintMatcher into one of the
|
||||
// ConstraintMatcher handlers
|
||||
func ConstraintMaker(name, op string, vv ...string) (ConstraintMatcher, error) {
|
||||
switch strings.ToLower(op) {
|
||||
case "", "eq", "=", "==", "===":
|
||||
return MustBeEqual(name, vv...)
|
||||
case "not eq", "ne", "!=", "!==":
|
||||
return MustNotBeEqual(name, vv...)
|
||||
case "like":
|
||||
return MustBeLike(name, vv...)
|
||||
case "not like":
|
||||
return MustNotBeLike(name, vv...)
|
||||
case "~":
|
||||
return MustMatch(name, vv...)
|
||||
case "!~":
|
||||
return MustNotMatch(name, vv...)
|
||||
default:
|
||||
return nil, ErrUnsupportedOp
|
||||
}
|
||||
}
|
||||
|
||||
func MustMakeConstraint(name, op string, vv ...string) ConstraintMatcher {
|
||||
c, err := ConstraintMaker(name, op, vv...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func MustBeEqual(name string, vv ...string) (ConstraintMatcher, error) {
|
||||
return &mustBeEqual{name: name, values: vv}, nil
|
||||
}
|
||||
|
||||
func MustNotBeEqual(name string, vv ...string) (ConstraintMatcher, error) {
|
||||
return &mustBeEqual{name: name, values: vv, not: true}, nil
|
||||
}
|
||||
|
||||
func (c mustBeEqual) Match(value string) bool {
|
||||
for _, v := range c.values {
|
||||
if value == v {
|
||||
return !c.not
|
||||
}
|
||||
}
|
||||
|
||||
return c.not
|
||||
}
|
||||
|
||||
func mustLikeMaker(name string, not bool, vv ...string) (*mustBeLike, error) {
|
||||
var (
|
||||
m = &mustBeLike{
|
||||
name: name,
|
||||
values: make([]string, len(vv)),
|
||||
not: not,
|
||||
}
|
||||
)
|
||||
|
||||
for i, v := range vv {
|
||||
v = strings.ReplaceAll(v, "%", "*")
|
||||
v = strings.ReplaceAll(v, "_", "?")
|
||||
m.values[i] = v
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func MustBeLike(name string, vv ...string) (ConstraintMatcher, error) {
|
||||
return mustLikeMaker(name, false, vv...)
|
||||
}
|
||||
|
||||
func MustNotBeLike(name string, vv ...string) (ConstraintMatcher, error) {
|
||||
return mustLikeMaker(name, true, vv...)
|
||||
}
|
||||
|
||||
func (c mustBeLike) Match(value string) bool {
|
||||
for _, v := range c.values {
|
||||
if m, _ := path.Match(v, value); m {
|
||||
return !c.not
|
||||
}
|
||||
}
|
||||
|
||||
return c.not
|
||||
}
|
||||
|
||||
func mustMatchMaker(name string, not bool, vv ...string) (*mustMatch, error) {
|
||||
var (
|
||||
m = &mustMatch{
|
||||
name: name,
|
||||
values: make([]*regexp.Regexp, len(vv)),
|
||||
not: not,
|
||||
}
|
||||
err error
|
||||
)
|
||||
|
||||
for i, v := range vv {
|
||||
if m.values[i], err = regexp.Compile(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func MustMatch(name string, vv ...string) (ConstraintMatcher, error) {
|
||||
return mustMatchMaker(name, false, vv...)
|
||||
}
|
||||
|
||||
func MustNotMatch(name string, vv ...string) (ConstraintMatcher, error) {
|
||||
return mustMatchMaker(name, true, vv...)
|
||||
|
||||
}
|
||||
|
||||
func (c mustMatch) Match(value string) bool {
|
||||
for _, v := range c.values {
|
||||
if v.MatchString(value) {
|
||||
return !c.not
|
||||
}
|
||||
}
|
||||
|
||||
return c.not
|
||||
}
|
||||
|
||||
func MatchFirst(checks ...func() bool) bool {
|
||||
for _, check := range checks {
|
||||
if check() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
98
server/pkg/eventbus/constraints_test.go
Normal file
98
server/pkg/eventbus/constraints_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConstraintMaking(t *testing.T) {
|
||||
var (
|
||||
a = assert.New(t)
|
||||
c ConstraintMatcher
|
||||
err error
|
||||
)
|
||||
|
||||
c, err = ConstraintMaker("", "foo")
|
||||
a.Nil(c)
|
||||
a.Error(err)
|
||||
|
||||
c, err = ConstraintMaker("", "eq")
|
||||
a.NoError(err)
|
||||
a.NotNil(c)
|
||||
a.Implements((*ConstraintMatcher)(nil), c)
|
||||
|
||||
a.NotNil(MustMakeConstraint("foo", "=", "bar"))
|
||||
}
|
||||
|
||||
func TestMustBeEqual(t *testing.T) {
|
||||
var (
|
||||
a = assert.New(t)
|
||||
c, err = MustBeEqual("", "foo")
|
||||
)
|
||||
|
||||
a.NoError(err)
|
||||
a.NotNil(c)
|
||||
a.True(c.Match("foo"))
|
||||
a.False(c.Match("bar"))
|
||||
}
|
||||
|
||||
func TestMustNotBeEqual(t *testing.T) {
|
||||
var (
|
||||
a = assert.New(t)
|
||||
c, err = MustNotBeEqual("", "foo")
|
||||
)
|
||||
|
||||
a.NoError(err)
|
||||
a.NotNil(c)
|
||||
a.False(c.Match("foo"))
|
||||
a.True(c.Match("bar"))
|
||||
}
|
||||
|
||||
func TestMustBeLike(t *testing.T) {
|
||||
var (
|
||||
a = assert.New(t)
|
||||
c, err = MustBeLike("", "foo*")
|
||||
)
|
||||
|
||||
a.NoError(err)
|
||||
a.NotNil(c)
|
||||
a.True(c.Match("fooBAZ"))
|
||||
a.False(c.Match("barBAZ"))
|
||||
}
|
||||
|
||||
func TestMustNotBeLike(t *testing.T) {
|
||||
var (
|
||||
a = assert.New(t)
|
||||
c, err = MustNotBeLike("", "foo*")
|
||||
)
|
||||
|
||||
a.NoError(err)
|
||||
a.NotNil(c)
|
||||
a.False(c.Match("fooBAZ"))
|
||||
a.True(c.Match("barBAZ"))
|
||||
}
|
||||
|
||||
func TestMustMatch(t *testing.T) {
|
||||
var (
|
||||
a = assert.New(t)
|
||||
c, err = MustMatch("", "foo.+")
|
||||
)
|
||||
|
||||
a.NoError(err)
|
||||
a.NotNil(c)
|
||||
a.True(c.Match("fooBAZ"))
|
||||
a.False(c.Match("barBAZ"))
|
||||
}
|
||||
|
||||
func TestMustNotMatch(t *testing.T) {
|
||||
var (
|
||||
a = assert.New(t)
|
||||
c, err = MustNotMatch("", "foo.*")
|
||||
)
|
||||
|
||||
a.NoError(err)
|
||||
a.NotNil(c)
|
||||
a.False(c.Match("fooBAZ"))
|
||||
a.True(c.Match("barBAZ"))
|
||||
}
|
||||
14
server/pkg/eventbus/debug.go
Normal file
14
server/pkg/eventbus/debug.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package eventbus
|
||||
|
||||
// Debug returns structured debug data
|
||||
func (b *eventbus) Debug() interface{} {
|
||||
var (
|
||||
o = make([]*handler, 0, len(b.handlers))
|
||||
)
|
||||
|
||||
for _, h := range b.handlers {
|
||||
o = append(o, h)
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
167
server/pkg/eventbus/eventbus.go
Normal file
167
server/pkg/eventbus/eventbus.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type (
|
||||
Event interface {
|
||||
// ResourceType from resource that fired the event
|
||||
ResourceType() string
|
||||
|
||||
// EventType returns type of event fired
|
||||
EventType() string
|
||||
|
||||
// Match tests if given constraints match
|
||||
// event's internal values
|
||||
Match(ConstraintMatcher) bool
|
||||
}
|
||||
|
||||
eventbus struct {
|
||||
// waitgroup for dispatch
|
||||
wg *sync.WaitGroup
|
||||
|
||||
// Read & write locking
|
||||
// prevent event handling during handler (un)registration
|
||||
l *sync.RWMutex
|
||||
|
||||
// list of registered handlers
|
||||
handlers map[uintptr]*handler
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// Global eventbus
|
||||
gEventBus *eventbus
|
||||
)
|
||||
|
||||
func init() {
|
||||
gEventBus = New()
|
||||
}
|
||||
|
||||
// Service returns global event bus service
|
||||
func Service() *eventbus {
|
||||
return gEventBus
|
||||
}
|
||||
|
||||
func Set(eb *eventbus) {
|
||||
gEventBus = eb
|
||||
}
|
||||
|
||||
func New() *eventbus {
|
||||
return &eventbus{
|
||||
wg: &sync.WaitGroup{},
|
||||
l: &sync.RWMutex{},
|
||||
handlers: make(map[uintptr]*handler),
|
||||
}
|
||||
}
|
||||
|
||||
// WaitFor is synchronous event dispatcher
|
||||
//
|
||||
// It waits for each handler and fails on first error
|
||||
func (b *eventbus) WaitFor(ctx context.Context, ev Event) (err error) {
|
||||
b.l.RLock()
|
||||
defer b.l.RUnlock()
|
||||
|
||||
for _, t := range b.find(ev) {
|
||||
err = func(ctx context.Context, t *handler) error {
|
||||
b.wg.Add(1)
|
||||
defer b.wg.Done()
|
||||
return t.Handle(ctx, ev)
|
||||
|
||||
}(ctx, t)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Dispatch runs events asynchronously
|
||||
func (b *eventbus) Dispatch(ctx context.Context, ev Event) {
|
||||
b.l.RLock()
|
||||
defer b.l.RUnlock()
|
||||
for _, t := range b.find(ev) {
|
||||
b.wg.Add(1)
|
||||
go func(ctx context.Context, t *handler) {
|
||||
defer b.wg.Done()
|
||||
_ = t.Handle(ctx, ev)
|
||||
}(ctx, t)
|
||||
}
|
||||
}
|
||||
|
||||
// Waits for all dispatched events
|
||||
//
|
||||
// Should only be used for testing
|
||||
func (b *eventbus) wait() {
|
||||
b.wg.Wait()
|
||||
}
|
||||
|
||||
// Finds all registered handlers compatible with given event
|
||||
//
|
||||
// It returns sorted handlers
|
||||
//
|
||||
// There is still room for improvement (performance wise) by indexing
|
||||
// resources and events of each handler.
|
||||
func (b *eventbus) find(ev Event) (tt HandlerSet) {
|
||||
if ev == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, t := range b.handlers {
|
||||
|
||||
if !t.Match(ev) {
|
||||
continue
|
||||
}
|
||||
|
||||
tt = append(tt, t)
|
||||
}
|
||||
|
||||
sort.Sort(tt)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Register creates a new handler with given handler, resource, event with other options and constraints
|
||||
//
|
||||
// It returns a handler identifier that can be used to remove (unregister) handler later
|
||||
func (b *eventbus) Register(h HandlerFn, ops ...HandlerRegOp) uintptr {
|
||||
b.l.Lock()
|
||||
defer b.l.Unlock()
|
||||
|
||||
var (
|
||||
handlers = NewHandler(h, ops...)
|
||||
ptr = uintptr(unsafe.Pointer(handlers))
|
||||
)
|
||||
|
||||
b.handlers[ptr] = handlers
|
||||
return ptr
|
||||
}
|
||||
|
||||
// Unregister removes one or more registered handlers
|
||||
func (b *eventbus) Unregister(ptrs ...uintptr) {
|
||||
b.l.Lock()
|
||||
defer b.l.Unlock()
|
||||
|
||||
for _, ptr := range ptrs {
|
||||
delete(b.handlers, ptr)
|
||||
}
|
||||
}
|
||||
|
||||
// UnregisterByResource removes one or more registered handlers that match the given resource
|
||||
func (b *eventbus) UnregisterByResource(r string) {
|
||||
b.l.Lock()
|
||||
defer b.l.Unlock()
|
||||
for p, h := range b.handlers {
|
||||
if h.resourceTypes[r] {
|
||||
delete(b.handlers, p)
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
109
server/pkg/eventbus/eventbus_test.go
Normal file
109
server/pkg/eventbus/eventbus_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
func TestEventbusRegUnreg(t *testing.T) {
|
||||
var (
|
||||
a = assert.New(t)
|
||||
|
||||
bus = New()
|
||||
)
|
||||
|
||||
a.Empty(bus.handlers)
|
||||
h1 := bus.Register(nil)
|
||||
a.NotZero(h1)
|
||||
h2 := bus.Register(nil)
|
||||
a.NotZero(h2)
|
||||
h3 := bus.Register(nil)
|
||||
a.NotZero(h3)
|
||||
a.Len(bus.handlers, 3)
|
||||
bus.Unregister(h1)
|
||||
a.Len(bus.handlers, 2)
|
||||
bus.Unregister(h2)
|
||||
a.Len(bus.handlers, 1)
|
||||
bus.Unregister(h3)
|
||||
a.Empty(bus.handlers)
|
||||
}
|
||||
|
||||
func BenchmarkEventbusHandlerLookup(b *testing.B) {
|
||||
var (
|
||||
bus = New()
|
||||
ptrs []uintptr
|
||||
)
|
||||
|
||||
// Register handlers
|
||||
for n := 0; n < b.N; n++ {
|
||||
ptrs = append(ptrs, bus.Register(nil))
|
||||
}
|
||||
|
||||
// Register find & fire handlers
|
||||
for n := 0; n < b.N; n++ {
|
||||
bus.find(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEventbusRegUnreg(b *testing.B) {
|
||||
var (
|
||||
bus = New()
|
||||
ptrs []uintptr
|
||||
)
|
||||
|
||||
// Register handlers
|
||||
for n := 0; n < b.N; n++ {
|
||||
ptrs = append(ptrs, bus.Register(nil))
|
||||
}
|
||||
|
||||
// Shuffle stored pinters
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand.Shuffle(len(ptrs), func(i, j int) { ptrs[i], ptrs[j] = ptrs[j], ptrs[i] })
|
||||
|
||||
// Unregister all pointers
|
||||
for _, ptr := range ptrs {
|
||||
bus.Unregister(ptr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventFiring(t *testing.T) {
|
||||
var (
|
||||
a = assert.New(t)
|
||||
|
||||
// Let's make sure our simple tests don't
|
||||
// cause any extra stress with dat race conditions
|
||||
i = &atomic.Int32{}
|
||||
|
||||
ev = func(ev string) Event {
|
||||
return &mockEvent{rType: "resource", eType: ev}
|
||||
}
|
||||
|
||||
ctx = context.Background()
|
||||
bus = New()
|
||||
)
|
||||
|
||||
bus.Register(func(ctx context.Context, ev Event) error { i.Inc(); return nil }, On("inc"), For("resource"))
|
||||
bus.Register(func(ctx context.Context, ev Event) error { i.Dec(); return nil }, On("dec"), For("resource"))
|
||||
bus.Register(func(ctx context.Context, ev Event) error { return fmt.Errorf("handl-err") }, On("err"), For("resource"))
|
||||
|
||||
a.Equal(int32(0), i.Load())
|
||||
a.NoError(bus.WaitFor(ctx, ev("inc")))
|
||||
a.Equal(int32(1), i.Load())
|
||||
a.NoError(bus.WaitFor(ctx, ev("inc")))
|
||||
a.Equal(int32(2), i.Load())
|
||||
a.NoError(bus.WaitFor(ctx, ev("dec")))
|
||||
a.Equal(int32(1), i.Load())
|
||||
a.EqualError(bus.WaitFor(ctx, ev("err")), "handl-err")
|
||||
|
||||
bus.Dispatch(ctx, ev("dec"))
|
||||
bus.Dispatch(ctx, ev("inc"))
|
||||
bus.Dispatch(ctx, ev("inc"))
|
||||
bus.wait()
|
||||
a.Equal(int32(2), i.Load())
|
||||
}
|
||||
115
server/pkg/eventbus/handlers.go
Normal file
115
server/pkg/eventbus/handlers.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/sentry"
|
||||
)
|
||||
|
||||
type (
|
||||
HandlerFn func(ctx context.Context, ev Event) error
|
||||
|
||||
handler struct {
|
||||
handler HandlerFn
|
||||
resourceTypes map[string]bool
|
||||
eventTypes map[string]bool
|
||||
constraints constraintSet
|
||||
weight int
|
||||
}
|
||||
|
||||
// @todo add sorting interface
|
||||
HandlerSet []*handler
|
||||
|
||||
HandlerRegOp func(t *handler)
|
||||
|
||||
eventInvokerSettable interface {
|
||||
SetInvoker(auth.Identifiable)
|
||||
}
|
||||
)
|
||||
|
||||
// Match matches handler with resource event
|
||||
func (t handler) Match(re Event) bool {
|
||||
if re == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(re.ResourceType()) == 0 || !t.resourceTypes[re.ResourceType()] {
|
||||
// Expecting to have valid resource type and match at least one
|
||||
// defined resource on the handler
|
||||
return false
|
||||
}
|
||||
|
||||
if len(re.EventType()) == 0 || !t.eventTypes[re.EventType()] {
|
||||
// Expecting to have valid event type and match at least one
|
||||
// defined event on the handler
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range t.constraints {
|
||||
// Should match all constraints
|
||||
if !re.Match(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (t handler) Handle(ctx context.Context, ev Event) error {
|
||||
defer sentry.Recover()
|
||||
|
||||
if eis, ok := ev.(eventInvokerSettable); ok {
|
||||
eis.SetInvoker(auth.GetIdentityFromContext(ctx))
|
||||
}
|
||||
|
||||
return t.handler(ctx, ev)
|
||||
}
|
||||
|
||||
func NewHandler(h HandlerFn, ops ...HandlerRegOp) *handler {
|
||||
var t = &handler{
|
||||
resourceTypes: make(map[string]bool),
|
||||
eventTypes: make(map[string]bool),
|
||||
handler: h,
|
||||
}
|
||||
|
||||
for _, op := range ops {
|
||||
op(t)
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func For(rr ...string) HandlerRegOp {
|
||||
return func(t *handler) {
|
||||
for _, r := range rr {
|
||||
t.resourceTypes[r] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func On(ee ...string) HandlerRegOp {
|
||||
return func(t *handler) {
|
||||
for _, e := range ee {
|
||||
t.eventTypes[e] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Constraint(c ConstraintMatcher) HandlerRegOp {
|
||||
return func(t *handler) {
|
||||
t.constraints = append(t.constraints, c)
|
||||
}
|
||||
}
|
||||
|
||||
func Weight(weight int) HandlerRegOp {
|
||||
return func(t *handler) {
|
||||
t.weight = weight
|
||||
}
|
||||
}
|
||||
|
||||
// handler sorting:
|
||||
|
||||
func (set HandlerSet) Len() int { return len(set) }
|
||||
func (set HandlerSet) Swap(i, j int) { set[i], set[j] = set[j], set[i] }
|
||||
func (set HandlerSet) Less(i, j int) bool { return set[i].weight < set[j].weight }
|
||||
201
server/pkg/eventbus/handlers_test.go
Normal file
201
server/pkg/eventbus/handlers_test.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
)
|
||||
|
||||
type (
|
||||
mockEvent struct {
|
||||
rType string
|
||||
eType string
|
||||
match func(c ConstraintMatcher) bool
|
||||
|
||||
identity auth.Identifiable
|
||||
}
|
||||
)
|
||||
|
||||
func (e mockEvent) ResourceType() string {
|
||||
return e.rType
|
||||
}
|
||||
|
||||
func (e mockEvent) EventType() string {
|
||||
return e.eType
|
||||
}
|
||||
|
||||
func (e mockEvent) Match(c ConstraintMatcher) bool {
|
||||
if e.match == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return e.match(c)
|
||||
}
|
||||
|
||||
func (e *mockEvent) SetInvoker(identity auth.Identifiable) {
|
||||
e.identity = identity
|
||||
}
|
||||
|
||||
func TestHandler_Match(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
ops []HandlerRegOp
|
||||
ev Event
|
||||
match bool
|
||||
}{
|
||||
{"nil event",
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{"empty resource",
|
||||
[]HandlerRegOp{For("foo"), On("bar")},
|
||||
&mockEvent{rType: "", eType: "bar"},
|
||||
false,
|
||||
},
|
||||
{"empty event",
|
||||
[]HandlerRegOp{For("foo"), On("bar")},
|
||||
&mockEvent{rType: "foo", eType: ""},
|
||||
false,
|
||||
},
|
||||
{"simple foo-bar test",
|
||||
[]HandlerRegOp{For("foo"), On("bar")},
|
||||
&mockEvent{rType: "foo", eType: "bar"},
|
||||
true,
|
||||
},
|
||||
{"ConstraintMatcher match",
|
||||
[]HandlerRegOp{For("foo"), On("bar"), Constraint(MustMakeConstraint("baz", "=", "baz"))},
|
||||
&mockEvent{
|
||||
rType: "foo",
|
||||
eType: "bar",
|
||||
match: func(c ConstraintMatcher) bool {
|
||||
return len(c.Values()) > 0 && c.Name() == c.Values()[0]
|
||||
}},
|
||||
true,
|
||||
},
|
||||
{"ConstraintMatcher mismatch",
|
||||
[]HandlerRegOp{For("foo"), On("bar"), Constraint(MustMakeConstraint("baz", "=", "baz"))},
|
||||
&mockEvent{
|
||||
rType: "foo",
|
||||
eType: "bar",
|
||||
match: func(c ConstraintMatcher) bool {
|
||||
return false
|
||||
}},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var handler = NewHandler(nil, c.ops...)
|
||||
if c.match {
|
||||
assert.True(t, handler.Match(c.ev), "Expecting to match")
|
||||
|
||||
} else {
|
||||
assert.False(t, handler.Match(c.ev), "Expecting to not match")
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_RegOps(t *testing.T) {
|
||||
makeTestHandler := func(t *handler) *handler {
|
||||
if t.resourceTypes == nil {
|
||||
t.resourceTypes = make(map[string]bool)
|
||||
}
|
||||
|
||||
if t.eventTypes == nil {
|
||||
t.eventTypes = make(map[string]bool)
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
exp *handler
|
||||
ops []HandlerRegOp
|
||||
}{
|
||||
{
|
||||
"empty",
|
||||
makeTestHandler(&handler{}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"resource types",
|
||||
makeTestHandler(&handler{resourceTypes: map[string]bool{"foo": true, "bar": true}}),
|
||||
[]HandlerRegOp{For("foo", "bar")},
|
||||
},
|
||||
{
|
||||
"event types",
|
||||
makeTestHandler(&handler{eventTypes: map[string]bool{"foo": true, "bar": true}}),
|
||||
[]HandlerRegOp{On("foo", "bar")},
|
||||
},
|
||||
{
|
||||
"weight",
|
||||
makeTestHandler(&handler{weight: 42}),
|
||||
[]HandlerRegOp{Weight(42)},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
assert.Equal(t, c.exp, NewHandler(nil, c.ops...))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerHandler(t *testing.T) {
|
||||
var (
|
||||
identity uint64 = 42
|
||||
|
||||
a = assert.New(t)
|
||||
ctx = context.Background()
|
||||
ev = &mockEvent{}
|
||||
passedthrough bool
|
||||
|
||||
trSimple = &handler{
|
||||
handler: func(ctx context.Context, ev Event) error {
|
||||
passedthrough = true
|
||||
a.Equal(identity, ev.(*mockEvent).identity.Identity())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
a.False(passedthrough)
|
||||
a.Nil(ev.identity)
|
||||
|
||||
a.NoError(trSimple.Handle(auth.SetIdentityToContext(ctx, auth.Authenticated(identity)), ev))
|
||||
|
||||
a.NotNil(ev.identity)
|
||||
a.Equal(identity, ev.identity.Identity())
|
||||
a.True(passedthrough, "expecting to pass through simple handler")
|
||||
}
|
||||
|
||||
func TestHandlerSorting(t *testing.T) {
|
||||
var (
|
||||
a = assert.New(t)
|
||||
tt = HandlerSet{
|
||||
NewHandler(nil, Weight(3)),
|
||||
NewHandler(nil, Weight(1)),
|
||||
NewHandler(nil, Weight(2)),
|
||||
}
|
||||
|
||||
w2s = func(tt HandlerSet) (out string) {
|
||||
for _, t := range tt {
|
||||
out += fmt.Sprintf("%d,", t.weight)
|
||||
}
|
||||
return
|
||||
}
|
||||
)
|
||||
|
||||
a.Equal(w2s(tt), "3,1,2,")
|
||||
sort.Sort(tt)
|
||||
a.Equal(w2s(tt), "1,2,3,")
|
||||
}
|
||||
Reference in New Issue
Block a user