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
843 lines
20 KiB
Go
843 lines
20 KiB
Go
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,
|
|
)
|
|
}
|
|
}
|
|
}
|