Extreact & refactor automation scripts & triggets

Automation (scripts and triggers) is now a standalone package and can be used
in other services.
This commit is contained in:
Denis Arh
2019-08-23 13:49:36 +02:00
parent 82e2c58d0a
commit 38fe06998c
42 changed files with 3875 additions and 1174 deletions
+38
View File
@@ -0,0 +1,38 @@
package automation
import (
"context"
"go.uber.org/zap"
"go.uber.org/zap/zapgrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
"github.com/cortezaproject/corteza-server/pkg/cli/options"
)
// Corredor standard connector to Corredor service via gRPC
func Corredor(ctx context.Context, opt options.ScriptRunnerOpt, logger *zap.Logger) (c *grpc.ClientConn, err error) {
if !opt.Enabled {
// Do not connect when script runner is not enabled
return
}
if opt.Log {
// Send logs to zap
//
// waiting for https://github.com/uber-go/zap/pull/538
grpclog.SetLogger(zapgrpc.NewLogger(logger.Named("grpc")))
}
var dopts = []grpc.DialOption{
// @todo insecure?
grpc.WithInsecure(),
}
if opt.MaxBackoffDelay > 0 {
dopts = append(dopts, grpc.WithBackoffMaxDelay(opt.MaxBackoffDelay))
}
return grpc.DialContext(ctx, opt.Addr, dopts...)
}
+67
View File
@@ -0,0 +1,67 @@
package automation
// Hello! This file is auto-generated.
type (
// ScriptSet slice of Script
//
// This type is auto-generated.
ScriptSet []*Script
)
// Walk iterates through every slice item and calls w(Script) err
//
// This function is auto-generated.
func (set ScriptSet) Walk(w func(*Script) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Script) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set ScriptSet) Filter(f func(*Script) (bool, error)) (out ScriptSet, err error) {
var ok bool
out = ScriptSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set ScriptSet) FindByID(ID uint64) *Script {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set ScriptSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
+91
View File
@@ -0,0 +1,91 @@
package automation
import (
"testing"
"errors"
"github.com/cortezaproject/corteza-server/internal/test"
)
// Hello! This file is auto-generated.
func TestScriptSetWalk(t *testing.T) {
value := make(ScriptSet, 3)
// check walk with no errors
{
err := value.Walk(func(*Script) error {
return nil
})
test.NoError(t, err, "Expected no returned error from Walk, got %+v", err)
}
// check walk with error
test.Error(t, value.Walk(func(*Script) error { return errors.New("Walk error") }), "Expected error from walk, got nil")
}
func TestScriptSetFilter(t *testing.T) {
value := make(ScriptSet, 3)
// filter nothing
{
set, err := value.Filter(func(*Script) (bool, error) {
return true, nil
})
test.NoError(t, err, "Didn't expect error when filtering set: %+v", err)
test.Assert(t, len(set) == len(value), "Expected equal length filter: %d != %d", len(value), len(set))
}
// filter one item
{
found := false
set, err := value.Filter(func(*Script) (bool, error) {
if !found {
found = true
return found, nil
}
return false, nil
})
test.NoError(t, err, "Didn't expect error when filtering set: %+v", err)
test.Assert(t, len(set) == 1, "Expected single item, got %d", len(value))
}
// filter error
{
_, err := value.Filter(func(*Script) (bool, error) {
return false, errors.New("Filter error")
})
test.Error(t, err, "Expected error, got %#v", err)
}
}
func TestScriptSetIDs(t *testing.T) {
value := make(ScriptSet, 3)
// construct objects
value[0] = new(Script)
value[1] = new(Script)
value[2] = new(Script)
// set ids
value[0].ID = 1
value[1].ID = 2
value[2].ID = 3
// Find existing
{
val := value.FindByID(2)
test.Assert(t, val.ID == 2, "Expected ID 2, got %d", val.ID)
}
// Find non-existing
{
val := value.FindByID(4)
test.Assert(t, val == nil, "Expected no value, got %#v", val)
}
// List IDs from set
{
val := value.IDs()
test.Assert(t, len(val) == len(value), "Expected ID count mismatch, %d != %d", len(val), len(value))
}
}
+122
View File
@@ -0,0 +1,122 @@
package automation
import (
"time"
"github.com/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/rh"
)
type (
Script struct {
ID uint64 `json:"scriptID,string" db:"id"`
Name string `json:"name" db:"name"`
// (URL) Where did we get the source from?
SourceRef string `json:"sourceRef" db:"source_ref"`
// Code
Source string `json:"source" db:"source"`
// No need to wait for script to return the value
Async bool `json:"async" db:"async"`
// Who is running this script?
// Leave it at 0 for the current user (security invoker) or
// set ID of specific user (security definer)
RunAs uint64 `json:"runAs,string" db:"rel_runner"`
// Where can we run this script? user-agent? corredor service?
RunInUA bool `json:"runInUA" db:"run_in_ua"`
// Are you doing something that can take more time?
// specify timeout (in milliseconds)
Timeout uint `json:"timeout" db:"timeout"`
// Is it critical to run this script successfully?
Critical bool `json:"critical" db:"critical"`
Enabled bool `json:"enabled" db:"enabled"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
CreatedBy uint64 `db:"created_by" json:"createdBy,string" `
UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"`
UpdatedBy uint64 `db:"updated_by" json:"updatedBy,string,omitempty" `
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
DeletedBy uint64 `db:"deleted_by" json:"deletedBy,string,omitempty" `
triggers TriggerSet
}
ScriptFilter struct {
Query string
Resource string
IncDeleted bool `json:"incDeleted"`
// Standard paging fields & helpers
rh.PageFilter
}
)
// IsValid - enabled, deleted?
func (s *Script) IsValid() bool {
return s != nil && s.Enabled && s.DeletedAt == nil
}
// Verify - sanity check of script's properties
func (s Script) Verify() error {
if s.RunAsDefined() && s.RunInUA {
return errors.New("user-agent engine does not support run-as-defined scripts")
}
if s.Critical && s.RunInUA {
return errors.New("user-agent engine scripts can not be critical")
}
return nil
}
// IsCompatible verifies if trigger can be added to a script
func (s *Script) CheckCompatibility(t *Trigger) error {
if s == nil {
return errors.New("not compatible with nil script")
}
if s == nil || t == nil {
return errors.New("not compatible with nil trigger")
}
if t.IsDeferred() {
if s.RunInUA {
return errors.New("deferred triggers are not compatible with user-agent scripts")
}
if s.RunAsInvoker() {
return errors.New("deferred triggers are not compatible with run-as-invoker scripts")
}
}
return nil
}
// FilterByEvent
//
// we will use the Trigger struct as a holder for conditions
func (set ScriptSet) FilterByEvent(event, resource string, cc ...TriggerConditionChecker) (out ScriptSet) {
out, _ = set.Filter(func(s *Script) (bool, error) {
return s.triggers.HasMatch(Trigger{Event: event, Resource: resource}, cc...), nil
})
return
}
// RunAsDefined - script should be run with pre-defined privileges (user)
func (s Script) RunAsDefined() bool {
return s.RunAs > 0
}
// RunAsInvoker - this script should run with invoker's privileges (user)
func (s Script) RunAsInvoker() bool {
return s.RunAs == 0
}
+140
View File
@@ -0,0 +1,140 @@
package automation
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"gopkg.in/Masterminds/squirrel.v1"
"github.com/cortezaproject/corteza-server/pkg/rh"
)
type (
// repository servs as a db storage layer for permission rules
scriptRepository struct {
dbh *factory.DB
// sql table reference
dbTablePrefix string
}
)
func ScriptRepository(db *factory.DB, dbTablePrefix string) *scriptRepository {
return &scriptRepository{
dbTablePrefix: dbTablePrefix,
dbh: db,
}
}
func (r *scriptRepository) With(ctx context.Context) *scriptRepository {
return &scriptRepository{
dbTablePrefix: r.dbTablePrefix,
dbh: r.db().With(ctx),
}
}
func (r *scriptRepository) db() *factory.DB {
return r.dbh
}
func (r scriptRepository) table() string {
return r.dbTablePrefix + "_automation_script"
}
func (r scriptRepository) columns() []string {
return []string{
"id",
"name",
"source_ref",
"source",
"async",
"rel_runner",
"run_in_ua",
"timeout",
"critical",
"enabled",
"created_at",
"created_by",
"updated_at",
"updated_by",
"deleted_at",
"deleted_by",
}
}
func (r *scriptRepository) query() squirrel.SelectBuilder {
return squirrel.
Select(r.columns()...).
From(r.table())
}
// FindByID finds specific script
func (r *scriptRepository) FindByID(ctx context.Context, scriptID uint64) (*Script, error) {
var (
rval = &Script{}
query = r.query().
Columns(r.columns()...).
Where("id = ?", scriptID)
)
return rval, rh.IsFound(rh.FetchOne(r.db(), query, rval), rval.ID > 0, errors.New("script not found"))
}
// Find - finds scripts using given filter
func (r *scriptRepository) Find(ctx context.Context, filter ScriptFilter) (set ScriptSet, f ScriptFilter, err error) {
f = filter
query := r.query()
if !filter.IncDeleted {
query = query.Where("deleted_at IS NULL")
}
if f.Query != "" {
q := "%" + f.Query + "%"
query = query.Where("name like ?", q)
}
if f.Resource != "" {
// Making partial trigger repo struct on the fly to help us calculate the name of the triggers table
ttable := (triggerRepository{dbTablePrefix: r.dbTablePrefix}).table()
query = query.Where(
fmt.Sprintf("id IN (SELECT rel_script FROM `%s` WHERE resource = ?", ttable),
f.Resource,
)
}
if f.Count, err = rh.Count(r.db(), query); err != nil || f.Count == 0 {
return
}
query = query.OrderBy("id ASC")
return set, f, rh.FetchPaged(r.db(), query, f.Page, f.PerPage, &set)
}
// FindAllRunnable - loads and returns all runnable scripts
func (r *scriptRepository) FindAllRunnable() (ScriptSet, error) {
rr := make([]*Script, 0)
return rr, errors.Wrap(rh.FetchAll(
r.db(),
r.query().Where("enabled AND deleted_at IS NULL"),
&rr,
), "could not load runnable scripts")
}
func (r *scriptRepository) Create(s *Script) (err error) {
return r.dbh.Transaction(func() error {
return r.dbh.Insert(r.table(), s)
})
}
func (r *scriptRepository) Update(s *Script) (err error) {
return r.dbh.Transaction(func() error {
return r.dbh.Update(r.table(), s, "id")
})
}
+48
View File
@@ -0,0 +1,48 @@
package automation
import (
"testing"
)
func TestScript_CheckCompatibility(t *testing.T) {
tests := []struct {
name string
s *Script
t *Trigger
wantErr bool
}{
{name: "both nil",
s: nil,
t: nil,
wantErr: true,
},
{name: "both vanilla",
s: &Script{},
t: &Trigger{},
wantErr: false,
},
{name: "deferred trigger with UA script",
s: &Script{RunInUA: true},
t: &Trigger{Event: EVENT_TYPE_INTERVAL},
wantErr: true,
},
{name: "deferred trigger with invoker security",
s: &Script{RunAs: 0},
t: &Trigger{Event: EVENT_TYPE_INTERVAL},
wantErr: true,
},
{name: "deferred trigger with invoker security",
s: &Script{RunAs: 1, RunInUA: false},
t: &Trigger{Event: EVENT_TYPE_INTERVAL},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.s.CheckCompatibility(tt.t); (err != nil) != tt.wantErr {
t.Errorf("CheckCompatibility() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
+322
View File
@@ -0,0 +1,322 @@
package automation
import (
"context"
"sync"
"time"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"go.uber.org/zap"
"github.com/cortezaproject/corteza-server/internal/auth"
"github.com/cortezaproject/corteza-server/pkg/sentry"
)
type (
service struct {
l sync.Mutex
logger *zap.Logger
c AutomationServiceConfig
// service will flush values on TRUE or just reload on FALSE
f chan bool
// internal list of runnable scripts (and their accompanying triggers)
runnables ScriptSet
srepo *scriptRepository
trepo *triggerRepository
}
ScriptsProvider interface {
FilterByEvent(event, resource string, cc ...TriggerConditionChecker) ScriptSet
}
WatcherService interface {
Watch(ctx context.Context)
}
AutomationServiceConfig struct {
DB *factory.DB
DbTablePrefix string
}
)
const (
watchInterval = time.Hour
)
// Service initializes service{} struct
//
// service{} struct handles scripts & triggers. It acts as a caching layer and
// proxy to repository where it verifies and enriches payloads
//
func Service(ctx context.Context, logger *zap.Logger, c AutomationServiceConfig) (svc *service) {
svc = &service{
logger: logger.Named("automation"),
c: c,
f: make(chan bool),
}
if c.DB != nil {
svc.srepo = ScriptRepository(c.DB, c.DbTablePrefix)
svc.trepo = TriggerRepository(c.DB, c.DbTablePrefix)
}
svc.Reload(ctx)
return
}
// Watch() Watches for changes
func (svc service) Watch(ctx context.Context) {
go func() {
defer sentry.Recover()
var ticker = time.NewTicker(watchInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
svc.Reload(ctx)
case <-svc.f:
svc.Reload(ctx)
}
}
}()
svc.logger.Debug("watcher initialized")
}
func (svc *service) Reload(ctx context.Context) {
svc.l.Lock()
defer svc.l.Unlock()
if svc.c.DB == nil {
return
}
var (
err error
ss ScriptSet
tt TriggerSet
)
ss, err = svc.srepo.With(ctx).FindAllRunnable()
svc.logger.Info("scripts loaded", zap.Error(err), zap.Int("count", len(tt)))
if err != nil {
return
}
// Only interested in valid scritps
ss, _ = ss.Filter(func(s *Script) (b bool, e error) {
return s.IsValid(), nil
})
tt, err = svc.trepo.With(ctx).FindAllRunnable()
svc.logger.Info("triggers loaded", zap.Error(err), zap.Int("count", len(tt)))
if err != nil {
return
}
_ = tt.Walk(func(t *Trigger) error {
s := ss.FindByID(t.ScriptID)
if t.IsValid() && s.CheckCompatibility(t) != nil {
// Add only compatible triggers
s.triggers = append(s.triggers, t)
}
return nil
})
}
// FindRunnableScripts scans internal list of runnable scripts and filters them by (trigger's) event and origin
func (svc service) FindRunnableScripts(event, origin string, cc ...TriggerConditionChecker) ScriptSet {
return svc.runnables.FilterByEvent(event, origin, cc...)
}
// updateRunnableScripts - updates script set (internal runnable scripts list)
func (svc service) updateRunnableScripts(n *Script) {
svc.l.Lock()
defer svc.l.Unlock()
ss := svc.runnables
for i := range svc.runnables {
if ss[i].ID != n.ID {
continue
}
if n.IsValid() {
// Valid, replace
ss[i] = n
}
// Invalid, remove
ss = append(ss[:i], ss[i+1:]...)
return
}
if n.IsValid() {
ss = append(ss, n)
}
return
}
// updateScriptsWithTrigger - finds the referenced script and updates its trigger set
func (svc service) updateScriptWithTrigger(n *Trigger) {
svc.l.Lock()
defer svc.l.Unlock()
ss := svc.runnables
for i := range ss {
if ss[i].ID != n.ScriptID {
continue
}
tt := ss[i].triggers
for i = range tt {
if n.IsValid() {
// Valid, replace
tt[i] = n
}
// Invalid, remove
tt = append(tt[:i], tt[i+i:]...)
return
}
if n.IsValid() {
tt = append(tt, n)
}
return
}
}
func (svc service) FindScriptByID(ctx context.Context, scriptID uint64) (*Script, error) {
return svc.srepo.FindByID(ctx, scriptID)
}
func (svc service) FindScripts(ctx context.Context, f ScriptFilter) (ScriptSet, ScriptFilter, error) {
return svc.srepo.Find(ctx, f)
}
// CreateScript - modifies script's props, pushes to repo & updates scripts cache
func (svc service) CreateScript(ctx context.Context, s *Script) (err error) {
s.ID = factory.Sonyflake.NextID()
s.CreatedAt = time.Now()
s.CreatedBy = auth.GetIdentityFromContext(ctx).Identity()
if err = svc.srepo.Create(s); err != nil {
return err
}
svc.updateRunnableScripts(s)
return
}
// UpdateScript - modifies script's props, pushes to repo & updates scripts cache
func (svc service) UpdateScript(ctx context.Context, s *Script) (err error) {
s.UpdatedAt = &time.Time{}
*s.UpdatedAt = time.Now()
s.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
// Ensure sanity
s.UpdatedAt, s.UpdatedBy = nil, 0
s.DeletedAt, s.DeletedBy = nil, 0
if err = svc.srepo.Update(s); err != nil {
return err
}
svc.updateRunnableScripts(s)
return
}
// DeleteScript - modifies script's props, pushes to repo & updates scripts cache
func (svc service) DeleteScript(ctx context.Context, s *Script) (err error) {
s.DeletedAt = &time.Time{}
*s.DeletedAt = time.Now()
s.DeletedBy = auth.GetIdentityFromContext(ctx).Identity()
// We're doing soft delete in the repo
if err = svc.srepo.Update(s); err != nil {
return err
}
svc.updateRunnableScripts(s)
return
}
func (svc service) FindTriggerByID(ctx context.Context, scriptID uint64) (*Trigger, error) {
return svc.trepo.FindByID(ctx, scriptID)
}
func (svc service) FindTriggers(ctx context.Context, f TriggerFilter) (TriggerSet, TriggerFilter, error) {
return svc.trepo.Find(ctx, f)
}
// CreateScript - modifies script's props, pushes to repo & updates scripts cache
func (svc service) CreateTrigger(ctx context.Context, s *Script, t *Trigger) (err error) {
if err = s.CheckCompatibility(t); err != nil {
return err
}
t.ID = factory.Sonyflake.NextID()
t.CreatedAt = time.Now()
t.CreatedBy = auth.GetIdentityFromContext(ctx).Identity()
t.ScriptID = s.ID
if err = svc.trepo.Create(t); err != nil {
return err
}
svc.updateScriptWithTrigger(t)
return
}
// UpdateTrigger - modifies script's props, pushes to repo & updates scripts cache
func (svc service) UpdateTrigger(ctx context.Context, s *Script, t *Trigger) (err error) {
if s.ID != t.ScriptID {
return errors.New("invalid script-trigger reference")
}
if err = s.CheckCompatibility(t); err != nil {
return err
}
t.UpdatedAt = &time.Time{}
*t.UpdatedAt = time.Now()
t.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
if err = svc.trepo.Update(t); err != nil {
return err
}
svc.updateScriptWithTrigger(t)
return
}
// DeleteTrigger - modifies script's props, pushes to repo & updates scripts cache
func (svc service) DeleteTrigger(ctx context.Context, t *Trigger) (err error) {
t.DeletedAt = &time.Time{}
*t.DeletedAt = time.Now()
t.DeletedBy = auth.GetIdentityFromContext(ctx).Identity()
// We're doing soft delete in the repo
if err = svc.trepo.Update(t); err != nil {
return err
}
svc.updateScriptWithTrigger(t)
return
}
+67
View File
@@ -0,0 +1,67 @@
package automation
// Hello! This file is auto-generated.
type (
// TriggerSet slice of Trigger
//
// This type is auto-generated.
TriggerSet []*Trigger
)
// Walk iterates through every slice item and calls w(Trigger) err
//
// This function is auto-generated.
func (set TriggerSet) Walk(w func(*Trigger) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Trigger) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set TriggerSet) Filter(f func(*Trigger) (bool, error)) (out TriggerSet, err error) {
var ok bool
out = TriggerSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set TriggerSet) FindByID(ID uint64) *Trigger {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set TriggerSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
+91
View File
@@ -0,0 +1,91 @@
package automation
import (
"testing"
"errors"
"github.com/cortezaproject/corteza-server/internal/test"
)
// Hello! This file is auto-generated.
func TestTriggerSetWalk(t *testing.T) {
value := make(TriggerSet, 3)
// check walk with no errors
{
err := value.Walk(func(*Trigger) error {
return nil
})
test.NoError(t, err, "Expected no returned error from Walk, got %+v", err)
}
// check walk with error
test.Error(t, value.Walk(func(*Trigger) error { return errors.New("Walk error") }), "Expected error from walk, got nil")
}
func TestTriggerSetFilter(t *testing.T) {
value := make(TriggerSet, 3)
// filter nothing
{
set, err := value.Filter(func(*Trigger) (bool, error) {
return true, nil
})
test.NoError(t, err, "Didn't expect error when filtering set: %+v", err)
test.Assert(t, len(set) == len(value), "Expected equal length filter: %d != %d", len(value), len(set))
}
// filter one item
{
found := false
set, err := value.Filter(func(*Trigger) (bool, error) {
if !found {
found = true
return found, nil
}
return false, nil
})
test.NoError(t, err, "Didn't expect error when filtering set: %+v", err)
test.Assert(t, len(set) == 1, "Expected single item, got %d", len(value))
}
// filter error
{
_, err := value.Filter(func(*Trigger) (bool, error) {
return false, errors.New("Filter error")
})
test.Error(t, err, "Expected error, got %#v", err)
}
}
func TestTriggerSetIDs(t *testing.T) {
value := make(TriggerSet, 3)
// construct objects
value[0] = new(Trigger)
value[1] = new(Trigger)
value[2] = new(Trigger)
// set ids
value[0].ID = 1
value[1].ID = 2
value[2].ID = 3
// Find existing
{
val := value.FindByID(2)
test.Assert(t, val.ID == 2, "Expected ID 2, got %d", val.ID)
}
// Find non-existing
{
val := value.FindByID(4)
test.Assert(t, val == nil, "Expected no value, got %#v", val)
}
// List IDs from set
{
val := value.IDs()
test.Assert(t, len(val) == len(value), "Expected ID count mismatch, %d != %d", len(val), len(value))
}
}
+111
View File
@@ -0,0 +1,111 @@
package automation
import (
"time"
"github.com/cortezaproject/corteza-server/pkg/rh"
)
type (
Event string
Trigger struct {
ID uint64 `json:"triggerID,string" db:"id"`
// Resource that triggered the event
// - "compose:" (unspec, general)
// - "compose:record"
// - "compose:namespace"
Resource string `json:"resource" db:"resource"`
// Event name, arbitrary string
// - "before"
// - "after"
// - "on"
// - "at"
Event string `json:"event" db:"event"`
// Arbitrary data for trigger condition
//
// It is caller's responsibility to encode, decode and verify conditions
Condition string `json:"condition" db:"condition"`
ScriptID uint64 `json:"scriptID,string" db:"rel_script"`
// Is trigger enabled or disabled?
Enabled bool `json:"enabled" db:"enabled"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
CreatedBy uint64 `db:"created_by" json:"createdBy,string" `
UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"`
UpdatedBy uint64 `db:"updated_by" json:"updatedBy,string,omitempty" `
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
DeletedBy uint64 `db:"deleted_by" json:"deletedBy,string,omitempty" `
}
TriggerFilter struct {
Resource string
Event string
ScriptID uint64
IncDeleted bool
// Standard paging fields & helpers
rh.PageFilter
}
TriggerConditionChecker func(string) bool
)
const (
EVENT_TYPE_INTERVAL = "interval"
EVENT_TYPE_TIMESTAMP = "at"
)
// IsValid checks if trigger is enabled and not deleted
func (t *Trigger) IsValid() bool {
return t != nil && t.Enabled && t.DeletedAt == nil
}
// IsDeferred - not called as consequence of a user's action (create, delete, update)
func (t Trigger) IsDeferred() bool {
return t.Event == EVENT_TYPE_INTERVAL || t.Event == EVENT_TYPE_TIMESTAMP
}
// HasMatch checks if any og the triggers in a set matches the given parameters
func (set TriggerSet) HasMatch(m Trigger, ff ...TriggerConditionChecker) bool {
withTriggers:
for _, t := range set {
if !t.IsValid() {
// only valid can match
continue withTriggers
}
if m.ID > 0 && m.ID != t.ID {
// Are we looking for a particular trigger?
continue withTriggers
}
if m.Resource != t.Resource {
// event should match
continue withTriggers
}
if m.Event != t.Event {
// event should match
continue withTriggers
}
// Go through all condition checking functions
// All of them should return true for trigger to match
for _, fn := range ff {
if !fn(t.Condition) {
continue withTriggers
}
}
return true
}
return false
}
+149
View File
@@ -0,0 +1,149 @@
package automation
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"gopkg.in/Masterminds/squirrel.v1"
"github.com/cortezaproject/corteza-server/pkg/rh"
)
type (
// repository servs as a db storage layer for permission rules
triggerRepository struct {
dbh *factory.DB
// sql table reference
dbTablePrefix string
}
)
func TriggerRepository(db *factory.DB, dbTablePrefix string) *triggerRepository {
return &triggerRepository{
dbTablePrefix: dbTablePrefix,
dbh: db,
}
}
func (r *triggerRepository) With(ctx context.Context) *triggerRepository {
return &triggerRepository{
dbTablePrefix: r.dbTablePrefix,
dbh: r.db().With(ctx),
}
}
func (r *triggerRepository) db() *factory.DB {
return r.dbh
}
func (r triggerRepository) table() string {
return r.dbTablePrefix + "_automation_trigger"
}
func (r triggerRepository) columns() []string {
return []string{
"id",
"event",
"resource",
"`condition`",
"rel_script",
"enabled",
"created_at",
"created_by",
"updated_at",
"updated_by",
"deleted_at",
"deleted_by",
}
}
func (r *triggerRepository) query() squirrel.SelectBuilder {
return squirrel.
Select(r.columns()...).
From(r.table())
}
// FindByID finds specific trigger
func (r *triggerRepository) FindByID(ctx context.Context, triggerID uint64) (*Trigger, error) {
var (
rval = &Trigger{}
query = r.query().
Columns(r.columns()...).
Where("id = ?", triggerID)
)
return rval, rh.IsFound(rh.FetchOne(r.db(), query, rval), rval.ID > 0, errors.New("trigger not found"))
}
// Find - finds triggers using given filter
func (r *triggerRepository) Find(ctx context.Context, filter TriggerFilter) (set TriggerSet, f TriggerFilter, err error) {
f = filter
query := r.query()
if f.ScriptID > 0 {
query = query.Where("rel_script = ?", f.ScriptID)
}
if f.Event != "" {
query = query.Where("resource = ?", f.Event)
}
if f.Resource != "" {
query = query.Where("resource = ?", f.Resource)
}
if !filter.IncDeleted {
query = query.Where("deleted_at IS NULL")
}
if f.Count, err = rh.Count(r.db(), query); err != nil || f.Count == 0 {
return
}
query = query.OrderBy("id ASC")
return set, f, rh.FetchPaged(r.db(), query, f.Page, f.PerPage, &set)
}
// FindAllRunnable - loads and returns all runnable triggers
func (r *triggerRepository) FindAllRunnable() (TriggerSet, error) {
rr := make([]*Trigger, 0)
return rr, errors.Wrap(rh.FetchAll(
r.db(),
r.query().Where("enabled AND deleted_at IS NULL"),
&rr,
), "could not load runnable triggers")
}
func (r *triggerRepository) Create(s *Trigger) (err error) {
return r.dbh.Transaction(func() error {
// Generate ID
s.ID = factory.Sonyflake.NextID()
if s.CreatedAt.IsZero() {
// Make sure time of creation is set
s.CreatedAt = time.Now()
}
// Ensure sanity
s.UpdatedAt, s.UpdatedBy = nil, 0
s.DeletedAt, s.DeletedBy = nil, 0
return r.dbh.Insert(r.table(), s)
})
}
func (r *triggerRepository) Update(s *Trigger) (err error) {
return r.dbh.Transaction(func() error {
s.UpdatedAt = &time.Time{}
*s.UpdatedAt = time.Now()
return r.dbh.Update(r.table(), s, "id")
})
}
+46
View File
@@ -0,0 +1,46 @@
package automation
import (
"testing"
)
func TestTriggerSet_HasMatch(t *testing.T) {
type args struct {
m Trigger
}
tests := []struct {
name string
set TriggerSet
args args
want bool
}{
{
name: "simple match",
set: TriggerSet{nil, &Trigger{}, &Trigger{Event: "e", Enabled: true}, nil, &Trigger{}},
args: args{m: Trigger{Event: "e"}},
want: true,
}, {
name: "simple miss",
set: TriggerSet{nil, &Trigger{}, &Trigger{Event: "e", Enabled: true}, nil, &Trigger{}},
args: args{m: Trigger{}},
want: false,
}, {
name: "specific",
set: TriggerSet{nil, &Trigger{}, &Trigger{ID: 2, Enabled: true}, nil, &Trigger{}},
args: args{m: Trigger{ID: 2}},
want: true,
}, {
name: "invalid",
set: TriggerSet{nil, &Trigger{}, &Trigger{Event: "e", Enabled: false}, nil, &Trigger{}},
args: args{m: Trigger{Event: "e"}},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.set.HasMatch(tt.args.m); got != tt.want {
t.Errorf("HasMatch() = %v, want %v", got, tt.want)
}
})
}
}
+4
View File
@@ -32,3 +32,7 @@ func (pf *PageFilter) NormalizePerPage(min, max, def uint) {
func (pf *PageFilter) NormalizePerPageWithDefaults() {
pf.PerPage = NormalizePerPage(pf.PerPage, PER_PAGE_MIN, PER_PAGE_MAX, PER_PAGE_DEFAULT)
}
func (pf *PageFilter) NormalizePerPageNoMax() {
pf.PerPage = NormalizePerPage(pf.PerPage, PER_PAGE_MIN, 0, PER_PAGE_DEFAULT)
}