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:
545
server/automation/service/access_control.gen.go
generated
Normal file
545
server/automation/service/access_control.gen.go
generated
Normal file
@@ -0,0 +1,545 @@
|
||||
package service
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
internalAuth "github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
systemTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
"github.com/spf13/cast"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
rbacService interface {
|
||||
Can(rbac.Session, string, rbac.Resource) bool
|
||||
Trace(rbac.Session, string, rbac.Resource) *rbac.Trace
|
||||
Grant(context.Context, ...*rbac.Rule) error
|
||||
FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet)
|
||||
}
|
||||
|
||||
accessControl struct {
|
||||
actionlog actionlog.Recorder
|
||||
|
||||
store store.Storer
|
||||
rbac rbacService
|
||||
}
|
||||
)
|
||||
|
||||
func AccessControl(s store.Storer) *accessControl {
|
||||
return &accessControl{
|
||||
store: s,
|
||||
rbac: rbac.Global(),
|
||||
actionlog: DefaultActionlog,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) bool {
|
||||
return svc.rbac.Can(rbac.ContextToSession(ctx), op, res)
|
||||
}
|
||||
|
||||
// Effective returns a list of effective permissions for all given resource
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee rbac.EffectiveSet) {
|
||||
for _, res := range rr {
|
||||
r := res.RbacResource()
|
||||
for op := range rbacResourceOperations(r) {
|
||||
ee.Push(r, op, svc.can(ctx, op, res))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Evaluate returns a list of permissions evaluated for the given user/roles combo
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) Trace(ctx context.Context, userID uint64, roles []uint64, rr ...string) (ee []*rbac.Trace, err error) {
|
||||
// Reusing the grant permission since this is who the feature is for
|
||||
if !svc.CanGrant(ctx) {
|
||||
// @todo should be altered to check grant permissions PER resource
|
||||
return nil, AccessControlErrNotAllowedToSetPermissions()
|
||||
}
|
||||
|
||||
var (
|
||||
resource rbac.Resource
|
||||
resources []rbac.Resource
|
||||
members systemTypes.RoleMemberSet
|
||||
)
|
||||
if len(rr) > 0 {
|
||||
resources = make([]rbac.Resource, 0, len(rr))
|
||||
for _, r := range rr {
|
||||
if err = rbacResourceValidator(r); err != nil {
|
||||
return nil, fmt.Errorf("can not use resource %q: %w", r, err)
|
||||
}
|
||||
|
||||
resource, err = svc.resourceLoader(ctx, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
} else {
|
||||
resources = svc.Resources()
|
||||
}
|
||||
|
||||
// User ID specified, load its roles
|
||||
if userID != 0 {
|
||||
if len(roles) > 0 {
|
||||
// should be prevented on the client
|
||||
return nil, fmt.Errorf("userID and roles are mutually exclusive")
|
||||
}
|
||||
|
||||
members, _, err = store.SearchRoleMembers(ctx, svc.store, systemTypes.RoleMemberFilter{Resource: fmt.Sprintf("corteza::system:user/%d", userID)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, m := range members {
|
||||
roles = append(roles, m.RoleID)
|
||||
}
|
||||
|
||||
for _, r := range internalAuth.AuthenticatedRoles() {
|
||||
roles = append(roles, r.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(roles) == 0 {
|
||||
// should be prevented on the client
|
||||
return nil, fmt.Errorf("no roles specified")
|
||||
}
|
||||
|
||||
session := rbac.ParamsToSession(ctx, userID, roles...)
|
||||
for _, res := range resources {
|
||||
r := res.RbacResource()
|
||||
for op := range rbacResourceOperations(r) {
|
||||
ee = append(ee, svc.rbac.Trace(session, op, res))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Resources returns list of resources
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) Resources() []rbac.Resource {
|
||||
return []rbac.Resource{
|
||||
rbac.NewResource(types.WorkflowRbacResource(0)),
|
||||
rbac.NewResource(types.ComponentRbacResource()),
|
||||
}
|
||||
}
|
||||
|
||||
// List returns list of operations on all resources
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) List() (out []map[string]string) {
|
||||
def := []map[string]string{
|
||||
{
|
||||
"type": types.WorkflowResourceType,
|
||||
"any": types.WorkflowRbacResource(0),
|
||||
"op": "read",
|
||||
},
|
||||
{
|
||||
"type": types.WorkflowResourceType,
|
||||
"any": types.WorkflowRbacResource(0),
|
||||
"op": "update",
|
||||
},
|
||||
{
|
||||
"type": types.WorkflowResourceType,
|
||||
"any": types.WorkflowRbacResource(0),
|
||||
"op": "delete",
|
||||
},
|
||||
{
|
||||
"type": types.WorkflowResourceType,
|
||||
"any": types.WorkflowRbacResource(0),
|
||||
"op": "undelete",
|
||||
},
|
||||
{
|
||||
"type": types.WorkflowResourceType,
|
||||
"any": types.WorkflowRbacResource(0),
|
||||
"op": "execute",
|
||||
},
|
||||
{
|
||||
"type": types.WorkflowResourceType,
|
||||
"any": types.WorkflowRbacResource(0),
|
||||
"op": "triggers.manage",
|
||||
},
|
||||
{
|
||||
"type": types.WorkflowResourceType,
|
||||
"any": types.WorkflowRbacResource(0),
|
||||
"op": "sessions.manage",
|
||||
},
|
||||
{
|
||||
"type": types.ComponentResourceType,
|
||||
"any": types.ComponentRbacResource(),
|
||||
"op": "grant",
|
||||
},
|
||||
{
|
||||
"type": types.ComponentResourceType,
|
||||
"any": types.ComponentRbacResource(),
|
||||
"op": "workflow.create",
|
||||
},
|
||||
{
|
||||
"type": types.ComponentResourceType,
|
||||
"any": types.ComponentRbacResource(),
|
||||
"op": "triggers.search",
|
||||
},
|
||||
{
|
||||
"type": types.ComponentResourceType,
|
||||
"any": types.ComponentRbacResource(),
|
||||
"op": "sessions.search",
|
||||
},
|
||||
{
|
||||
"type": types.ComponentResourceType,
|
||||
"any": types.ComponentRbacResource(),
|
||||
"op": "workflows.search",
|
||||
},
|
||||
{
|
||||
"type": types.ComponentResourceType,
|
||||
"any": types.ComponentRbacResource(),
|
||||
"op": "resource-translations.manage",
|
||||
},
|
||||
}
|
||||
|
||||
func(svc interface{}) {
|
||||
if svc, is := svc.(interface{}).(interface{ list() []map[string]string }); is {
|
||||
def = append(def, svc.list()...)
|
||||
}
|
||||
}(svc)
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// Grant applies one or more RBAC rules
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) Grant(ctx context.Context, rr ...*rbac.Rule) error {
|
||||
if !svc.CanGrant(ctx) {
|
||||
// @todo should be altered to check grant permissions PER resource
|
||||
return AccessControlErrNotAllowedToSetPermissions()
|
||||
}
|
||||
|
||||
for _, r := range rr {
|
||||
err := rbacResourceValidator(r.Resource, r.Operation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := svc.rbac.Grant(ctx, rr...); err != nil {
|
||||
return AccessControlErrGeneric().Wrap(err)
|
||||
}
|
||||
|
||||
svc.logGrants(ctx, rr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) logGrants(ctx context.Context, rr []*rbac.Rule) {
|
||||
if svc.actionlog == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, r := range rr {
|
||||
g := AccessControlActionGrant(&accessControlActionProps{r})
|
||||
g.log = r.String()
|
||||
g.resource = r.Resource
|
||||
|
||||
svc.actionlog.Record(ctx, g.ToAction())
|
||||
}
|
||||
}
|
||||
|
||||
// FindRules find all rules based on filters
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) FindRules(ctx context.Context, roleID uint64, rr ...string) (out rbac.RuleSet, err error) {
|
||||
if !svc.CanGrant(ctx) {
|
||||
return nil, AccessControlErrNotAllowedToSetPermissions()
|
||||
}
|
||||
|
||||
out, err = svc.FindRulesByRoleID(ctx, roleID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var resources []rbac.Resource
|
||||
if len(rr) > 0 {
|
||||
resources = make([]rbac.Resource, 0, len(rr))
|
||||
for _, r := range rr {
|
||||
if err = rbacResourceValidator(r); err != nil {
|
||||
return nil, fmt.Errorf("can not use resource %q: %w", r, err)
|
||||
}
|
||||
|
||||
resources = append(resources, rbac.NewResource(r))
|
||||
}
|
||||
} else {
|
||||
resources = svc.Resources()
|
||||
}
|
||||
|
||||
return out.FilterResource(resources...), nil
|
||||
}
|
||||
|
||||
// FindRulesByRoleID find all rules for a specific role
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) FindRulesByRoleID(ctx context.Context, roleID uint64) (rbac.RuleSet, error) {
|
||||
if !svc.CanGrant(ctx) {
|
||||
return nil, AccessControlErrNotAllowedToSetPermissions()
|
||||
}
|
||||
|
||||
return svc.rbac.FindRulesByRoleID(roleID), nil
|
||||
}
|
||||
|
||||
// CanReadWorkflow checks if current user can read workflow
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanReadWorkflow(ctx context.Context, r *types.Workflow) bool {
|
||||
return svc.can(ctx, "read", r)
|
||||
}
|
||||
|
||||
// CanUpdateWorkflow checks if current user can update workflow
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanUpdateWorkflow(ctx context.Context, r *types.Workflow) bool {
|
||||
return svc.can(ctx, "update", r)
|
||||
}
|
||||
|
||||
// CanDeleteWorkflow checks if current user can delete workflow
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanDeleteWorkflow(ctx context.Context, r *types.Workflow) bool {
|
||||
return svc.can(ctx, "delete", r)
|
||||
}
|
||||
|
||||
// CanUndeleteWorkflow checks if current user can undelete workflow
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanUndeleteWorkflow(ctx context.Context, r *types.Workflow) bool {
|
||||
return svc.can(ctx, "undelete", r)
|
||||
}
|
||||
|
||||
// CanExecuteWorkflow checks if current user can execute workflow
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanExecuteWorkflow(ctx context.Context, r *types.Workflow) bool {
|
||||
return svc.can(ctx, "execute", r)
|
||||
}
|
||||
|
||||
// CanManageTriggersOnWorkflow checks if current user can manage workflow triggers
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanManageTriggersOnWorkflow(ctx context.Context, r *types.Workflow) bool {
|
||||
return svc.can(ctx, "triggers.manage", r)
|
||||
}
|
||||
|
||||
// CanManageSessionsOnWorkflow checks if current user can manage workflow sessions
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanManageSessionsOnWorkflow(ctx context.Context, r *types.Workflow) bool {
|
||||
return svc.can(ctx, "sessions.manage", r)
|
||||
}
|
||||
|
||||
// CanGrant checks if current user can manage automation permissions
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanGrant(ctx context.Context) bool {
|
||||
r := &types.Component{}
|
||||
return svc.can(ctx, "grant", r)
|
||||
}
|
||||
|
||||
// CanCreateWorkflow checks if current user can create workflows
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanCreateWorkflow(ctx context.Context) bool {
|
||||
r := &types.Component{}
|
||||
return svc.can(ctx, "workflow.create", r)
|
||||
}
|
||||
|
||||
// CanSearchTriggers checks if current user can list, search or filter triggers
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanSearchTriggers(ctx context.Context) bool {
|
||||
r := &types.Component{}
|
||||
return svc.can(ctx, "triggers.search", r)
|
||||
}
|
||||
|
||||
// CanSearchSessions checks if current user can list, search or filter sessions
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanSearchSessions(ctx context.Context) bool {
|
||||
r := &types.Component{}
|
||||
return svc.can(ctx, "sessions.search", r)
|
||||
}
|
||||
|
||||
// CanSearchWorkflows checks if current user can list, search or filter workflows
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanSearchWorkflows(ctx context.Context) bool {
|
||||
r := &types.Component{}
|
||||
return svc.can(ctx, "workflows.search", r)
|
||||
}
|
||||
|
||||
// CanManageResourceTranslations checks if current user can list, search, create, or update resource translations
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) CanManageResourceTranslations(ctx context.Context) bool {
|
||||
r := &types.Component{}
|
||||
return svc.can(ctx, "resource-translations.manage", r)
|
||||
}
|
||||
|
||||
// rbacResourceValidator validates known component's resource by routing it to the appropriate validator
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbacResourceValidator(r string, oo ...string) error {
|
||||
switch rbac.ResourceType(r) {
|
||||
case types.WorkflowResourceType:
|
||||
return rbacWorkflowResourceValidator(r, oo...)
|
||||
case types.ComponentResourceType:
|
||||
return rbacComponentResourceValidator(r, oo...)
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown resource type %q", r)
|
||||
}
|
||||
|
||||
// resourceLoader loads resource from store
|
||||
//
|
||||
// # Notes
|
||||
// Function assumes existence of loader functions for all resource types
|
||||
//
|
||||
// This function is auto-generated
|
||||
func (svc accessControl) resourceLoader(ctx context.Context, resource string) (rbac.Resource, error) {
|
||||
var (
|
||||
hasWildcard = false
|
||||
resourceType, ids = rbac.ParseResourceID(resource)
|
||||
)
|
||||
|
||||
for _, id := range ids {
|
||||
if id == 0 {
|
||||
hasWildcard = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
switch rbac.ResourceType(resourceType) {
|
||||
case types.WorkflowResourceType:
|
||||
if hasWildcard {
|
||||
return rbac.NewResource(types.WorkflowRbacResource(ids[0])), nil
|
||||
}
|
||||
|
||||
return loadWorkflow(ctx, svc.store, ids[0])
|
||||
case types.ComponentResourceType:
|
||||
return &types.Component{}, nil
|
||||
}
|
||||
|
||||
_ = ids
|
||||
return nil, fmt.Errorf("unknown resource type %q", resourceType)
|
||||
}
|
||||
|
||||
// rbacResourceOperations returns defined operations for a requested resource
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbacResourceOperations(r string) map[string]bool {
|
||||
switch rbac.ResourceType(r) {
|
||||
case types.WorkflowResourceType:
|
||||
return map[string]bool{
|
||||
"read": true,
|
||||
"update": true,
|
||||
"delete": true,
|
||||
"undelete": true,
|
||||
"execute": true,
|
||||
"triggers.manage": true,
|
||||
"sessions.manage": true,
|
||||
}
|
||||
case types.ComponentResourceType:
|
||||
return map[string]bool{
|
||||
"grant": true,
|
||||
"workflow.create": true,
|
||||
"triggers.search": true,
|
||||
"sessions.search": true,
|
||||
"workflows.search": true,
|
||||
"resource-translations.manage": true,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// rbacWorkflowResourceValidator checks validity of RBAC resource and operations
|
||||
//
|
||||
// # Notes
|
||||
// Can be called without operations to check for validity of resource string only
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbacWorkflowResourceValidator(r string, oo ...string) error {
|
||||
if !strings.HasPrefix(r, types.WorkflowResourceType) {
|
||||
// expecting resource to always include path
|
||||
return fmt.Errorf("invalid resource type")
|
||||
}
|
||||
|
||||
defOps := rbacResourceOperations(r)
|
||||
for _, o := range oo {
|
||||
if !defOps[o] {
|
||||
return fmt.Errorf("invalid operation '%s' for workflow resource", o)
|
||||
}
|
||||
}
|
||||
|
||||
const sep = "/"
|
||||
var (
|
||||
pp = strings.Split(strings.Trim(r[len(types.WorkflowResourceType):], sep), sep)
|
||||
prc = []string{
|
||||
"ID",
|
||||
}
|
||||
)
|
||||
|
||||
if len(pp) != len(prc) {
|
||||
return fmt.Errorf("invalid resource path structure")
|
||||
}
|
||||
|
||||
for i := 0; i < len(pp); i++ {
|
||||
if pp[i] != "*" {
|
||||
if i > 0 && pp[i-1] == "*" {
|
||||
return fmt.Errorf("invalid path wildcard level (%d) for workflow resource", i)
|
||||
}
|
||||
|
||||
if _, err := cast.ToUint64E(pp[i]); err != nil {
|
||||
return fmt.Errorf("invalid reference for %s: '%s'", prc[i], pp[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rbacComponentResourceValidator checks validity of RBAC resource and operations
|
||||
//
|
||||
// # Notes
|
||||
// Can be called without operations to check for validity of resource string only
|
||||
//
|
||||
// This function is auto-generated
|
||||
func rbacComponentResourceValidator(r string, oo ...string) error {
|
||||
if r != types.ComponentResourceType+"/" {
|
||||
// expecting resource to always include path
|
||||
return fmt.Errorf("invalid component resource, expecting " + types.ComponentResourceType + "/")
|
||||
}
|
||||
|
||||
defOps := rbacResourceOperations(r)
|
||||
for _, o := range oo {
|
||||
if !defOps[o] {
|
||||
return fmt.Errorf("invalid operation '%s' for automation component resource", o)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
284
server/automation/service/access_control_actions.gen.go
generated
Normal file
284
server/automation/service/access_control_actions.gen.go
generated
Normal file
@@ -0,0 +1,284 @@
|
||||
package service
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
// automation/service/access_control_actions.yaml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
"github.com/cortezaproject/corteza/server/pkg/rbac"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
accessControlActionProps struct {
|
||||
rule *rbac.Rule
|
||||
}
|
||||
|
||||
accessControlAction struct {
|
||||
timestamp time.Time
|
||||
resource string
|
||||
action string
|
||||
log string
|
||||
severity actionlog.Severity
|
||||
|
||||
// prefix for error when action fails
|
||||
errorMessage string
|
||||
|
||||
props *accessControlActionProps
|
||||
}
|
||||
|
||||
accessControlLogMetaKey struct{}
|
||||
accessControlPropsMetaKey struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
// just a placeholder to cover template cases w/o fmt package use
|
||||
_ = fmt.Println
|
||||
)
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Props methods
|
||||
// setRule updates accessControlActionProps's rule
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *accessControlActionProps) setRule(rule *rbac.Rule) *accessControlActionProps {
|
||||
p.rule = rule
|
||||
return p
|
||||
}
|
||||
|
||||
// Serialize converts accessControlActionProps to actionlog.Meta
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p accessControlActionProps) Serialize() actionlog.Meta {
|
||||
var (
|
||||
m = make(actionlog.Meta)
|
||||
)
|
||||
|
||||
if p.rule != nil {
|
||||
m.Set("rule.operation", p.rule.Operation, true)
|
||||
m.Set("rule.roleID", p.rule.RoleID, true)
|
||||
m.Set("rule.access", p.rule.Access, true)
|
||||
m.Set("rule.resource", p.rule.Resource, true)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// tr translates string and replaces meta value placeholder with values
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p accessControlActionProps) Format(in string, err error) string {
|
||||
var (
|
||||
pairs = []string{"{{err}}"}
|
||||
// first non-empty string
|
||||
fns = func(ii ...interface{}) string {
|
||||
for _, i := range ii {
|
||||
if s := fmt.Sprintf("%v", i); len(s) > 0 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
pairs = append(pairs, err.Error())
|
||||
} else {
|
||||
pairs = append(pairs, "nil")
|
||||
}
|
||||
|
||||
if p.rule != nil {
|
||||
// replacement for "{{rule}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{rule}}",
|
||||
fns(
|
||||
p.rule.Operation,
|
||||
p.rule.RoleID,
|
||||
p.rule.Access,
|
||||
p.rule.Resource,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{rule.operation}}", fns(p.rule.Operation))
|
||||
pairs = append(pairs, "{{rule.roleID}}", fns(p.rule.RoleID))
|
||||
pairs = append(pairs, "{{rule.access}}", fns(p.rule.Access))
|
||||
pairs = append(pairs, "{{rule.resource}}", fns(p.rule.Resource))
|
||||
}
|
||||
return strings.NewReplacer(pairs...).Replace(in)
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action methods
|
||||
|
||||
// String returns loggable description as string
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (a *accessControlAction) String() string {
|
||||
var props = &accessControlActionProps{}
|
||||
|
||||
if a.props != nil {
|
||||
props = a.props
|
||||
}
|
||||
|
||||
return props.Format(a.log, nil)
|
||||
}
|
||||
|
||||
func (e *accessControlAction) ToAction() *actionlog.Action {
|
||||
return &actionlog.Action{
|
||||
Resource: e.resource,
|
||||
Action: e.action,
|
||||
Severity: e.severity,
|
||||
Description: e.String(),
|
||||
Meta: e.props.Serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action constructors
|
||||
|
||||
// AccessControlActionGrant returns "automation:access_control.grant" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func AccessControlActionGrant(props ...*accessControlActionProps) *accessControlAction {
|
||||
a := &accessControlAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:access_control",
|
||||
action: "grant",
|
||||
log: "grant",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error constructors
|
||||
|
||||
// AccessControlErrGeneric returns "automation:access_control.generic" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func AccessControlErrGeneric(mm ...*accessControlActionProps) *errors.Error {
|
||||
var p = &accessControlActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("failed to complete request due to internal error", nil),
|
||||
|
||||
errors.Meta("type", "generic"),
|
||||
errors.Meta("resource", "automation:access_control"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(accessControlLogMetaKey{}, "{err}"),
|
||||
errors.Meta(accessControlPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "access-control.errors.generic"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// AccessControlErrNotAllowedToSetPermissions returns "automation:access_control.notAllowedToSetPermissions" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func AccessControlErrNotAllowedToSetPermissions(mm ...*accessControlActionProps) *errors.Error {
|
||||
var p = &accessControlActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to set permissions", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToSetPermissions"),
|
||||
errors.Meta("resource", "automation:access_control"),
|
||||
|
||||
errors.Meta(accessControlPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "access-control.errors.notAllowedToSetPermissions"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
// recordAction is a service helper function wraps function that can return error
|
||||
//
|
||||
// It will wrap unrecognized/internal errors with generic errors.
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (svc accessControl) recordAction(ctx context.Context, props *accessControlActionProps, actionFn func(...*accessControlActionProps) *accessControlAction, err error) error {
|
||||
if svc.actionlog == nil || actionFn == nil {
|
||||
// action log disabled or no action fn passed, return error as-is
|
||||
return err
|
||||
} else if err == nil {
|
||||
// action completed w/o error, record it
|
||||
svc.actionlog.Record(ctx, actionFn(props).ToAction())
|
||||
return nil
|
||||
}
|
||||
|
||||
a := actionFn(props).ToAction()
|
||||
|
||||
// Extracting error information and recording it as action
|
||||
a.Error = err.Error()
|
||||
|
||||
switch c := err.(type) {
|
||||
case *errors.Error:
|
||||
m := c.Meta()
|
||||
|
||||
a.Error = err.Error()
|
||||
a.Severity = actionlog.Severity(m.AsInt("severity"))
|
||||
a.Description = props.Format(m.AsString(accessControlLogMetaKey{}), err)
|
||||
|
||||
if p, has := m[accessControlPropsMetaKey{}]; has {
|
||||
a.Meta = p.(*accessControlActionProps).Serialize()
|
||||
}
|
||||
|
||||
svc.actionlog.Record(ctx, a)
|
||||
default:
|
||||
svc.actionlog.Record(ctx, a)
|
||||
}
|
||||
|
||||
// Original error is passed on
|
||||
return err
|
||||
}
|
||||
25
server/automation/service/access_control_actions.yaml
Normal file
25
server/automation/service/access_control_actions.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
# List of security/audit events and errors that we need to log
|
||||
|
||||
resource: automation:access_control
|
||||
service: accessControl
|
||||
|
||||
# Default sensitivity for actions
|
||||
defaultActionSeverity: notice
|
||||
|
||||
# default severity for errors
|
||||
defaultErrorSeverity: alert
|
||||
|
||||
import:
|
||||
- github.com/cortezaproject/corteza/server/pkg/rbac
|
||||
|
||||
props:
|
||||
- name: rule
|
||||
type: "*rbac.Rule"
|
||||
fields: [ operation, roleID, access, resource ]
|
||||
|
||||
actions:
|
||||
- action: grant
|
||||
|
||||
errors:
|
||||
- error: notAllowedToSetPermissions
|
||||
message: "not allowed to set permissions"
|
||||
106
server/automation/service/registry.go
Normal file
106
server/automation/service/registry.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/expr"
|
||||
)
|
||||
|
||||
type (
|
||||
registry struct {
|
||||
lock *sync.RWMutex
|
||||
functions map[string]*types.Function
|
||||
types map[string]expr.Type
|
||||
}
|
||||
|
||||
handler interface {
|
||||
Functions() []*types.Function
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
defaultRegistry = initRegistry()
|
||||
)
|
||||
|
||||
func Registry() *registry {
|
||||
return defaultRegistry
|
||||
}
|
||||
|
||||
func initRegistry() *registry {
|
||||
return ®istry{
|
||||
lock: &sync.RWMutex{},
|
||||
functions: make(map[string]*types.Function),
|
||||
types: make(map[string]expr.Type),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registry) AddFunctions(ff ...*types.Function) {
|
||||
defer r.lock.Unlock()
|
||||
r.lock.Lock()
|
||||
for _, fn := range ff {
|
||||
r.functions[fn.Ref] = fn
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registry) AddHandlers(hh ...handler) {
|
||||
for _, h := range hh {
|
||||
r.AddFunctions(h.Functions()...)
|
||||
}
|
||||
}
|
||||
|
||||
func (r registry) Function(ref string) *types.Function {
|
||||
defer r.lock.RUnlock()
|
||||
r.lock.RLock()
|
||||
return r.functions[ref]
|
||||
}
|
||||
|
||||
func (r registry) Functions() []*types.Function {
|
||||
var (
|
||||
rr = make([]string, 0, len(r.functions))
|
||||
ff = make([]*types.Function, 0, len(r.functions))
|
||||
)
|
||||
|
||||
for ref, f := range r.functions {
|
||||
// flag for UI weather this function step is disabled or not
|
||||
f.Disabled = !DefaultWorkflow.corredorOpt.Enabled && ref == "corredorExec"
|
||||
rr = append(rr, ref)
|
||||
}
|
||||
|
||||
sort.Strings(rr)
|
||||
|
||||
for _, ref := range rr {
|
||||
ff = append(ff, r.functions[ref])
|
||||
}
|
||||
|
||||
return ff
|
||||
}
|
||||
|
||||
func (r *registry) AddTypes(tt ...expr.Type) {
|
||||
defer r.lock.Unlock()
|
||||
r.lock.Lock()
|
||||
for _, t := range tt {
|
||||
r.types[t.Type()] = t
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registry) Type(ref string) expr.Type {
|
||||
defer r.lock.RUnlock()
|
||||
r.lock.RLock()
|
||||
return r.types[ref]
|
||||
}
|
||||
|
||||
func (r *registry) Types() []string {
|
||||
var (
|
||||
rr = make([]string, 0, len(r.types))
|
||||
)
|
||||
|
||||
for ref := range r.types {
|
||||
rr = append(rr, ref)
|
||||
}
|
||||
|
||||
sort.Strings(rr)
|
||||
|
||||
return rr
|
||||
}
|
||||
165
server/automation/service/service.go
Normal file
165
server/automation/service/service.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/automation"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/corredor"
|
||||
"github.com/cortezaproject/corteza/server/pkg/expr"
|
||||
"github.com/cortezaproject/corteza/server/pkg/id"
|
||||
"github.com/cortezaproject/corteza/server/pkg/objstore"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
sysTypes "github.com/cortezaproject/corteza/server/system/types"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
websocketSender interface {
|
||||
Send(kind string, payload interface{}, userIDs ...uint64) error
|
||||
}
|
||||
|
||||
Config struct {
|
||||
ActionLog options.ActionLogOpt
|
||||
Workflow options.WorkflowOpt
|
||||
Corredor options.CorredorOpt
|
||||
}
|
||||
|
||||
userService interface {
|
||||
FindByAny(ctx context.Context, identifier interface{}) (*sysTypes.User, error)
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultObjectStore objstore.Store
|
||||
|
||||
// DefaultStore is an interface to storage backend(s)
|
||||
// ng (next-gen) is a temporary prefix
|
||||
// so that we can differentiate between it and the file-only store
|
||||
DefaultStore store.Storer
|
||||
|
||||
DefaultLogger *zap.Logger
|
||||
|
||||
// DefaultAccessControl Access control checking
|
||||
DefaultAccessControl *accessControl
|
||||
|
||||
DefaultActionlog actionlog.Recorder
|
||||
|
||||
DefaultUser userService
|
||||
DefaultWorkflow *workflow
|
||||
DefaultTrigger *trigger
|
||||
DefaultSession *session
|
||||
|
||||
// wrapper around time.Now() that will aid service testing
|
||||
now = func() *time.Time {
|
||||
c := time.Now().Round(time.Second)
|
||||
return &c
|
||||
}
|
||||
|
||||
// wrapper around nextID that will aid service testing
|
||||
nextID = func() uint64 {
|
||||
return id.Next()
|
||||
}
|
||||
)
|
||||
|
||||
func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websocketSender, c Config) (err error) {
|
||||
var (
|
||||
//hcd = healthcheck.Defaults()
|
||||
)
|
||||
|
||||
// we're doing conversion to avoid having
|
||||
// store interface exposed or generated inside app package
|
||||
DefaultStore = s
|
||||
|
||||
DefaultLogger = log.Named("workflow")
|
||||
|
||||
{
|
||||
tee := zap.NewNop()
|
||||
policy := actionlog.MakeProductionPolicy()
|
||||
|
||||
if !c.ActionLog.Enabled {
|
||||
policy = actionlog.MakeDisabledPolicy()
|
||||
} else if c.ActionLog.Debug {
|
||||
policy = actionlog.MakeDebugPolicy()
|
||||
tee = log
|
||||
}
|
||||
|
||||
DefaultActionlog = actionlog.NewService(DefaultStore, log, tee, policy)
|
||||
}
|
||||
|
||||
DefaultAccessControl = AccessControl(s)
|
||||
|
||||
DefaultSession = Session(DefaultLogger.Named("session"), c.Workflow, ws)
|
||||
DefaultWorkflow = Workflow(DefaultLogger.Named("workflow"), c.Corredor, c.Workflow)
|
||||
DefaultTrigger = Trigger(DefaultLogger.Named("trigger"), c.Workflow)
|
||||
|
||||
DefaultWorkflow.triggers = DefaultTrigger
|
||||
|
||||
Registry().AddTypes(
|
||||
&expr.Any{},
|
||||
&expr.Array{},
|
||||
&expr.Boolean{},
|
||||
&expr.ID{},
|
||||
&expr.Integer{},
|
||||
&expr.UnsignedInteger{},
|
||||
&expr.Float{},
|
||||
&expr.String{},
|
||||
&expr.Handle{},
|
||||
&expr.DateTime{},
|
||||
&expr.Duration{},
|
||||
&expr.KV{},
|
||||
&expr.KVV{},
|
||||
&expr.Reader{},
|
||||
&expr.Vars{},
|
||||
&expr.HttpRequest{},
|
||||
&expr.Bytes{},
|
||||
|
||||
&automation.EmailMessage{},
|
||||
)
|
||||
|
||||
automation.HttpRequestHandler(Registry())
|
||||
automation.LogHandler(Registry())
|
||||
automation.QueueHandler(Registry())
|
||||
automation.JsenvHandler(Registry())
|
||||
automation.Oauth2Handler(Registry())
|
||||
automation.LoopHandler(Registry(), DefaultWorkflow.parser)
|
||||
automation.CorredorHandler(Registry(), corredor.Service())
|
||||
automation.EmailHandler(Registry())
|
||||
automation.JwtHandler(Registry())
|
||||
automation.ApigwBodyHandler(Registry())
|
||||
return
|
||||
}
|
||||
|
||||
func Activate(ctx context.Context) (err error) {
|
||||
if err = DefaultWorkflow.Load(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func Watchers(ctx context.Context) {
|
||||
DefaultSession.Watch(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Data is stale when new date does not match updatedAt or createdAt (before first update)
|
||||
func isStale(new *time.Time, updatedAt *time.Time, createdAt time.Time) bool {
|
||||
if new == nil {
|
||||
// Change to true for stale-data-check
|
||||
return false
|
||||
}
|
||||
|
||||
if updatedAt != nil {
|
||||
return !new.Equal(*updatedAt)
|
||||
}
|
||||
|
||||
return new.Equal(createdAt)
|
||||
}
|
||||
|
||||
// trim1st removes 1st param and returns only error
|
||||
func trim1st(_ interface{}, err error) error {
|
||||
return err
|
||||
}
|
||||
588
server/automation/service/session.go
Normal file
588
server/automation/service/session.go
Normal file
@@ -0,0 +1,588 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/expr"
|
||||
"github.com/cortezaproject/corteza/server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/pkg/sentry"
|
||||
"github.com/cortezaproject/corteza/server/pkg/wfexec"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
"github.com/modern-go/reflect2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
promptSender interface {
|
||||
Send(kind string, payload interface{}, userIDs ...uint64) error
|
||||
}
|
||||
|
||||
session struct {
|
||||
store store.Storer
|
||||
actionlog actionlog.Recorder
|
||||
ac sessionAccessController
|
||||
opt options.WorkflowOpt
|
||||
log *zap.Logger
|
||||
mux sync.RWMutex
|
||||
pool map[uint64]*types.Session
|
||||
spawnQueue chan *spawn
|
||||
promptSender promptSender
|
||||
}
|
||||
|
||||
spawn struct {
|
||||
workflowID uint64
|
||||
session chan *wfexec.Session
|
||||
graph *wfexec.Graph
|
||||
invoker auth.Identifiable
|
||||
runner auth.Identifiable
|
||||
trace bool
|
||||
callStack []uint64
|
||||
}
|
||||
|
||||
sessionAccessController interface {
|
||||
CanSearchSessions(context.Context) bool
|
||||
CanManageSessionsOnWorkflow(context.Context, *types.Workflow) bool
|
||||
}
|
||||
|
||||
WaitFn func(ctx context.Context) (*expr.Vars, uint64, wfexec.SessionStatus, types.Stacktrace, error)
|
||||
)
|
||||
|
||||
const (
|
||||
// when the state changes, state-change-handler is called and for non-fatal,
|
||||
// non-interactive or non-delay steps (that are much more frequent) we need
|
||||
// to limit how often the store is updated with the updated session info
|
||||
//
|
||||
// We use the size of the stacktrace and for every F (see the value of the constant)
|
||||
// we flush the session info to the store.
|
||||
sessionStateFlushFrequency = 1000
|
||||
)
|
||||
|
||||
func Session(log *zap.Logger, opt options.WorkflowOpt, ps promptSender) *session {
|
||||
return &session{
|
||||
log: log,
|
||||
opt: opt,
|
||||
actionlog: DefaultActionlog,
|
||||
store: DefaultStore,
|
||||
ac: DefaultAccessControl,
|
||||
pool: make(map[uint64]*types.Session),
|
||||
spawnQueue: make(chan *spawn),
|
||||
promptSender: ps,
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *session) Search(ctx context.Context, filter types.SessionFilter) (rr types.SessionSet, f types.SessionFilter, err error) {
|
||||
var (
|
||||
sap = &sessionActionProps{filter: &filter}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
if !svc.ac.CanSearchSessions(ctx) {
|
||||
return SessionErrNotAllowedToSearch()
|
||||
}
|
||||
|
||||
if rr, f, err = store.SearchAutomationSessions(ctx, svc.store, filter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return rr, f, svc.recordAction(ctx, sap, SessionActionSearch, err)
|
||||
}
|
||||
|
||||
func (svc *session) LookupByID(ctx context.Context, sessionID uint64) (res *types.Session, err error) {
|
||||
var (
|
||||
sap = &sessionActionProps{session: &types.Session{ID: sessionID}}
|
||||
wf *types.Workflow
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
|
||||
if !svc.ac.CanSearchSessions(ctx) {
|
||||
return SessionErrNotAllowedToRead()
|
||||
}
|
||||
|
||||
if res, err = loadSession(ctx, s, sessionID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if wf, err = loadWorkflow(ctx, s, res.WorkflowID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !svc.ac.CanManageSessionsOnWorkflow(ctx, wf) {
|
||||
return SessionErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return res, svc.recordAction(ctx, sap, SessionActionLookup, err)
|
||||
}
|
||||
|
||||
func (svc *session) resumeAll(ctx context.Context) error {
|
||||
// In theory we could resume active/pending/prompt sessions from persistent store
|
||||
// so that they can survive server termination
|
||||
|
||||
// @todo resume active sessions from storage
|
||||
// load all active sessions from store and load them into the pool
|
||||
//
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *session) suspendAll(ctx context.Context) error {
|
||||
// In theory we could suspend active/pending/prompt sessions to persistent store
|
||||
// so that they can survive server termination
|
||||
|
||||
// @todo suspend active sessions to storage:
|
||||
// stop watcher queue
|
||||
// run gc
|
||||
// stop worker on each session
|
||||
// flush session to store (like we're doing in the status handler
|
||||
return nil
|
||||
}
|
||||
|
||||
// PendingPrompts returns all prompts on all sessions owned by current user
|
||||
func (svc *session) PendingPrompts(ctx context.Context) (pp []*wfexec.PendingPrompt) {
|
||||
var (
|
||||
i = auth.GetIdentityFromContext(ctx)
|
||||
)
|
||||
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
|
||||
svc.mux.RLock()
|
||||
defer svc.mux.RUnlock()
|
||||
|
||||
pp = make([]*wfexec.PendingPrompt, 0, len(svc.pool))
|
||||
for _, s := range svc.pool {
|
||||
pp = append(pp, s.PendingPrompts(i.Identity())...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Start new workflow session on a specific step with a given identity and scope
|
||||
//
|
||||
// # Start is an asynchronous operation
|
||||
//
|
||||
// Please note that context passed to the function is NOT the the one that is
|
||||
// used for the execution of the workflow. See watch function!
|
||||
//
|
||||
// It does not check user's permissions to execute workflow(s) so it should be used only when !
|
||||
func (svc *session) Start(ctx context.Context, g *wfexec.Graph, ssp types.SessionStartParams) (wait WaitFn, _ uint64, err error) {
|
||||
var (
|
||||
start wfexec.Step
|
||||
)
|
||||
|
||||
if g == nil {
|
||||
return nil, 0, errors.InvalidData("cannot start workflow, uninitialized graph")
|
||||
}
|
||||
|
||||
if len(ssp.CallStack) > svc.opt.CallStackSize {
|
||||
return nil, 0, WorkflowErrMaximumCallStackSizeExceeded()
|
||||
}
|
||||
|
||||
ssp.CallStack = append(ssp.CallStack, ssp.WorkflowID)
|
||||
|
||||
if ssp.Invoker == nil {
|
||||
return nil, 0, errors.InvalidData("cannot start workflow without user")
|
||||
}
|
||||
|
||||
if ssp.Runner == nil {
|
||||
ssp.Runner = ssp.Invoker
|
||||
}
|
||||
|
||||
if ssp.StepID == 0 {
|
||||
// starting step is not explicitly set
|
||||
// find orphan step
|
||||
switch oo := g.Orphans(); len(oo) {
|
||||
case 1:
|
||||
start = oo[0]
|
||||
case 0:
|
||||
return nil, 0, errors.InvalidData("could not find starting step")
|
||||
default:
|
||||
return nil, 0, errors.InvalidData("cannot start workflow session multiple starting steps found")
|
||||
}
|
||||
} else if start = g.StepByID(ssp.StepID); start == nil {
|
||||
return nil, 0, errors.InvalidData("trigger staring step references non-existing step")
|
||||
} else if len(g.Parents(g.StepByID(ssp.StepID))) > 0 {
|
||||
return nil, 0, errors.InvalidData("cannot start workflow on a step with parents")
|
||||
}
|
||||
|
||||
var (
|
||||
ses = svc.spawn(g, ssp.WorkflowID, ssp.Trace, ssp.CallStack, ssp.Runner, ssp.Invoker)
|
||||
)
|
||||
|
||||
ses.CreatedAt = *now()
|
||||
ses.CreatedBy = ssp.Invoker.Identity()
|
||||
ses.Status = types.SessionStarted
|
||||
ses.Apply(ssp)
|
||||
|
||||
_ = ssp.Input.AssignFieldValue("eventType", expr.Must(expr.NewString(ssp.EventType)))
|
||||
_ = ssp.Input.AssignFieldValue("resourceType", expr.Must(expr.NewString(ssp.ResourceType)))
|
||||
_ = ssp.Input.AssignFieldValue("invoker", expr.Must(expr.NewAny(ssp.Invoker)))
|
||||
_ = ssp.Input.AssignFieldValue("runner", expr.Must(expr.NewAny(ssp.Runner)))
|
||||
|
||||
if err = ses.Exec(ctx, start, ssp.Input); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return func(ctx context.Context) (scope *expr.Vars, sessionID uint64, status wfexec.SessionStatus, stacktrace types.Stacktrace, err error) {
|
||||
sessionID = ses.ID
|
||||
scope, status, stacktrace, err = ses.WaitResults(ctx)
|
||||
return
|
||||
}, ses.ID, nil
|
||||
}
|
||||
|
||||
// Resume resumes suspended session/state
|
||||
//
|
||||
// Session can only be resumed by knowing session and state ID. Resume is an asynchronous operation
|
||||
//
|
||||
// There is minimum access-control deep inside wfexec.Session.Resume function
|
||||
// that compares identity with state owner
|
||||
func (svc *session) Resume(sessionID, stateID uint64, i auth.Identifiable, input *expr.Vars) error {
|
||||
var (
|
||||
ctx = auth.SetIdentityToContext(context.Background(), i)
|
||||
)
|
||||
|
||||
svc.mux.RLock()
|
||||
defer svc.mux.RUnlock()
|
||||
ses := svc.pool[sessionID]
|
||||
if ses == nil {
|
||||
return errors.NotFound("session not found")
|
||||
}
|
||||
|
||||
resPrompt, err := ses.Resume(ctx, stateID, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = svc.promptSender.Send("workflowSessionResumed", resPrompt, resPrompt.OwnerId); err != nil {
|
||||
svc.log.Error("failed to send prompt resume status to user", zap.Error(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Terminates session ID
|
||||
func (svc *session) Cancel(ctx context.Context, sessionID uint64) (err error) {
|
||||
svc.mux.RLock()
|
||||
|
||||
var (
|
||||
wf *types.Workflow
|
||||
ses = svc.pool[sessionID]
|
||||
)
|
||||
|
||||
// unlock right away!
|
||||
// when session is canceled, handler pick it up and
|
||||
// locks it again
|
||||
svc.mux.RUnlock()
|
||||
|
||||
if ses == nil {
|
||||
return errors.NotFound("session not found or already canceled")
|
||||
}
|
||||
|
||||
if wf, err = loadWorkflow(ctx, svc.store, ses.WorkflowID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !svc.ac.CanManageSessionsOnWorkflow(ctx, wf) {
|
||||
return SessionErrNotAllowedToManage()
|
||||
}
|
||||
|
||||
ses.Cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
// spawns a new session
|
||||
//
|
||||
// We need initial context for the session because we want to catch all cancellations or timeouts from there
|
||||
// and not from any potential HTTP requests or similar temporary context that can prematurely destroy a workflow session
|
||||
func (svc *session) spawn(g *wfexec.Graph, workflowID uint64, trace bool, callStack []uint64, runner, invoker auth.Identifiable) (ses *types.Session) {
|
||||
s := &spawn{
|
||||
workflowID: workflowID,
|
||||
session: make(chan *wfexec.Session, 1),
|
||||
graph: g,
|
||||
trace: trace,
|
||||
callStack: callStack,
|
||||
invoker: invoker,
|
||||
runner: runner,
|
||||
}
|
||||
|
||||
// Send new-session request
|
||||
svc.spawnQueue <- s
|
||||
|
||||
// blocks until session is set
|
||||
ses = types.NewSession(<-s.session)
|
||||
if !svc.opt.StackTraceEnabled {
|
||||
ses.DisableStacktrace()
|
||||
}
|
||||
|
||||
if svc.opt.StackTraceFull {
|
||||
ses.FullStacktrace()
|
||||
}
|
||||
|
||||
if trace {
|
||||
ses.Traced()
|
||||
}
|
||||
|
||||
svc.mux.Lock()
|
||||
svc.pool[ses.ID] = ses
|
||||
svc.mux.Unlock()
|
||||
return ses
|
||||
}
|
||||
|
||||
// Watch looks over session's spawn queue
|
||||
func (svc *session) Watch(ctx context.Context) {
|
||||
gcTicker := time.NewTicker(time.Second)
|
||||
lpTicker := time.NewTicker(time.Second * 30)
|
||||
|
||||
go func() {
|
||||
defer sentry.Recover()
|
||||
defer gcTicker.Stop()
|
||||
defer svc.log.Info("stopped")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case s := <-svc.spawnQueue:
|
||||
var execCtx = context.Background()
|
||||
|
||||
opts := []wfexec.SessionOpt{
|
||||
wfexec.SetWorkflowID(s.workflowID),
|
||||
wfexec.SetCallStack(s.callStack...),
|
||||
wfexec.SetHandler(svc.stateChangeHandler(ctx)),
|
||||
}
|
||||
|
||||
if svc.opt.ExecDebug {
|
||||
log := svc.log.
|
||||
Named("exec").
|
||||
With(logger.Uint64("workflowID", s.workflowID)).
|
||||
With(logger.Uint64("runnerID", s.runner.Identity())).
|
||||
With(logger.Uint64s("runnerRoles", s.runner.Roles()))
|
||||
|
||||
opts = append(
|
||||
opts,
|
||||
wfexec.SetLogger(log),
|
||||
wfexec.SetDumpStacktraceOnPanic(true),
|
||||
)
|
||||
}
|
||||
|
||||
// Encode runner into execution context
|
||||
// runner is used as identity and for access control
|
||||
execCtx = auth.SetIdentityToContext(execCtx, s.runner)
|
||||
|
||||
// Encode invoker into execution context
|
||||
// invoker is used
|
||||
execCtx = context.WithValue(execCtx, workflowInvokerCtxKey{}, s.invoker)
|
||||
|
||||
s.session <- wfexec.NewSession(execCtx, s.graph, opts...)
|
||||
// case time for a pool cleanup
|
||||
// @todo cleanup pool when sessions are complete
|
||||
|
||||
case <-gcTicker.C:
|
||||
svc.gc()
|
||||
|
||||
case <-lpTicker.C:
|
||||
svc.logPending()
|
||||
}
|
||||
}
|
||||
|
||||
// @todo serialize sessions & suspended states
|
||||
//svc.suspendAll(ctx)
|
||||
}()
|
||||
|
||||
svc.log.Debug("watcher initialized")
|
||||
}
|
||||
|
||||
// garbage collection for stale sessions
|
||||
func (svc *session) gc() {
|
||||
svc.mux.Lock()
|
||||
defer svc.mux.Unlock()
|
||||
|
||||
for _, s := range svc.pool {
|
||||
if s.GC() {
|
||||
delete(svc.pool, s.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// garbage collection for stale sessions
|
||||
func (svc *session) logPending() {
|
||||
svc.mux.RLock()
|
||||
defer svc.mux.RUnlock()
|
||||
|
||||
var (
|
||||
total = len(svc.pool)
|
||||
pending, pending1m, pending1h, pending1d int
|
||||
)
|
||||
|
||||
for _, s := range svc.pool {
|
||||
switch age := now().Sub(s.CreatedAt); {
|
||||
case age > time.Hour*24:
|
||||
pending1d++
|
||||
case age > time.Hour:
|
||||
pending1h++
|
||||
case age > time.Minute:
|
||||
pending1m++
|
||||
default:
|
||||
pending++
|
||||
}
|
||||
}
|
||||
|
||||
if total > 0 {
|
||||
svc.log.Info(
|
||||
"workflow session garbage collector stats",
|
||||
zap.Int("total", total),
|
||||
zap.Int("pending", pending),
|
||||
zap.Int("pending1m", pending1m),
|
||||
zap.Int("pending1h", pending1h),
|
||||
zap.Int("pending1d", pending1d),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// stateChangeHandler keeps track of session status changes and frequently stores session into db
|
||||
func (svc *session) stateChangeHandler(ctx context.Context) wfexec.StateChangeHandler {
|
||||
return func(status wfexec.SessionStatus, state *wfexec.State, s *wfexec.Session) (err error) {
|
||||
svc.mux.Lock()
|
||||
defer svc.mux.Unlock()
|
||||
|
||||
// Handle potential state change handler panics and gracefully error out
|
||||
defer func() {
|
||||
reason := recover()
|
||||
if reflect2.IsNil(reason) {
|
||||
return
|
||||
}
|
||||
|
||||
err = fmt.Errorf("automation/service/session.go#stateChangeHandler panic: %v", reason)
|
||||
}()
|
||||
|
||||
ses := svc.pool[s.ID()]
|
||||
if ses == nil {
|
||||
log := svc.log.With(
|
||||
logger.Uint64("sessionID", s.ID()),
|
||||
zap.Stringer("status", status),
|
||||
)
|
||||
|
||||
log.Warn("could not find session to update")
|
||||
return
|
||||
}
|
||||
|
||||
ses.FlushCounter++
|
||||
|
||||
var (
|
||||
// By default, we want to update session when new status is prompted, delayed, completed, canceled or failed
|
||||
// But if status is active, we'll flush it every X frames (sessionStateFlushFrequency)
|
||||
update = true
|
||||
|
||||
frame *wfexec.Frame
|
||||
)
|
||||
|
||||
// Building a frame deep-copies the whole scope, so only do it when the
|
||||
// variables are going to be read back: when the session is traced, or
|
||||
// when it is settling on a status someone will want explained.
|
||||
//
|
||||
// ElapsedTime is calculated on append; the oldest frames can be
|
||||
// dropped, so the session keeps track of when tracing started.
|
||||
if state != nil && svc.opt.StackTraceEnabled {
|
||||
if ses.KeepsFrameScope() || status != wfexec.SessionActive {
|
||||
frame = state.MakeFrame()
|
||||
} else {
|
||||
frame = state.MakeLightFrame()
|
||||
}
|
||||
|
||||
ses.AppendRuntimeStacktrace(frame)
|
||||
}
|
||||
|
||||
switch status {
|
||||
case wfexec.SessionPrompted:
|
||||
ses.SuspendedAt = now()
|
||||
ses.Status = types.SessionPrompted
|
||||
|
||||
// Send the pending prompts to user
|
||||
if svc.promptSender != nil {
|
||||
for _, pp := range s.UnsentPendingPrompts() {
|
||||
// Prevent prompts from being re-sent
|
||||
//
|
||||
// This here collects all of the pending prompts and (re-)sends them.
|
||||
// The problem occurs if you display multiple prompts in parallel --
|
||||
// they get duplicated.
|
||||
//
|
||||
// Patching back-end with this simple fix, but we should probably
|
||||
// refactor the logic that handles prompting a bit.
|
||||
pp.Original.MarkSent()
|
||||
|
||||
if err := svc.promptSender.Send("workflowSessionPrompt", pp, pp.OwnerId); err != nil {
|
||||
svc.log.Error("failed to send prompt to user", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case wfexec.SessionDelayed:
|
||||
ses.SuspendedAt = now()
|
||||
ses.Status = types.SessionSuspended
|
||||
|
||||
case wfexec.SessionCompleted:
|
||||
ses.SuspendedAt = nil
|
||||
ses.CompletedAt = now()
|
||||
ses.Status = types.SessionCompleted
|
||||
|
||||
case wfexec.SessionFailed:
|
||||
ses.SuspendedAt = nil
|
||||
ses.CompletedAt = now()
|
||||
if state != nil {
|
||||
ses.Error = state.Error()
|
||||
}
|
||||
ses.Status = types.SessionFailed
|
||||
|
||||
case wfexec.SessionCanceled:
|
||||
ses.SuspendedAt = nil
|
||||
ses.CompletedAt = now()
|
||||
ses.Status = types.SessionCanceled
|
||||
|
||||
default:
|
||||
// force update every X iterations
|
||||
update = ses.FlushCounter >= sessionStateFlushFrequency
|
||||
if !update {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the counter whenever we flush due to whatever scenario
|
||||
ses.FlushCounter = 0
|
||||
ses.CopyRuntimeStacktrace()
|
||||
if err := store.UpsertAutomationSession(ctx, svc.store, ses); err != nil {
|
||||
log := svc.log.With(
|
||||
logger.Uint64("sessionID", s.ID()),
|
||||
zap.Stringer("status", status),
|
||||
)
|
||||
|
||||
log.Error("failed to update session", zap.Error(err))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func loadSession(ctx context.Context, s store.Storer, sessionID uint64) (res *types.Session, err error) {
|
||||
if sessionID == 0 {
|
||||
return nil, SessionErrInvalidID()
|
||||
}
|
||||
|
||||
if res, err = store.LookupAutomationSessionByID(ctx, s, sessionID); errors.IsNotFound(err) {
|
||||
return nil, SessionErrNotFound()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
638
server/automation/service/session_actions.gen.go
generated
Normal file
638
server/automation/service/session_actions.gen.go
generated
Normal file
@@ -0,0 +1,638 @@
|
||||
package service
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
// automation/service/session_actions.yaml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
sessionActionProps struct {
|
||||
session *types.Session
|
||||
new *types.Session
|
||||
update *types.Session
|
||||
filter *types.SessionFilter
|
||||
}
|
||||
|
||||
sessionAction struct {
|
||||
timestamp time.Time
|
||||
resource string
|
||||
action string
|
||||
log string
|
||||
severity actionlog.Severity
|
||||
|
||||
// prefix for error when action fails
|
||||
errorMessage string
|
||||
|
||||
props *sessionActionProps
|
||||
}
|
||||
|
||||
sessionLogMetaKey struct{}
|
||||
sessionPropsMetaKey struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
// just a placeholder to cover template cases w/o fmt package use
|
||||
_ = fmt.Println
|
||||
)
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Props methods
|
||||
// setSession updates sessionActionProps's session
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *sessionActionProps) setSession(session *types.Session) *sessionActionProps {
|
||||
p.session = session
|
||||
return p
|
||||
}
|
||||
|
||||
// setNew updates sessionActionProps's new
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *sessionActionProps) setNew(new *types.Session) *sessionActionProps {
|
||||
p.new = new
|
||||
return p
|
||||
}
|
||||
|
||||
// setUpdate updates sessionActionProps's update
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *sessionActionProps) setUpdate(update *types.Session) *sessionActionProps {
|
||||
p.update = update
|
||||
return p
|
||||
}
|
||||
|
||||
// setFilter updates sessionActionProps's filter
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *sessionActionProps) setFilter(filter *types.SessionFilter) *sessionActionProps {
|
||||
p.filter = filter
|
||||
return p
|
||||
}
|
||||
|
||||
// Serialize converts sessionActionProps to actionlog.Meta
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p sessionActionProps) Serialize() actionlog.Meta {
|
||||
var (
|
||||
m = make(actionlog.Meta)
|
||||
)
|
||||
|
||||
if p.session != nil {
|
||||
m.Set("session.ID", p.session.ID, true)
|
||||
}
|
||||
if p.new != nil {
|
||||
m.Set("new.ID", p.new.ID, true)
|
||||
}
|
||||
if p.update != nil {
|
||||
m.Set("update.ID", p.update.ID, true)
|
||||
}
|
||||
if p.filter != nil {
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// tr translates string and replaces meta value placeholder with values
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p sessionActionProps) Format(in string, err error) string {
|
||||
var (
|
||||
pairs = []string{"{{err}}"}
|
||||
// first non-empty string
|
||||
fns = func(ii ...interface{}) string {
|
||||
for _, i := range ii {
|
||||
if s := fmt.Sprintf("%v", i); len(s) > 0 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
pairs = append(pairs, err.Error())
|
||||
} else {
|
||||
pairs = append(pairs, "nil")
|
||||
}
|
||||
|
||||
if p.session != nil {
|
||||
// replacement for "{{session}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{session}}",
|
||||
fns(
|
||||
p.session.ID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{session.ID}}", fns(p.session.ID))
|
||||
}
|
||||
|
||||
if p.new != nil {
|
||||
// replacement for "{{new}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{new}}",
|
||||
fns(
|
||||
p.new.ID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{new.ID}}", fns(p.new.ID))
|
||||
}
|
||||
|
||||
if p.update != nil {
|
||||
// replacement for "{{update}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{update}}",
|
||||
fns(
|
||||
p.update.ID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{update.ID}}", fns(p.update.ID))
|
||||
}
|
||||
|
||||
if p.filter != nil {
|
||||
// replacement for "{{filter}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{filter}}",
|
||||
fns(),
|
||||
)
|
||||
}
|
||||
return strings.NewReplacer(pairs...).Replace(in)
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action methods
|
||||
|
||||
// String returns loggable description as string
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (a *sessionAction) String() string {
|
||||
var props = &sessionActionProps{}
|
||||
|
||||
if a.props != nil {
|
||||
props = a.props
|
||||
}
|
||||
|
||||
return props.Format(a.log, nil)
|
||||
}
|
||||
|
||||
func (e *sessionAction) ToAction() *actionlog.Action {
|
||||
return &actionlog.Action{
|
||||
Resource: e.resource,
|
||||
Action: e.action,
|
||||
Severity: e.severity,
|
||||
Description: e.String(),
|
||||
Meta: e.props.Serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action constructors
|
||||
|
||||
// SessionActionSearch returns "automation:session.search" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionActionSearch(props ...*sessionActionProps) *sessionAction {
|
||||
a := &sessionAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:session",
|
||||
action: "search",
|
||||
log: "searched for matching sessions",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// SessionActionLookup returns "automation:session.lookup" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionActionLookup(props ...*sessionActionProps) *sessionAction {
|
||||
a := &sessionAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:session",
|
||||
action: "lookup",
|
||||
log: "looked-up for a {{session}}",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// SessionActionCreate returns "automation:session.create" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionActionCreate(props ...*sessionActionProps) *sessionAction {
|
||||
a := &sessionAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:session",
|
||||
action: "create",
|
||||
log: "created {{session}}",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// SessionActionUpdate returns "automation:session.update" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionActionUpdate(props ...*sessionActionProps) *sessionAction {
|
||||
a := &sessionAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:session",
|
||||
action: "update",
|
||||
log: "updated {{session}}",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// SessionActionDelete returns "automation:session.delete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionActionDelete(props ...*sessionActionProps) *sessionAction {
|
||||
a := &sessionAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:session",
|
||||
action: "delete",
|
||||
log: "deleted {{session}}",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// SessionActionUndelete returns "automation:session.undelete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionActionUndelete(props ...*sessionActionProps) *sessionAction {
|
||||
a := &sessionAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:session",
|
||||
action: "undelete",
|
||||
log: "undeleted {{session}}",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error constructors
|
||||
|
||||
// SessionErrGeneric returns "automation:session.generic" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionErrGeneric(mm ...*sessionActionProps) *errors.Error {
|
||||
var p = &sessionActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("failed to complete request due to internal error", nil),
|
||||
|
||||
errors.Meta("type", "generic"),
|
||||
errors.Meta("resource", "automation:session"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(sessionLogMetaKey{}, "{err}"),
|
||||
errors.Meta(sessionPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "session.errors.generic"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SessionErrNotFound returns "automation:session.notFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionErrNotFound(mm ...*sessionActionProps) *errors.Error {
|
||||
var p = &sessionActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("session not found", nil),
|
||||
|
||||
errors.Meta("type", "notFound"),
|
||||
errors.Meta("resource", "automation:session"),
|
||||
|
||||
errors.Meta(sessionPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "session.errors.notFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SessionErrInvalidID returns "automation:session.invalidID" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionErrInvalidID(mm ...*sessionActionProps) *errors.Error {
|
||||
var p = &sessionActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("invalid ID", nil),
|
||||
|
||||
errors.Meta("type", "invalidID"),
|
||||
errors.Meta("resource", "automation:session"),
|
||||
|
||||
errors.Meta(sessionPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "session.errors.invalidID"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SessionErrStaleData returns "automation:session.staleData" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionErrStaleData(mm ...*sessionActionProps) *errors.Error {
|
||||
var p = &sessionActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("session was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version", nil),
|
||||
|
||||
errors.Meta("type", "staleData"),
|
||||
errors.Meta("resource", "automation:session"),
|
||||
|
||||
errors.Meta(sessionPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "session.errors.staleData"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SessionErrNotAllowedToRead returns "automation:session.notAllowedToRead" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionErrNotAllowedToRead(mm ...*sessionActionProps) *errors.Error {
|
||||
var p = &sessionActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to read this session", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToRead"),
|
||||
errors.Meta("resource", "automation:session"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(sessionLogMetaKey{}, "failed to read {{session}}; insufficient permissions"),
|
||||
errors.Meta(sessionPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "session.errors.notAllowedToRead"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SessionErrNotAllowedToSearch returns "automation:session.notAllowedToSearch" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionErrNotAllowedToSearch(mm ...*sessionActionProps) *errors.Error {
|
||||
var p = &sessionActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to search or list sessions", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToSearch"),
|
||||
errors.Meta("resource", "automation:session"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(sessionLogMetaKey{}, "failed to search or list session; insufficient permissions"),
|
||||
errors.Meta(sessionPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "session.errors.notAllowedToSearch"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SessionErrNotAllowedToDelete returns "automation:session.notAllowedToDelete" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionErrNotAllowedToDelete(mm ...*sessionActionProps) *errors.Error {
|
||||
var p = &sessionActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to delete this session", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToDelete"),
|
||||
errors.Meta("resource", "automation:session"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(sessionLogMetaKey{}, "failed to delete {{session}}; insufficient permissions"),
|
||||
errors.Meta(sessionPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "session.errors.notAllowedToDelete"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// SessionErrNotAllowedToManage returns "automation:session.notAllowedToManage" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func SessionErrNotAllowedToManage(mm ...*sessionActionProps) *errors.Error {
|
||||
var p = &sessionActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to manage session's workflow", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToManage"),
|
||||
errors.Meta("resource", "automation:session"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(sessionLogMetaKey{}, "failed to manage {{session}}; insufficient permissions"),
|
||||
errors.Meta(sessionPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "session.errors.notAllowedToManage"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
// recordAction is a service helper function wraps function that can return error
|
||||
//
|
||||
// It will wrap unrecognized/internal errors with generic errors.
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (svc session) recordAction(ctx context.Context, props *sessionActionProps, actionFn func(...*sessionActionProps) *sessionAction, err error) error {
|
||||
if svc.actionlog == nil || actionFn == nil {
|
||||
// action log disabled or no action fn passed, return error as-is
|
||||
return err
|
||||
} else if err == nil {
|
||||
// action completed w/o error, record it
|
||||
svc.actionlog.Record(ctx, actionFn(props).ToAction())
|
||||
return nil
|
||||
}
|
||||
|
||||
a := actionFn(props).ToAction()
|
||||
|
||||
// Extracting error information and recording it as action
|
||||
a.Error = err.Error()
|
||||
|
||||
switch c := err.(type) {
|
||||
case *errors.Error:
|
||||
m := c.Meta()
|
||||
|
||||
a.Error = err.Error()
|
||||
a.Severity = actionlog.Severity(m.AsInt("severity"))
|
||||
a.Description = props.Format(m.AsString(sessionLogMetaKey{}), err)
|
||||
|
||||
if p, has := m[sessionPropsMetaKey{}]; has {
|
||||
a.Meta = p.(*sessionActionProps).Serialize()
|
||||
}
|
||||
|
||||
svc.actionlog.Record(ctx, a)
|
||||
default:
|
||||
svc.actionlog.Record(ctx, a)
|
||||
}
|
||||
|
||||
// Original error is passed on
|
||||
return err
|
||||
}
|
||||
76
server/automation/service/session_actions.yaml
Normal file
76
server/automation/service/session_actions.yaml
Normal file
@@ -0,0 +1,76 @@
|
||||
# List of loggable service actions
|
||||
|
||||
resource: automation:session
|
||||
service: session
|
||||
|
||||
import:
|
||||
- github.com/cortezaproject/corteza/server/automation/types
|
||||
|
||||
# Default sensitivity for actions
|
||||
defaultActionSeverity: info
|
||||
|
||||
# default severity for errors
|
||||
defaultErrorSeverity: error
|
||||
|
||||
|
||||
props:
|
||||
- name: session
|
||||
type: "*types.Session"
|
||||
fields: [ ID ]
|
||||
- name: new
|
||||
type: "*types.Session"
|
||||
fields: [ ID ]
|
||||
- name: update
|
||||
type: "*types.Session"
|
||||
fields: [ ID ]
|
||||
- name: filter
|
||||
type: "*types.SessionFilter"
|
||||
|
||||
actions:
|
||||
- action: search
|
||||
log: "searched for matching sessions"
|
||||
severity: info
|
||||
|
||||
- action: lookup
|
||||
log: "looked-up for a {{session}}"
|
||||
severity: info
|
||||
|
||||
- action: create
|
||||
log: "created {{session}}"
|
||||
|
||||
- action: update
|
||||
log: "updated {{session}}"
|
||||
|
||||
- action: delete
|
||||
log: "deleted {{session}}"
|
||||
|
||||
- action: undelete
|
||||
log: "undeleted {{session}}"
|
||||
|
||||
errors:
|
||||
- error: notFound
|
||||
message: "session not found"
|
||||
|
||||
- error: invalidID
|
||||
message: "invalid ID"
|
||||
|
||||
- error: staleData
|
||||
message: "session was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version"
|
||||
severity: warning
|
||||
|
||||
- error: notAllowedToRead
|
||||
message: "not allowed to read this session"
|
||||
log: "failed to read {{session}}; insufficient permissions"
|
||||
|
||||
- error: notAllowedToSearch
|
||||
message: "not allowed to search or list sessions"
|
||||
log: "failed to search or list session; insufficient permissions"
|
||||
|
||||
- error: notAllowedToDelete
|
||||
message: "not allowed to delete this session"
|
||||
log: "failed to delete {{session}}; insufficient permissions"
|
||||
|
||||
- error: notAllowedToManage
|
||||
message: "not allowed to manage session's workflow"
|
||||
log: "failed to manage {{session}}; insufficient permissions"
|
||||
|
||||
234
server/automation/service/session_test.go
Normal file
234
server/automation/service/session_test.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/wfexec"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest/observer"
|
||||
)
|
||||
|
||||
func TestSession_Start(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
ses = &session{}
|
||||
g = wfexec.NewGraph()
|
||||
ctx = context.Background()
|
||||
|
||||
err error
|
||||
)
|
||||
|
||||
_, _, err = ses.Start(ctx, g, types.SessionStartParams{Invoker: auth.Anonymous()})
|
||||
req.EqualError(err, "could not find starting step")
|
||||
|
||||
g.AddStep(wfexec.NewGenericStep(nil))
|
||||
_, _, err = ses.Start(ctx, g, types.SessionStartParams{StepID: 4321, Invoker: auth.Anonymous()})
|
||||
req.EqualError(err, "trigger staring step references non-existing step")
|
||||
|
||||
// Adding another orphaned step and starting session w/o explicitly specifying the starting step
|
||||
g.AddStep(wfexec.NewGenericStep(nil))
|
||||
_, _, err = ses.Start(ctx, g, types.SessionStartParams{Invoker: auth.Anonymous()})
|
||||
req.EqualError(err, "cannot start workflow session multiple starting steps found")
|
||||
|
||||
// add a generic step with a known ID so we can use it as a starting point
|
||||
s := wfexec.NewGenericStep(nil)
|
||||
s.SetID(42)
|
||||
g.AddStep(s)
|
||||
// add parents to the 42 step
|
||||
g.AddStep(wfexec.NewGenericStep(nil), s)
|
||||
_, _, err = ses.Start(ctx, g, types.SessionStartParams{StepID: 42, Invoker: auth.Anonymous()})
|
||||
req.EqualError(err, "cannot start workflow on a step with parents")
|
||||
}
|
||||
|
||||
func TestSessionStackTraces(t *testing.T) {
|
||||
stepID := uint64(0)
|
||||
getStepID := func() uint64 {
|
||||
stepID++
|
||||
return uint64(stepID)
|
||||
}
|
||||
|
||||
iter1 := &wfexec.Frame{StepID: getStepID(), ParentID: 0, Action: "iterator initialized"}
|
||||
iter1stepExpr := &wfexec.Frame{StepID: getStepID(), ParentID: iter1.StepID, Action: ""}
|
||||
iter1stepCnt := &wfexec.Frame{StepID: getStepID(), ParentID: iter1stepExpr.StepID, Action: "loop continue"}
|
||||
|
||||
iter2 := &wfexec.Frame{StepID: getStepID(), ParentID: iter1.StepID, Action: "iterator initialized"}
|
||||
iter2stepExpr := &wfexec.Frame{StepID: getStepID(), ParentID: iter2.StepID, Action: ""}
|
||||
iter2stepCnt := &wfexec.Frame{StepID: getStepID(), ParentID: iter2stepExpr.StepID, Action: "loop continue"}
|
||||
|
||||
rando1 := &wfexec.Frame{StepID: getStepID(), ParentID: iter2.StepID, Action: ""}
|
||||
rando2 := &wfexec.Frame{StepID: getStepID(), ParentID: iter2.StepID, Action: ""}
|
||||
|
||||
trace := []*wfexec.Frame{
|
||||
// first loop
|
||||
iter1,
|
||||
iter1stepExpr,
|
||||
iter1stepCnt,
|
||||
iter1,
|
||||
iter1stepExpr,
|
||||
iter1stepCnt,
|
||||
iter1,
|
||||
|
||||
// second loop
|
||||
iter2,
|
||||
iter2stepExpr,
|
||||
iter2stepCnt,
|
||||
iter2,
|
||||
iter2stepExpr,
|
||||
iter2stepCnt,
|
||||
iter2,
|
||||
|
||||
rando1,
|
||||
rando2,
|
||||
}
|
||||
|
||||
ses := &types.Session{}
|
||||
|
||||
for _, f := range trace {
|
||||
ses.AppendRuntimeStacktrace(f)
|
||||
}
|
||||
|
||||
require.Len(t, ses.RuntimeStacktrace, 10)
|
||||
|
||||
require.Equal(t, iter1.StepID, ses.RuntimeStacktrace[0].StepID)
|
||||
require.Equal(t, iter1stepExpr.StepID, ses.RuntimeStacktrace[1].StepID)
|
||||
require.Equal(t, iter1stepCnt.StepID, ses.RuntimeStacktrace[2].StepID)
|
||||
require.Equal(t, iter1.StepID, ses.RuntimeStacktrace[3].StepID)
|
||||
require.Equal(t, iter2.StepID, ses.RuntimeStacktrace[4].StepID)
|
||||
require.Equal(t, iter2stepExpr.StepID, ses.RuntimeStacktrace[5].StepID)
|
||||
require.Equal(t, iter2stepCnt.StepID, ses.RuntimeStacktrace[6].StepID)
|
||||
require.Equal(t, iter2.StepID, ses.RuntimeStacktrace[7].StepID)
|
||||
require.Equal(t, rando1.StepID, ses.RuntimeStacktrace[8].StepID)
|
||||
require.Equal(t, rando2.StepID, ses.RuntimeStacktrace[9].StepID)
|
||||
}
|
||||
|
||||
// Before:
|
||||
// goos: darwin
|
||||
// goarch: arm64
|
||||
// pkg: github.com/cortezaproject/corteza/server/automation/service
|
||||
// BenchmarkSessionStackTraces_1000-12 42758 28594 ns/op 87288 B/op 15 allocs/op
|
||||
// BenchmarkSessionStackTraces_10000-12 3430 348308 ns/op 1160442 B/op 23 allocs/op
|
||||
// BenchmarkSessionStackTraces_100000-12 229 5299188 ns/op 13317368 B/op 33 allocs/op
|
||||
// BenchmarkSessionStackTraces_1000000-12 19 52896750 ns/op 128431352 B/op 43 allocs/op
|
||||
// BenchmarkSessionStackTraces_10000000-12 3 436311292 ns/op 1202337018 B/op 53 allocs/op
|
||||
|
||||
// After:
|
||||
// goos: darwin
|
||||
// goarch: arm64
|
||||
// pkg: github.com/cortezaproject/corteza/server/automation/service
|
||||
// BenchmarkSessionStackTraces_1000-12 40142 29860 ns/op 120 B/op 4 allocs/op
|
||||
// BenchmarkSessionStackTraces_10000-12 4387 264930 ns/op 120 B/op 4 allocs/op
|
||||
// BenchmarkSessionStackTraces_100000-12 447 2735284 ns/op 120 B/op 4 allocs/op
|
||||
// BenchmarkSessionStackTraces_1000000-12 42 26596636 ns/op 120 B/op 4 allocs/op
|
||||
// BenchmarkSessionStackTraces_10000000-12 4 265553667 ns/op 120 B/op 4 allocs/op
|
||||
func benchmarkSessionStackTraces(b *testing.B, iters int) {
|
||||
stepID := uint64(0)
|
||||
getStepID := func() uint64 {
|
||||
stepID++
|
||||
return uint64(stepID)
|
||||
}
|
||||
|
||||
iter := &wfexec.Frame{
|
||||
StepID: getStepID(),
|
||||
Action: "iterator initialized",
|
||||
}
|
||||
|
||||
loop := []*wfexec.Frame{iter}
|
||||
loop = append(loop, &wfexec.Frame{
|
||||
StepID: getStepID(),
|
||||
ParentID: iter.ParentID,
|
||||
Action: "",
|
||||
})
|
||||
loop = append(loop, &wfexec.Frame{
|
||||
StepID: getStepID(),
|
||||
ParentID: loop[len(loop)-1].StepID,
|
||||
Action: "loop continue",
|
||||
})
|
||||
|
||||
frames := []*wfexec.Frame{}
|
||||
for x := 0; x < iters; x++ {
|
||||
frames = append(frames, loop...)
|
||||
}
|
||||
|
||||
frames = append(frames, iter)
|
||||
frames = append(frames, &wfexec.Frame{
|
||||
StepID: getStepID(),
|
||||
ParentID: iter.ParentID,
|
||||
Action: "termination",
|
||||
})
|
||||
|
||||
b.StopTimer()
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
ses := &types.Session{}
|
||||
b.StartTimer()
|
||||
for _, f := range frames {
|
||||
ses.AppendRuntimeStacktrace(f)
|
||||
}
|
||||
b.StopTimer()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSessionStackTraces_1000(b *testing.B) {
|
||||
benchmarkSessionStackTraces(b, 1000)
|
||||
}
|
||||
|
||||
func BenchmarkSessionStackTraces_10000(b *testing.B) {
|
||||
benchmarkSessionStackTraces(b, 10000)
|
||||
}
|
||||
|
||||
func BenchmarkSessionStackTraces_100000(b *testing.B) {
|
||||
benchmarkSessionStackTraces(b, 100000)
|
||||
}
|
||||
|
||||
func BenchmarkSessionStackTraces_1000000(b *testing.B) {
|
||||
benchmarkSessionStackTraces(b, 1000000)
|
||||
}
|
||||
|
||||
func BenchmarkSessionStackTraces_10000000(b *testing.B) {
|
||||
benchmarkSessionStackTraces(b, 10000000)
|
||||
}
|
||||
|
||||
func TestSession_logPending(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
core, o = observer.New(zap.NewAtomicLevelAt(zap.InfoLevel))
|
||||
ref = time.Now()
|
||||
nextSesID uint64
|
||||
mkSes = func(age time.Duration) *types.Session {
|
||||
nextSesID++
|
||||
return &types.Session{ID: nextSesID, CreatedAt: ref.Add(-age)}
|
||||
}
|
||||
|
||||
svc = &session{
|
||||
log: zap.New(core),
|
||||
pool: map[uint64]*types.Session{
|
||||
1: mkSes(time.Second * 5),
|
||||
2: mkSes(time.Minute * 5),
|
||||
3: mkSes(time.Hour * 5),
|
||||
4: mkSes(time.Hour * 30),
|
||||
5: mkSes(time.Hour * 72),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// pin the clock so bucketing does not depend on test runtime
|
||||
now = func() *time.Time { return &ref }
|
||||
defer func() { now = func() *time.Time { c := time.Now(); return &c } }()
|
||||
|
||||
svc.logPending()
|
||||
|
||||
entries := o.All()
|
||||
req.Len(entries, 1)
|
||||
|
||||
fields := entries[0].ContextMap()
|
||||
req.EqualValues(5, fields["total"])
|
||||
req.EqualValues(1, fields["pending"])
|
||||
req.EqualValues(1, fields["pending1m"])
|
||||
req.EqualValues(1, fields["pending1h"])
|
||||
req.EqualValues(2, fields["pending1d"])
|
||||
}
|
||||
728
server/automation/service/trigger.go
Normal file
728
server/automation/service/trigger.go
Normal file
@@ -0,0 +1,728 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
cmpEvent "github.com/cortezaproject/corteza/server/compose/service/event"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"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/id"
|
||||
"github.com/cortezaproject/corteza/server/pkg/label"
|
||||
"github.com/cortezaproject/corteza/server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"github.com/cortezaproject/corteza/server/pkg/wfexec"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
sysEvent "github.com/cortezaproject/corteza/server/system/service/event"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
trigger struct {
|
||||
eventbus triggerEventTriggerHandler
|
||||
store store.Storer
|
||||
actionlog actionlog.Recorder
|
||||
ac triggerAccessController
|
||||
|
||||
opt options.WorkflowOpt
|
||||
|
||||
log *zap.Logger
|
||||
|
||||
// maps registered triggers (value, uintptr) to trigger ID (key, uint64)
|
||||
// this will keep track of all our trigger registrations and help us do a cleanup on
|
||||
// trigger update.
|
||||
triggers map[uint64]uintptr
|
||||
|
||||
reg map[uint64]map[uint64]uintptr
|
||||
|
||||
workflow *workflow
|
||||
session *session
|
||||
|
||||
mux *sync.RWMutex
|
||||
}
|
||||
|
||||
triggerAccessController interface {
|
||||
CanSearchTriggers(context.Context) bool
|
||||
CanManageTriggersOnWorkflow(context.Context, *types.Workflow) bool
|
||||
CanExecuteWorkflow(context.Context, *types.Workflow) bool
|
||||
}
|
||||
|
||||
triggerEventTriggerHandler interface {
|
||||
Register(h eventbus.HandlerFn, ops ...eventbus.HandlerRegOp) uintptr
|
||||
Unregister(ptrs ...uintptr)
|
||||
}
|
||||
|
||||
triggerUpdateHandler func(ctx context.Context, ns *types.Trigger) (triggerChanges, error)
|
||||
triggerChanges uint8
|
||||
|
||||
varsEncoder interface {
|
||||
EncodeVars() (*expr.Vars, error)
|
||||
}
|
||||
|
||||
varsDecoder interface {
|
||||
DecodeVars(*expr.Vars) error
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
triggerUnchanged triggerChanges = 0
|
||||
triggerChanged triggerChanges = 1
|
||||
triggerLabelsChanged triggerChanges = 2
|
||||
)
|
||||
|
||||
func Trigger(log *zap.Logger, opt options.WorkflowOpt) *trigger {
|
||||
return &trigger{
|
||||
log: log,
|
||||
opt: opt,
|
||||
eventbus: eventbus.Service(),
|
||||
actionlog: DefaultActionlog,
|
||||
store: DefaultStore,
|
||||
ac: DefaultAccessControl,
|
||||
session: DefaultSession,
|
||||
workflow: DefaultWorkflow,
|
||||
triggers: make(map[uint64]uintptr),
|
||||
reg: make(map[uint64]map[uint64]uintptr),
|
||||
mux: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *trigger) Search(ctx context.Context, filter types.TriggerFilter) (rr types.TriggerSet, f types.TriggerFilter, err error) {
|
||||
var (
|
||||
wap = &triggerActionProps{filter: &filter}
|
||||
)
|
||||
|
||||
err = func() (err error) {
|
||||
if !svc.ac.CanSearchTriggers(ctx) {
|
||||
return TriggerErrNotAllowedToSearch()
|
||||
}
|
||||
|
||||
if len(filter.Labels) > 0 {
|
||||
filter.LabeledIDs, err = label.Search(
|
||||
ctx,
|
||||
svc.store,
|
||||
types.Trigger{}.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.SearchAutomationTriggers(ctx, svc.store, filter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = label.Load(ctx, svc.store, toLabeledTriggers(rr)...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return rr, filter, svc.recordAction(ctx, wap, TriggerActionSearch, err)
|
||||
}
|
||||
|
||||
// SearchOnManual finds first matching onManual trigger and returns it
|
||||
//
|
||||
// In case stepID is 0, first trigger is returned
|
||||
func (svc *trigger) SearchOnManual(ctx context.Context, workflowID, stepID uint64) (*types.Trigger, error) {
|
||||
tt, _, err := svc.Search(ctx, types.TriggerFilter{
|
||||
WorkflowID: id.Strings(workflowID),
|
||||
EventType: "onManual",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stepID == 0 && len(tt) > 0 {
|
||||
return tt[0], nil
|
||||
}
|
||||
|
||||
for _, t := range tt {
|
||||
if t.StepID == stepID {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (svc *trigger) LookupByID(ctx context.Context, triggerID uint64) (res *types.Trigger, err error) {
|
||||
var (
|
||||
wap = &triggerActionProps{trigger: &types.Trigger{ID: triggerID}}
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
|
||||
if !svc.ac.CanSearchTriggers(ctx) {
|
||||
return TriggerErrNotAllowedToRead()
|
||||
}
|
||||
|
||||
if res, err = loadTrigger(ctx, s, triggerID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = label.Load(ctx, svc.store, res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return res, svc.recordAction(ctx, wap, TriggerActionLookup, err)
|
||||
}
|
||||
|
||||
// Create adds new trigger resource and saves it into store
|
||||
// It updates service's cache
|
||||
func (svc *trigger) Create(ctx context.Context, new *types.Trigger) (res *types.Trigger, err error) {
|
||||
var (
|
||||
wap = &triggerActionProps{new: new}
|
||||
cUser = auth.GetIdentityFromContext(ctx).Identity()
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
var (
|
||||
wf *types.Workflow
|
||||
)
|
||||
|
||||
if wf, err = loadWorkflow(ctx, svc.store, new.WorkflowID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !svc.ac.CanManageTriggersOnWorkflow(ctx, wf) {
|
||||
return TriggerErrNotAllowedToCreate()
|
||||
}
|
||||
|
||||
res = &types.Trigger{
|
||||
ID: nextID(),
|
||||
Enabled: new.Enabled,
|
||||
WorkflowID: new.WorkflowID,
|
||||
StepID: new.StepID,
|
||||
ResourceType: new.ResourceType,
|
||||
EventType: new.EventType,
|
||||
Constraints: new.Constraints,
|
||||
Input: new.Input,
|
||||
Labels: new.Labels,
|
||||
Meta: new.Meta,
|
||||
OwnedBy: cUser,
|
||||
CreatedAt: *now(),
|
||||
CreatedBy: cUser,
|
||||
}
|
||||
wap.new = res
|
||||
|
||||
if err = store.CreateAutomationTrigger(ctx, s, res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = label.Create(ctx, s, res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore workflow issues as those are defined by the workflow itself.
|
||||
// Internal errors should still be reported.
|
||||
if err = svc.registerWorkflow(ctx, wf, res); err != nil {
|
||||
if _, ok := err.(types.WorkflowIssueSet); ok {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
return res, svc.recordAction(ctx, wap, TriggerActionCreate, err)
|
||||
}
|
||||
|
||||
// Update modifies existing trigger resource in the store
|
||||
func (svc *trigger) Update(ctx context.Context, upd *types.Trigger) (*types.Trigger, error) {
|
||||
return svc.updater(ctx, upd.ID, TriggerActionUpdate, func(ctx context.Context, res *types.Trigger) (triggerChanges, error) {
|
||||
if err := svc.canManageTrigger(ctx, res, TriggerErrNotAllowedToUpdate()); err != nil {
|
||||
return triggerUnchanged, err
|
||||
}
|
||||
|
||||
handler := svc.handleUpdate(upd)
|
||||
return handler(ctx, res)
|
||||
})
|
||||
}
|
||||
|
||||
func (svc *trigger) DeleteByID(ctx context.Context, triggerID uint64) error {
|
||||
return trim1st(svc.updater(ctx, triggerID, TriggerActionDelete, svc.handleDelete))
|
||||
}
|
||||
|
||||
func (svc *trigger) UndeleteByID(ctx context.Context, triggerID uint64) error {
|
||||
return trim1st(svc.updater(ctx, triggerID, TriggerActionUndelete, svc.handleUndelete))
|
||||
}
|
||||
|
||||
func (svc trigger) updater(ctx context.Context, triggerID uint64, action func(...*triggerActionProps) *triggerAction, fn triggerUpdateHandler) (*types.Trigger, error) {
|
||||
var (
|
||||
changes triggerChanges
|
||||
res *types.Trigger
|
||||
aProps = &triggerActionProps{trigger: &types.Trigger{ID: triggerID}}
|
||||
err error
|
||||
)
|
||||
|
||||
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
|
||||
res, err = loadTrigger(ctx, s, triggerID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = label.Load(ctx, svc.store, res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.setTrigger(res)
|
||||
aProps.setUpdate(res)
|
||||
|
||||
if changes, err = fn(ctx, res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if changes&triggerChanged > 0 {
|
||||
if err = store.UpdateAutomationTrigger(ctx, svc.store, res); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if changes&triggerLabelsChanged > 0 {
|
||||
if err = label.Update(ctx, s, res); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return res, svc.recordAction(ctx, aProps, action, err)
|
||||
}
|
||||
|
||||
func (svc trigger) handleUpdate(upd *types.Trigger) triggerUpdateHandler {
|
||||
return func(ctx context.Context, res *types.Trigger) (changes triggerChanges, err error) {
|
||||
if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) {
|
||||
return triggerUnchanged, TriggerErrStaleData()
|
||||
}
|
||||
|
||||
if res.Enabled != upd.Enabled {
|
||||
changes |= triggerChanged
|
||||
res.Enabled = upd.Enabled
|
||||
}
|
||||
|
||||
if upd.Labels != nil {
|
||||
if label.Changed(res.Labels, upd.Labels) {
|
||||
changes |= triggerLabelsChanged
|
||||
res.Labels = upd.Labels
|
||||
}
|
||||
}
|
||||
|
||||
if res.StepID != upd.StepID {
|
||||
changes |= triggerChanged
|
||||
res.StepID = upd.StepID
|
||||
}
|
||||
|
||||
if res.EventType != upd.EventType {
|
||||
changes |= triggerChanged
|
||||
res.EventType = upd.EventType
|
||||
}
|
||||
|
||||
if res.ResourceType != upd.ResourceType {
|
||||
changes |= triggerChanged
|
||||
res.ResourceType = upd.ResourceType
|
||||
}
|
||||
|
||||
if upd.Meta != nil {
|
||||
if !reflect.DeepEqual(upd.Meta, res.Meta) {
|
||||
changes |= triggerChanged
|
||||
res.Meta = upd.Meta
|
||||
}
|
||||
}
|
||||
|
||||
if upd.Input != nil {
|
||||
if !reflect.DeepEqual(upd.Input, res.Input) {
|
||||
changes |= triggerChanged
|
||||
res.Input = upd.Input
|
||||
}
|
||||
}
|
||||
|
||||
if upd.Constraints != nil {
|
||||
if !reflect.DeepEqual(upd.Constraints, res.Constraints) {
|
||||
changes |= triggerChanged
|
||||
res.Constraints = upd.Constraints
|
||||
}
|
||||
}
|
||||
|
||||
if res.OwnedBy != upd.OwnedBy {
|
||||
// @todo need to check against access control if current user can modify owner
|
||||
changes |= triggerChanged
|
||||
res.OwnedBy = upd.OwnedBy
|
||||
}
|
||||
|
||||
if changes&triggerChanged > 0 {
|
||||
res.UpdatedAt = now()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (svc trigger) handleDelete(ctx context.Context, res *types.Trigger) (triggerChanges, error) {
|
||||
if err := svc.canManageTrigger(ctx, res, TriggerErrNotAllowedToDelete()); err != nil {
|
||||
return triggerUnchanged, err
|
||||
}
|
||||
|
||||
if res.DeletedAt != nil {
|
||||
// trigger already deleted
|
||||
return triggerUnchanged, nil
|
||||
}
|
||||
|
||||
res.DeletedAt = now()
|
||||
return triggerChanged, nil
|
||||
}
|
||||
|
||||
func (svc trigger) handleUndelete(ctx context.Context, res *types.Trigger) (triggerChanges, error) {
|
||||
if err := svc.canManageTrigger(ctx, res, TriggerErrNotAllowedToUndelete()); err != nil {
|
||||
return triggerUnchanged, err
|
||||
}
|
||||
|
||||
if res.DeletedAt == nil {
|
||||
// trigger not deleted
|
||||
return triggerUnchanged, nil
|
||||
}
|
||||
|
||||
res.DeletedAt = nil
|
||||
return triggerChanged, nil
|
||||
}
|
||||
|
||||
func (svc trigger) canManageTrigger(ctx context.Context, res *types.Trigger, permErr error) error {
|
||||
if wf, err := loadWorkflow(ctx, svc.store, res.WorkflowID); err != nil {
|
||||
return err
|
||||
} else if !svc.ac.CanManageTriggersOnWorkflow(ctx, wf) {
|
||||
return permErr
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// registers all triggers on all given workflows
|
||||
// before registering triggers on a workflow, all workflow triggers are unregistered
|
||||
func (svc *trigger) registerWorkflows(ctx context.Context, workflows ...*types.Workflow) error {
|
||||
// load ALL triggers directly from store
|
||||
tt, _, err := store.SearchAutomationTriggers(ctx, svc.store, types.TriggerFilter{
|
||||
WorkflowID: id.Strings(types.WorkflowSet(workflows).IDs()...),
|
||||
Deleted: filter.StateInclusive,
|
||||
Disabled: filter.StateExcluded,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, wf := range workflows {
|
||||
svc.unregisterWorkflows(wf)
|
||||
|
||||
if !wf.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if wf.DeletedAt != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(wf.Issues) > 0 {
|
||||
// workflow was processed before and issues were detected
|
||||
// and stored on the workflow; no need to continue
|
||||
continue
|
||||
}
|
||||
|
||||
if err = svc.registerWorkflow(ctx, wf, tt.FilterByWorkflowID(wf.ID)...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updates trigger handler registration
|
||||
//
|
||||
// Loads associated workflow and registers specific trigger
|
||||
func (svc *trigger) updateTriggerRegistration(ctx context.Context, t *types.Trigger) error {
|
||||
wf, err := loadWorkflow(ctx, svc.store, t.WorkflowID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return svc.registerWorkflow(ctx, wf, t)
|
||||
}
|
||||
|
||||
// registers one workflow and a set of triggers
|
||||
func (svc *trigger) registerWorkflow(ctx context.Context, wf *types.Workflow, tt ...*types.Trigger) (err error) {
|
||||
var (
|
||||
runAs auth.Identifiable
|
||||
)
|
||||
|
||||
// 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 auth.SetIdentityToContext(ctx, auth.ServiceUser())
|
||||
}
|
||||
|
||||
if !svc.opt.Register {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !wf.Enabled || len(wf.Issues) > 0 {
|
||||
// do not even try to register disabled
|
||||
// workflows or workflows with issues
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(types.TriggerSet(tt).FilterByWorkflowID(wf.ID)) < len(tt) {
|
||||
return fmt.Errorf("all triggers must reference the given workflow")
|
||||
}
|
||||
|
||||
if wis := validateWorkflowTriggers(wf, tt...); len(wis) > 0 {
|
||||
// skip trigger registration of there is a trigger related issue(s)
|
||||
// on a specific workflow.
|
||||
//
|
||||
// this really happens since we run all validation on save,
|
||||
// but there might be workflow from the time when these checks were
|
||||
// not in place
|
||||
return nil
|
||||
}
|
||||
|
||||
if wf.RunAs > 0 {
|
||||
if runAs, err = DefaultUser.FindByAny(sysUserCtx(), wf.RunAs); err != nil {
|
||||
return fmt.Errorf("failed to load run-as user %d: %w", wf.RunAs, err)
|
||||
} else if !runAs.Valid() {
|
||||
return fmt.Errorf("invalid user %d used for workflow run-as", wf.RunAs)
|
||||
}
|
||||
}
|
||||
|
||||
svc.registerTriggers(wf, runAs, tt...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerTriggers registers workflows triggers to eventbus
|
||||
//
|
||||
// It preloads run-as identity and finds a starting step for each trigger
|
||||
func (svc *trigger) registerTriggers(wf *types.Workflow, runAs auth.Identifiable, tt ...*types.Trigger) {
|
||||
var (
|
||||
handlerFn eventbus.HandlerFn
|
||||
err error
|
||||
g *wfexec.Graph
|
||||
issues types.WorkflowIssueSet
|
||||
wfLog = svc.log.
|
||||
With(logger.Uint64("workflowID", wf.ID))
|
||||
|
||||
// register only enabled, undeleted workflows
|
||||
registerWorkflow = wf.Enabled && wf.DeletedAt == nil
|
||||
)
|
||||
|
||||
// convert only registrable and workflows without issues
|
||||
if registerWorkflow && len(wf.Issues) == 0 {
|
||||
// Convert workflow only when valid (no issues, enable, not delete)
|
||||
if g, issues = Convert(svc.workflow, wf); len(issues) > 0 {
|
||||
wfLog.Error("failed to convert workflow to graph", zap.Error(issues))
|
||||
_ = issues.Walk(func(i *types.WorkflowIssue) error {
|
||||
wfLog.Debug("workflow issue found: "+i.Description, zap.Any("culprit", i.Culprit))
|
||||
return nil
|
||||
})
|
||||
g = nil
|
||||
}
|
||||
}
|
||||
|
||||
defer svc.mux.Unlock()
|
||||
svc.mux.Lock()
|
||||
|
||||
for _, t := range tt {
|
||||
log := wfLog.With(logger.Uint64("triggerID", t.ID))
|
||||
|
||||
// always unregister
|
||||
if svc.reg[wf.ID] == nil {
|
||||
svc.reg[wf.ID] = make(map[uint64]uintptr)
|
||||
} else if ptr := svc.reg[wf.ID][t.ID]; ptr != 0 {
|
||||
// unregister handlers for this trigger if they exist
|
||||
svc.eventbus.Unregister(ptr)
|
||||
}
|
||||
|
||||
// do not register disabled or deleted triggers
|
||||
if !registerWorkflow || !t.Enabled || t.DeletedAt != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
cnstr eventbus.ConstraintMatcher
|
||||
ops = make([]eventbus.HandlerRegOp, 0, len(t.Constraints)+2)
|
||||
)
|
||||
|
||||
if g == nil {
|
||||
handlerFn = func(_ context.Context, ev eventbus.Event) error {
|
||||
return errors.Internal(
|
||||
"trigger %s on %s failed due to invalid workflow %d: %s",
|
||||
ev.EventType(),
|
||||
ev.ResourceType(),
|
||||
wf.ID,
|
||||
wf.Issues,
|
||||
).Wrap(wf.Issues)
|
||||
}
|
||||
} else {
|
||||
handlerFn = makeWorkflowHandler(svc.workflow, wf, t)
|
||||
}
|
||||
|
||||
ops = append(
|
||||
ops,
|
||||
eventbus.On(t.EventType),
|
||||
eventbus.For(t.ResourceType),
|
||||
)
|
||||
|
||||
for _, c := range t.Constraints {
|
||||
if cnstr, err = eventbus.ConstraintMaker(c.Name, c.Op, c.Values...); err != nil {
|
||||
log.Debug(
|
||||
"failed to make constraint for workflow trigger",
|
||||
zap.Any("constraint", c),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
ops = append(ops, eventbus.Constraint(cnstr))
|
||||
}
|
||||
}
|
||||
|
||||
svc.reg[wf.ID][t.ID] = svc.eventbus.Register(handlerFn, ops...)
|
||||
|
||||
log.Debug("trigger registered",
|
||||
zap.String("eventType", t.EventType),
|
||||
zap.String("resourceType", t.ResourceType),
|
||||
zap.Any("constraints", t.Constraints),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *trigger) unregisterWorkflows(wwf ...*types.Workflow) {
|
||||
defer svc.mux.Unlock()
|
||||
svc.mux.Lock()
|
||||
|
||||
for _, wf := range wwf {
|
||||
for triggerID, ptr := range svc.reg[wf.ID] {
|
||||
svc.eventbus.Unregister(ptr)
|
||||
svc.log.Debug("trigger unregistered", logger.Uint64("triggerID", triggerID), logger.Uint64("workflowID", wf.ID))
|
||||
delete(svc.triggers, wf.ID)
|
||||
}
|
||||
|
||||
delete(svc.reg, wf.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *trigger) unregisterTriggers(tt ...*types.Trigger) {
|
||||
defer svc.mux.Unlock()
|
||||
svc.mux.Lock()
|
||||
|
||||
for _, t := range tt {
|
||||
if svc.reg[t.WorkflowID] == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ptr, has := svc.reg[t.WorkflowID][t.ID]; has {
|
||||
svc.eventbus.Unregister(ptr)
|
||||
svc.log.Debug("trigger unregistered", logger.Uint64("triggerID", t.ID), logger.Uint64("workflowID", t.WorkflowID))
|
||||
delete(svc.triggers, t.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadTrigger(ctx context.Context, s store.Storer, triggerID uint64) (res *types.Trigger, err error) {
|
||||
if triggerID == 0 {
|
||||
return nil, TriggerErrInvalidID()
|
||||
}
|
||||
|
||||
if res, err = store.LookupAutomationTriggerByID(ctx, s, triggerID); errors.IsNotFound(err) {
|
||||
return nil, TriggerErrNotFound()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func loadWorkflowTriggers(ctx context.Context, s store.Storer, workflowID uint64) (tt types.TriggerSet, err error) {
|
||||
if workflowID == 0 {
|
||||
return nil, TriggerErrInvalidID()
|
||||
}
|
||||
|
||||
if tt, _, err = store.SearchAutomationTriggers(ctx, s, types.TriggerFilter{WorkflowID: id.Strings(workflowID)}); errors.IsNotFound(err) {
|
||||
return nil, TriggerErrNotFound()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// toLabeledTriggers converts to []label.LabeledResource
|
||||
func toLabeledTriggers(set []*types.Trigger) []label.LabeledResource {
|
||||
if len(set) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ll := make([]label.LabeledResource, len(set))
|
||||
for i := range set {
|
||||
ll[i] = set[i]
|
||||
}
|
||||
|
||||
return ll
|
||||
}
|
||||
|
||||
// Checks if triggers are compatible with the workflow
|
||||
//
|
||||
// It ignores disabled triggers and does not care if triggers are in fact bond to the
|
||||
// given workflow
|
||||
func validateWorkflowTriggers(wf *types.Workflow, tt ...*types.Trigger) (wis types.WorkflowIssueSet) {
|
||||
var (
|
||||
// @todo find a better way how to flag events type that require
|
||||
// run-as param to be set
|
||||
// Possible solution: flag in definition that generates static
|
||||
// list w/o the need of cross-component imports
|
||||
requireRunAs = []eventbus.Event{
|
||||
sysEvent.SinkOnRequest(nil, nil),
|
||||
sysEvent.QueueOnMessage(nil),
|
||||
sysEvent.SystemOnInterval(),
|
||||
sysEvent.SystemOnTimestamp(),
|
||||
cmpEvent.ComposeOnInterval(),
|
||||
cmpEvent.ComposeOnTimestamp(),
|
||||
}
|
||||
)
|
||||
|
||||
for i, t := range tt {
|
||||
if !t.Enabled || t.DeletedAt != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ev := range requireRunAs {
|
||||
if t.ResourceType == ev.ResourceType() && t.EventType == ev.EventType() {
|
||||
if wf.RunAs == 0 {
|
||||
wis = wis.Append(
|
||||
errors.InvalidData("%s for %s requires run-as to be set", ev.ResourceType(), ev.EventType()),
|
||||
map[string]int{"trigger": i},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if wf.Meta != nil && wf.Meta.SubWorkflow {
|
||||
wis = wis.Append(
|
||||
errors.InvalidData("workflow marked as sub-workflow cannot have enabled triggers"),
|
||||
map[string]int{"trigger": i},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
706
server/automation/service/trigger_actions.gen.go
generated
Normal file
706
server/automation/service/trigger_actions.gen.go
generated
Normal file
@@ -0,0 +1,706 @@
|
||||
package service
|
||||
|
||||
// This file is auto-generated.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
//
|
||||
// Definitions file that controls how this file is generated:
|
||||
// automation/service/trigger_actions.yaml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
triggerActionProps struct {
|
||||
trigger *types.Trigger
|
||||
new *types.Trigger
|
||||
update *types.Trigger
|
||||
filter *types.TriggerFilter
|
||||
}
|
||||
|
||||
triggerAction struct {
|
||||
timestamp time.Time
|
||||
resource string
|
||||
action string
|
||||
log string
|
||||
severity actionlog.Severity
|
||||
|
||||
// prefix for error when action fails
|
||||
errorMessage string
|
||||
|
||||
props *triggerActionProps
|
||||
}
|
||||
|
||||
triggerLogMetaKey struct{}
|
||||
triggerPropsMetaKey struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
// just a placeholder to cover template cases w/o fmt package use
|
||||
_ = fmt.Println
|
||||
)
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Props methods
|
||||
// setTrigger updates triggerActionProps's trigger
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *triggerActionProps) setTrigger(trigger *types.Trigger) *triggerActionProps {
|
||||
p.trigger = trigger
|
||||
return p
|
||||
}
|
||||
|
||||
// setNew updates triggerActionProps's new
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *triggerActionProps) setNew(new *types.Trigger) *triggerActionProps {
|
||||
p.new = new
|
||||
return p
|
||||
}
|
||||
|
||||
// setUpdate updates triggerActionProps's update
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *triggerActionProps) setUpdate(update *types.Trigger) *triggerActionProps {
|
||||
p.update = update
|
||||
return p
|
||||
}
|
||||
|
||||
// setFilter updates triggerActionProps's filter
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p *triggerActionProps) setFilter(filter *types.TriggerFilter) *triggerActionProps {
|
||||
p.filter = filter
|
||||
return p
|
||||
}
|
||||
|
||||
// Serialize converts triggerActionProps to actionlog.Meta
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p triggerActionProps) Serialize() actionlog.Meta {
|
||||
var (
|
||||
m = make(actionlog.Meta)
|
||||
)
|
||||
|
||||
if p.trigger != nil {
|
||||
m.Set("trigger.ID", p.trigger.ID, true)
|
||||
}
|
||||
if p.new != nil {
|
||||
m.Set("new.ID", p.new.ID, true)
|
||||
}
|
||||
if p.update != nil {
|
||||
m.Set("update.ID", p.update.ID, true)
|
||||
}
|
||||
if p.filter != nil {
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// tr translates string and replaces meta value placeholder with values
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (p triggerActionProps) Format(in string, err error) string {
|
||||
var (
|
||||
pairs = []string{"{{err}}"}
|
||||
// first non-empty string
|
||||
fns = func(ii ...interface{}) string {
|
||||
for _, i := range ii {
|
||||
if s := fmt.Sprintf("%v", i); len(s) > 0 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
pairs = append(pairs, err.Error())
|
||||
} else {
|
||||
pairs = append(pairs, "nil")
|
||||
}
|
||||
|
||||
if p.trigger != nil {
|
||||
// replacement for "{{trigger}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{trigger}}",
|
||||
fns(
|
||||
p.trigger.ID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{trigger.ID}}", fns(p.trigger.ID))
|
||||
}
|
||||
|
||||
if p.new != nil {
|
||||
// replacement for "{{new}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{new}}",
|
||||
fns(
|
||||
p.new.ID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{new.ID}}", fns(p.new.ID))
|
||||
}
|
||||
|
||||
if p.update != nil {
|
||||
// replacement for "{{update}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{update}}",
|
||||
fns(
|
||||
p.update.ID,
|
||||
),
|
||||
)
|
||||
pairs = append(pairs, "{{update.ID}}", fns(p.update.ID))
|
||||
}
|
||||
|
||||
if p.filter != nil {
|
||||
// replacement for "{{filter}}" (in order how fields are defined)
|
||||
pairs = append(
|
||||
pairs,
|
||||
"{{filter}}",
|
||||
fns(),
|
||||
)
|
||||
}
|
||||
return strings.NewReplacer(pairs...).Replace(in)
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action methods
|
||||
|
||||
// String returns loggable description as string
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (a *triggerAction) String() string {
|
||||
var props = &triggerActionProps{}
|
||||
|
||||
if a.props != nil {
|
||||
props = a.props
|
||||
}
|
||||
|
||||
return props.Format(a.log, nil)
|
||||
}
|
||||
|
||||
func (e *triggerAction) ToAction() *actionlog.Action {
|
||||
return &actionlog.Action{
|
||||
Resource: e.resource,
|
||||
Action: e.action,
|
||||
Severity: e.severity,
|
||||
Description: e.String(),
|
||||
Meta: e.props.Serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Action constructors
|
||||
|
||||
// TriggerActionSearch returns "automation:trigger.search" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerActionSearch(props ...*triggerActionProps) *triggerAction {
|
||||
a := &triggerAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:trigger",
|
||||
action: "search",
|
||||
log: "searched for matching triggers",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// TriggerActionLookup returns "automation:trigger.lookup" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerActionLookup(props ...*triggerActionProps) *triggerAction {
|
||||
a := &triggerAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:trigger",
|
||||
action: "lookup",
|
||||
log: "looked-up for a {{trigger}}",
|
||||
severity: actionlog.Info,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// TriggerActionCreate returns "automation:trigger.create" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerActionCreate(props ...*triggerActionProps) *triggerAction {
|
||||
a := &triggerAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:trigger",
|
||||
action: "create",
|
||||
log: "created {{trigger}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// TriggerActionUpdate returns "automation:trigger.update" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerActionUpdate(props ...*triggerActionProps) *triggerAction {
|
||||
a := &triggerAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:trigger",
|
||||
action: "update",
|
||||
log: "updated {{trigger}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// TriggerActionDelete returns "automation:trigger.delete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerActionDelete(props ...*triggerActionProps) *triggerAction {
|
||||
a := &triggerAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:trigger",
|
||||
action: "delete",
|
||||
log: "deleted {{trigger}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// TriggerActionUndelete returns "automation:trigger.undelete" action
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerActionUndelete(props ...*triggerActionProps) *triggerAction {
|
||||
a := &triggerAction{
|
||||
timestamp: time.Now(),
|
||||
resource: "automation:trigger",
|
||||
action: "undelete",
|
||||
log: "undeleted {{trigger}}",
|
||||
severity: actionlog.Notice,
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
a.props = props[0]
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
// Error constructors
|
||||
|
||||
// TriggerErrGeneric returns "automation:trigger.generic" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerErrGeneric(mm ...*triggerActionProps) *errors.Error {
|
||||
var p = &triggerActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("failed to complete request due to internal error", nil),
|
||||
|
||||
errors.Meta("type", "generic"),
|
||||
errors.Meta("resource", "automation:trigger"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(triggerLogMetaKey{}, "{err}"),
|
||||
errors.Meta(triggerPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.generic"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// TriggerErrNotFound returns "automation:trigger.notFound" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerErrNotFound(mm ...*triggerActionProps) *errors.Error {
|
||||
var p = &triggerActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("trigger not found", nil),
|
||||
|
||||
errors.Meta("type", "notFound"),
|
||||
errors.Meta("resource", "automation:trigger"),
|
||||
|
||||
errors.Meta(triggerPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notFound"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// TriggerErrInvalidID returns "automation:trigger.invalidID" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerErrInvalidID(mm ...*triggerActionProps) *errors.Error {
|
||||
var p = &triggerActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("invalid ID", nil),
|
||||
|
||||
errors.Meta("type", "invalidID"),
|
||||
errors.Meta("resource", "automation:trigger"),
|
||||
|
||||
errors.Meta(triggerPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.invalidID"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// TriggerErrStaleData returns "automation:trigger.staleData" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerErrStaleData(mm ...*triggerActionProps) *errors.Error {
|
||||
var p = &triggerActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("trigger was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version", nil),
|
||||
|
||||
errors.Meta("type", "staleData"),
|
||||
errors.Meta("resource", "automation:trigger"),
|
||||
|
||||
errors.Meta(triggerPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.staleData"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// TriggerErrNotAllowedToRead returns "automation:trigger.notAllowedToRead" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerErrNotAllowedToRead(mm ...*triggerActionProps) *errors.Error {
|
||||
var p = &triggerActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to read this trigger", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToRead"),
|
||||
errors.Meta("resource", "automation:trigger"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(triggerLogMetaKey{}, "failed to read {{trigger.ID}}; insufficient permissions"),
|
||||
errors.Meta(triggerPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notAllowedToRead"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// TriggerErrNotAllowedToSearch returns "automation:trigger.notAllowedToSearch" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerErrNotAllowedToSearch(mm ...*triggerActionProps) *errors.Error {
|
||||
var p = &triggerActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to search or list triggers", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToSearch"),
|
||||
errors.Meta("resource", "automation:trigger"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(triggerLogMetaKey{}, "failed to search or list trigger; insufficient permissions"),
|
||||
errors.Meta(triggerPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notAllowedToSearch"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// TriggerErrNotAllowedToCreate returns "automation:trigger.notAllowedToCreate" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerErrNotAllowedToCreate(mm ...*triggerActionProps) *errors.Error {
|
||||
var p = &triggerActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to create triggers", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToCreate"),
|
||||
errors.Meta("resource", "automation:trigger"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(triggerLogMetaKey{}, "failed to create trigger; insufficient permissions"),
|
||||
errors.Meta(triggerPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notAllowedToCreate"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// TriggerErrNotAllowedToUpdate returns "automation:trigger.notAllowedToUpdate" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerErrNotAllowedToUpdate(mm ...*triggerActionProps) *errors.Error {
|
||||
var p = &triggerActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to update this trigger", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToUpdate"),
|
||||
errors.Meta("resource", "automation:trigger"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(triggerLogMetaKey{}, "failed to update {{trigger.ID}}; insufficient permissions"),
|
||||
errors.Meta(triggerPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notAllowedToUpdate"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// TriggerErrNotAllowedToDelete returns "automation:trigger.notAllowedToDelete" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerErrNotAllowedToDelete(mm ...*triggerActionProps) *errors.Error {
|
||||
var p = &triggerActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to delete this trigger", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToDelete"),
|
||||
errors.Meta("resource", "automation:trigger"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(triggerLogMetaKey{}, "failed to delete {{trigger.ID}}; insufficient permissions"),
|
||||
errors.Meta(triggerPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notAllowedToDelete"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// TriggerErrNotAllowedToUndelete returns "automation:trigger.notAllowedToUndelete" as *errors.Error
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func TriggerErrNotAllowedToUndelete(mm ...*triggerActionProps) *errors.Error {
|
||||
var p = &triggerActionProps{}
|
||||
if len(mm) > 0 {
|
||||
p = mm[0]
|
||||
}
|
||||
|
||||
var e = errors.New(
|
||||
errors.KindInternal,
|
||||
|
||||
p.Format("not allowed to undelete this trigger", nil),
|
||||
|
||||
errors.Meta("type", "notAllowedToUndelete"),
|
||||
errors.Meta("resource", "automation:trigger"),
|
||||
|
||||
// action log entry; no formatting, it will be applied inside recordAction fn.
|
||||
errors.Meta(triggerLogMetaKey{}, "failed to undelete {{trigger.ID}}; insufficient permissions"),
|
||||
errors.Meta(triggerPropsMetaKey{}, p),
|
||||
|
||||
// translation namespace & key
|
||||
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
|
||||
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notAllowedToUndelete"),
|
||||
|
||||
errors.StackSkip(1),
|
||||
)
|
||||
|
||||
if len(mm) > 0 {
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
// recordAction is a service helper function wraps function that can return error
|
||||
//
|
||||
// It will wrap unrecognized/internal errors with generic errors.
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (svc trigger) recordAction(ctx context.Context, props *triggerActionProps, actionFn func(...*triggerActionProps) *triggerAction, err error) error {
|
||||
if svc.actionlog == nil || actionFn == nil {
|
||||
// action log disabled or no action fn passed, return error as-is
|
||||
return err
|
||||
} else if err == nil {
|
||||
// action completed w/o error, record it
|
||||
svc.actionlog.Record(ctx, actionFn(props).ToAction())
|
||||
return nil
|
||||
}
|
||||
|
||||
a := actionFn(props).ToAction()
|
||||
|
||||
// Extracting error information and recording it as action
|
||||
a.Error = err.Error()
|
||||
|
||||
switch c := err.(type) {
|
||||
case *errors.Error:
|
||||
m := c.Meta()
|
||||
|
||||
a.Error = err.Error()
|
||||
a.Severity = actionlog.Severity(m.AsInt("severity"))
|
||||
a.Description = props.Format(m.AsString(triggerLogMetaKey{}), err)
|
||||
|
||||
if p, has := m[triggerPropsMetaKey{}]; has {
|
||||
a.Meta = p.(*triggerActionProps).Serialize()
|
||||
}
|
||||
|
||||
svc.actionlog.Record(ctx, a)
|
||||
default:
|
||||
svc.actionlog.Record(ctx, a)
|
||||
}
|
||||
|
||||
// Original error is passed on
|
||||
return err
|
||||
}
|
||||
84
server/automation/service/trigger_actions.yaml
Normal file
84
server/automation/service/trigger_actions.yaml
Normal file
@@ -0,0 +1,84 @@
|
||||
# List of loggable service actions
|
||||
|
||||
resource: automation:trigger
|
||||
service: trigger
|
||||
|
||||
import:
|
||||
- github.com/cortezaproject/corteza/server/automation/types
|
||||
|
||||
# Default sensitivity for actions
|
||||
defaultActionSeverity: notice
|
||||
|
||||
# default severity for errors
|
||||
defaultErrorSeverity: error
|
||||
|
||||
|
||||
props:
|
||||
- name: trigger
|
||||
type: "*types.Trigger"
|
||||
fields: [ ID ]
|
||||
- name: new
|
||||
type: "*types.Trigger"
|
||||
fields: [ ID ]
|
||||
- name: update
|
||||
type: "*types.Trigger"
|
||||
fields: [ ID ]
|
||||
- name: filter
|
||||
type: "*types.TriggerFilter"
|
||||
|
||||
|
||||
actions:
|
||||
- action: search
|
||||
log: "searched for matching triggers"
|
||||
severity: info
|
||||
|
||||
- action: lookup
|
||||
log: "looked-up for a {{trigger}}"
|
||||
severity: info
|
||||
|
||||
- action: create
|
||||
log: "created {{trigger}}"
|
||||
|
||||
- action: update
|
||||
log: "updated {{trigger}}"
|
||||
|
||||
- action: delete
|
||||
log: "deleted {{trigger}}"
|
||||
|
||||
- action: undelete
|
||||
log: "undeleted {{trigger}}"
|
||||
|
||||
errors:
|
||||
- error: notFound
|
||||
message: "trigger not found"
|
||||
|
||||
- error: invalidID
|
||||
message: "invalid ID"
|
||||
|
||||
- error: staleData
|
||||
message: "trigger was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version"
|
||||
severity: warning
|
||||
|
||||
- error: notAllowedToRead
|
||||
message: "not allowed to read this trigger"
|
||||
log: "failed to read {{trigger.ID}}; insufficient permissions"
|
||||
|
||||
- error: notAllowedToSearch
|
||||
message: "not allowed to search or list triggers"
|
||||
log: "failed to search or list trigger; insufficient permissions"
|
||||
|
||||
- error: notAllowedToCreate
|
||||
message: "not allowed to create triggers"
|
||||
log: "failed to create trigger; insufficient permissions"
|
||||
|
||||
- error: notAllowedToUpdate
|
||||
message: "not allowed to update this trigger"
|
||||
log: "failed to update {{trigger.ID}}; insufficient permissions"
|
||||
|
||||
- error: notAllowedToDelete
|
||||
message: "not allowed to delete this trigger"
|
||||
log: "failed to delete {{trigger.ID}}; insufficient permissions"
|
||||
|
||||
- error: notAllowedToUndelete
|
||||
message: "not allowed to undelete this trigger"
|
||||
log: "failed to undelete {{trigger.ID}}; insufficient permissions"
|
||||
52
server/automation/service/trigger_test.go
Normal file
52
server/automation/service/trigger_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
sysEvent "github.com/cortezaproject/corteza/server/system/service/event"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateWorkflowTriggersEmpty(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
|
||||
issues = validateWorkflowTriggers(
|
||||
&types.Workflow{},
|
||||
)
|
||||
)
|
||||
|
||||
req.Empty(issues)
|
||||
}
|
||||
func TestValidateWorkflowTriggersRunAs(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
soi = sysEvent.SystemOnInterval()
|
||||
|
||||
issues = validateWorkflowTriggers(
|
||||
&types.Workflow{},
|
||||
&types.Trigger{
|
||||
Enabled: true,
|
||||
ResourceType: soi.ResourceType(),
|
||||
EventType: soi.EventType(),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
req.Len(issues, 1)
|
||||
req.Contains(issues[0].String(), "requires run-as to be set")
|
||||
}
|
||||
|
||||
func TestValidateWorkflowTriggersSubWorkflow(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
|
||||
issues = validateWorkflowTriggers(
|
||||
&types.Workflow{Meta: &types.WorkflowMeta{SubWorkflow: true}},
|
||||
&types.Trigger{Enabled: true},
|
||||
)
|
||||
)
|
||||
|
||||
req.Len(issues, 1)
|
||||
req.Contains(issues[0].String(), "marked as sub-workflow")
|
||||
}
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
1033
server/automation/service/workflow_actions.gen.go
generated
Normal file
1033
server/automation/service/workflow_actions.gen.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
120
server/automation/service/workflow_actions.yaml
Normal file
120
server/automation/service/workflow_actions.yaml
Normal file
@@ -0,0 +1,120 @@
|
||||
# List of loggable service actions
|
||||
|
||||
resource: automation:workflow
|
||||
service: workflow
|
||||
|
||||
import:
|
||||
- github.com/cortezaproject/corteza/server/automation/types
|
||||
|
||||
# Default sensitivity for actions
|
||||
defaultActionSeverity: notice
|
||||
|
||||
# default severity for errors
|
||||
defaultErrorSeverity: error
|
||||
|
||||
|
||||
props:
|
||||
- name: workflow
|
||||
type: "*types.Workflow"
|
||||
fields: [ handle, ID ]
|
||||
- name: new
|
||||
type: "*types.Workflow"
|
||||
fields: [ handle, ID ]
|
||||
- name: update
|
||||
type: "*types.Workflow"
|
||||
fields: [ handle, ID ]
|
||||
- name: trigger
|
||||
type: "*types.Trigger"
|
||||
fields: [ eventType, resourceType, ID, stepID, ]
|
||||
- name: filter
|
||||
type: "*types.WorkflowFilter"
|
||||
|
||||
|
||||
actions:
|
||||
- action: search
|
||||
log: "searched for matching workflows"
|
||||
severity: info
|
||||
|
||||
- action: lookup
|
||||
log: "looked-up for a {{workflow}}"
|
||||
severity: info
|
||||
|
||||
- action: create
|
||||
log: "created {{workflow}}"
|
||||
|
||||
- action: update
|
||||
log: "updated {{workflow}}"
|
||||
|
||||
- action: delete
|
||||
log: "deleted {{workflow}}"
|
||||
|
||||
- action: undelete
|
||||
log: "undeleted {{workflow}}"
|
||||
|
||||
- action: execute
|
||||
# NOTE: only explicitly triggered workflow execution is logged
|
||||
log: "{{workflow}} executed"
|
||||
|
||||
errors:
|
||||
- error: notFound
|
||||
message: "workflow not found"
|
||||
|
||||
- error: invalidID
|
||||
message: "invalid ID"
|
||||
|
||||
- error: disabled
|
||||
message: "disabled workflow or trigger"
|
||||
|
||||
- error: invalidHandle
|
||||
message: "invalid handle"
|
||||
|
||||
- error: missingName
|
||||
message: "missing name"
|
||||
|
||||
- error: staleData
|
||||
message: "workflow was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version"
|
||||
severity: warning
|
||||
|
||||
- error: notAllowedToRead
|
||||
message: "not allowed to read this workflow"
|
||||
log: "failed to read {{workflow.handle}}; insufficient permissions"
|
||||
|
||||
- error: notAllowedToSearch
|
||||
message: "not allowed to search or list workflows"
|
||||
log: "failed to search or list workflow; insufficient permissions"
|
||||
|
||||
- error: notAllowedToCreate
|
||||
message: "not allowed to create workflows"
|
||||
log: "failed to create workflow; insufficient permissions"
|
||||
|
||||
- error: notAllowedToUpdate
|
||||
message: "not allowed to update this workflow"
|
||||
log: "failed to update {{workflow}}; insufficient permissions"
|
||||
|
||||
- error: notAllowedToDelete
|
||||
message: "not allowed to delete this workflow"
|
||||
log: "failed to delete {{workflow}}; insufficient permissions"
|
||||
|
||||
- error: notAllowedToUndelete
|
||||
message: "not allowed to undelete this workflow"
|
||||
log: "failed to undelete {{workflow}}; insufficient permissions"
|
||||
|
||||
- error: notAllowedToExecute
|
||||
message: "not allowed to execute this workflow"
|
||||
log: "failed to execute {{workflow}}; insufficient permissions"
|
||||
|
||||
- error: unknownWorkflowStep
|
||||
message: "unknown workflow step"
|
||||
log: "failed to execute {{workflow}}; unknown workflow step"
|
||||
|
||||
- error: handleNotUnique
|
||||
message: "workflow handle not unique"
|
||||
log: "duplicate handle used for workflow ({{workflow}})"
|
||||
|
||||
- error: notAllowedToExecuteCorredorStep
|
||||
message: "not allowed to run corredorExec function, corredor is disabled"
|
||||
log: "failed to execute {{workflow}} with corredorExec function step; corredor is disabled"
|
||||
|
||||
- error: maximumCallStackSizeExceeded
|
||||
message: "maximum call stack size exceeded"
|
||||
log: "maximum call stack size exceeded"
|
||||
943
server/automation/service/workflow_converter.go
Normal file
943
server/automation/service/workflow_converter.go
Normal file
@@ -0,0 +1,943 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza/server/pkg/expr"
|
||||
"github.com/cortezaproject/corteza/server/pkg/wfexec"
|
||||
"github.com/cortezaproject/corteza/server/system/automation"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
wfExecutor interface {
|
||||
handleToID(string) uint64
|
||||
Exec(ctx context.Context, workflowID uint64, p types.WorkflowExecParams) (*expr.Vars, uint64, types.Stacktrace, error)
|
||||
}
|
||||
|
||||
workflowConverter struct {
|
||||
// workflow function registry
|
||||
reg *registry
|
||||
parser expr.Parsable
|
||||
log *zap.Logger
|
||||
|
||||
graphs wfExecutor
|
||||
}
|
||||
)
|
||||
|
||||
func Convert(wfService *workflow, wf *types.Workflow) (*wfexec.Graph, types.WorkflowIssueSet) {
|
||||
conv := &workflowConverter{
|
||||
reg: wfService.reg,
|
||||
parser: wfService.parser,
|
||||
log: wfService.log,
|
||||
graphs: wfService,
|
||||
}
|
||||
|
||||
return conv.makeGraph(wf)
|
||||
}
|
||||
|
||||
// Converts workflow definition to wf execution graph
|
||||
func (svc workflowConverter) makeGraph(def *types.Workflow) (*wfexec.Graph, types.WorkflowIssueSet) {
|
||||
var (
|
||||
g = wfexec.NewGraph()
|
||||
wfii = types.WorkflowIssueSet{}
|
||||
IDs = make(map[uint64]int)
|
||||
)
|
||||
|
||||
// Basic step verification
|
||||
for i, s := range def.Steps {
|
||||
if _, has := IDs[s.ID]; has {
|
||||
wfii = wfii.Append(
|
||||
fmt.Errorf("step ID not unique"),
|
||||
map[string]int{"step": i, "duplicate": IDs[s.ID]},
|
||||
)
|
||||
} else {
|
||||
IDs[s.ID] = i
|
||||
}
|
||||
}
|
||||
|
||||
// if we have one or more duplicated-id error we need to abort right away
|
||||
// because all further step positions in issue culprit will be invalid
|
||||
if len(wfii) > 0 {
|
||||
return nil, wfii
|
||||
}
|
||||
// remove all steps that should not go through
|
||||
// resolving procedure and should not be added to the graph
|
||||
ss, _ := def.Steps.Filter(func(step *types.WorkflowStep) (bool, error) {
|
||||
switch {
|
||||
case step.Kind == types.WorkflowStepKindVisual:
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
})
|
||||
|
||||
for g.Len() < len(ss) {
|
||||
progress := false
|
||||
|
||||
for _, step := range ss {
|
||||
if g.StepByID(step.ID) != nil {
|
||||
// resolved
|
||||
continue
|
||||
}
|
||||
|
||||
// Collect all incoming and outgoing paths
|
||||
inPaths := make([]*types.WorkflowPath, 0, 8)
|
||||
outPaths := make([]*types.WorkflowPath, 0, 8)
|
||||
for _, path := range def.Paths {
|
||||
if path.ChildID == step.ID {
|
||||
inPaths = append(inPaths, path)
|
||||
} else if path.ParentID == step.ID {
|
||||
outPaths = append(outPaths, path)
|
||||
}
|
||||
}
|
||||
|
||||
stepIssues := verifyStep(step, inPaths, outPaths)
|
||||
|
||||
if resolved, err := svc.workflowStepDefConv(g, def, step, inPaths, outPaths); err != nil {
|
||||
switch aux := err.(type) {
|
||||
case types.WorkflowIssueSet:
|
||||
stepIssues = append(stepIssues, aux...)
|
||||
continue
|
||||
case error:
|
||||
stepIssues = stepIssues.Append(err, nil)
|
||||
}
|
||||
} else if resolved {
|
||||
progress = true
|
||||
}
|
||||
|
||||
wfii = append(wfii, stepIssues.SetCulprit("step", IDs[step.ID])...)
|
||||
}
|
||||
|
||||
if !progress {
|
||||
// nothing resolved for 1 cycle;
|
||||
// report every step that is still waiting on a neighbour
|
||||
//
|
||||
// steps that are unresolved for their own reasons (unknown
|
||||
// function, missing argument, ...) already have their issues
|
||||
// collected above
|
||||
for _, step := range ss {
|
||||
if g.StepByID(step.ID) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
children []uint64
|
||||
parents []uint64
|
||||
)
|
||||
|
||||
for _, path := range def.Paths {
|
||||
if path.ParentID == step.ID && g.StepByID(path.ChildID) == nil {
|
||||
children = append(children, path.ChildID)
|
||||
}
|
||||
|
||||
if path.ChildID == step.ID && g.StepByID(path.ParentID) == nil {
|
||||
parents = append(parents, path.ParentID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(children) == 0 && len(parents) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
wfii = wfii.Append(fmt.Errorf(
|
||||
"failed to resolve dependencies for %s step %d, waiting for unresolved parent steps %v and child steps %v",
|
||||
step.Kind,
|
||||
step.ID,
|
||||
parents,
|
||||
children,
|
||||
), map[string]int{"step": IDs[step.ID]})
|
||||
}
|
||||
|
||||
if len(wfii) == 0 {
|
||||
// resolving stalled but nothing explains why
|
||||
wfii = wfii.Append(fmt.Errorf("failed to resolve workflow step dependencies"), map[string]int{})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// add parents that were still unresolved when a join gateway was created
|
||||
for _, step := range ss {
|
||||
if step.Kind != types.WorkflowStepKindGateway || step.Ref != "join" {
|
||||
continue
|
||||
}
|
||||
|
||||
resolved, is := g.StepByID(step.ID).(interface{ AddPath(wfexec.Step) })
|
||||
if !is {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, path := range def.Paths {
|
||||
if path.ChildID != step.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
if parent := g.StepByID(path.ParentID); parent != nil {
|
||||
resolved.AddPath(parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for pos, path := range def.Paths {
|
||||
if g.StepByID(path.ChildID) == nil {
|
||||
wfii = wfii.Append(fmt.Errorf("failed to resolve step with ID %d", path.ChildID), map[string]int{"path": pos})
|
||||
continue
|
||||
}
|
||||
|
||||
if g.StepByID(path.ParentID) == nil {
|
||||
wfii = wfii.Append(fmt.Errorf("failed to resolve step with ID %d", path.ParentID), map[string]int{"path": pos})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(wfii) > 0 {
|
||||
// pointless to fill the if there are errors
|
||||
continue
|
||||
}
|
||||
|
||||
g.AddParent(
|
||||
g.StepByID(path.ChildID),
|
||||
g.StepByID(path.ParentID),
|
||||
)
|
||||
}
|
||||
|
||||
if len(wfii) > 0 {
|
||||
return nil, wfii.Distinct()
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// converts all step definitions into workflow.Step instances
|
||||
//
|
||||
// if this func returns nil for step and error, assume unresolved dependencies
|
||||
func (svc workflowConverter) workflowStepDefConv(g *wfexec.Graph, def *types.Workflow, s *types.WorkflowStep, in, out []*types.WorkflowPath) (bool, error) {
|
||||
if err := svc.parseExpressions(s.Arguments...); err != nil {
|
||||
return false, errors.Internal("failed to parse step arguments expressions for %s: %s", s.Kind, err).Wrap(err)
|
||||
}
|
||||
|
||||
if err := svc.parseExpressions(s.Results...); err != nil {
|
||||
return false, errors.Internal("failed to parse step results expressions for %s: %s", s.Kind, err).Wrap(err)
|
||||
}
|
||||
|
||||
conv, err := func() (wfexec.Step, error) {
|
||||
switch s.Kind {
|
||||
case types.WorkflowStepKindVisual:
|
||||
return nil, nil
|
||||
|
||||
case types.WorkflowStepKindDebug:
|
||||
return svc.convDebugStep(s)
|
||||
|
||||
case types.WorkflowStepKindExpressions:
|
||||
return svc.convExpressionStep(s)
|
||||
|
||||
case types.WorkflowStepKindGateway:
|
||||
return svc.convGateway(g, s, in, out)
|
||||
|
||||
case types.WorkflowStepKindFunction, types.WorkflowStepKindIterator:
|
||||
return svc.convFunctionStep(g, s, out)
|
||||
|
||||
case types.WorkflowStepKindError:
|
||||
return svc.convErrorStep(s)
|
||||
|
||||
case types.WorkflowStepKindTermination:
|
||||
return svc.convTerminationStep()
|
||||
|
||||
case types.WorkflowStepKindPrompt:
|
||||
return svc.convPromptStep(s)
|
||||
|
||||
case types.WorkflowStepKindDelay:
|
||||
return svc.convDelayStep(s)
|
||||
|
||||
case types.WorkflowStepKindErrHandler:
|
||||
return svc.convErrorHandlerStep(g, s, out)
|
||||
|
||||
case types.WorkflowStepKindBreak:
|
||||
return svc.convBreakStep()
|
||||
|
||||
case types.WorkflowStepKindContinue:
|
||||
return svc.convContinueStep()
|
||||
|
||||
case types.WorkflowStepKindExecWorkflow:
|
||||
return svc.convExecWorkflowStep(def, s)
|
||||
|
||||
default:
|
||||
return nil, errors.Internal("unsupported step kind %q", s.Kind)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if conv != nil {
|
||||
conv.SetID(s.ID)
|
||||
g.AddStep(conv)
|
||||
return true, err
|
||||
} else {
|
||||
// signal caller that we were unable to
|
||||
// resolve definition at the moment
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (svc workflowConverter) convGateway(g *wfexec.Graph, s *types.WorkflowStep, in, out []*types.WorkflowPath) (wfexec.Step, error) {
|
||||
switch s.Ref {
|
||||
case "fork":
|
||||
return wfexec.ForkGateway(), nil
|
||||
|
||||
case "join":
|
||||
var (
|
||||
ss []wfexec.Step
|
||||
)
|
||||
// Collect parents that are already in the graph; the rest are added
|
||||
// once the whole graph is resolved.
|
||||
//
|
||||
// Waiting for all of them here deadlocks on an iterator with its exit
|
||||
// path leading to this gateway: the iterator can not be resolved
|
||||
// before the gateway is in the graph.
|
||||
for _, p := range in {
|
||||
if parent := g.StepByID(p.ParentID); parent != nil {
|
||||
ss = append(ss, parent)
|
||||
}
|
||||
}
|
||||
|
||||
return wfexec.JoinGateway(ss...), nil
|
||||
|
||||
case "incl", "excl":
|
||||
var (
|
||||
pp []*wfexec.GatewayPath
|
||||
)
|
||||
|
||||
for _, c := range out {
|
||||
child := g.StepByID(c.ChildID)
|
||||
if child == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(c.Expr) > 0 {
|
||||
if err := svc.parser.ParseEvaluators(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// wrapping with fn to make sure that we're dealing with the right wf path inside gw-path tester fn
|
||||
err := func(c types.WorkflowPath) error {
|
||||
p, err := wfexec.NewGatewayPath(child, func(ctx context.Context, scope *expr.Vars) (bool, error) {
|
||||
if len(c.Expr) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return c.Test(ctx, scope)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pp = append(pp, p)
|
||||
return nil
|
||||
}(*c)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if s.Ref == "excl" {
|
||||
return wfexec.ExclGateway(pp...)
|
||||
} else {
|
||||
return wfexec.InclGateway(pp...)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.Internal("unexpected workflow configuration")
|
||||
}
|
||||
|
||||
func (svc workflowConverter) convErrorHandlerStep(g *wfexec.Graph, s *types.WorkflowStep, out []*types.WorkflowPath) (wfexec.Step, error) {
|
||||
var (
|
||||
results = &expr.Vars{}
|
||||
)
|
||||
|
||||
// evaluate results and set target path for each expression
|
||||
for _, r := range s.Results {
|
||||
err := results.Set(r.Expr, r.Target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
switch len(out) {
|
||||
case 0:
|
||||
return nil, fmt.Errorf("expecting at least one path out of error handling step")
|
||||
case 1:
|
||||
// remove error handler
|
||||
return types.ErrorHandlerStep(nil, results), nil
|
||||
case 2:
|
||||
errorHandler := g.StepByID(out[1].ChildID)
|
||||
if errorHandler == nil {
|
||||
// wait for it to be resolved
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return types.ErrorHandlerStep(errorHandler, results), nil
|
||||
|
||||
default:
|
||||
// this might be extended in the future to allow different paths using expression
|
||||
// but then again, this can be solved by gateway path following the error handling step
|
||||
return nil, fmt.Errorf("max 2 paths out of error handling step")
|
||||
}
|
||||
}
|
||||
|
||||
func (svc workflowConverter) convExpressionStep(s *types.WorkflowStep) (wfexec.Step, error) {
|
||||
return types.ExpressionsStep(s.Arguments...), nil
|
||||
}
|
||||
|
||||
// internal debug step that can log entire
|
||||
func (svc workflowConverter) convDebugStep(_ *types.WorkflowStep) (wfexec.Step, error) {
|
||||
return types.DebugStep(svc.log), nil
|
||||
}
|
||||
|
||||
func (svc workflowConverter) convFunctionStep(g *wfexec.Graph, s *types.WorkflowStep, out []*types.WorkflowPath) (wfexec.Step, error) {
|
||||
reg := Registry()
|
||||
|
||||
if def := reg.Function(s.Ref); def == nil {
|
||||
return nil, errors.Internal("unknown function %q", s.Ref)
|
||||
} else {
|
||||
if def.Kind != string(s.Kind) {
|
||||
return nil, fmt.Errorf("unexpected %s on %s step", def.Kind, s.Kind)
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
isIterator = def.Kind == types.FunctionKindIterator
|
||||
)
|
||||
|
||||
if isIterator {
|
||||
if def.Iterator == nil {
|
||||
return nil, errors.Internal("iterator handler for %q not set", s.Ref)
|
||||
}
|
||||
} else {
|
||||
if def.Handler == nil {
|
||||
return nil, errors.Internal("function handler for %q not set", s.Ref)
|
||||
}
|
||||
}
|
||||
|
||||
if err = def.Parameters.VerifyArguments(s.Arguments); err != nil {
|
||||
return nil, errors.Internal("failed to verify argument expressions for %s %s: %s", s.Kind, s.Ref, err).Wrap(err)
|
||||
}
|
||||
|
||||
if err = def.Results.VerifyResults(s.Results); err != nil {
|
||||
return nil, errors.Internal("failed to verify result expressions for %s %s: %s", s.Kind, s.Ref, err).Wrap(err)
|
||||
}
|
||||
|
||||
if isIterator {
|
||||
if len(out) != 2 {
|
||||
return nil, fmt.Errorf("expecting exactly 2 outbound paths for iterator")
|
||||
}
|
||||
|
||||
var (
|
||||
next = g.StepByID(out[0].ChildID)
|
||||
exit = g.StepByID(out[1].ChildID)
|
||||
)
|
||||
|
||||
if next == nil || exit == nil {
|
||||
// wait for steps to be resolved
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return types.IteratorStep(def, s.Arguments, s.Results, next, exit)
|
||||
|
||||
} else {
|
||||
return types.FunctionStep(def, s.Arguments, s.Results)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// creates error step
|
||||
//
|
||||
// Expects ZERO outgoing paths and
|
||||
func (svc workflowConverter) convErrorStep(s *types.WorkflowStep) (wfexec.Step, error) {
|
||||
const (
|
||||
argName = "message"
|
||||
)
|
||||
|
||||
var (
|
||||
args = types.ExprSet(s.Arguments)
|
||||
)
|
||||
|
||||
return wfexec.NewGenericStep(func(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
|
||||
var (
|
||||
msg string
|
||||
result, err = args.Eval(ctx, r.Scope)
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.Has(argName) {
|
||||
str, _ := expr.NewString(expr.Must(result.Select(argName)))
|
||||
msg = str.GetValue()
|
||||
} else {
|
||||
if aux, is := args.GetByTarget(argName).Value.(string); is {
|
||||
msg = aux
|
||||
} else {
|
||||
msg = "ERROR"
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.Automation("%s", msg)
|
||||
}), nil
|
||||
}
|
||||
|
||||
// converts termination definition to wfexec.Step
|
||||
func (svc workflowConverter) convTerminationStep() (wfexec.Step, error) {
|
||||
return wfexec.NewGenericStep(func(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
|
||||
return wfexec.Termination(), nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
// converts prompt definition to wfexec.Step
|
||||
//
|
||||
// If input is not passed (session was not resumed from suspension after prompt), we send wait-for-input
|
||||
// result that signals session it needs to suspend and wait for the input.
|
||||
// At this point, we take the current scope run it through step arguments and store the output with the suspended state
|
||||
//
|
||||
// After session is resumed, input should be set (not nil) and evaluated through results.
|
||||
func (svc workflowConverter) convPromptStep(s *types.WorkflowStep) (wfexec.Step, error) {
|
||||
// Use expression step as base for prompt step
|
||||
var (
|
||||
args = types.ExprSet(s.Arguments)
|
||||
res = types.ExprSet(s.Results)
|
||||
)
|
||||
const (
|
||||
ownerArgName = "owner"
|
||||
)
|
||||
|
||||
return wfexec.NewGenericStep(func(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
|
||||
if r.Input == nil {
|
||||
// input is only set (not nil) when session is resumed on prompt step
|
||||
// suspend the session and wait for input
|
||||
|
||||
// @todo take scope, eval with arguments and add it to suspended state
|
||||
payload, err := args.Eval(ctx, r.Scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ownerId uint64 = 0
|
||||
if payload.Has(ownerArgName) {
|
||||
// @note for now only types.User and uint64 are supported.
|
||||
// At some point we should extend this to handles, emails, usernames, ...
|
||||
// but for now this should be fine
|
||||
|
||||
// @todo should we validate existence here?
|
||||
// I'm not sure how to bring in a thing to validate it so I omitted it for now.
|
||||
param := expr.Must(payload.Select(ownerArgName))
|
||||
switch casted := param.(type) {
|
||||
case *automation.User:
|
||||
ownerId = casted.GetValue().ID
|
||||
case *expr.ID:
|
||||
ownerId = casted.GetValue()
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot select owner: unsupported type provided %T", casted)
|
||||
}
|
||||
} else {
|
||||
if i := auth.GetIdentityFromContextWithKey(ctx, workflowInvokerCtxKey{}); i != nil {
|
||||
ownerId = i.Identity()
|
||||
}
|
||||
}
|
||||
|
||||
return wfexec.Prompt(ownerId, s.Ref, payload), nil
|
||||
}
|
||||
|
||||
results, err := res.Eval(ctx, r.Scope.MustMerge(r.Input))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
// converts delay definition to wfexec.Step
|
||||
func (svc workflowConverter) convDelayStep(s *types.WorkflowStep) (wfexec.Step, error) {
|
||||
return types.DelayStep(s.Arguments), nil
|
||||
}
|
||||
|
||||
func (svc workflowConverter) convBreakStep() (wfexec.Step, error) {
|
||||
return wfexec.NewGenericStep(func(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
|
||||
return wfexec.LoopBreak(), nil
|
||||
}), nil
|
||||
|
||||
}
|
||||
|
||||
func (svc workflowConverter) convContinueStep() (wfexec.Step, error) {
|
||||
return wfexec.NewGenericStep(func(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
|
||||
return wfexec.LoopContinue(), nil
|
||||
}), nil
|
||||
|
||||
}
|
||||
|
||||
// creates workflow-executor step
|
||||
//
|
||||
// Expects max ONE outgoing paths and ONE incoming path
|
||||
func (svc workflowConverter) convExecWorkflowStep(wf *types.Workflow, s *types.WorkflowStep) (_ wfexec.Step, err error) {
|
||||
const (
|
||||
// sub-workflow's scope
|
||||
// expecting expr.Vars
|
||||
argNameScope = "scope"
|
||||
|
||||
// workflow id or handle
|
||||
argNameWorkflow = "workflow"
|
||||
)
|
||||
|
||||
return wfexec.NewGenericStep(func(ctx context.Context, r *wfexec.ExecRequest) (resp wfexec.ExecResponse, err error) {
|
||||
var (
|
||||
p = types.WorkflowExecParams{
|
||||
// sub-workflows should not be executed asynchronously
|
||||
// might lead to uncontrolled resource consumption
|
||||
Async: false,
|
||||
|
||||
// @todo figure out how check if trace is enabled
|
||||
Trace: false,
|
||||
|
||||
// always wait for the sub-workflow even when deferred
|
||||
Wait: true,
|
||||
|
||||
Input: expr.EmptyVars(),
|
||||
|
||||
// who's calling sub-workflow?
|
||||
CallerSessionID: r.SessionID,
|
||||
CallerStepID: s.ID,
|
||||
CallerWorkflowID: wf.ID,
|
||||
}
|
||||
|
||||
result *expr.Vars
|
||||
|
||||
workflowID uint64
|
||||
workflowHandle string
|
||||
)
|
||||
|
||||
if ap, err := processArguments(ctx, s.Arguments, r.Scope); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if ap.string(argNameWorkflow, &workflowHandle) {
|
||||
if workflowID = svc.graphs.handleToID(workflowHandle); workflowID == 0 {
|
||||
return nil, errors.NotFound("workflow with handle %s not found", workflowHandle)
|
||||
}
|
||||
} else if !ap.uint64(argNameWorkflow, &workflowID) {
|
||||
return nil, errors.InvalidData("workflow ID must be provided")
|
||||
}
|
||||
|
||||
// let's make sure we're not calling ourselves
|
||||
if p.CallerWorkflowID == workflowID {
|
||||
return nil, errors.InvalidData("recursive workflow call is not allowed")
|
||||
}
|
||||
|
||||
// scope for the sub-workflow
|
||||
ap.vars(argNameScope, p.Input)
|
||||
}
|
||||
|
||||
result, _, _, err = svc.graphs.Exec(ctx, workflowID, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result, err = types.ExprSet(s.Results).Eval(ctx, result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (svc workflowConverter) parseExpressions(ee ...*types.Expr) (err error) {
|
||||
for _, e := range ee {
|
||||
|
||||
if len(strings.TrimSpace(e.Expr)) > 0 {
|
||||
if err = svc.parser.ParseEvaluators(e); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = e.SetType(exprTypeSetter(svc.reg, e)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range e.Tests {
|
||||
if err = svc.parser.ParseEvaluators(t); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyStep(s *types.WorkflowStep, in, out types.WorkflowPathSet) types.WorkflowIssueSet {
|
||||
const (
|
||||
arguments = "argument"
|
||||
results = "result"
|
||||
outbound = "outbound path"
|
||||
inbound = "inbound path"
|
||||
)
|
||||
|
||||
var (
|
||||
ii = types.WorkflowIssueSet{}
|
||||
|
||||
// checks if count of arguments, results, outbound or inbound paths is between (incl) min and max
|
||||
count = func(min, max int, typ string) func() error {
|
||||
return func() error {
|
||||
var (
|
||||
l int
|
||||
)
|
||||
|
||||
switch typ {
|
||||
case arguments:
|
||||
l = len(s.Arguments)
|
||||
case results:
|
||||
l = len(s.Results)
|
||||
case outbound:
|
||||
l = len(out)
|
||||
case inbound:
|
||||
l = len(in)
|
||||
}
|
||||
|
||||
switch {
|
||||
case max == 0 && min == max && l != min:
|
||||
return errors.Internal("%s step does not expect any %ss", s.Kind, typ)
|
||||
|
||||
case max > 0 && min == max && l != min:
|
||||
return errors.Internal("%s step expects exactly %d %s(s)", s.Kind, min, typ)
|
||||
|
||||
case l < min:
|
||||
return errors.Internal("%s step expects at least %d %s(s)", s.Kind, min, typ)
|
||||
|
||||
case max > 0 && l > max:
|
||||
return errors.Internal("%s step expects no more than %d %s(s)", s.Kind, max, typ)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if reference is set on the step
|
||||
requiredRef = func() error {
|
||||
if s.Ref == "" {
|
||||
return errors.Internal("%s step expects reference", s.Kind)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if reference is set on the step
|
||||
numericRef = func() error {
|
||||
// @todo SUBWF enable this back
|
||||
// if 0 == cast.ToUint64(s.Ref) {
|
||||
// return errors.Internal("%s step expects workflow reference ID", s.Kind)
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reference should not be set on the step
|
||||
noRef = func() error {
|
||||
if s.Ref != "" {
|
||||
return errors.Internal("%s step does not expects reference", s.Kind)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// check for corredor function step(s) are allowed or not
|
||||
checkDisabledFunc = func() error {
|
||||
if !DefaultWorkflow.corredorOpt.Enabled && s.Ref == "corredorExec" {
|
||||
return WorkflowErrNotAllowedToExecuteCorredorStep()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checks if argument is present
|
||||
checkArg = func(argName string, tt ...expr.Type) func() error {
|
||||
return func() error {
|
||||
msgArg := types.ExprSet(s.Arguments).GetByTarget(argName)
|
||||
if msgArg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, typ := range tt {
|
||||
if msgArg.Type == typ.Type() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Internal("unexpected type %q for argument %q on step type %q", msgArg.Type, argName, s.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
// checks if argument is present
|
||||
requiredArg = func(argName string, tt ...expr.Type) func() error {
|
||||
return func() error {
|
||||
if msgArg := types.ExprSet(s.Arguments).GetByTarget(argName); msgArg == nil {
|
||||
return errors.Internal("%s step expects to have '%s' argument", s.Kind, argName)
|
||||
}
|
||||
|
||||
return checkArg(argName, tt...)()
|
||||
}
|
||||
}
|
||||
|
||||
// should not have any arguments, results, outbound or inbound paths
|
||||
zero = func(typ string) func() error { return count(0, 0, typ) }
|
||||
|
||||
// step should not have any outbound paths
|
||||
last = func() error { return count(0, 0, outbound)() }
|
||||
|
||||
// gw sanity check
|
||||
gatewayCheck = func(checks ...func() error) []func() error {
|
||||
switch s.Ref {
|
||||
case "join":
|
||||
return append(checks, count(1, -1, inbound))
|
||||
|
||||
case "fork", "incl", "excl":
|
||||
return append(checks, count(1, -1, outbound))
|
||||
}
|
||||
|
||||
return append(checks, func() error { return fmt.Errorf("unknown gateway type") })
|
||||
}
|
||||
|
||||
checks = make([]func() error, 0)
|
||||
)
|
||||
|
||||
switch s.Kind {
|
||||
case types.WorkflowStepKindErrHandler:
|
||||
checks = append(checks,
|
||||
noRef,
|
||||
zero(arguments),
|
||||
count(1, 2, outbound),
|
||||
)
|
||||
|
||||
case types.WorkflowStepKindDebug:
|
||||
checks = append(checks,
|
||||
noRef,
|
||||
zero(results),
|
||||
count(0, 1, outbound),
|
||||
)
|
||||
|
||||
case types.WorkflowStepKindVisual:
|
||||
checks = append(checks,
|
||||
zero(results),
|
||||
zero(arguments),
|
||||
)
|
||||
|
||||
case types.WorkflowStepKindExpressions:
|
||||
checks = append(checks,
|
||||
noRef,
|
||||
zero(results),
|
||||
count(1, -1, arguments),
|
||||
count(0, 1, outbound),
|
||||
)
|
||||
|
||||
case types.WorkflowStepKindGateway:
|
||||
checks = append(checks, gatewayCheck(zero(arguments), zero(results))...)
|
||||
|
||||
case types.WorkflowStepKindError:
|
||||
checks = append(checks,
|
||||
requiredArg("message", expr.String{}),
|
||||
count(0, 1, arguments),
|
||||
zero(results),
|
||||
last,
|
||||
)
|
||||
|
||||
case types.WorkflowStepKindTermination:
|
||||
checks = append(checks,
|
||||
noRef,
|
||||
zero(arguments),
|
||||
zero(results),
|
||||
last,
|
||||
)
|
||||
|
||||
case types.WorkflowStepKindFunction:
|
||||
checks = append(checks,
|
||||
requiredRef,
|
||||
numericRef,
|
||||
checkDisabledFunc,
|
||||
count(0, 1, outbound),
|
||||
)
|
||||
|
||||
case types.WorkflowStepKindIterator:
|
||||
checks = append(checks,
|
||||
requiredRef,
|
||||
count(2, 2, outbound),
|
||||
)
|
||||
|
||||
case types.WorkflowStepKindPrompt:
|
||||
checks = append(checks,
|
||||
requiredRef,
|
||||
count(0, 1, outbound),
|
||||
)
|
||||
|
||||
case types.WorkflowStepKindDelay:
|
||||
checks = append(checks,
|
||||
checkArg("timestamp", expr.DateTime{}),
|
||||
checkArg("offset", expr.Duration{}),
|
||||
count(1, 1, arguments),
|
||||
zero(results),
|
||||
count(0, 1, outbound),
|
||||
)
|
||||
|
||||
case types.WorkflowStepKindBreak:
|
||||
checks = append(checks,
|
||||
noRef,
|
||||
zero(arguments),
|
||||
zero(results),
|
||||
count(0, 1, outbound),
|
||||
last,
|
||||
)
|
||||
|
||||
case types.WorkflowStepKindContinue:
|
||||
checks = append(checks,
|
||||
noRef,
|
||||
zero(arguments),
|
||||
zero(results),
|
||||
count(0, 1, outbound),
|
||||
last,
|
||||
)
|
||||
|
||||
case types.WorkflowStepKindExecWorkflow:
|
||||
checks = append(checks,
|
||||
noRef,
|
||||
// required "workflow" with handle or ID
|
||||
requiredArg("workflow", expr.Handle{}, expr.ID{}),
|
||||
|
||||
// optional "scope" as expr.Vars
|
||||
checkArg("scope", expr.Vars{}),
|
||||
|
||||
// expecting at least workflow + optional scope, nothing more
|
||||
count(1, 2, arguments),
|
||||
|
||||
count(0, 1, outbound),
|
||||
)
|
||||
|
||||
case "":
|
||||
return ii.Append(fmt.Errorf("missing step kind"), nil)
|
||||
|
||||
default:
|
||||
return ii.Append(fmt.Errorf("unknown step kind '%s'", s.Kind), nil)
|
||||
|
||||
}
|
||||
|
||||
for _, check := range checks {
|
||||
if err := check(); err != nil {
|
||||
ii = ii.Append(err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return ii
|
||||
}
|
||||
75
server/automation/service/workflow_step_args.go
Normal file
75
server/automation/service/workflow_step_args.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/expr"
|
||||
)
|
||||
|
||||
type (
|
||||
// argsProc is a helper to process arguments
|
||||
//
|
||||
// when initialised it evaluates expressions using given scope
|
||||
// it provides methods to extract values from the result
|
||||
argsProc struct {
|
||||
expr types.ExprSet
|
||||
result *expr.Vars
|
||||
}
|
||||
)
|
||||
|
||||
func processArguments(ctx context.Context, expr []*types.Expr, scope *expr.Vars) (p *argsProc, err error) {
|
||||
p = &argsProc{expr: expr}
|
||||
|
||||
if p.result, err = p.expr.Eval(ctx, scope); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *argsProc) bool(name string, val *bool) bool { return getStepArg(p, name, val) }
|
||||
func (p *argsProc) string(name string, val *string) bool { return getStepArg(p, name, val) }
|
||||
func (p *argsProc) uint64(name string, val *uint64) bool { return getStepArg(p, name, val) }
|
||||
|
||||
// vars handles assigning of expr.Vars
|
||||
//
|
||||
// not as straightforward as plain types
|
||||
func (p *argsProc) vars(name string, val *expr.Vars) bool {
|
||||
// make an auxiliary variable to hold the results q
|
||||
aux := make(map[string]expr.TypedValue)
|
||||
|
||||
// extract it
|
||||
if !getStepArg(p, name, &aux) {
|
||||
return false
|
||||
}
|
||||
|
||||
if val == nil {
|
||||
panic("initialize Vars before calling the function")
|
||||
}
|
||||
|
||||
vars, _ := expr.NewVars(aux)
|
||||
*val = *vars
|
||||
return true
|
||||
}
|
||||
|
||||
// getStepArg is a helper to extract step argument from result
|
||||
func getStepArg[T any](p *argsProc, arg string, val *T) bool {
|
||||
var (
|
||||
aux any
|
||||
)
|
||||
|
||||
if p.result.Has(arg) {
|
||||
aux = expr.Must(p.result.Select(arg)).Get()
|
||||
} else if exp := p.expr.GetByTarget(arg); exp != nil {
|
||||
aux = p.expr.GetByTarget(arg).Value.(T)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
if conv, is := aux.(T); is {
|
||||
*val = conv
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
83
server/automation/service/workflow_step_args_test.go
Normal file
83
server/automation/service/workflow_step_args_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cortezaproject/corteza/server/automation/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/expr"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProcessArguments(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
p, err = processArguments(context.Background(), []*types.Expr{}, nil)
|
||||
|
||||
tBool bool
|
||||
tStr string
|
||||
tUint uint64
|
||||
tVars *expr.Vars
|
||||
)
|
||||
|
||||
req.NoError(err)
|
||||
req.NotNil(p)
|
||||
|
||||
req.False(p.bool("not-there", &tBool))
|
||||
req.False(tBool)
|
||||
|
||||
req.False(p.string("not-there", &tStr))
|
||||
req.Empty(tStr)
|
||||
|
||||
req.False(p.uint64("not-there", &tUint))
|
||||
req.Zero(tUint)
|
||||
|
||||
req.False(p.vars("not-there", tVars))
|
||||
req.Zero(tVars)
|
||||
})
|
||||
|
||||
t.Run("expr", func(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
ee = []*types.Expr{
|
||||
types.NewTypedExpr("tBool", "true", &expr.Boolean{}),
|
||||
types.NewTypedExpr("tString", "\"foo\"", &expr.String{}),
|
||||
types.NewTypedExpr("tUint", "42", &expr.UnsignedInteger{}),
|
||||
types.NewTypedExpr("tVars", "{}", &expr.Vars{}),
|
||||
types.NewTypedExpr("tVars.foo", "\"bar\"", &expr.String{}),
|
||||
}
|
||||
)
|
||||
|
||||
req.NoError(expr.NewGvalParser().ParseEvaluators(func() []expr.Evaluator {
|
||||
oo := make([]expr.Evaluator, len(ee))
|
||||
for i, e := range ee {
|
||||
oo[i] = e
|
||||
}
|
||||
return oo
|
||||
}()...))
|
||||
|
||||
var (
|
||||
p, err = processArguments(context.Background(), ee, nil)
|
||||
|
||||
tBool bool
|
||||
tStr string
|
||||
tUint uint64
|
||||
tVars = &expr.Vars{}
|
||||
)
|
||||
|
||||
req.NoError(err)
|
||||
req.NotNil(p)
|
||||
|
||||
req.True(p.bool("tBool", &tBool))
|
||||
req.True(tBool)
|
||||
|
||||
req.True(p.string("tString", &tStr))
|
||||
req.Equal("foo", tStr)
|
||||
|
||||
req.True(p.uint64("tUint", &tUint))
|
||||
req.Equal(uint64(42), tUint)
|
||||
|
||||
req.True(p.vars("tVars", tVars))
|
||||
req.Equal(expr.Must(expr.NewVars(map[string]any{"foo": "bar"})), tVars)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user