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:
842
server/automation/service/workflow.go
Normal file
842
server/automation/service/workflow.go
Normal file
@@ -0,0 +1,842 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
intAuth "github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/eventbus"
|
||||
"github.com/cortezaproject/corteza/server/pkg/expr"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/handle"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/pkg/label"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/pkg/wfexec"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
workflow struct {
|
||||
eventbus workflowEventTriggerHandler
|
||||
store store.Storer
|
||||
actionlog actionlog.Recorder
|
||||
ac workflowAccessController
|
||||
triggers *trigger
|
||||
session *session
|
||||
|
||||
opt options.WorkflowOpt
|
||||
|
||||
log *zap.Logger
|
||||
|
||||
// cache of workflows, graphs to workflow ID (key, uint64)
|
||||
cache map[uint64]*wfCacheItem
|
||||
muxCache *sync.RWMutex
|
||||
|
||||
// handle to workflow index
|
||||
wIndex map[string]uint64
|
||||
muxWIndex *sync.RWMutex
|
||||
|
||||
// workflow function registry
|
||||
reg *registry
|
||||
corredorOpt options.CorredorOpt
|
||||
|
||||
parser expr.Parsable
|
||||
}
|
||||
|
||||
wfCacheItem struct {
|
||||
wf *types.Workflow
|
||||
|
||||
// caching exec graph
|
||||
g *wfexec.Graph
|
||||
|
||||
// caching user we'll executing workflow with
|
||||
runAs intAuth.Identifiable
|
||||
}
|
||||
|
||||
workflowAccessController interface {
|
||||
CanSearchWorkflows(context.Context) bool
|
||||
CanCreateWorkflow(context.Context) bool
|
||||
CanReadWorkflow(context.Context, *types.Workflow) bool
|
||||
CanUpdateWorkflow(context.Context, *types.Workflow) bool
|
||||
CanDeleteWorkflow(context.Context, *types.Workflow) bool
|
||||
CanUndeleteWorkflow(context.Context, *types.Workflow) bool
|
||||
CanManageSessionsOnWorkflow(context.Context, *types.Workflow) bool
|
||||
|
||||
Grant(ctx context.Context, rr ...*rbac.Rule) error
|
||||
|
||||
workflowExecController
|
||||
}
|
||||
|
||||
workflowExecController interface {
|
||||
CanExecuteWorkflow(context.Context, *types.Workflow) bool
|
||||
}
|
||||
|
||||
workflowEventTriggerHandler interface {
|
||||
Register(h eventbus.HandlerFn, ops ...eventbus.HandlerRegOp) uintptr
|
||||
Unregister(ptrs ...uintptr)
|
||||
}
|
||||
|
||||
workflowUpdateHandler func(ctx context.Context, ns *types.Workflow) (workflowChanges, error)
|
||||
workflowChanges uint8
|
||||
|
||||
workflowInvokerCtxKey struct{}
|
||||
)
|
||||
|
||||
const (
|
||||
workflowUnchanged workflowChanges = 0
|
||||
workflowChanged workflowChanges = 1
|
||||
workflowLabelsChanged workflowChanges = 2
|
||||
workflowDefChanged workflowChanges = 4
|
||||
)
|
||||
|
||||
func Workflow(log *zap.Logger, corredorOpt options.CorredorOpt, opt options.WorkflowOpt) *workflow {
|
||||
return &workflow{
|
||||
log: log,
|
||||
opt: opt,
|
||||
actionlog: DefaultActionlog,
|
||||
store: DefaultStore,
|
||||
ac: DefaultAccessControl,
|
||||
triggers: DefaultTrigger,
|
||||
session: DefaultSession,
|
||||
eventbus: eventbus.Service(),
|
||||
cache: make(map[uint64]*wfCacheItem),
|
||||
wIndex: make(map[string]uint64),
|
||||
muxCache: &sync.RWMutex{},
|
||||
muxWIndex: &sync.RWMutex{},
|
||||
parser: expr.NewParser(),
|
||||
reg: Registry(),
|
||||
corredorOpt: corredorOpt,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *workflow) Search(ctx context.Context, filter types.WorkflowFilter) (rr types.WorkflowSet, f types.WorkflowFilter, err error) {
|
||||
var (
|
||||
wap = &workflowActionProps{filter: &filter}
|
||||
)
|
||||
|
||||
// For each fetched item, store backend will check if it is valid or not
|
||||
filter.Check = func(res *types.Workflow) (bool, error) {
|
||||
if !svc.ac.CanReadWorkflow(ctx, res) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err = func() (err error) {
|
||||
if !svc.ac.CanSearchWorkflows(ctx) {
|
||||
return WorkflowErrNotAllowedToSearch()
|
||||
}
|
||||
|
||||
if len(filter.Labels) > 0 {
|
||||
filter.LabeledIDs, err = label.Search(
|
||||
ctx,
|
||||
svc.store,
|
||||
types.Workflow{}.LabelResourceKind(),
|
||||
filter.Labels,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// labels specified but no labeled resources found
|
||||
if len(filter.LabeledIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if rr, f, err = store.SearchAutomationWorkflows(ctx, svc.store, filter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = label.Load(ctx, svc.store, toLabeledWorkflows(rr)...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return rr, f, svc.recordAction(ctx, wap, WorkflowActionSearch, err)
|
||||
}
|
||||
|
||||
func (svc *workflow) LookupByID(ctx context.Context, workflowID uint64) (wf *types.Workflow, err error) {
|
||||
var (
|
||||
wap = &workflowActionProps{workflow: &types.Workflow{ID: workflowID}}
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
|
||||
if wf, err = loadWorkflow(ctx, s, workflowID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !svc.ac.CanReadWorkflow(ctx, wf) {
|
||||
return WorkflowErrNotAllowedToRead()
|
||||
}
|
||||
|
||||
if err = label.Load(ctx, svc.store, wf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return wf, svc.recordAction(ctx, wap, WorkflowActionLookup, err)
|
||||
}
|
||||
|
||||
// Create adds new workflow resource and saves it into store
|
||||
// It updates service's cache
|
||||
func (svc *workflow) Create(ctx context.Context, new *types.Workflow) (wf *types.Workflow, err error) {
|
||||
var (
|
||||
wap = &workflowActionProps{workflow: new}
|
||||
cUser = intAuth.GetIdentityFromContext(ctx).Identity()
|
||||
g *wfexec.Graph
|
||||
runAs intAuth.Identifiable
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
if !svc.ac.CanCreateWorkflow(ctx) {
|
||||
return WorkflowErrNotAllowedToCreate()
|
||||
}
|
||||
|
||||
if new.Meta.Name == "" {
|
||||
return WorkflowErrMissingName()
|
||||
}
|
||||
|
||||
if !handle.IsValid(new.Handle) {
|
||||
return WorkflowErrInvalidHandle()
|
||||
}
|
||||
|
||||
if err = svc.uniqueCheck(ctx, new); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wf = &types.Workflow{
|
||||
ID: nextID(),
|
||||
Handle: new.Handle,
|
||||
Labels: new.Labels,
|
||||
Meta: new.Meta,
|
||||
Enabled: new.Enabled,
|
||||
Trace: new.Trace,
|
||||
KeepSessions: new.KeepSessions,
|
||||
|
||||
Scope: new.Scope,
|
||||
Steps: new.Steps,
|
||||
Paths: new.Paths,
|
||||
|
||||
// @todo need to check against access control if current user can modify security descriptor
|
||||
RunAs: new.RunAs,
|
||||
OwnedBy: cUser,
|
||||
CreatedAt: *now(),
|
||||
CreatedBy: cUser,
|
||||
}
|
||||
|
||||
wap.workflow = wf
|
||||
|
||||
if g, runAs, err = svc.validateWorkflow(ctx, wf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
svc.updateCache(wf, runAs, g)
|
||||
|
||||
if len(wf.Issues) == 0 {
|
||||
if err = svc.triggers.registerWorkflows(ctx, wf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = store.CreateAutomationWorkflow(ctx, s, wf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = label.Create(ctx, s, wf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
wap.setNew(wf)
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
return wf, svc.recordAction(ctx, wap, WorkflowActionCreate, err)
|
||||
}
|
||||
|
||||
// Update modifies existing workflow resource in the store
|
||||
func (svc *workflow) Update(ctx context.Context, upd *types.Workflow) (*types.Workflow, error) {
|
||||
return svc.updater(ctx, upd.ID, WorkflowActionUpdate, func(ctx context.Context, res *types.Workflow) (workflowChanges, error) {
|
||||
if upd.Meta.Name == "" {
|
||||
return workflowUnchanged, WorkflowErrMissingName()
|
||||
}
|
||||
|
||||
if !svc.ac.CanUpdateWorkflow(ctx, res) {
|
||||
return workflowUnchanged, WorkflowErrNotAllowedToUpdate()
|
||||
}
|
||||
|
||||
handler := svc.handleUpdate(upd)
|
||||
return handler(ctx, res)
|
||||
})
|
||||
}
|
||||
|
||||
func (svc *workflow) DeleteByID(ctx context.Context, workflowID uint64) error {
|
||||
return trim1st(svc.updater(ctx, workflowID, WorkflowActionDelete, func(ctx context.Context, res *types.Workflow) (workflowChanges, error) {
|
||||
changes, err := svc.handleDelete(ctx, res)
|
||||
if err != nil {
|
||||
return workflowUnchanged, err
|
||||
}
|
||||
|
||||
return changes, err
|
||||
}))
|
||||
}
|
||||
|
||||
func (svc *workflow) UndeleteByID(ctx context.Context, workflowID uint64) error {
|
||||
return trim1st(svc.updater(ctx, workflowID, WorkflowActionUndelete, func(ctx context.Context, res *types.Workflow) (workflowChanges, error) {
|
||||
var (
|
||||
changes, err = svc.handleUndelete(ctx, res)
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return workflowUnchanged, err
|
||||
}
|
||||
|
||||
return changes, err
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
func (svc workflow) uniqueCheck(ctx context.Context, res *types.Workflow) (err error) {
|
||||
if res.Handle != "" {
|
||||
if e, _ := store.LookupAutomationWorkflowByHandle(ctx, svc.store, res.Handle); e != nil && e.ID != res.ID {
|
||||
return WorkflowErrHandleNotUnique()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *workflow) updater(ctx context.Context, workflowID uint64, action func(...*workflowActionProps) *workflowAction, fn workflowUpdateHandler) (*types.Workflow, error) {
|
||||
var (
|
||||
changes workflowChanges
|
||||
res *types.Workflow
|
||||
aProps = &workflowActionProps{workflow: &types.Workflow{ID: workflowID}}
|
||||
err error
|
||||
g *wfexec.Graph
|
||||
runAs intAuth.Identifiable
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
|
||||
res, err = loadWorkflow(ctx, s, workflowID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = label.Load(ctx, svc.store, res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.setWorkflow(res)
|
||||
aProps.setUpdate(res)
|
||||
|
||||
if changes, err = fn(ctx, res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if g, runAs, err = svc.validateWorkflow(ctx, res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
svc.updateCache(res, runAs, g)
|
||||
|
||||
if len(res.Issues) == 0 {
|
||||
if err = svc.triggers.registerWorkflows(ctx, res); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if changes&workflowChanged > 0 || len(res.Issues) > 0 {
|
||||
if err = store.UpdateAutomationWorkflow(ctx, svc.store, res); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if changes&workflowLabelsChanged > 0 {
|
||||
if err = label.Update(ctx, s, res); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
return res, svc.recordAction(ctx, aProps, action, err)
|
||||
}
|
||||
|
||||
func (svc *workflow) handleToID(h string) uint64 {
|
||||
svc.muxWIndex.RLock()
|
||||
defer svc.muxWIndex.RUnlock()
|
||||
|
||||
return svc.wIndex[h]
|
||||
}
|
||||
|
||||
func (svc workflow) handleUpdate(upd *types.Workflow) workflowUpdateHandler {
|
||||
return func(ctx context.Context, res *types.Workflow) (changes workflowChanges, err error) {
|
||||
if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) {
|
||||
return workflowUnchanged, WorkflowErrStaleData()
|
||||
}
|
||||
|
||||
if upd.Handle != res.Handle && !handle.IsValid(upd.Handle) {
|
||||
return workflowUnchanged, WorkflowErrInvalidHandle()
|
||||
}
|
||||
|
||||
if err := svc.uniqueCheck(ctx, upd); err != nil {
|
||||
return workflowUnchanged, err
|
||||
}
|
||||
|
||||
if !svc.ac.CanUpdateWorkflow(ctx, res) {
|
||||
return workflowUnchanged, WorkflowErrNotAllowedToUpdate()
|
||||
}
|
||||
|
||||
if res.Handle != upd.Handle {
|
||||
changes |= workflowChanged
|
||||
res.Handle = upd.Handle
|
||||
}
|
||||
|
||||
if res.Enabled != upd.Enabled {
|
||||
changes |= workflowChanged | workflowDefChanged
|
||||
res.Enabled = upd.Enabled
|
||||
}
|
||||
|
||||
if upd.Labels != nil {
|
||||
if label.Changed(res.Labels, upd.Labels) {
|
||||
changes |= workflowLabelsChanged
|
||||
res.Labels = upd.Labels
|
||||
}
|
||||
}
|
||||
|
||||
if res.Trace != upd.Trace {
|
||||
changes |= workflowChanged | workflowDefChanged
|
||||
res.Trace = upd.Trace
|
||||
}
|
||||
|
||||
if res.KeepSessions != upd.KeepSessions {
|
||||
changes |= workflowChanged | workflowDefChanged
|
||||
res.KeepSessions = upd.KeepSessions
|
||||
}
|
||||
|
||||
if upd.Meta != nil {
|
||||
if !reflect.DeepEqual(upd.Meta, res.Meta) {
|
||||
changes |= workflowChanged
|
||||
res.Meta = upd.Meta
|
||||
}
|
||||
}
|
||||
|
||||
if upd.Scope != nil {
|
||||
if !reflect.DeepEqual(upd.Scope, res.Scope) {
|
||||
changes |= workflowChanged | workflowDefChanged
|
||||
res.Scope = upd.Scope
|
||||
}
|
||||
}
|
||||
|
||||
if upd.Steps != nil {
|
||||
if !reflect.DeepEqual(upd.Steps, res.Steps) {
|
||||
changes |= workflowChanged | workflowDefChanged
|
||||
res.Steps = upd.Steps
|
||||
}
|
||||
}
|
||||
|
||||
if upd.Paths != nil {
|
||||
if !reflect.DeepEqual(upd.Paths, res.Paths) {
|
||||
changes |= workflowChanged | workflowDefChanged
|
||||
res.Paths = upd.Paths
|
||||
}
|
||||
}
|
||||
|
||||
if res.RunAs != upd.RunAs {
|
||||
// @todo need to check against access control if current user can modify security descriptor
|
||||
changes |= workflowChanged | workflowDefChanged
|
||||
res.RunAs = upd.RunAs
|
||||
}
|
||||
|
||||
if res.OwnedBy != upd.OwnedBy {
|
||||
// @todo need to check against access control if current user can modify owner
|
||||
changes |= workflowChanged
|
||||
res.OwnedBy = upd.OwnedBy
|
||||
}
|
||||
|
||||
if changes&workflowChanged > 0 {
|
||||
res.UpdatedAt = now()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (svc workflow) handleDelete(ctx context.Context, res *types.Workflow) (workflowChanges, error) {
|
||||
if !svc.ac.CanDeleteWorkflow(ctx, res) {
|
||||
return workflowUnchanged, WorkflowErrNotAllowedToDelete()
|
||||
}
|
||||
|
||||
if res.DeletedAt != nil {
|
||||
// workflow already deleted
|
||||
return workflowUnchanged, nil
|
||||
}
|
||||
|
||||
res.DeletedAt = now()
|
||||
return workflowChanged, nil
|
||||
}
|
||||
|
||||
func (svc workflow) handleUndelete(ctx context.Context, res *types.Workflow) (workflowChanges, error) {
|
||||
if !svc.ac.CanUndeleteWorkflow(ctx, res) {
|
||||
return workflowUnchanged, WorkflowErrNotAllowedToUndelete()
|
||||
}
|
||||
|
||||
if res.DeletedAt == nil {
|
||||
// workflow not deleted
|
||||
return workflowUnchanged, nil
|
||||
}
|
||||
|
||||
res.DeletedAt = nil
|
||||
return workflowChanged, nil
|
||||
}
|
||||
|
||||
func (svc *workflow) Load(ctx context.Context) error {
|
||||
var (
|
||||
set, _, err = store.SearchAutomationWorkflows(ctx, svc.store, types.WorkflowFilter{
|
||||
Deleted: filter.StateInclusive,
|
||||
Disabled: filter.StateExcluded,
|
||||
SubWorkflow: filter.StateInclusive,
|
||||
})
|
||||
g *wfexec.Graph
|
||||
runAs intAuth.Identifiable
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc.muxWIndex.Lock()
|
||||
defer svc.muxWIndex.Unlock()
|
||||
|
||||
for _, wf := range set {
|
||||
svc.wIndex[wf.Handle] = wf.ID
|
||||
|
||||
if g, runAs, err = svc.validateWorkflow(ctx, wf); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
svc.updateCache(wf, runAs, g)
|
||||
}
|
||||
|
||||
return svc.triggers.registerWorkflows(ctx, set...)
|
||||
}
|
||||
|
||||
// updateCache
|
||||
func (svc *workflow) updateCache(wf *types.Workflow, runAs intAuth.Identifiable, g *wfexec.Graph) {
|
||||
defer svc.muxCache.Unlock()
|
||||
svc.muxCache.Lock()
|
||||
|
||||
if wf.Executable() {
|
||||
svc.wIndex[wf.Handle] = wf.ID
|
||||
svc.cache[wf.ID] = &wfCacheItem{g: g, wf: wf, runAs: runAs}
|
||||
} else {
|
||||
// remove deleted
|
||||
delete(svc.cache, wf.ID)
|
||||
delete(svc.wIndex, wf.Handle)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (svc *workflow) Exec(ctx context.Context, workflowID uint64, p types.WorkflowExecParams) (*expr.Vars, uint64, types.Stacktrace, error) {
|
||||
var (
|
||||
wap = &workflowActionProps{}
|
||||
t *types.Trigger
|
||||
results *expr.Vars
|
||||
wait WaitFn
|
||||
stacktrace types.Stacktrace
|
||||
sessionID uint64
|
||||
)
|
||||
|
||||
err := func() (err error) {
|
||||
svc.muxCache.Lock()
|
||||
if nil == svc.cache[workflowID] || nil == svc.cache[workflowID].wf {
|
||||
svc.muxCache.Unlock()
|
||||
return WorkflowErrNotFound()
|
||||
}
|
||||
|
||||
wf := svc.cache[workflowID].wf
|
||||
svc.muxCache.Unlock()
|
||||
|
||||
wap.setWorkflow(wf)
|
||||
|
||||
if !svc.ac.CanExecuteWorkflow(ctx, wf) {
|
||||
return WorkflowErrNotAllowedToExecute()
|
||||
}
|
||||
|
||||
if !wf.Enabled && !p.Trace {
|
||||
return WorkflowErrDisabled()
|
||||
}
|
||||
|
||||
// Find the trigger.
|
||||
// @todo can we cache this as well?
|
||||
t, err = func() (*types.Trigger, error) {
|
||||
if p.CallerWorkflowID > 0 {
|
||||
// skip triggers checking when executed as sub-workflow
|
||||
// @todo be more strict and allow this ONLY when workflow is flagged as a sub-workflow
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var tt types.TriggerSet
|
||||
// Load triggers directly from the store. At this point we do not care
|
||||
// about trigger search or read permissions
|
||||
tt, err = loadWorkflowTriggers(ctx, svc.store, workflowID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tt) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if p.StepID == 0 && len(tt) > 0 {
|
||||
return tt[0], nil
|
||||
} else {
|
||||
for _, tMatch := range tt {
|
||||
if tMatch.StepID == p.StepID {
|
||||
return tMatch, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !p.Trace {
|
||||
// when not doing a trace (designing the workflow)
|
||||
// we need to be more strict and disallow execution of
|
||||
// the misconfigured workflows and use of disabled triggers
|
||||
if t == nil {
|
||||
return nil, WorkflowErrUnknownWorkflowStep()
|
||||
} else if !t.Enabled {
|
||||
return nil, WorkflowErrDisabled()
|
||||
}
|
||||
}
|
||||
|
||||
if t != nil {
|
||||
wap.setTrigger(t)
|
||||
p.StepID = t.StepID
|
||||
p.EventType = t.EventType
|
||||
p.ResourceType = t.ResourceType
|
||||
|
||||
// merge with input from trigger
|
||||
// with trigger input vars are overwritten by input vars
|
||||
p.Input = t.Input.MustMerge(p.Input)
|
||||
} else {
|
||||
p.EventType = "onTrace"
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
wait, sessionID, err = svc.exec(ctx, wf, p)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Async && !p.Wait && wf.CheckDeferred() {
|
||||
// deferred workflow, return right away and keep the workflow session
|
||||
// running without waiting for the execution
|
||||
return nil
|
||||
}
|
||||
|
||||
// wait for the workflow to complete
|
||||
// reuse scope for results
|
||||
// this will be decoded back to event properties
|
||||
results, sessionID, _, stacktrace, err = wait(ctx)
|
||||
return err
|
||||
}()
|
||||
|
||||
return results, sessionID, stacktrace, svc.recordAction(ctx, wap, WorkflowActionExecute, err)
|
||||
}
|
||||
|
||||
// validates workflow by trying to convert it to graph and checking assigned triggers
|
||||
func (svc *workflow) validateWorkflow(ctx context.Context, wf *types.Workflow) (g *wfexec.Graph, runAs intAuth.Identifiable, err error) {
|
||||
var (
|
||||
tt []*types.Trigger
|
||||
)
|
||||
|
||||
g, wf.Issues = Convert(svc, wf)
|
||||
|
||||
tt, _, err = store.SearchAutomationTriggers(ctx, svc.store, types.TriggerFilter{
|
||||
WorkflowID: id.Strings(types.WorkflowSet{wf}.IDs()...),
|
||||
Deleted: filter.StateExcluded,
|
||||
Disabled: filter.StateExcluded,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
wf.Issues = append(wf.Issues, validateWorkflowTriggers(wf, tt...)...)
|
||||
|
||||
// Returns context with identity set to service user
|
||||
//
|
||||
// Current user (identity in the context) might not have
|
||||
// sufficient privileges to load info about invoker and runner
|
||||
sysUserCtx := func() context.Context {
|
||||
return intAuth.SetIdentityToContext(ctx, intAuth.ServiceUser())
|
||||
}
|
||||
|
||||
// @todo this might not be the smartest thing, users might get invalidated after
|
||||
// we add cache them as workflow runners
|
||||
if wf.RunAs > 0 {
|
||||
if runAs, err = DefaultUser.FindByAny(sysUserCtx(), wf.RunAs); err != nil {
|
||||
wf.Issues = wf.Issues.Append(fmt.Errorf("failed to load run-as user %d: %w", wf.RunAs, err), nil)
|
||||
} else if !runAs.Valid() {
|
||||
wf.Issues = wf.Issues.Append(fmt.Errorf("invalid user %d used for workflow run-as", wf.RunAs), nil)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (svc *workflow) exec(ctx context.Context, wf *types.Workflow, p types.WorkflowExecParams) (WaitFn, uint64, error) {
|
||||
if wf.Issues != nil {
|
||||
return nil, 0, wf.Issues
|
||||
}
|
||||
|
||||
defer svc.muxCache.Unlock()
|
||||
svc.muxCache.Lock()
|
||||
|
||||
if svc.cache[wf.ID] == nil {
|
||||
return nil, 0, WorkflowErrInvalidID()
|
||||
}
|
||||
|
||||
var (
|
||||
g = svc.cache[wf.ID].g
|
||||
runAs = svc.cache[wf.ID].runAs
|
||||
|
||||
scope *expr.Vars
|
||||
)
|
||||
|
||||
// merge workflow scope with the input
|
||||
scope = wf.Scope.MustMerge(p.Input)
|
||||
|
||||
return svc.session.Start(ctx, g, types.SessionStartParams{
|
||||
Invoker: intAuth.GetIdentityFromContext(ctx),
|
||||
Runner: runAs,
|
||||
|
||||
WorkflowID: wf.ID,
|
||||
KeepFor: wf.KeepSessions,
|
||||
Trace: wf.Trace || p.Trace,
|
||||
Input: scope,
|
||||
StepID: p.StepID,
|
||||
EventType: p.EventType,
|
||||
ResourceType: p.ResourceType,
|
||||
|
||||
CallStack: wfexec.GetContextCallStack(ctx),
|
||||
})
|
||||
}
|
||||
|
||||
func makeWorkflowHandler(svc *workflow, wf *types.Workflow, t *types.Trigger) eventbus.HandlerFn {
|
||||
return func(ctx context.Context, ev eventbus.Event) (err error) {
|
||||
var (
|
||||
scope *expr.Vars
|
||||
)
|
||||
|
||||
if dec, is := ev.(varsEncoder); is {
|
||||
scope, err = dec.EncodeVars()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
wait, _, err := svc.exec(ctx, wf, types.WorkflowExecParams{
|
||||
StepID: t.StepID,
|
||||
EventType: t.EventType,
|
||||
ResourceType: t.ResourceType,
|
||||
Input: t.Input.MustMerge(scope),
|
||||
|
||||
Trace: wf.Trace,
|
||||
Async: false,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if wf.CheckDeferred() {
|
||||
// deferred workflow, return right away and keep the workflow session
|
||||
// running without waiting for the execution
|
||||
return
|
||||
}
|
||||
|
||||
// wait for the workflow to complete
|
||||
// reuse scope for results
|
||||
// this will be decoded back to event properties
|
||||
scope, _, _, _, err = wait(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if dec, is := ev.(varsDecoder); is {
|
||||
return dec.DecodeVars(scope)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func loadWorkflow(ctx context.Context, s store.Storer, workflowID uint64) (res *types.Workflow, err error) {
|
||||
if workflowID == 0 {
|
||||
return nil, WorkflowErrInvalidID()
|
||||
}
|
||||
|
||||
if res, err = store.LookupAutomationWorkflowByID(ctx, s, workflowID); errors.IsNotFound(err) {
|
||||
return nil, WorkflowErrNotFound()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// toLabeledWorkflows converts to []label.LabeledResource
|
||||
func toLabeledWorkflows(set []*types.Workflow) []label.LabeledResource {
|
||||
if len(set) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ll := make([]label.LabeledResource, len(set))
|
||||
for i := range set {
|
||||
ll[i] = set[i]
|
||||
}
|
||||
|
||||
return ll
|
||||
}
|
||||
|
||||
func exprTypeSetter(reg *registry, e *types.Expr) func(string) (expr.Type, error) {
|
||||
return func(name string) (expr.Type, error) {
|
||||
if name == "" {
|
||||
name = "Any"
|
||||
}
|
||||
|
||||
if typ := reg.Type(name); typ != nil {
|
||||
return typ, nil
|
||||
} else {
|
||||
return nil, errors.NotFound(
|
||||
"unknown or unregistered type %q used for expression %q on %q",
|
||||
name,
|
||||
e.Expr,
|
||||
e.Target,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user