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

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
package automation
// 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/automation/apigw_body_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/http"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"io"
)
var _ wfexec.ExecResponse
type (
apigwBodyHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h apigwBodyHandler) register() {
h.reg.AddFunctions(
h.Read(),
h.ReadFile(),
)
}
type (
apigwBodyReadArgs struct {
hasRequest bool
Request *http.Request
}
apigwBodyReadResults struct {
Body string
}
)
// Read function Read request body from integration gateway
//
// expects implementation of read function:
//
// func (h apigwBodyHandler) read(ctx context.Context, args *apigwBodyReadArgs) (results *apigwBodyReadResults, err error) {
// return
// }
func (h apigwBodyHandler) Read() *atypes.Function {
return &atypes.Function{
Ref: "apigwBodyRead",
Kind: "function",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Read request body from integration gateway",
},
Parameters: []*atypes.Param{
{
Name: "request",
Types: []string{"HttpRequest"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Request",
},
},
},
Results: []*atypes.Param{
{
Name: "body",
Types: []string{"String"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &apigwBodyReadArgs{
hasRequest: in.Has("request"),
}
)
if err = in.Decode(args); err != nil {
return
}
var results *apigwBodyReadResults
if results, err = h.read(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Body (string) to String
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("String").Cast(results.Body); err != nil {
return
} else if err = expr.Assign(out, "body", tval); err != nil {
return
}
}
return
},
}
}
type (
apigwBodyReadFileArgs struct {
hasRequest bool
Request *http.Request
hasName bool
Name string
}
apigwBodyReadFileResults struct {
File io.Reader
FileName string
Exists bool
}
)
// ReadFile function Read file from integration gateway
//
// expects implementation of readFile function:
//
// func (h apigwBodyHandler) readFile(ctx context.Context, args *apigwBodyReadFileArgs) (results *apigwBodyReadFileResults, err error) {
// return
// }
func (h apigwBodyHandler) ReadFile() *atypes.Function {
return &atypes.Function{
Ref: "apigwBodyReadFile",
Kind: "function",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Read file from integration gateway",
},
Parameters: []*atypes.Param{
{
Name: "request",
Types: []string{"HttpRequest"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Request",
},
},
{
Name: "name",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Name",
},
},
},
Results: []*atypes.Param{
{
Name: "file",
Types: []string{"Reader"},
},
{
Name: "fileName",
Types: []string{"String"},
},
{
Name: "exists",
Types: []string{"Boolean"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &apigwBodyReadFileArgs{
hasRequest: in.Has("request"),
hasName: in.Has("name"),
}
)
if err = in.Decode(args); err != nil {
return
}
var results *apigwBodyReadFileResults
if results, err = h.readFile(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.File (io.Reader) to Reader
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Reader").Cast(results.File); err != nil {
return
} else if err = expr.Assign(out, "file", tval); err != nil {
return
}
}
{
// converting results.FileName (string) to String
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("String").Cast(results.FileName); err != nil {
return
} else if err = expr.Assign(out, "fileName", tval); err != nil {
return
}
}
{
// converting results.Exists (bool) to Boolean
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Boolean").Cast(results.Exists); err != nil {
return
} else if err = expr.Assign(out, "exists", tval); err != nil {
return
}
}
return
},
}
}

View File

@@ -0,0 +1,67 @@
package automation
import (
"context"
"errors"
"io"
"mime/multipart"
"net/http"
"strings"
)
type (
apigwBodyHandler struct {
reg apigwBodyHandlerRegistry
}
)
func ApigwBodyHandler(reg queueHandlerRegistry) *apigwBodyHandler {
h := &apigwBodyHandler{
reg: reg,
}
h.register()
return h
}
func (h apigwBodyHandler) read(ctx context.Context, args *apigwBodyReadArgs) (res *apigwBodyReadResults, err error) {
res = &apigwBodyReadResults{}
bb, err := io.ReadAll(args.Request.Body)
if err != nil {
return
}
res.Body = string(bb)
return
}
func (h apigwBodyHandler) readFile(ctx context.Context, args *apigwBodyReadFileArgs) (res *apigwBodyReadFileResults, err error) {
// @note the contents are already parsed; will probably need to rework behind the scenes bits
res = &apigwBodyReadFileResults{
Exists: true,
}
var fh *multipart.FileHeader
res.File, fh, err = args.Request.FormFile(args.Name)
if err != nil {
// In case the file is missing or empty, do this
// @todo do we want to just check if missing?
if errors.Is(err, http.ErrMissingFile) || strings.Contains(err.Error(), "EOF") {
res.Exists = false
err = nil
} else {
return
}
}
if !res.Exists {
return
}
res.FileName = fh.Filename
return
}

View File

@@ -0,0 +1,40 @@
name: apigwBody
imports:
- github.com/cortezaproject/corteza/server/pkg/http
- io
functions:
read:
meta:
short: Read request body from integration gateway
params:
request:
meta: { label: Request }
required: true
types:
- { wf: HttpRequest, go: '*http.Request' }
results:
body:
wf: String
readFile:
meta:
short: Read file from integration gateway
params:
request:
meta: { label: Request }
required: true
types:
- { wf: HttpRequest, go: '*http.Request' }
name:
meta: { label: Name }
required: true
types:
- { wf: String }
results:
file:
wf: Reader
fileName:
wf: String
exists:
wf: Boolean

View File

@@ -0,0 +1,124 @@
package automation
// 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/automation/corredor_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
var _ wfexec.ExecResponse
type (
corredorHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h corredorHandler) register() {
h.reg.AddFunctions(
h.Exec(),
)
}
type (
corredorExecArgs struct {
hasScript bool
Script string
hasArgs bool
Args *expr.Vars
}
corredorExecResults struct {
Results *expr.Vars
}
)
// Exec function Corredor automation script executor
//
// expects implementation of exec function:
//
// func (h corredorHandler) exec(ctx context.Context, args *corredorExecArgs) (results *corredorExecResults, err error) {
// return
// }
func (h corredorHandler) Exec() *atypes.Function {
return &atypes.Function{
Ref: "corredorExec",
Kind: "function",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Corredor automation script executor",
Description: "Executes script in Corredor Automation server",
},
Parameters: []*atypes.Param{
{
Name: "script",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Script",
},
},
{
Name: "args",
Types: []string{"Vars"},
Meta: &atypes.ParamMeta{
Label: "Arguments",
},
},
},
Results: []*atypes.Param{
{
Name: "results",
Types: []string{"Vars"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &corredorExecArgs{
hasScript: in.Has("script"),
hasArgs: in.Has("args"),
}
)
if err = in.Decode(args); err != nil {
return
}
var results *corredorExecResults
if results, err = h.exec(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Results (*expr.Vars) to Vars
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Vars").Cast(results.Results); err != nil {
return
} else if err = expr.Assign(out, "results", tval); err != nil {
return
}
}
return
},
}
}

View File

@@ -0,0 +1,118 @@
package automation
import (
"context"
"encoding/json"
"strconv"
"github.com/cortezaproject/corteza/server/pkg/corredor"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/system/service/event"
)
type (
corredorHandler struct {
reg corredorHandlerRegistry
svc corredorServiceExecutor
}
corredorServiceExecutor interface {
Exec(ctx context.Context, scriptName string, args corredor.ScriptArgs) (err error)
}
scriptArgs struct {
payload map[string]interface{}
}
)
func CorredorHandler(reg corredorHandlerRegistry, svc corredorServiceExecutor) *corredorHandler {
h := &corredorHandler{
reg: reg,
svc: svc,
}
h.register()
return h
}
func (h corredorHandler) exec(ctx context.Context, args *corredorExecArgs) (r *corredorExecResults, err error) {
sArgs := makeScriptArgs(args.Args)
if err = h.svc.Exec(ctx, args.Script, sArgs); err != nil {
return
}
r = &corredorExecResults{}
if r.Results, err = expr.NewVars(sArgs.payload); err != nil {
return nil, err
}
return
}
func makeScriptArgs(in *expr.Vars) *scriptArgs {
payload := in.Dict()
sanitizeMapStringInterface(payload)
return &scriptArgs{payload: payload}
}
// mimic onManual event on system:
func (scriptArgs) ResourceType() string { return event.SystemOnManual().ResourceType() }
func (scriptArgs) EventType() string { return event.SystemOnManual().EventType() }
func (scriptArgs) Match(eventbus.ConstraintMatcher) bool { return false }
func (a *scriptArgs) Encode() (enc map[string][]byte, err error) {
enc = make(map[string][]byte)
for k, v := range a.payload {
enc[k], err = json.Marshal(v)
if err != nil {
return nil, err
}
}
return
}
func (a *scriptArgs) Decode(enc map[string][]byte) (err error) {
a.payload = make(map[string]interface{})
for k, v := range enc {
var aux interface{}
if err = json.Unmarshal(v, &aux); err != nil {
return err
}
a.payload[k] = aux
}
return
}
// Sanitizing all uint64 values that are encoded into JSON
func sanitizeMapStringInterface(m map[string]interface{}) {
var sw func(interface{}) string
sw = func(i interface{}) (s string) {
switch v := i.(type) {
case uint64:
// make sure uint64 values on fields ending with ID
// are properly encoded as strings
s = strconv.FormatUint(v, 10)
case map[string]interface{}:
sanitizeMapStringInterface(v)
case []interface{}:
for _, vv := range i.([]interface{}) {
sw(vv)
}
}
return
}
for k := range m {
if s := sw(m[k]); len(s) > 0 {
m[k] = sw(m[k])
}
}
}

View File

@@ -0,0 +1,15 @@
name: corredor
functions:
exec:
kind: function
meta:
short: Corredor automation script executor
description: Executes script in Corredor Automation server
params:
script: { meta: { label: Script }, types: [ { wf: String } ], required: true }
args: { meta: { label: Arguments }, types: [ { wf: Vars, go: '*expr.Vars' } ] }
results:
results:
wf: Vars
go: '*expr.Vars'

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,251 @@
package automation
import (
"context"
"fmt"
"io"
"io/ioutil"
"strings"
"github.com/cortezaproject/corteza/server/pkg/mail"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
gomail "gopkg.in/mail.v2"
)
type (
emailHandler struct {
reg emailHandlerRegistry
}
messageArgs interface {
GetReplyTo() (bool, string, *sysTypes.User)
GetFrom() (bool, string, *sysTypes.User)
GetTo() (bool, string, map[string]string, *sysTypes.User)
GetCc() (bool, string, map[string]string, *sysTypes.User)
GetHtml() (bool, string, io.Reader)
GetPlain() (bool, string, io.Reader)
}
)
func EmailHandler(reg emailHandlerRegistry) *emailHandler {
h := &emailHandler{
reg: reg,
}
h.register()
return h
}
func (h emailHandler) send(_ context.Context, args *emailSendArgs) (err error) {
msg := mail.New()
_, s, r := args.GetSubject()
if r != nil {
aux, _ := ioutil.ReadAll(r)
s = string(aux)
}
if err = h.procArgs(msg, s, args); err != nil {
return
}
return mail.Send(msg)
}
func (h emailHandler) message(_ context.Context, args *emailMessageArgs) (*emailMessageResults, error) {
var err error
msg := mail.New()
_, s, r := args.GetSubject()
if r != nil {
aux, _ := ioutil.ReadAll(r)
s = string(aux)
}
if err = h.procArgs(msg, s, args); err != nil {
return nil, err
}
return &emailMessageResults{Message: &emailMessage{msg: msg}}, nil
}
func (h emailHandler) sendMessage(_ context.Context, args *emailSendMessageArgs) (err error) {
if args.Message.msg == nil {
return fmt.Errorf("email message not initialized")
}
return mail.Send(args.Message.msg)
}
func (h emailHandler) setSubject(_ context.Context, args *emailSetSubjectArgs) (err error) {
if args.Message.msg == nil {
return fmt.Errorf("email message not initialized")
}
args.Message.msg.SetHeader("Subject", args.Subject)
return nil
}
func (h emailHandler) setHeaders(_ context.Context, args *emailSetHeadersArgs) (err error) {
if args.Message.msg == nil {
return fmt.Errorf("email message not initialized")
}
args.Message.msg.SetHeaders(args.Headers)
return nil
}
func (h emailHandler) setHeader(_ context.Context, args *emailSetHeaderArgs) (err error) {
if args.Message.msg == nil {
return fmt.Errorf("email message not initialized")
}
if args.hasValue {
args.Message.msg.SetHeader(args.Name, append(args.Message.msg.GetHeader(args.Name), args.Value)...)
} else {
args.Message.msg.SetHeader(args.Name)
}
return nil
}
func (h emailHandler) setAddress(_ context.Context, args *emailSetAddressArgs) (err error) {
if args.Message.msg == nil {
return fmt.Errorf("email message not initialized")
}
args.Message.msg.SetAddressHeader(args.Type, args.Address, args.Name)
return nil
}
func (h emailHandler) addAddress(_ context.Context, args *emailAddAddressArgs) (err error) {
if args.Message.msg == nil {
return fmt.Errorf("email message not initialized")
}
mm := args.Message.msg.GetHeader(args.Type)
mm = append(mm, args.Message.msg.FormatAddress(args.Address, args.Name))
args.Message.msg.SetHeader(args.Type, mm...)
return nil
}
func (h emailHandler) attach(_ context.Context, args *emailAttachArgs) (err error) {
if args.Message.msg == nil {
return fmt.Errorf("email message not initialized")
}
var (
r = args.contentStream
)
if r == nil {
r = strings.NewReader(args.contentString)
}
args.Message.msg.AttachReader(args.Name, r)
return
}
func (h emailHandler) embed(_ context.Context, args *emailEmbedArgs) (err error) {
if args.Message.msg == nil {
return fmt.Errorf("email message not initialized")
}
args.Message.msg.EmbedReader(args.Name, args.Content)
return
}
func (h emailHandler) procArgs(msg *gomail.Message, subject string, args messageArgs) (err error) {
msg.SetHeader("Subject", subject)
var (
// if text/plain exists,
// we'll add text/html as alternative body!
hasPlain bool
)
if has, s, r := args.GetPlain(); has {
if r != nil {
aux, _ := ioutil.ReadAll(r)
s = string(aux)
}
msg.SetBody("text/plain", s)
hasPlain = true
}
if has, s, r := args.GetHtml(); has {
if r != nil {
aux, _ := ioutil.ReadAll(r)
s = string(aux)
}
if hasPlain {
msg.AddAlternative("text/html", s)
} else {
msg.SetBody("text/html", s)
}
}
if has, s, m, u := args.GetTo(); has {
if err = h.procEmailRecipients(msg, "To", s, m, u); err != nil {
return
}
}
if has, s, m, u := args.GetCc(); has {
if err = h.procEmailRecipients(msg, "Cc", s, m, u); err != nil {
return
}
}
if has, s, u := args.GetReplyTo(); has {
if err = h.procEmailRecipients(msg, "ReplyTo", s, nil, u); err != nil {
return
}
}
if has, s, u := args.GetFrom(); has {
if err = h.procEmailRecipients(msg, "From", s, nil, u); err != nil {
return
}
}
return
}
func (h emailHandler) procEmailRecipients(msg *gomail.Message, field string, s string, mm map[string]string, u *sysTypes.User) (err error) {
var (
rr = make([]string, 0)
)
switch {
case len(s) > 0:
var (
email string
name string
)
name = ""
if spaceAt := strings.Index(s, " "); spaceAt > -1 {
// proc <email> <name> ("foo@bar.baz foo baz")
email, name = s[:spaceAt], strings.TrimSpace(s[spaceAt+1:])
} else {
// proc <email>
email = s
}
rr = append(rr, msg.FormatAddress(email, name))
case len(mm) > 0:
for email, name := range mm {
rr = append(rr, msg.FormatAddress(email, name))
}
case u != nil:
rr = append(rr, msg.FormatAddress(u.Email, u.Name))
}
msg.SetHeader(field, rr...)
return nil
}

View File

@@ -0,0 +1,200 @@
name: email
imports:
- io
- sysTypes github.com/cortezaproject/corteza/server/system/types
messageParams: &messageParams
subject:
meta:
label: Subject
types:
- { wf: String }
- { wf: Reader, suffix: Stream }
replyTo:
meta:
label: Reply to
types:
- { wf: String }
- { wf: User, go: '*sysTypes.User' }
from:
meta:
label: Sender
types:
- { wf: String }
- { wf: User, go: '*sysTypes.User' }
to:
meta:
label: Recipients
types:
- { wf: String }
- { wf: KV }
- { wf: User, go: '*sysTypes.User' }
cc:
meta:
label: CC
types:
- { wf: String }
- { wf: KV }
- { wf: User, go: '*sysTypes.User' }
html:
meta:
label: HTML message body
types:
- { wf: String, suffix: String }
- { wf: Reader, suffix: Stream }
plain:
meta:
label: Plain text message body
types:
- { wf: String, suffix: String }
- { wf: Reader, suffix: Stream }
messageParam: &messageParam
required: true
meta:
label: Message to be sent
types:
- { wf: EmailMessage }
messageResults: &messageResults
message: { wf: EmailMessage }
functions:
send:
kind: function
meta:
short: Email
description: Sends email directly
params: *messageParams
message:
kind: function
meta:
short: Email builder
description: Constructs new email message from basic parameters without sending it
params: *messageParams
results: *messageResults
sendMessage:
kind: function
meta:
short: Email sender
description: Sends email message
params:
message: *messageParam
setSubject:
kind: function
meta:
short: Email subject
description: Sets subject on the email message
params:
message: *messageParam
subject:
required: true
meta:
label: Subject
types:
- { wf: String }
setHeaders:
kind: function
meta:
short: Email headers
description: Sets message headers (overrides any existing headers, subject, recipients)
params:
message: *messageParam
headers:
required: true
meta:
label: Headers
types:
- { wf: KVV }
setHeader:
kind: function
meta:
short: Email header
description: Sets or removes specific header without changing the rest
params:
message: *messageParam
name:
required: true
meta: { label: Name }
types: [ { wf: String } ]
value:
meta: { label: Value, description: Raw header value. Omiting value will remove header. }
types: [ { wf: String } ]
setAddress:
kind: function
meta:
short: Email set address
description: Sets the recipient, sender or reply-to addresses
params:
message: *messageParam
type:
required: true
meta: { label: Type, description: One of From, ReplyTo, To, CC, BCC }
types: [ { wf: String } ]
address:
required: true
meta: { label: Address }
types: [ { wf: String } ]
name:
meta: { label: Name }
types: [ { wf: String } ]
addAddress:
kind: function
meta:
short: Email add address
description: Adds new recipient, sender or reply-to address
params:
message: *messageParam
type:
required: true
meta: { label: Type, description: One of From, ReplyTo, To, CC, BCC }
types: [ { wf: String } ]
address:
required: true
meta: { label: Address }
types: [ { wf: String } ]
name:
meta: { label: Name }
types: [ { wf: String } ]
attach:
kind: function
meta:
short: Email attachment
description: Attach content to an email message
params:
message: *messageParam
content:
required: true
meta: { label: Content }
types: [ { wf: Reader, suffix: Stream }, { wf: String } ]
name:
meta: { label: Name }
types: [ { wf: String } ]
embed:
kind: function
meta:
short: Email embedded attachment
description: Embed file (image) to an email message
params:
message: *messageParam
content:
required: true
meta: { label: Content }
types: [ { wf: Reader } ]
name:
meta: { label: Name }
types: [ { wf: String } ]

View File

@@ -0,0 +1,68 @@
package automation
// 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/automation/expr_types.yaml
import (
"context"
"fmt"
. "github.com/cortezaproject/corteza/server/pkg/expr"
"sync"
)
var _ = context.Background
var _ = fmt.Errorf
// EmailMessage is an expression type, wrapper for *emailMessage type
type EmailMessage struct {
value *emailMessage
mux sync.RWMutex
}
// NewEmailMessage creates new instance of EmailMessage expression type
func NewEmailMessage(val interface{}) (*EmailMessage, error) {
if c, err := CastToEmailMessage(val); err != nil {
return nil, fmt.Errorf("unable to create EmailMessage: %w", err)
} else {
return &EmailMessage{value: c}, nil
}
}
// Get return underlying value on EmailMessage
func (t *EmailMessage) Get() interface{} {
t.mux.RLock()
defer t.mux.RUnlock()
return t.value
}
// GetValue returns underlying value on EmailMessage
func (t *EmailMessage) GetValue() *emailMessage {
t.mux.RLock()
defer t.mux.RUnlock()
return t.value
}
// Type return type name
func (EmailMessage) Type() string { return "EmailMessage" }
// Cast converts value to *emailMessage
func (EmailMessage) Cast(val interface{}) (TypedValue, error) {
return NewEmailMessage(val)
}
// Assign new value to EmailMessage
//
// value is first passed through CastToEmailMessage
func (t *EmailMessage) Assign(val interface{}) error {
if c, err := CastToEmailMessage(val); err != nil {
return err
} else {
t.value = c
return nil
}
}

View File

@@ -0,0 +1,61 @@
package automation
import (
"fmt"
"io"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/http"
"gopkg.in/mail.v2"
)
type (
emailMessage struct {
// only basic implementation for now
// we can manipulate mail.Message internals through
// specialized wf functions (message, setSubject, setHeaders, ...)
msg *mail.Message
}
)
func CastToEmailMessage(val interface{}) (out *emailMessage, err error) {
switch val := expr.UntypedValue(val).(type) {
case *emailMessage:
if val.msg == nil {
val.msg = mail.NewMessage()
}
return val, nil
case nil:
return &emailMessage{msg: mail.NewMessage()}, nil
default:
return nil, fmt.Errorf("unable to cast type %T to %T", val, out)
}
}
func ReadRequestBody(in interface{}) (s string) {
var (
b []byte
err error
)
switch val := in.(type) {
case *http.Request:
b, err = io.ReadAll(val.Body)
case io.Reader:
b, err = io.ReadAll(val)
default:
b = []byte{}
}
if err != nil {
return ""
}
return string(b)
}
func (t *EmailMessage) Clone() (expr.TypedValue, error) {
msg := *t.value.msg
return NewEmailMessage(&msg)
}

View File

@@ -0,0 +1,19 @@
package: automation
types:
EmailMessage:
# using ad-hoc type for now, we'll port this to something internal
# when email-gateway is implemented
as: '*emailMessage'
struct:
# - { name: 'subject', exprType: 'String', goType: 'string' }
# - { name: 'addresses', exprType: 'Array', goType: '[]*emailAddress' }
# - { name: 'headers', exprType: 'KVV', goType: 'map[string][]string' }
# - { name: 'replyTo', exprType: 'KV', goType: 'map[string]string', mode: ro }
# - { name: 'to', exprType: 'KV', goType: 'map[string]string', mode: ro }
# - { name: 'cc', exprType: 'KV', goType: 'map[string]string', mode: ro }
# - { name: 'bcc', exprType: 'KV', goType: 'map[string]string', mode: ro }
# - { name: 'parts', exprType: 'Any', goType: 'map[string]io.Reader', mode: ro }
# - { name: 'embedded', exprType: 'Any', goType: 'map[string]io.Reader', mode: ro }
# - { name: 'attachments', exprType: 'Any', goType: 'map[string]io.Reader', mode: ro }

View File

@@ -0,0 +1,356 @@
package automation
// 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/automation/http_request_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"io"
"net/http"
"net/url"
"time"
)
var _ wfexec.ExecResponse
type (
httpRequestHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h httpRequestHandler) register() {
h.reg.AddFunctions(
h.Send(),
)
}
type (
httpRequestSendArgs struct {
hasUrl bool
Url string
hasMethod bool
Method string
hasParams bool
Params url.Values
hasHeaders bool
Headers http.Header
hasHeaderAuthBearer bool
HeaderAuthBearer string
hasHeaderAuthUsername bool
HeaderAuthUsername string
hasHeaderAuthPassword bool
HeaderAuthPassword string
hasHeaderUserAgent bool
HeaderUserAgent string
hasHeaderContentType bool
HeaderContentType string
hasTimeout bool
Timeout time.Duration
hasForm bool
Form url.Values
hasBody bool
Body interface{}
bodyString string
bodyStream io.Reader
bodyRaw interface{}
}
httpRequestSendResults struct {
Status string
StatusCode int64
Headers map[string][]string
ContentLength int64
ContentType string
Body io.Reader
}
)
func (a httpRequestSendArgs) GetBody() (bool, string, io.Reader, interface{}) {
return a.hasBody, a.bodyString, a.bodyStream, a.bodyRaw
}
// Send function HTTP request
//
// expects implementation of send function:
//
// func (h httpRequestHandler) send(ctx context.Context, args *httpRequestSendArgs) (results *httpRequestSendResults, err error) {
// return
// }
func (h httpRequestHandler) Send() *atypes.Function {
return &atypes.Function{
Ref: "httpRequestSend",
Kind: "function",
Labels: map[string]string{"http request": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "HTTP request",
Description: "Sends HTTP requests",
},
Parameters: []*atypes.Param{
{
Name: "url",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "URL",
Visual: map[string]interface{}{"input": map[string]interface{}{"type": "url"}},
},
},
{
Name: "method",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Method",
Visual: map[string]interface{}{"input": map[string]interface{}{"properties": map[string]interface{}{"options": []interface{}{map[string]interface{}{"text": "GET", "value": "GET"}, map[string]interface{}{"text": "HEAD", "value": "HEAD"}, map[string]interface{}{"text": "POST", "value": "POST"}, map[string]interface{}{"text": "PUT", "value": "PUT"}, map[string]interface{}{"text": "PATCH", "value": "PATCH"}, map[string]interface{}{"text": "DELETE", "value": "DELETE"}, map[string]interface{}{"text": "CONNECT", "value": "CONNECT"}, map[string]interface{}{"text": "OPTIONS", "value": "OPTIONS"}, map[string]interface{}{"text": "TRACE", "value": "TRACE"}}}, "type": "select"}},
},
},
{
Name: "params",
Types: []string{"KVV"},
Meta: &atypes.ParamMeta{
Label: "Parameters",
},
},
{
Name: "headers",
Types: []string{"KVV"},
Meta: &atypes.ParamMeta{
Label: "Headers",
Visual: map[string]interface{}{"input": map[string]interface{}{"type": "http-headers"}},
},
},
{
Name: "headerAuthBearer",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Bearer token",
},
},
{
Name: "headerAuthUsername",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Basic auth username",
},
},
{
Name: "headerAuthPassword",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Basic auth password",
},
},
{
Name: "headerUserAgent",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "User agent",
},
},
{
Name: "headerContentType",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Content type",
},
},
{
Name: "timeout",
Types: []string{"Duration"},
Meta: &atypes.ParamMeta{
Label: "Timeout",
},
},
{
Name: "form",
Types: []string{"KVV"},
Meta: &atypes.ParamMeta{
Label: "Form",
},
},
{
Name: "body",
Types: []string{"String", "Reader", "Any"},
Meta: &atypes.ParamMeta{
Label: "Body",
},
},
},
Results: []*atypes.Param{
{
Name: "status",
Types: []string{"String"},
},
{
Name: "statusCode",
Types: []string{"Integer"},
},
{
Name: "headers",
Types: []string{"KVV"},
},
{
Name: "contentLength",
Types: []string{"Integer"},
},
{
Name: "contentType",
Types: []string{"String"},
},
{
Name: "body",
Types: []string{"Reader"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &httpRequestSendArgs{
hasUrl: in.Has("url"),
hasMethod: in.Has("method"),
hasParams: in.Has("params"),
hasHeaders: in.Has("headers"),
hasHeaderAuthBearer: in.Has("headerAuthBearer"),
hasHeaderAuthUsername: in.Has("headerAuthUsername"),
hasHeaderAuthPassword: in.Has("headerAuthPassword"),
hasHeaderUserAgent: in.Has("headerUserAgent"),
hasHeaderContentType: in.Has("headerContentType"),
hasTimeout: in.Has("timeout"),
hasForm: in.Has("form"),
hasBody: in.Has("body"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Body argument
if args.hasBody {
aux := expr.Must(expr.Select(in, "body"))
switch aux.Type() {
case h.reg.Type("String").Type():
args.bodyString = aux.Get().(string)
case h.reg.Type("Reader").Type():
args.bodyStream = aux.Get().(io.Reader)
case h.reg.Type("Any").Type():
args.bodyRaw = aux.Get().(interface{})
}
}
var results *httpRequestSendResults
if results, err = h.send(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Status (string) to String
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("String").Cast(results.Status); err != nil {
return
} else if err = expr.Assign(out, "status", tval); err != nil {
return
}
}
{
// converting results.StatusCode (int64) to Integer
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Integer").Cast(results.StatusCode); err != nil {
return
} else if err = expr.Assign(out, "statusCode", tval); err != nil {
return
}
}
{
// converting results.Headers (map[string][]string) to KVV
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("KVV").Cast(results.Headers); err != nil {
return
} else if err = expr.Assign(out, "headers", tval); err != nil {
return
}
}
{
// converting results.ContentLength (int64) to Integer
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Integer").Cast(results.ContentLength); err != nil {
return
} else if err = expr.Assign(out, "contentLength", tval); err != nil {
return
}
}
{
// converting results.ContentType (string) to String
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("String").Cast(results.ContentType); err != nil {
return
} else if err = expr.Assign(out, "contentType", tval); err != nil {
return
}
}
{
// converting results.Body (io.Reader) to Reader
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Reader").Cast(results.Body); err != nil {
return
} else if err = expr.Assign(out, "body", tval); err != nil {
return
}
}
return
},
}
}

View File

@@ -0,0 +1,162 @@
package automation
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/cortezaproject/corteza/server/pkg/version"
)
type (
httpRequestHandler struct {
reg httpRequestHandlerRegistry
}
)
func HttpRequestHandler(reg httpRequestHandlerRegistry) *httpRequestHandler {
h := &httpRequestHandler{
reg: reg,
}
h.register()
return h
}
func (h httpRequestHandler) send(ctx context.Context, args *httpRequestSendArgs) (r *httpRequestSendResults, err error) {
var (
req *http.Request
rsp *http.Response
)
r = &httpRequestSendResults{}
req, err = h.makeRequest(ctx, args)
if err != nil {
return nil, err
}
if args.Timeout > 0 {
var tfn context.CancelFunc
ctx, tfn = context.WithTimeout(ctx, args.Timeout)
req = req.WithContext(ctx)
defer tfn()
}
rsp, err = http.DefaultClient.Do(req)
if err != nil {
return
}
r.StatusCode = int64(rsp.StatusCode)
r.Status = rsp.Status
r.Headers = rsp.Header
r.ContentLength = rsp.ContentLength
r.ContentType = rsp.Header.Get("Content-Type")
r.Body = rsp.Body
return
}
func (h httpRequestHandler) makeRequest(ctx context.Context, args *httpRequestSendArgs) (req *http.Request, err error) {
args.Method = strings.ToUpper(args.Method)
if args.Method == "" && (len(args.Form) > 0 || args.Body != nil) {
// when no method is set and form or body are passed
args.Method = http.MethodPost
}
err = func() error {
switch args.Method {
case http.MethodPost, http.MethodPut, http.MethodPatch:
default:
return nil
}
// @todo handle (multiple) file upload as well
if len(args.Form) > 0 {
if args.Body != nil {
return fmt.Errorf("cannot not use form and body parameters at the same time")
}
if !args.hasHeaderContentType {
args.HeaderContentType = "application/x-www-form-urlencoded"
}
args.bodyStream = &bytes.Buffer{}
if _, err = args.bodyStream.(*bytes.Buffer).WriteString(args.Form.Encode()); err != nil {
return err
}
return nil
}
if args.hasBody && args.bodyStream == nil {
if args.bodyString != "" {
args.bodyStream = strings.NewReader(args.bodyString)
} else {
switch raw := args.bodyRaw.(type) {
case string:
args.bodyStream = strings.NewReader(raw)
case []byte:
args.bodyStream = bytes.NewReader(raw)
case io.Reader:
args.bodyStream = raw
default:
args.bodyStream = &bytes.Buffer{}
return json.NewEncoder(args.bodyStream.(*bytes.Buffer)).Encode(args.bodyRaw)
}
}
}
return nil
}()
if err != nil {
return nil, err
}
if args.hasParams {
purl, err := url.Parse(args.Url)
if err != nil {
return nil, err
}
purl.RawQuery = args.Params.Encode()
args.Url = purl.String()
}
req, err = http.NewRequestWithContext(ctx, args.Method, args.Url, args.bodyStream)
if err != nil {
return nil, err
}
if args.Headers == nil {
args.Headers = make(http.Header)
}
if args.HeaderUserAgent == "" {
args.HeaderUserAgent = "Corteza-Automation-Client/" + version.Version
}
args.Headers.Set("User-Agent", args.HeaderUserAgent)
if len(args.HeaderContentType) > 0 {
args.Headers.Add("Content-Type", args.HeaderContentType)
}
req.Header = args.Headers
switch {
case len(args.HeaderAuthBearer) > 0:
req.Header.Add("Authorization", "Bearer "+args.HeaderAuthBearer)
case len(args.HeaderAuthUsername+args.HeaderAuthPassword) > 0:
req.SetBasicAuth(args.HeaderAuthUsername, args.HeaderAuthPassword)
}
return
}

View File

@@ -0,0 +1,130 @@
name: httpRequest
imports:
- time
- net/http
- net/url
- io
params:
url: &url
required: true
types:
- { wf: String }
meta: { label: URL, visual: { input: { type: url } } }
method: &method
required: true
types:
- { wf: String }
meta:
label: Method
visual:
input:
type: select
properties:
options:
- { value: "GET", text: "GET" }
- { value: "HEAD", text: "HEAD" }
- { value: "POST", text: "POST" }
- { value: "PUT", text: "PUT" }
- { value: "PATCH", text: "PATCH" }
- { value: "DELETE", text: "DELETE" }
- { value: "CONNECT", text: "CONNECT" }
- { value: "OPTIONS", text: "OPTIONS" }
- { value: "TRACE", text: "TRACE" }
params: &params
meta: { label: Parameters }
types:
- { wf: KVV, go: 'url.Values' }
headers: &headers
types:
- { wf: KVV, go: 'http.Header' }
meta: { label: Headers, visual: { input: { type: http-headers } } }
headerAuthBearer: &headerAuthBearer
meta: { label: Bearer token }
types:
- { wf: String }
headerAuthUsername: &headerAuthUsername
meta: { label: Basic auth username }
types:
- { wf: String }
headerAuthPassword: &headerAuthPassword
meta: { label: Basic auth password }
types:
- { wf: String }
headerContentType: &headerContentType
meta: { label: Content type }
types:
- { wf: String }
headerUserAgent: &headerUserAgent
meta: { label: User agent }
types:
- { wf: String }
timeout: &timeout
meta: { label: Timeout }
types:
- { wf: Duration }
form: &form
meta: { label: Form }
types:
- { wf: KVV, go: 'url.Values' }
body: &body
meta: { label: Body }
types:
- { wf: String, suffix: String }
- { wf: Reader, suffix: Stream }
- { wf: Any, suffix: Raw }
results:
status: &rStatus
wf: String
stautsCode: &rStatusCode
wf: Integer
headers: &rHeaders
wf: KVV
contentLength: &rContentLength
wf: Integer
contentType: &rContentType
wf: String
body: &rBody
wf: Reader
functions:
send:
meta:
short: HTTP request
description: Sends HTTP requests
labels:
"http request": "step,workflow"
params:
url: *url
method: *method
params: *params
headers: *headers
headerAuthBearer: *headerAuthBearer
headerAuthUsername: *headerAuthUsername
headerAuthPassword: *headerAuthPassword
headerUserAgent: *headerUserAgent
headerContentType: *headerContentType
timeout: *timeout
form: *form
body: *body
results:
status: *rStatus
statusCode: *rStatusCode
headers: *rHeaders
contentLength: *rContentLength
contentType: *rContentType
body: *rBody

View File

@@ -0,0 +1,89 @@
package automation
import (
"context"
"errors"
"io/ioutil"
"net/http"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestHttpRequestMaker(t *testing.T) {
validateBody := func(r *require.Assertions, req *http.Request, expected string) {
reader, err := req.GetBody()
r.NoError(err)
body, err := ioutil.ReadAll(reader)
r.NoError(err)
r.Equal(expected, string(body))
}
t.Run("basic get", func(t *testing.T) {
var (
r = require.New(t)
req, err = httpRequestHandler{}.makeRequest(context.Background(), &httpRequestSendArgs{
Url: "http://localhost/test",
Method: "GET",
})
)
r.NoError(err)
r.Equal("GET", req.Method)
r.Equal("http://localhost/test", req.URL.String())
})
t.Run("basic timeout", func(t *testing.T) {
var (
r = require.New(t)
ctx = context.Background()
_, err = httpRequestHandler{}.send(ctx, &httpRequestSendArgs{
Timeout: time.Nanosecond,
HeaderAuthBearer: "foo",
Url: "http://localhost/test",
Method: "GET",
})
)
r.True(errors.Is(err, context.DeadlineExceeded))
})
t.Run("post form", func(t *testing.T) {
var (
r = require.New(t)
in = &httpRequestSendArgs{
Form: url.Values(map[string][]string{
"a": {"a"},
"b": {"b", "b"},
"i": {"42"},
}),
}
req, err = httpRequestHandler{}.makeRequest(context.Background(), in)
)
r.NoError(err)
r.Equal("POST", req.Method)
validateBody(r, req, "a=a&b=b&b=b&i=42")
})
t.Run("basic auth", func(t *testing.T) {
var (
r = require.New(t)
in = &httpRequestSendArgs{
HeaderAuthUsername: "foo",
HeaderAuthPassword: "bar",
}
req, err = httpRequestHandler{}.makeRequest(context.Background(), in)
)
r.NoError(err)
r.Len(req.Header["Authorization"], 1)
r.Equal(req.Header["Authorization"][0], "Basic Zm9vOmJhcg==")
})
}

View File

@@ -0,0 +1,97 @@
package automation
import (
"bufio"
"context"
. "github.com/cortezaproject/corteza/server/pkg/expr"
)
type (
// iterates from start to stop by step
sequenceIterator struct {
counter, cFirst, cLast, cStep int64
}
)
func (i *sequenceIterator) More(context.Context, *Vars) (bool, error) {
return i.more(), nil
}
func (i *sequenceIterator) more() bool {
return i.counter < i.cLast
}
func (i *sequenceIterator) Start(context.Context, *Vars) error { return nil }
func (i *sequenceIterator) Next(context.Context, *Vars) (*Vars, error) {
out := &Vars{}
out.Set("counter", i.counter)
out.Set("isFirst", i.counter == i.cFirst)
i.counter = i.counter + i.cStep
out.Set("isLast", !i.more())
return out, nil
}
type (
// iterates from start to stop by step
conditionIterator struct {
expr Evaluable
}
)
func (i *conditionIterator) More(ctx context.Context, scope *Vars) (bool, error) {
return i.expr.Test(ctx, scope)
}
func (i *conditionIterator) Start(context.Context, *Vars) error { return nil }
func (i *conditionIterator) Next(context.Context, *Vars) (*Vars, error) {
return &Vars{}, nil
}
type (
// iterates from start to stop by step
collectionIterator struct {
ptr int
set []TypedValue
}
)
func (i *collectionIterator) More(context.Context, *Vars) (bool, error) {
return i.ptr < len(i.set), nil
}
func (i *collectionIterator) Start(context.Context, *Vars) error { i.ptr = 0; return nil }
func (i *collectionIterator) Next(context.Context, *Vars) (out *Vars, err error) {
out = &Vars{}
out.Set("item", i.set[i.ptr])
i.ptr++
return out, nil
}
type (
// iterates from start to stop by step
lineIterator struct {
s *bufio.Scanner
}
)
func (i *lineIterator) More(context.Context, *Vars) (bool, error) {
return i.s.Scan(), nil
}
func (i *lineIterator) Start(context.Context, *Vars) error {
return nil
}
func (i *lineIterator) Next(context.Context, *Vars) (*Vars, error) {
if err := i.s.Err(); err != nil {
return nil, err
}
out := &Vars{}
out.Set("line", i.s.Text())
return out, nil
}

View File

@@ -0,0 +1,198 @@
package automation
// 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/automation/jsenv_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"io"
)
var _ wfexec.ExecResponse
type (
jsenvHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h jsenvHandler) register() {
h.reg.AddFunctions(
h.Execute(),
)
}
type (
jsenvExecuteArgs struct {
hasScope bool
Scope interface{}
scopeAny interface{}
scopeStream io.Reader
hasSource bool
Source string
}
jsenvExecuteResults struct {
ResultString string
ResultInt int64
ResultBool bool
ResultAny interface{}
}
)
func (a jsenvExecuteArgs) GetScope() (bool, interface{}, io.Reader) {
return a.hasScope, a.scopeAny, a.scopeStream
}
// Execute function Process arbitrary data in jsenv
//
// expects implementation of execute function:
//
// func (h jsenvHandler) execute(ctx context.Context, args *jsenvExecuteArgs) (results *jsenvExecuteResults, err error) {
// return
// }
func (h jsenvHandler) Execute() *atypes.Function {
return &atypes.Function{
Ref: "jsenvExecute",
Kind: "function",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Process arbitrary data in jsenv",
},
Parameters: []*atypes.Param{
{
Name: "scope",
Types: []string{"Any", "Reader"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Scope",
},
},
{
Name: "source",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Source",
},
},
},
Results: []*atypes.Param{
{
Name: "resultString",
Types: []string{"String"},
},
{
Name: "resultInt",
Types: []string{"Integer"},
},
{
Name: "resultBool",
Types: []string{"Boolean"},
},
{
Name: "resultAny",
Types: []string{"Any"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &jsenvExecuteArgs{
hasScope: in.Has("scope"),
hasSource: in.Has("source"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Scope argument
if args.hasScope {
aux := expr.Must(expr.Select(in, "scope"))
switch aux.Type() {
case h.reg.Type("Any").Type():
args.scopeAny = aux.Get().(interface{})
case h.reg.Type("Reader").Type():
args.scopeStream = aux.Get().(io.Reader)
}
}
var results *jsenvExecuteResults
if results, err = h.execute(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.ResultString (string) to String
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("String").Cast(results.ResultString); err != nil {
return
} else if err = expr.Assign(out, "resultString", tval); err != nil {
return
}
}
{
// converting results.ResultInt (int64) to Integer
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Integer").Cast(results.ResultInt); err != nil {
return
} else if err = expr.Assign(out, "resultInt", tval); err != nil {
return
}
}
{
// converting results.ResultBool (bool) to Boolean
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Boolean").Cast(results.ResultBool); err != nil {
return
} else if err = expr.Assign(out, "resultBool", tval); err != nil {
return
}
}
{
// converting results.ResultAny (interface{}) to Any
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Any").Cast(results.ResultAny); err != nil {
return
} else if err = expr.Assign(out, "resultAny", tval); err != nil {
return
}
}
return
},
}
}

View File

@@ -0,0 +1,78 @@
package automation
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/jsenv"
)
type (
jsenvHandler struct {
reg jsenvHandlerRegistry
}
)
func JsenvHandler(reg queueHandlerRegistry) *jsenvHandler {
h := &jsenvHandler{
reg: reg,
}
h.register()
return h
}
func initVm() jsenv.Vm {
tr := jsenv.NewTransformer(jsenv.LoaderJS, jsenv.TargetNoop)
vm := jsenv.New(tr)
// register a request body reader
vm.Register("readRequestBody", ReadRequestBody)
return vm
}
func (h jsenvHandler) execute(ctx context.Context, args *jsenvExecuteArgs) (res *jsenvExecuteResults, err error) {
vm := initVm()
res = &jsenvExecuteResults{}
if !args.hasSource {
err = fmt.Errorf("could not process payload, function missing")
return
}
if !args.hasScope {
err = fmt.Errorf("could not process payload, scope missing")
return
}
fn, err := vm.RegisterFunction(args.Source)
if err != nil {
err = fmt.Errorf("could not register jsenv function: %s", err)
return
}
out, err := fn.Exec(vm.New(expr.UntypedValue(args.Scope)))
if err != nil {
err = fmt.Errorf("could not exec jsenv function: %s", err)
return
}
switch vv := out.(type) {
case int64:
res.ResultInt = int64(vv)
case string:
res.ResultString = string(vv)
case bool:
res.ResultBool = vv
default:
res.ResultAny = vv
}
return
}

View File

@@ -0,0 +1,28 @@
imports:
- io
functions:
execute:
meta:
short: Process arbitrary data in jsenv
params:
scope:
meta: { label: Scope }
required: true
types:
- { wf: Any }
- { wf: Reader, suffix: Stream }
source:
meta: { label: Source }
required: true
types:
- { wf: String }
results:
resultString:
wf: String
resultInt:
wf: Integer
resultBool:
wf: Boolean
resultAny:
wf: Any

View File

@@ -0,0 +1,199 @@
package automation
import (
"context"
"errors"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/cortezaproject/corteza/server/pkg/expr"
h "github.com/cortezaproject/corteza/server/pkg/http"
"github.com/stretchr/testify/require"
)
func Test_jsenvHandler(t *testing.T) {
type (
exp struct {
s string
i int64
a interface{}
}
tf struct {
name string
exp *exp
err error
params *jsenvExecuteArgs
}
)
var (
handler = &jsenvHandler{}
tcc = []tf{
{
name: "jsenv handler check payload",
err: errors.New(`could not process payload, scope missing`),
params: &jsenvExecuteArgs{
hasScope: false,
hasSource: true,
},
},
{
name: "jsenv handler check payload",
err: errors.New(`could not process payload, function missing`),
params: &jsenvExecuteArgs{
hasScope: true,
hasSource: false,
},
},
{
name: "jsenv handler invalid function",
err: errors.New(`could not register jsenv function: SyntaxError: SyntaxError: (anonymous): Line 1:74 Unexpected token function (and 1 more errors)`),
params: &jsenvExecuteArgs{
hasScope: true,
hasSource: true,
Source: `invalid function here...`,
},
},
{
name: "jsenv handler check payload",
err: errors.New(``),
params: &jsenvExecuteArgs{
hasScope: true,
hasSource: true,
Source: `return nonexistent`,
},
},
{
name: "jsenv handler parse request body json to string",
exp: &exp{
s: `bar`,
i: 0,
},
params: &jsenvExecuteArgs{
hasScope: true,
hasSource: true,
Scope: mustAny(expr.NewAny(makeRequest(t, `{"foo":"bar"}`))),
Source: `const b = JSON.parse(readRequestBody(input)); return b.foo;`,
},
},
{
name: "jsenv handler parse request body json to int",
exp: &exp{
s: ``,
i: 42,
},
params: &jsenvExecuteArgs{
hasScope: true,
hasSource: true,
Scope: mustAny(expr.NewAny(makeRequest(t, `{"foo":42}`))),
Source: `const b = JSON.parse(readRequestBody(input)); return b.foo;`,
},
},
{
name: "jsenv handler parse request body json to int",
exp: &exp{
s: ``,
i: 42,
},
params: &jsenvExecuteArgs{
hasScope: true,
hasSource: true,
Scope: mustAny(expr.NewAny(makeRequest(t, `42`))),
Source: `const b = readRequestBody(input); return parseInt(b);`,
},
},
{
name: "jsenv handler parse request body try catch",
exp: &exp{
s: `caught`,
i: 0,
},
params: &jsenvExecuteArgs{
hasScope: true,
hasSource: true,
Scope: mustAny(expr.NewAny(makeRequest(t, `42`))),
Source: `try { const b = readRequestBody(input_NONEXISTENT); } catch (e) { return 'caught'; }`,
},
},
{
name: "jsenv handler parse request body json to float",
exp: &exp{
s: ``,
i: 0,
a: 42.690,
},
params: &jsenvExecuteArgs{
hasScope: true,
hasSource: true,
Scope: mustAny(expr.NewAny(makeRequest(t, `42.690`))),
Source: `const b = readRequestBody(input); return parseFloat(b);`,
},
},
{
name: "jsenv handler input scope",
exp: &exp{
s: ``,
i: 41,
},
params: &jsenvExecuteArgs{
hasScope: true,
hasSource: true,
Scope: mustAny(expr.NewAny(makeRequest(t, `42`))),
Source: `const b = readRequestBody(input); return b - 1;`,
},
},
}
)
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
var (
req = require.New(t)
ctx = context.Background()
)
out, err := handler.execute(ctx, tc.params)
if tc.err == nil {
req.NoError(err)
} else {
req.Error(err)
}
if tc.exp != nil {
req.Equal(tc.exp.s, out.ResultString)
req.Equal(tc.exp.i, out.ResultInt)
if tc.exp.a != nil {
req.Equal(tc.exp.a, out.ResultAny)
}
}
})
}
}
func makeRequest(t *testing.T, b string) *h.Request {
r, err := http.NewRequest("POST", "/foo", ioutil.NopCloser(strings.NewReader(b)))
if err != nil {
t.Error(err)
}
ar, err := h.NewRequest(r)
if err != nil {
t.Error(err)
}
return ar
}
func mustAny(v *expr.Any, err error) *expr.Any {
if err != nil {
panic(err)
}
return v
}

View File

@@ -0,0 +1,197 @@
package automation
// 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/automation/jwt_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"io"
)
var _ wfexec.ExecResponse
type (
jwtHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h jwtHandler) register() {
h.reg.AddFunctions(
h.Generate(),
)
}
type (
jwtGenerateArgs struct {
hasScope bool
Scope string
hasHeader bool
Header interface{}
headerVars map[string]expr.TypedValue
headerString string
hasPayload bool
Payload interface{}
payloadVars map[string]expr.TypedValue
payloadString string
hasSecret bool
Secret interface{}
secretString string
secretStream io.Reader
}
jwtGenerateResults struct {
Token string
}
)
func (a jwtGenerateArgs) GetHeader() (bool, map[string]expr.TypedValue, string) {
return a.hasHeader, a.headerVars, a.headerString
}
func (a jwtGenerateArgs) GetPayload() (bool, map[string]expr.TypedValue, string) {
return a.hasPayload, a.payloadVars, a.payloadString
}
func (a jwtGenerateArgs) GetSecret() (bool, string, io.Reader) {
return a.hasSecret, a.secretString, a.secretStream
}
// Generate function Generate JWT
//
// expects implementation of generate function:
//
// func (h jwtHandler) generate(ctx context.Context, args *jwtGenerateArgs) (results *jwtGenerateResults, err error) {
// return
// }
func (h jwtHandler) Generate() *atypes.Function {
return &atypes.Function{
Ref: "jwtGenerate",
Kind: "function",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Generate JWT",
},
Parameters: []*atypes.Param{
{
Name: "scope",
Types: []string{"String"},
Meta: &atypes.ParamMeta{
Label: "Scope",
},
},
{
Name: "header",
Types: []string{"Vars", "String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Header",
},
},
{
Name: "payload",
Types: []string{"Vars", "String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Payload",
},
},
{
Name: "secret",
Types: []string{"String", "Reader"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Secret",
},
},
},
Results: []*atypes.Param{
{
Name: "token",
Types: []string{"String"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &jwtGenerateArgs{
hasScope: in.Has("scope"),
hasHeader: in.Has("header"),
hasPayload: in.Has("payload"),
hasSecret: in.Has("secret"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Header argument
if args.hasHeader {
aux := expr.Must(expr.Select(in, "header"))
switch aux.Type() {
case h.reg.Type("Vars").Type():
args.headerVars = aux.Get().(map[string]expr.TypedValue)
case h.reg.Type("String").Type():
args.headerString = aux.Get().(string)
}
}
// Converting Payload argument
if args.hasPayload {
aux := expr.Must(expr.Select(in, "payload"))
switch aux.Type() {
case h.reg.Type("Vars").Type():
args.payloadVars = aux.Get().(map[string]expr.TypedValue)
case h.reg.Type("String").Type():
args.payloadString = aux.Get().(string)
}
}
// Converting Secret argument
if args.hasSecret {
aux := expr.Must(expr.Select(in, "secret"))
switch aux.Type() {
case h.reg.Type("String").Type():
args.secretString = aux.Get().(string)
case h.reg.Type("Reader").Type():
args.secretStream = aux.Get().(io.Reader)
}
}
var results *jwtGenerateResults
if results, err = h.generate(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.Token (string) to String
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("String").Cast(results.Token); err != nil {
return
} else if err = expr.Assign(out, "token", tval); err != nil {
return
}
}
return
},
}
}

View File

@@ -0,0 +1,106 @@
package automation
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"strings"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jws"
"github.com/lestrrat-go/jwx/jwt"
)
type (
jwtHandler struct {
reg jwtHandlerRegistry
}
)
func JwtHandler(reg jwtHandlerRegistry) *jwtHandler {
h := &jwtHandler{
reg: reg,
}
h.register()
return h
}
func (h jwtHandler) generate(ctx context.Context, args *jwtGenerateArgs) (res *jwtGenerateResults, err error) {
var (
auxp = make(map[string]interface{})
auxh = make(map[string]interface{})
)
if !args.hasPayload {
err = fmt.Errorf("could not generate JWT, payload missing")
return
}
if !args.hasSecret {
err = fmt.Errorf("could not generate JWT, secret or cert missing")
return
}
for k, v := range args.headerVars {
auxh[k] = v.Get()
}
for k, v := range args.payloadVars {
auxp[k] = v.Get()
}
if args.payloadString != "" {
if err = json.Unmarshal([]byte(args.payloadString), &auxp); err != nil {
return
}
}
if args.headerString != "" {
if err = json.Unmarshal([]byte(args.headerString), &auxh); err != nil {
return
}
}
// check for delimiters
auxp["scope"] = strings.FieldsFunc(args.Scope, func(r rune) bool {
return r == ' ' || r == ','
})
var (
key interface{}
tkn = jwt.New()
tokenBytes []byte
headers = jws.NewHeaders()
)
for k, v := range auxh {
_ = headers.Set(k, v)
}
for k, v := range auxp {
if err = tkn.Set(k, v); err != nil {
return
}
}
if decodedKey, _ := pem.Decode([]byte(args.secretString)); decodedKey != nil {
if key, err = x509.ParsePKCS8PrivateKey(decodedKey.Bytes); err != nil {
return nil, err
}
tokenBytes, err = jwt.Sign(tkn, jwa.RS256, key, jwt.WithHeaders(headers))
} else {
key = []byte(args.secretString)
tokenBytes, err = jwt.Sign(tkn, jwa.HS256, key, jwt.WithHeaders(headers))
}
if err != nil {
return nil, fmt.Errorf("could not sign token: %w", err)
}
return &jwtGenerateResults{Token: string(tokenBytes)}, nil
}

View File

@@ -0,0 +1,34 @@
imports:
- io
functions:
generate:
meta:
short: Generate JWT
params:
scope:
meta: { label: Scope }
require: false
types:
- { wf: String }
header:
meta: { label: Header }
required: true
types:
- { wf: Vars, suffix: Vars, go: 'map[string]expr.TypedValue' }
- { wf: String, suffix: String }
payload:
meta: { label: Payload }
required: true
types:
- { wf: Vars, suffix: Vars, go: 'map[string]expr.TypedValue' }
- { wf: String, suffix: String }
secret:
meta: { label: Secret }
required: true
types:
- { wf: String, suffix: String }
- { wf: Reader, suffix: Stream }
results:
token:
wf: String

View File

@@ -0,0 +1,130 @@
package automation
import (
"context"
"fmt"
"testing"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/stretchr/testify/require"
)
const prKey string = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCn2MSVSyMzdZ5Q
cD0KMZAuhAmWE06qS/agpkG9OmvxsBAXjS8/2oaLCtmpF7Sx7XEJOaVUz8caty0L
Qjpovv5ZNqY0S/A+KahayTVXvNhjtwVb4kSYv8Tfv1H72CJAsHEi++DuFJ3uJyW5
zAblXpY96dCLKd4R6BOwQSAqpqAgCBJX3NiM1CtB6zvdYcGPxUJmFOxUSdU779WY
IloTDCi9lY/SWgt5tHb38jiCv8uZ9B+sgD6PXXqjnOnji+XXJb3zGrmyeSBvhgF+
bqlMcOXaufikzdq8v6wrW6zPsdDCYqtbpyku0pVz5JuN/czItS4fFGaQ+hjYnnq4
nU5YKGhjAgMBAAECggEAPlzd/ZJjS9VhswVgyI7NwVqxrR8TVVbQFbRwLHyuaqg9
8mI0sgbhgnvPj3INYyaTnxfaA/8HPTfd9pbu2MhN/Ju/eSLV6mLT+JdVyHmT9Mil
pxQU5KQr4+5T6bzOTTbBcnwfgJYMb9X/wF68GTDhpbNgFrTBm+mclxo7d11dlUiQ
bdA2KBhIM54kKcWEO90AP2FiC/cRSbv8afHVb0Fxy1SBAGVDSscUaEwgyHkRf6H7
Mmt4xhfBwLZZk5pVa97pZsas7L0sm4BtyYSs4elJesuo3DTpezZSzWHNreJcCL00
v7pm9ZWv9PYeUcQqSw6Ws2fhsydiN93o8s/fPpF0IQKBgQDQqvKPjGo3HLRnBmpc
nhUVQgjdh/QobJCGBjdxR/C3NkcatNLONddhefqovfb2jITdHmRKW+7XgRq/q457
mZ2JPvvryiWcVqf6D91tSoFYrVg5nU1zr6nTga1BpTH00/eNEXxjMAAguPj3m7Sw
3BP7L8yZZI1pOOjzVzuISEDlGwKBgQDN62ZXzRqorn5t+mDywTAJ2qAyZzoDAtUP
CjANOKBCECqE9cOW1qBDJcaEF7G+xyB22CMciOELjGXWw6ZwFWjc23tDCEkoHoia
5IWvwNs5ncdPCuOm3aay9w2dx9Idy+Eq5Vpe4XDlnZvEfzjwXX3gbQ5YS9kbNE2r
XtmgD/BmWQKBgBNKqrBA0BUWT0tzGWRErThQ6ZbpmdYe62Gos3mCqCuYFgzPCOpN
qgL2DwmIvotexG3ZAHardzJvWjS8PKkKs7jbnNjY0I9ap58D1nnjOIAlTpHNDDsU
04OdapI2Hp8+9ZUSN8jHyEs+Lq5ds9/iCOrhKW5JEJXY0BinSPa5j15fAoGBAM2I
dqCQollXwe34CaiD13UeeOOWUTsMKqlWW9v2d085X5dSzyTRmSksnVbfZ5SqoOa+
mV0z6pxiSIvywUACvqYjlIa10H9w6pzgF+fzMV3y9CsbDVtSxb7ABSFFf54qD9eH
EYq+rrchd4bMDYMtbiUB9V2AZ3VV4Wh5xfKTtjoRAoGAec/nBmX/oHiG/3T2LEmo
bTT+/eXc4LUzgHvFG6HW40PI8T2TfMTUGQH+90zkraryUF5PNoUF58tooDAcMWXm
DPEHFHeWn8T4obfuUjTw1mwj7Xjzr40HDitjIQa0bvBVwdYyEgiEw1CaeaUpq9Z8
ElGWoRHT87pfoZjbdPz7a4Y=
-----END PRIVATE KEY-----`
func TestJwtHandler(t *testing.T) {
type (
tf struct {
name string
exp string
err error
params *jwtGenerateArgs
}
)
var (
handler = &jwtHandler{}
tcc = []tf{
{
name: "proxy processer with auth headers",
exp: "eyJhbGciOiJSUzI1NiIsImtleSI6InZhbHVlIiwidHlwIjoiSldUIn0.eyJrZXkiOiJ2YWx1ZSIsInNjb3BlIjpbInNjb3BlIiwic2NvcGUyIl19.OnnLBsJdrZwAPeMjYIJPlZPW7bZdp-JbgNKFrRPnPXcuQVAUxK-R69-kDZbdsRZFOaU-AC52Tz4Ft3-SzADgFroSpoNjpEdJwbaKANlG_pm2b8-1pXSc0YzhY7DqBs4iae2pI0FGpeT_dza6kp9TL3NQfgqjx05Q3Gz5-3Kk32MD3zIvpUXgwkPbb4XLJxiY2Ra1dRWVI5Guk4GyLA19b7Z-DrHg1GE9mDy_NwZZD994Iri9e5zmcAikRtfHO7guPtBKVwhvt3u37wXtRgEYMyFQAn2ZSZaTytK8161Y-TOcdLVlqy4OfasaVt1pP0aNI9GGz5R-OVCOghW7TqZ6YQ",
err: nil,
params: &jwtGenerateArgs{
Scope: `scope scope2`,
headerVars: must(expr.CastToVars(map[string]interface{}{"key": "value"})),
payloadVars: must(expr.CastToVars(map[string]interface{}{"key": "value"})),
secretString: prKey,
hasHeader: true,
hasPayload: true,
hasSecret: true,
},
},
{
name: "sign with secret",
exp: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSIsInNjb3BlIjpbInNjb3BlIiwic2NvcGUyIl19.hvdRtewS0d8wPHF-oNHj0BaeOhgmmJkmerzg_qUI1Uo",
params: &jwtGenerateArgs{
Scope: `scope scope2`,
hasHeader: true,
payloadVars: must(expr.CastToVars(map[string]interface{}{"key": "value"})),
hasPayload: true,
hasSecret: true,
secretString: "*silly-secret*",
},
},
{
name: "proxy processer with auth headers",
exp: "",
err: fmt.Errorf("could not generate JWT, payload missing"),
params: &jwtGenerateArgs{
hasHeader: true,
hasPayload: false,
hasSecret: true,
},
},
{
name: "proxy processer with auth headers",
exp: "",
err: fmt.Errorf("could not generate JWT, secret or cert missing"),
params: &jwtGenerateArgs{
hasHeader: true,
hasPayload: true,
hasSecret: false,
},
},
}
)
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
var (
req = require.New(t)
ctx = context.Background()
)
res, err := handler.generate(ctx, tc.params)
if tc.err == nil {
req.NoError(err)
} else {
req.EqualError(err, tc.err.Error())
}
if res != nil {
req.Equal(tc.exp, res.Token)
}
})
}
}
func must(v map[string]expr.TypedValue, err error) map[string]expr.TypedValue {
if err != nil {
panic(err)
}
return v
}

View File

@@ -0,0 +1,274 @@
package automation
// 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/automation/log_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
var _ wfexec.ExecResponse
type (
logHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h logHandler) register() {
h.reg.AddFunctions(
h.Debug(),
h.Info(),
h.Warn(),
h.Error(),
)
}
type (
logDebugArgs struct {
hasMessage bool
Message string
hasFields bool
Fields map[string]string
}
)
// Debug function Log debug message
//
// expects implementation of debug function:
//
// func (h logHandler) debug(ctx context.Context, args *logDebugArgs) (err error) {
// return
// }
func (h logHandler) Debug() *atypes.Function {
return &atypes.Function{
Ref: "logDebug",
Kind: "function",
Labels: map[string]string{"debug": "step", "logger": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Log debug message",
},
Parameters: []*atypes.Param{
{
Name: "message",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Message",
},
},
{
Name: "fields",
Types: []string{"KV"},
Meta: &atypes.ParamMeta{
Label: "Fields",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &logDebugArgs{
hasMessage: in.Has("message"),
hasFields: in.Has("fields"),
}
)
if err = in.Decode(args); err != nil {
return
}
return out, h.debug(ctx, args)
},
}
}
type (
logInfoArgs struct {
hasMessage bool
Message string
hasFields bool
Fields map[string]string
}
)
// Info function Log info message
//
// expects implementation of info function:
//
// func (h logHandler) info(ctx context.Context, args *logInfoArgs) (err error) {
// return
// }
func (h logHandler) Info() *atypes.Function {
return &atypes.Function{
Ref: "logInfo",
Kind: "function",
Labels: map[string]string{"debug": "step", "logger": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Log info message",
},
Parameters: []*atypes.Param{
{
Name: "message",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Message",
},
},
{
Name: "fields",
Types: []string{"KV"},
Meta: &atypes.ParamMeta{
Label: "Fields",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &logInfoArgs{
hasMessage: in.Has("message"),
hasFields: in.Has("fields"),
}
)
if err = in.Decode(args); err != nil {
return
}
return out, h.info(ctx, args)
},
}
}
type (
logWarnArgs struct {
hasMessage bool
Message string
hasFields bool
Fields map[string]string
}
)
// Warn function Log warning message
//
// expects implementation of warn function:
//
// func (h logHandler) warn(ctx context.Context, args *logWarnArgs) (err error) {
// return
// }
func (h logHandler) Warn() *atypes.Function {
return &atypes.Function{
Ref: "logWarn",
Kind: "function",
Labels: map[string]string{"debug": "step", "logger": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Log warning message",
},
Parameters: []*atypes.Param{
{
Name: "message",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Message",
},
},
{
Name: "fields",
Types: []string{"KV"},
Meta: &atypes.ParamMeta{
Label: "Fields",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &logWarnArgs{
hasMessage: in.Has("message"),
hasFields: in.Has("fields"),
}
)
if err = in.Decode(args); err != nil {
return
}
return out, h.warn(ctx, args)
},
}
}
type (
logErrorArgs struct {
hasMessage bool
Message string
hasFields bool
Fields map[string]string
}
)
// Error function Log error message
//
// expects implementation of error function:
//
// func (h logHandler) error(ctx context.Context, args *logErrorArgs) (err error) {
// return
// }
func (h logHandler) Error() *atypes.Function {
return &atypes.Function{
Ref: "logError",
Kind: "function",
Labels: map[string]string{"debug": "step", "logger": "step,workflow"},
Meta: &atypes.FunctionMeta{
Short: "Log error message",
},
Parameters: []*atypes.Param{
{
Name: "message",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Message",
},
},
{
Name: "fields",
Types: []string{"KV"},
Meta: &atypes.ParamMeta{
Label: "Fields",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &logErrorArgs{
hasMessage: in.Has("message"),
hasFields: in.Has("fields"),
}
)
if err = in.Decode(args); err != nil {
return
}
return out, h.error(ctx, args)
},
}
}

View File

@@ -0,0 +1,68 @@
package automation
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/logger"
"go.uber.org/zap"
)
type (
logHandler struct {
reg logHandlerRegistry
logger *zap.Logger
}
)
func zapFields(fields map[string]string) []zap.Field {
ff := make([]zap.Field, 0, len(fields))
for k, v := range fields {
ff = append(ff, zap.String(k, v))
}
return ff
}
func LogHandler(reg logHandlerRegistry) *logHandler {
h := &logHandler{
reg: reg,
// this is a temporary solution how to get to the logging facility
// without being dependent on workflow logging settings
//
// there are still general logging settings that have a final say what is logged and what not
//
// With LOG_DEBUG=true you'll see all log levels from these functions.
// With debug logging disabled and LOG_LEVEL set on error, warn, info or debug, you'll filter out
// logging by verbosity:
// error will show
// and with LOG_LEVEL=error on you'll just see logged errors
// while LOG_LEVEL=info will show info, warn and error but ignore debug.
logger: logger.Default().
Named("workflow").
WithOptions(zap.WithCaller(false)),
}
h.register()
return h
}
func (h logHandler) debug(_ context.Context, args *logDebugArgs) (err error) {
h.logger.Debug(args.Message, zapFields(args.Fields)...)
return nil
}
func (h logHandler) info(_ context.Context, args *logInfoArgs) (err error) {
h.logger.Info(args.Message, zapFields(args.Fields)...)
return nil
}
func (h logHandler) warn(_ context.Context, args *logWarnArgs) (err error) {
h.logger.Warn(args.Message, zapFields(args.Fields)...)
return nil
}
func (h logHandler) error(_ context.Context, args *logErrorArgs) (err error) {
h.logger.Error(args.Message, zapFields(args.Fields)...)
return nil
}

View File

@@ -0,0 +1,47 @@
params:
message: &message
meta: { label: Message }
required: true
types:
- { wf: String }
fields: &fields
meta: { label: Fields }
types:
- { wf: KV }
labels: &labels
debug: "step"
logger: "step,workflow"
functions:
debug:
meta:
short: Log debug message
labels: *labels
params:
message: *message
fields: *fields
info:
meta:
short: Log info message
labels: *labels
params:
message: *message
fields: *fields
warn:
meta:
short: Log warning message
labels: *labels
params:
message: *message
fields: *fields
error:
meta:
short: Log error message
labels: *labels
params:
message: *message
fields: *fields

View File

@@ -0,0 +1,83 @@
package automation
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
)
func observableLog() (*zap.Logger, *observer.ObservedLogs) {
core, o := observer.New(zap.NewAtomicLevelAt(zap.DebugLevel))
return zap.New(core), o
}
func TestLogHandler_Debug(t *testing.T) {
var (
req = require.New(t)
log, obs = observableLog()
handler = &logHandler{logger: log}
err = handler.debug(context.Background(), &logDebugArgs{Message: "123abc", Fields: map[string]string{"foo": "bar"}})
)
req.NoError(err)
req.Equal(1, obs.Len())
entry := obs.TakeAll()[0]
req.Contains(entry.Message, "123abc")
req.Equal(entry.Level, zap.DebugLevel)
req.Contains(entry.ContextMap(), "foo")
}
func TestLogHandler_Info(t *testing.T) {
var (
req = require.New(t)
log, obs = observableLog()
handler = &logHandler{logger: log}
err = handler.info(context.Background(), &logInfoArgs{Message: "123abc", Fields: map[string]string{"foo": "bar"}})
)
req.NoError(err)
req.Equal(1, obs.Len())
entry := obs.TakeAll()[0]
req.Contains(entry.Message, "123abc")
req.Equal(entry.Level, zap.InfoLevel)
req.Contains(entry.ContextMap(), "foo")
}
func TestLogHandler_Warn(t *testing.T) {
var (
req = require.New(t)
log, obs = observableLog()
handler = &logHandler{logger: log}
err = handler.warn(context.Background(), &logWarnArgs{Message: "123abc", Fields: map[string]string{"foo": "bar"}})
)
req.NoError(err)
req.Equal(1, obs.Len())
entry := obs.TakeAll()[0]
req.Contains(entry.Message, "123abc")
req.Equal(entry.Level, zap.WarnLevel)
req.Contains(entry.ContextMap(), "foo")
}
func TestLogHandler_Error(t *testing.T) {
var (
req = require.New(t)
log, obs = observableLog()
handler = &logHandler{logger: log}
err = handler.error(context.Background(), &logErrorArgs{Message: "123abc", Fields: map[string]string{"foo": "bar"}})
)
req.NoError(err)
req.Equal(1, obs.Len())
entry := obs.TakeAll()[0]
req.Contains(entry.Message, "123abc")
req.Equal(entry.Level, zap.ErrorLevel)
req.Contains(entry.ContextMap(), "foo")
}

View File

@@ -0,0 +1,305 @@
package automation
// 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/automation/loop_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"io"
)
var _ wfexec.ExecResponse
type (
loopHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h loopHandler) register() {
h.reg.AddFunctions(
h.Sequence(),
h.Do(),
h.Each(),
h.Lines(),
)
}
type (
loopSequenceArgs struct {
hasFirst bool
First int64
hasLast bool
Last int64
hasStep bool
Step int64
}
loopSequenceResults struct {
Counter int64
IsFirst bool
IsLast bool
}
)
// Sequence function Iterates over sequence of numbers
//
// expects implementation of sequence function:
//
// func (h loopHandler) sequence(ctx context.Context, args *loopSequenceArgs) (results *loopSequenceResults, err error) {
// return
// }
func (h loopHandler) Sequence() *atypes.Function {
return &atypes.Function{
Ref: "loopSequence",
Kind: "iterator",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Iterates over sequence of numbers",
},
Parameters: []*atypes.Param{
{
Name: "first",
Types: []string{"Integer"},
Meta: &atypes.ParamMeta{
Label: "First",
},
},
{
Name: "last",
Types: []string{"Integer"},
Meta: &atypes.ParamMeta{
Label: "Last",
},
},
{
Name: "step",
Types: []string{"Integer"},
Meta: &atypes.ParamMeta{
Label: "Step",
},
},
},
Results: []*atypes.Param{
{
Name: "counter",
Types: []string{"Integer"},
},
{
Name: "isFirst",
Types: []string{"Boolean"},
},
{
Name: "isLast",
Types: []string{"Boolean"},
},
},
Iterator: func(ctx context.Context, in *expr.Vars) (out wfexec.IteratorHandler, err error) {
var (
args = &loopSequenceArgs{
hasFirst: in.Has("first"),
hasLast: in.Has("last"),
hasStep: in.Has("step"),
}
)
if err = in.Decode(args); err != nil {
return
}
return h.sequence(ctx, args)
},
}
}
type (
loopDoArgs struct {
hasWhile bool
While string
}
)
// Do function Condition
//
// expects implementation of do function:
//
// func (h loopHandler) do(ctx context.Context, args *loopDoArgs) (err error) {
// return
// }
func (h loopHandler) Do() *atypes.Function {
return &atypes.Function{
Ref: "loopDo",
Kind: "iterator",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Condition",
Description: "Iterates while condition is true",
},
Parameters: []*atypes.Param{
{
Name: "while",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "While expression",
Description: "Expression to be evaluated each iteration; loop will continue until expression is true",
},
},
},
Iterator: func(ctx context.Context, in *expr.Vars) (out wfexec.IteratorHandler, err error) {
var (
args = &loopDoArgs{
hasWhile: in.Has("while"),
}
)
if err = in.Decode(args); err != nil {
return
}
return h.do(ctx, args)
},
}
}
type (
loopEachArgs struct {
hasItems bool
Items []expr.TypedValue
}
loopEachResults struct {
Item interface{}
}
)
// Each function Items
//
// expects implementation of each function:
//
// func (h loopHandler) each(ctx context.Context, args *loopEachArgs) (results *loopEachResults, err error) {
// return
// }
func (h loopHandler) Each() *atypes.Function {
return &atypes.Function{
Ref: "loopEach",
Kind: "iterator",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Items",
Description: "Iterates over set of items",
},
Parameters: []*atypes.Param{
{
Name: "items",
Types: []string{"Array"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Items",
},
},
},
Results: []*atypes.Param{
{
Name: "item",
Types: []string{"Any"},
},
},
Iterator: func(ctx context.Context, in *expr.Vars) (out wfexec.IteratorHandler, err error) {
var (
args = &loopEachArgs{
hasItems: in.Has("items"),
}
)
if err = in.Decode(args); err != nil {
return
}
return h.each(ctx, args)
},
}
}
type (
loopLinesArgs struct {
hasStream bool
Stream io.Reader
}
loopLinesResults struct {
Line string
}
)
// Lines function Stream lines
//
// expects implementation of lines function:
//
// func (h loopHandler) lines(ctx context.Context, args *loopLinesArgs) (results *loopLinesResults, err error) {
// return
// }
func (h loopHandler) Lines() *atypes.Function {
return &atypes.Function{
Ref: "loopLines",
Kind: "iterator",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Stream lines",
Description: "Iterates over lines from stream",
},
Parameters: []*atypes.Param{
{
Name: "stream",
Types: []string{"Reader"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Stream",
},
},
},
Results: []*atypes.Param{
{
Name: "line",
Types: []string{"String"},
},
},
Iterator: func(ctx context.Context, in *expr.Vars) (out wfexec.IteratorHandler, err error) {
var (
args = &loopLinesArgs{
hasStream: in.Has("stream"),
}
)
if err = in.Decode(args); err != nil {
return
}
return h.lines(ctx, args)
},
}
}

View File

@@ -0,0 +1,85 @@
package automation
import (
"bufio"
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
type (
loopHandler struct {
reg loopHandlerRegistry
parser expr.Parsable
}
)
func LoopHandler(reg loopHandlerRegistry, p expr.Parsable) *loopHandler {
h := &loopHandler{
reg: reg,
parser: p,
}
h.register()
return h
}
func signOf(n int64) int64 {
if n > 0 {
return 1
} else if n < 0 {
return -1
}
return 0
}
func (h loopHandler) sequence(_ context.Context, args *loopSequenceArgs) (wfexec.IteratorHandler, error) {
if !args.hasFirst {
args.First = 0
}
if !args.hasLast {
args.Last = 1
}
if !args.hasStep {
args.Step = 1
}
sign := signOf(args.Step)
if args.First*sign >= args.Last*sign {
return nil, fmt.Errorf("failed to initialize counter iterator with first value greater than last or with zero step")
}
i := &sequenceIterator{
counter: args.First,
cFirst: args.First,
cLast: args.Last,
cStep: args.Step,
}
return i, nil
}
func (h loopHandler) do(_ context.Context, args *loopDoArgs) (wfexec.IteratorHandler, error) {
var (
i = &conditionIterator{}
err error
)
if i.expr, err = h.parser.Parse(args.While); err != nil {
return nil, err
}
return i, nil
}
func (h loopHandler) each(_ context.Context, args *loopEachArgs) (wfexec.IteratorHandler, error) {
return &collectionIterator{set: args.Items}, nil
}
func (h loopHandler) lines(_ context.Context, args *loopLinesArgs) (wfexec.IteratorHandler, error) {
return &lineIterator{s: bufio.NewScanner(args.Stream)}, nil
}

View File

@@ -0,0 +1,64 @@
name: loop
imports:
- io
functions:
sequence:
kind: iterator
meta:
short: Iterates over sequence of numbers
params:
first: { meta: { label: First }, types: [ { wf: Integer } ] }
last: { meta: { label: Last }, types: [ { wf: Integer } ] }
step: { meta: { label: Step }, types: [ { wf: Integer } ] }
results:
counter:
wf: Integer
isFirst:
wf: Boolean
isLast:
wf: Boolean
do:
kind: iterator
meta:
short: Condition
description: Iterates while condition is true
params:
while:
required: true
types: [ { wf: String } ]
meta:
label: While expression
description: Expression to be evaluated each iteration; loop will continue until expression is true
each:
kind: iterator
meta:
short: Items
description: Iterates over set of items
params:
items:
meta: { label: Items }
required: true
types: [ { wf: Array } ]
results:
item:
wf: Any
go: 'interface{}'
lines:
kind: iterator
meta:
short: Stream lines
description: Iterates over lines from stream
params:
stream:
meta: { label: Stream }
required: true
types: [ { wf: Reader, go: "io.Reader" } ]
results:
line:
wf: String
go: string

View File

@@ -0,0 +1,183 @@
package automation
// 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/automation/oauth2_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
var _ wfexec.ExecResponse
type (
oauth2HandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h oauth2Handler) register() {
h.reg.AddFunctions(
h.Authenticate(),
)
}
type (
oauth2AuthenticateArgs struct {
hasClient bool
Client string
hasSecret bool
Secret string
hasScope bool
Scope string
hasTokenUrl bool
TokenUrl string
}
oauth2AuthenticateResults struct {
AccessToken string
RefreshToken string
Token interface{}
}
)
// Authenticate function Authentication: OAUTH2
//
// expects implementation of authenticate function:
//
// func (h oauth2Handler) authenticate(ctx context.Context, args *oauth2AuthenticateArgs) (results *oauth2AuthenticateResults, err error) {
// return
// }
func (h oauth2Handler) Authenticate() *atypes.Function {
return &atypes.Function{
Ref: "oauth2Authenticate",
Kind: "function",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Authentication: OAUTH2",
},
Parameters: []*atypes.Param{
{
Name: "client",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Client",
},
},
{
Name: "secret",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Secret",
},
},
{
Name: "scope",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Scope",
},
},
{
Name: "tokenUrl",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Token URL",
},
},
},
Results: []*atypes.Param{
{
Name: "accessToken",
Types: []string{"String"},
},
{
Name: "refreshToken",
Types: []string{"String"},
},
{
Name: "token",
Types: []string{"Any"},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &oauth2AuthenticateArgs{
hasClient: in.Has("client"),
hasSecret: in.Has("secret"),
hasScope: in.Has("scope"),
hasTokenUrl: in.Has("tokenUrl"),
}
)
if err = in.Decode(args); err != nil {
return
}
var results *oauth2AuthenticateResults
if results, err = h.authenticate(ctx, args); err != nil {
return
}
out = &expr.Vars{}
{
// converting results.AccessToken (string) to String
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("String").Cast(results.AccessToken); err != nil {
return
} else if err = expr.Assign(out, "accessToken", tval); err != nil {
return
}
}
{
// converting results.RefreshToken (string) to String
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("String").Cast(results.RefreshToken); err != nil {
return
} else if err = expr.Assign(out, "refreshToken", tval); err != nil {
return
}
}
{
// converting results.Token (interface{}) to Any
var (
tval expr.TypedValue
)
if tval, err = h.reg.Type("Any").Cast(results.Token); err != nil {
return
} else if err = expr.Assign(out, "token", tval); err != nil {
return
}
}
return
},
}
}

View File

@@ -0,0 +1,88 @@
package automation
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/cortezaproject/corteza/server/pkg/http/auth"
"golang.org/x/oauth2"
)
type (
secureStoragerTodo struct{}
oauth2Handler struct {
reg oauth2HandlerRegistry
}
)
func Oauth2Handler(reg oauth2HandlerRegistry) *oauth2Handler {
h := &oauth2Handler{
reg: reg,
}
h.register()
return h
}
func (h oauth2Handler) authenticate(ctx context.Context, args *oauth2AuthenticateArgs) (res *oauth2AuthenticateResults, err error) {
var (
u *url.URL
token *oauth2.Token
)
if !args.hasClient {
err = fmt.Errorf("could not init auth, client key missing")
return
}
if !args.hasSecret {
err = fmt.Errorf("could not init auth, secret key missing")
return
}
if !args.hasScope {
err = fmt.Errorf("could not init auth, scope missing")
return
}
if !args.hasTokenUrl {
err = fmt.Errorf("could not init auth, token url missing")
return
}
if u, err = url.Parse(args.TokenUrl); err != nil {
err = fmt.Errorf("could not init auth, token url invalid: %s", err)
return
}
params := auth.Oauth2Params{
Client: args.Client,
Secret: args.Secret,
Scope: strings.Fields(args.Scope),
TokenUrl: u,
}
servicer, err := auth.NewOauth2(params, http.DefaultClient, secureStoragerTodo{})
if err != nil {
err = fmt.Errorf("could not init servicer: %s", err)
return
}
// call servicer, fetch the token
if token, err = servicer.Do(ctx); err != nil {
return
}
res = &oauth2AuthenticateResults{
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
Token: token,
}
return
}

View File

@@ -0,0 +1,33 @@
functions:
authenticate:
meta:
short: "Authentication: OAUTH2"
params:
client:
meta: { label: Client }
required: true
types:
- { wf: String }
secret:
meta: { label: Secret }
required: true
types:
- { wf: String }
scope:
meta: { label: Scope }
required: true
types:
- { wf: String }
tokenUrl:
meta: { label: Token URL }
required: true
types:
- { wf: String }
results:
accessToken:
wf: String
refreshToken:
wf: String
token:
wf: Any

View File

@@ -0,0 +1,109 @@
package automation
// 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/automation/queue_handler.yaml
import (
"context"
atypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"io"
)
var _ wfexec.ExecResponse
type (
queueHandlerRegistry interface {
AddFunctions(ff ...*atypes.Function)
Type(ref string) expr.Type
}
)
func (h queueHandler) register() {
h.reg.AddFunctions(
h.Write(),
)
}
type (
queueWriteArgs struct {
hasPayload bool
Payload interface{}
payloadString string
payloadStream io.Reader
hasQueue bool
Queue string
}
)
func (a queueWriteArgs) GetPayload() (bool, string, io.Reader) {
return a.hasPayload, a.payloadString, a.payloadStream
}
// Write function Queue message send
//
// expects implementation of write function:
//
// func (h queueHandler) write(ctx context.Context, args *queueWriteArgs) (err error) {
// return
// }
func (h queueHandler) Write() *atypes.Function {
return &atypes.Function{
Ref: "queueWrite",
Kind: "function",
Labels: map[string]string(nil),
Meta: &atypes.FunctionMeta{
Short: "Queue message send",
},
Parameters: []*atypes.Param{
{
Name: "payload",
Types: []string{"String", "Reader"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Payload",
},
},
{
Name: "queue",
Types: []string{"String"}, Required: true,
Meta: &atypes.ParamMeta{
Label: "Queue",
},
},
},
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &queueWriteArgs{
hasPayload: in.Has("payload"),
hasQueue: in.Has("queue"),
}
)
if err = in.Decode(args); err != nil {
return
}
// Converting Payload argument
if args.hasPayload {
aux := expr.Must(expr.Select(in, "payload"))
switch aux.Type() {
case h.reg.Type("String").Type():
args.payloadString = aux.Get().(string)
case h.reg.Type("Reader").Type():
args.payloadStream = aux.Get().(io.Reader)
}
}
return out, h.write(ctx, args)
},
}
}

View File

@@ -0,0 +1,39 @@
package automation
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/messagebus"
)
type (
queueHandler struct {
reg queueHandlerRegistry
}
)
func QueueHandler(reg queueHandlerRegistry) *queueHandler {
h := &queueHandler{
reg: reg,
}
h.register()
return h
}
func (h queueHandler) write(ctx context.Context, args *queueWriteArgs) (err error) {
if !args.hasQueue {
return fmt.Errorf("could not send message to queue, queue missing")
}
if !args.hasPayload {
return fmt.Errorf("could not send message to queue, payload missing")
}
go func() {
messagebus.Service().Push(args.Queue, []byte(args.payloadString))
}()
return nil
}

View File

@@ -0,0 +1,20 @@
imports:
- io
functions:
write:
meta:
short: Queue message send
params:
payload:
meta: { label: Payload }
required: true
types:
- { wf: String, suffix: String }
- { wf: Reader, suffix: Stream }
queue:
meta: { label: Queue }
required: true
types:
- { wf: String }

View File

@@ -0,0 +1,24 @@
package automation
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
component: schema.#component & {
handle: "automation"
resources: {
"workflow": workflow
"session": session
"trigger": trigger
}
rbac: operations: {
"grant": description: "Manage automation permissions"
"workflow.create": description: "Create workflows"
"triggers.search": description: "List, search or filter triggers"
"sessions.search": description: "List, search or filter sessions"
"workflows.search": description: "List, search or filter workflows"
"resource-translations.manage": description: "List, search, create, or update resource translations"
}
}

View File

@@ -0,0 +1,394 @@
package envoy
// 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"
"strings"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
"github.com/pkg/errors"
)
type (
// StoreDecoder is responsible for generating Envoy nodes from already stored
// resources which can then be managed by Envoy and imported via an encoder.
StoreDecoder struct{}
filterWrap struct {
rt string
f envoyx.ResourceFilter
}
)
const (
paramsKeyStorer = "storer"
paramsKeyDAL = "dal"
)
var (
// @todo temporary fix to make unused pkg/id not throw errors
_ = id.Next
)
// Decode returns a set of envoy nodes based on the provided params
//
// StoreDecoder expects the DecodeParam of `storer` and `dal` which conform
// to the store.Storer and dal.FullService interfaces.
func (d StoreDecoder) Decode(ctx context.Context, p envoyx.DecodeParams) (out envoyx.NodeSet, err error) {
// @todo we can optionally not require them based on what we're doing
s, err := d.getStorer(p)
if err != nil {
return
}
dl, err := d.getDal(p)
if err != nil {
return
}
return d.decode(ctx, s, dl, p)
}
func (d StoreDecoder) decode(ctx context.Context, s store.Storer, dl dal.FullService, p envoyx.DecodeParams) (out envoyx.NodeSet, err error) {
// Preprocessing and basic filtering (to omit what this decoder can't handle)
wrappedFilters := d.prepFilters(p.Filter)
// Get all scoped nodes
scopedNodes, err := d.getScopeNodes(ctx, s, dl, wrappedFilters)
if err != nil {
return
}
// Get all reference nodes
refNodes, refRefs, err := d.getReferenceNodes(ctx, s, dl, wrappedFilters)
if err != nil {
return
}
// Process filters to get the envoy nodes
err = func() (err error) {
var aux envoyx.NodeSet
for i, wf := range wrappedFilters {
switch wf.rt {
case types.WorkflowResourceType:
aux, err = d.decodeWorkflow(ctx, s, dl, d.makeWorkflowFilter(scopedNodes[i], refNodes[i], wf.f))
if err != nil {
return
}
for _, a := range aux {
a.Identifiers = a.Identifiers.Merge(wf.f.Identifiers)
a.References = envoyx.MergeRefs(a.References, refRefs[i])
}
out = append(out, aux...)
case types.TriggerResourceType:
aux, err = d.decodeTrigger(ctx, s, dl, d.makeTriggerFilter(scopedNodes[i], refNodes[i], wf.f))
if err != nil {
return
}
for _, a := range aux {
a.Identifiers = a.Identifiers.Merge(wf.f.Identifiers)
a.References = envoyx.MergeRefs(a.References, refRefs[i])
}
out = append(out, aux...)
default:
aux, err = d.extendDecoder(ctx, s, dl, p, wf.rt, refNodes[i], wf.f)
if err != nil {
return
}
for _, a := range aux {
a.Identifiers = a.Identifiers.Merge(wf.f.Identifiers)
a.References = envoyx.MergeRefs(a.References, refRefs[i])
}
out = append(out, aux...)
}
}
return
}()
if err != nil {
err = errors.Wrap(err, "failed to decode filters")
return
}
return
} // // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource workflow
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d StoreDecoder) decodeWorkflow(ctx context.Context, s store.Storer, dl dal.FullService, f types.WorkflowFilter) (out envoyx.NodeSet, err error) {
// @todo this might need to be improved.
// Currently, no resource is vast enough to pose a problem.
rr, _, err := store.SearchAutomationWorkflows(ctx, s, f)
if err != nil {
return
}
for _, r := range rr {
var n *envoyx.Node
n, err = WorkflowToEnvoyNode(r)
if err != nil {
return
}
out = append(out, n)
}
aux, err := d.extendedWorkflowDecoder(ctx, s, dl, f, out)
if err != nil {
return
}
out = append(out, aux...)
return
}
func WorkflowToEnvoyNode(r *types.Workflow) (node *envoyx.Node, err error) {
// Identifiers
ii := envoyx.MakeIdentifiers(
r.Handle,
r.ID,
)
// Handle references
// Omit any non-defined values
refs := map[string]envoyx.Ref{}
if r.CreatedBy > 0 {
refs["CreatedBy"] = envoyx.Ref{
ResourceType: "corteza::system:user",
Identifiers: envoyx.MakeIdentifiers(r.CreatedBy),
}
}
if r.DeletedBy > 0 {
refs["DeletedBy"] = envoyx.Ref{
ResourceType: "corteza::system:user",
Identifiers: envoyx.MakeIdentifiers(r.DeletedBy),
}
}
if r.OwnedBy > 0 {
refs["OwnedBy"] = envoyx.Ref{
ResourceType: "corteza::system:user",
Identifiers: envoyx.MakeIdentifiers(r.OwnedBy),
}
}
if r.RunAs > 0 {
refs["RunAs"] = envoyx.Ref{
ResourceType: "corteza::system:user",
Identifiers: envoyx.MakeIdentifiers(r.RunAs),
}
}
if r.UpdatedBy > 0 {
refs["UpdatedBy"] = envoyx.Ref{
ResourceType: "corteza::system:user",
Identifiers: envoyx.MakeIdentifiers(r.UpdatedBy),
}
}
var scope envoyx.Scope
node = &envoyx.Node{
Resource: r,
ResourceType: types.WorkflowResourceType,
Identifiers: ii,
References: refs,
Scope: scope,
}
return
}
// Resource should define a custom filter builder
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource trigger
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d StoreDecoder) decodeTrigger(ctx context.Context, s store.Storer, dl dal.FullService, f types.TriggerFilter) (out envoyx.NodeSet, err error) {
// @todo this might need to be improved.
// Currently, no resource is vast enough to pose a problem.
rr, _, err := store.SearchAutomationTriggers(ctx, s, f)
if err != nil {
return
}
for _, r := range rr {
var n *envoyx.Node
n, err = TriggerToEnvoyNode(r)
if err != nil {
return
}
out = append(out, n)
}
return
}
func TriggerToEnvoyNode(r *types.Trigger) (node *envoyx.Node, err error) {
// Identifiers
ii := envoyx.MakeIdentifiers(
r.ID,
)
// Handle references
// Omit any non-defined values
refs := map[string]envoyx.Ref{}
if r.CreatedBy > 0 {
refs["CreatedBy"] = envoyx.Ref{
ResourceType: "corteza::system:user",
Identifiers: envoyx.MakeIdentifiers(r.CreatedBy),
}
}
if r.DeletedBy > 0 {
refs["DeletedBy"] = envoyx.Ref{
ResourceType: "corteza::system:user",
Identifiers: envoyx.MakeIdentifiers(r.DeletedBy),
}
}
if r.OwnedBy > 0 {
refs["OwnedBy"] = envoyx.Ref{
ResourceType: "corteza::system:user",
Identifiers: envoyx.MakeIdentifiers(r.OwnedBy),
}
}
if r.UpdatedBy > 0 {
refs["UpdatedBy"] = envoyx.Ref{
ResourceType: "corteza::system:user",
Identifiers: envoyx.MakeIdentifiers(r.UpdatedBy),
}
}
if r.WorkflowID > 0 {
refs["WorkflowID"] = envoyx.Ref{
ResourceType: "corteza::automation:workflow",
Identifiers: envoyx.MakeIdentifiers(r.WorkflowID),
}
}
var scope envoyx.Scope
node = &envoyx.Node{
Resource: r,
ResourceType: types.TriggerResourceType,
Identifiers: ii,
References: refs,
Scope: scope,
}
return
}
// Resource should define a custom filter builder
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utilities
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d StoreDecoder) getStorer(p envoyx.DecodeParams) (s store.Storer, err error) {
aux, ok := p.Params[paramsKeyStorer]
if ok {
s, ok = aux.(store.Storer)
if ok {
return
}
}
err = errors.Errorf("store decoder expects a storer conforming to store.Storer interface")
return
}
func (d StoreDecoder) getDal(p envoyx.DecodeParams) (dl dal.FullService, err error) {
aux, ok := p.Params[paramsKeyDAL]
if ok {
dl, ok = aux.(dal.FullService)
if ok {
return
}
}
err = errors.Errorf("store decoder expects a DAL conforming to dal.FullService interface")
return
}
func (d StoreDecoder) prepFilters(ff map[string]envoyx.ResourceFilter) (out []filterWrap) {
out = make([]filterWrap, 0, len(ff))
for rt, f := range ff {
// Handle resources that don't belong to this decoder
if !strings.HasPrefix(rt, "corteza::automation") {
continue
}
out = append(out, filterWrap{rt: rt, f: f})
}
return
}
func (d StoreDecoder) getScopeNodes(ctx context.Context, s store.Storer, dl dal.FullService, ff []filterWrap) (scopes envoyx.NodeSet, err error) {
// Get all requested scopes
scopes = make(envoyx.NodeSet, len(ff))
// @note skipping scope logic since it's currently only supported within
// Compose resources.
return
}
// getReferenceNodes returns all of the nodes referenced by the nodes defined by the filters
//
// The nodes are provided as a slice (the same order as the filters) and as a map for easier lookups.
func (d StoreDecoder) getReferenceNodes(ctx context.Context, s store.Storer, dl dal.FullService, ff []filterWrap) (nodes []map[string]*envoyx.Node, refs []map[string]envoyx.Ref, err error) {
nodes = make([]map[string]*envoyx.Node, len(ff))
refs = make([]map[string]envoyx.Ref, len(ff))
err = func() (err error) {
for i, a := range ff {
if len(a.f.Refs) == 0 {
continue
}
auxr := make(map[string]*envoyx.Node, len(a.f.Refs))
auxa := make(map[string]envoyx.Ref)
for field, ref := range a.f.Refs {
f := ref.ResourceFilter()
aux, err := d.decode(ctx, s, dl, envoyx.DecodeParams{
Type: envoyx.DecodeTypeStore,
Filter: f,
})
if err != nil {
return err
}
// @todo consider changing this.
// Currently it's required because the .decode may return some
// nested nodes as well.
// Consider a flag or a new function.
aux = envoyx.NodesForResourceType(ref.ResourceType, aux...)
if len(aux) == 0 {
return fmt.Errorf("invalid reference %v", ref)
}
if len(aux) > 1 {
return fmt.Errorf("ambiguous reference: too many resources returned %v", a.f)
}
auxr[field] = aux[0]
auxa[field] = aux[0].ToRef()
}
nodes[i] = auxr
refs[i] = auxa
}
return
}()
if err != nil {
err = errors.Wrap(err, "failed to decode node references")
return
}
return
}

View File

@@ -0,0 +1,64 @@
package envoy
import (
"context"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
)
func (e StoreEncoder) prepare(ctx context.Context, p envoyx.EncodeParams, s store.Storer, rt string, nn envoyx.NodeSet) (err error) {
return
}
func (d StoreDecoder) extendDecoder(ctx context.Context, s store.Storer, dl dal.FullService, _ envoyx.DecodeParams, rt string, nodes map[string]*envoyx.Node, f envoyx.ResourceFilter) (out envoyx.NodeSet, err error) {
return
}
func (d StoreDecoder) makeWorkflowFilter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter) (out types.WorkflowFilter) {
out.Limit = auxf.Limit
ids, hh := auxf.Identifiers.IdentsAsStrings()
_ = ids
_ = hh
out.WorkflowID = ids
if len(hh) > 0 {
out.Handle = hh[0]
}
return
}
func (d StoreDecoder) makeTriggerFilter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter) (out types.TriggerFilter) {
out.Limit = auxf.Limit
ids, hh := auxf.Identifiers.Idents()
_ = ids
_ = hh
out.TriggerID = id.Strings(ids...)
return
}
func (d StoreDecoder) extendedWorkflowDecoder(ctx context.Context, s store.Storer, dl dal.FullService, f types.WorkflowFilter, base envoyx.NodeSet) (out envoyx.NodeSet, err error) {
for _, b := range base {
wf := b.Resource.(*types.Workflow)
filters, err := d.decodeTrigger(ctx, s, dl, types.TriggerFilter{
WorkflowID: id.Strings(wf.ID),
})
if err != nil {
return nil, err
}
out = append(out, filters...)
}
return
}

View File

@@ -0,0 +1,557 @@
package envoy
// 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"
"strconv"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
"github.com/pkg/errors"
)
type (
// StoreEncoder is responsible for encoding Corteza resources into the
// database via the Storer or the DAL interface
//
// @todo consider having a different encoder for the DAL resources
StoreEncoder struct{}
)
// Prepare performs some initial processing on the resource before it can be encoded
//
// Preparation runs validation, default value initialization, matching with
// already existing instances, ...
//
// The prepare function receives a set of nodes grouped by the resource type.
// This enables some batching optimization and simplifications when it comes to
// matching with existing resources.
//
// Prepare does not receive any placeholder nodes which are used solely
// for dependency resolution.
func (e StoreEncoder) Prepare(ctx context.Context, p envoyx.EncodeParams, rt string, nn envoyx.NodeSet) (err error) {
s, err := e.grabStorer(p)
if err != nil {
return
}
switch rt {
case types.WorkflowResourceType:
return e.prepareWorkflow(ctx, p, s, nn)
case types.TriggerResourceType:
return e.prepareTrigger(ctx, p, s, nn)
default:
return e.prepare(ctx, p, s, rt, nn)
}
return
}
// Encode encodes the given Corteza resources into the primary store
//
// Encoding should not do any additional processing apart from matching with
// dependencies and runtime validation
//
// The Encode function is called for every resource type where the resource
// appears at the root of the dependency tree.
// All of the root-level resources for that resource type are passed into the function.
// The encoding function must traverse the branches to encode all of the dependencies.
//
// This flow is used to simplify the flow of how resources are encoded into YAML
// (and other documents) as well as to simplify batching.
//
// Encode does not receive any placeholder nodes which are used solely
// for dependency resolution.
func (e StoreEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tree envoyx.Traverser) (err error) {
s, err := e.grabStorer(p)
if err != nil {
return
}
switch rt {
case types.WorkflowResourceType:
return e.encodeWorkflows(ctx, p, s, nodes, tree)
case types.TriggerResourceType:
return e.encodeTriggers(ctx, p, s, nodes, tree)
default:
return e.encode(ctx, p, s, rt, nodes, tree)
}
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource workflow
// // // // // // // // // // // // // // // // // // // // // // // // //
// prepareWorkflow prepares the resources of the given type for encoding
func (e StoreEncoder) prepareWorkflow(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet) (err error) {
// Grab an index of already existing resources of this type
// @note since these resources should be fairly low-volume and existing for
// a short time (and because we batch by resource type); fetching them all
// into memory shouldn't hurt too much.
// @todo do some benchmarks and potentially implement some smarter check such as
// a bloom filter or something similar.
// Get node scopes
scopedNodes, err := e.getScopeNodes(ctx, s, nn)
if err != nil {
err = errors.Wrap(err, "failed to get scope nodes")
return
}
// Initializing the index here (and using a hashmap) so it's not escaped to the heap
existing := make(map[int]types.Workflow, len(nn))
err = e.matchupWorkflows(ctx, s, existing, scopedNodes, nn)
if err != nil {
err = errors.Wrap(err, "failed to matchup existing Workflows")
return
}
for i, n := range nn {
if n.Resource == nil {
panic("unexpected state: cannot call prepareWorkflow with nodes without a defined Resource")
}
res, ok := n.Resource.(*types.Workflow)
if !ok {
panic("unexpected resource type: node expecting type of workflow")
}
existing, hasExisting := existing[i]
// Run expressions on the nodes
err = e.runEvals(ctx, hasExisting, n)
if err != nil {
return
}
if hasExisting {
// On existing, we don't need to re-do identifiers and references; simply
// changing up the internal resource is enough.
//
// In the future, we can pass down the tree and re-do the deps like that
switch n.Config.MergeAlg {
case envoyx.OnConflictPanic:
err = errors.Errorf("resource %v already exists", n.Identifiers.Slice)
return
case envoyx.OnConflictReplace:
// Replace; simple ID change should do the trick
res.ID = existing.ID
case envoyx.OnConflictSkip:
// Replace the node's resource with the fetched one
res = &existing
// @todo merging
}
} else {
// @todo actually a bottleneck. As per sonyflake docs, it can at most
// generate up to 2**8 (256) IDs per 10ms in a single thread.
// How can we improve this?
res.ID = id.Next()
}
// We can skip validation/defaults when the resource is overwritten by
// the one already stored (the panic one errors out anyway) since it
// should already be ok.
if !hasExisting || n.Config.MergeAlg != envoyx.OnConflictSkip {
err = e.setWorkflowDefaults(res)
if err != nil {
return err
}
err = e.validateWorkflow(res)
if err != nil {
return err
}
}
n.Resource = res
}
return
}
// encodeWorkflows encodes a set of resource into the database
func (e StoreEncoder) encodeWorkflows(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) {
for _, n := range nn {
err = e.encodeWorkflow(ctx, p, s, n, tree)
if err != nil {
return
}
}
return
}
// encodeWorkflow encodes the resource into the database
func (e StoreEncoder) encodeWorkflow(ctx context.Context, p envoyx.EncodeParams, s store.Storer, n *envoyx.Node, tree envoyx.Traverser) (err error) {
// Grab dependency references
var auxID uint64
err = func() (err error) {
for fieldLabel, ref := range n.References {
auxID = safeParentID(tree, n, ref)
if auxID == 0 {
continue
}
err = n.Resource.SetValue(fieldLabel, 0, auxID)
if err != nil {
return
}
}
return
}()
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("failed to set dependency references for %s %v", n.ResourceType, n.Identifiers.Slice))
return
}
// Flush to the DB
if !n.Evaluated.Skip {
err = store.UpsertAutomationWorkflow(ctx, s, n.Resource.(*types.Workflow))
if err != nil {
err = errors.Wrap(err, "failed to upsert Workflow")
return
}
}
// Handle resources nested under it
//
// @todo how can we remove the OmitPlaceholderNodes call the same way we did for
// the root function calls?
err = func() (err error) {
for rt, nn := range envoyx.NodesByResourceType(tree.Children(n)...) {
nn = envoyx.OmitPlaceholderNodes(nn...)
switch rt {
}
}
return
}()
if err != nil {
err = errors.Wrap(err, "failed to encode nested resources")
return
}
return
}
// matchupWorkflows returns an index with indicates what resources already exist
func (e StoreEncoder) matchupWorkflows(ctx context.Context, s store.Storer, uu map[int]types.Workflow, scopes envoyx.NodeSet, nn envoyx.NodeSet) (err error) {
// @todo might need to do it smarter then this.
// Most resources won't really be that vast so this should be acceptable for now.
aa, _, err := store.SearchAutomationWorkflows(ctx, s, types.WorkflowFilter{})
if err != nil {
return
}
idMap := make(map[uint64]*types.Workflow, len(aa))
strMap := make(map[string]*types.Workflow, len(aa))
for _, a := range aa {
strMap[a.Handle] = a
idMap[a.ID] = a
}
var aux *types.Workflow
var ok bool
for i, n := range nn {
for _, idf := range n.Identifiers.Slice {
if id, err := strconv.ParseUint(idf, 10, 64); err == nil {
aux, ok = idMap[id]
if ok {
uu[i] = *aux
// When any identifier matches we can end it
break
}
}
aux, ok = strMap[idf]
if ok {
uu[i] = *aux
// When any identifier matches we can end it
break
}
}
}
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource trigger
// // // // // // // // // // // // // // // // // // // // // // // // //
// prepareTrigger prepares the resources of the given type for encoding
func (e StoreEncoder) prepareTrigger(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet) (err error) {
// Grab an index of already existing resources of this type
// @note since these resources should be fairly low-volume and existing for
// a short time (and because we batch by resource type); fetching them all
// into memory shouldn't hurt too much.
// @todo do some benchmarks and potentially implement some smarter check such as
// a bloom filter or something similar.
// Get node scopes
scopedNodes, err := e.getScopeNodes(ctx, s, nn)
if err != nil {
err = errors.Wrap(err, "failed to get scope nodes")
return
}
// Initializing the index here (and using a hashmap) so it's not escaped to the heap
existing := make(map[int]types.Trigger, len(nn))
err = e.matchupTriggers(ctx, s, existing, scopedNodes, nn)
if err != nil {
err = errors.Wrap(err, "failed to matchup existing Triggers")
return
}
for i, n := range nn {
if n.Resource == nil {
panic("unexpected state: cannot call prepareTrigger with nodes without a defined Resource")
}
res, ok := n.Resource.(*types.Trigger)
if !ok {
panic("unexpected resource type: node expecting type of trigger")
}
existing, hasExisting := existing[i]
// Run expressions on the nodes
err = e.runEvals(ctx, hasExisting, n)
if err != nil {
return
}
if hasExisting {
// On existing, we don't need to re-do identifiers and references; simply
// changing up the internal resource is enough.
//
// In the future, we can pass down the tree and re-do the deps like that
switch n.Config.MergeAlg {
case envoyx.OnConflictPanic:
err = errors.Errorf("resource %v already exists", n.Identifiers.Slice)
return
case envoyx.OnConflictReplace:
// Replace; simple ID change should do the trick
res.ID = existing.ID
case envoyx.OnConflictSkip:
// Replace the node's resource with the fetched one
res = &existing
// @todo merging
}
} else {
// @todo actually a bottleneck. As per sonyflake docs, it can at most
// generate up to 2**8 (256) IDs per 10ms in a single thread.
// How can we improve this?
res.ID = id.Next()
}
// We can skip validation/defaults when the resource is overwritten by
// the one already stored (the panic one errors out anyway) since it
// should already be ok.
if !hasExisting || n.Config.MergeAlg != envoyx.OnConflictSkip {
err = e.setTriggerDefaults(res)
if err != nil {
return err
}
err = e.validateTrigger(res)
if err != nil {
return err
}
}
n.Resource = res
}
return
}
// encodeTriggers encodes a set of resource into the database
func (e StoreEncoder) encodeTriggers(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) {
for _, n := range nn {
err = e.encodeTrigger(ctx, p, s, n, tree)
if err != nil {
return
}
}
return
}
// encodeTrigger encodes the resource into the database
func (e StoreEncoder) encodeTrigger(ctx context.Context, p envoyx.EncodeParams, s store.Storer, n *envoyx.Node, tree envoyx.Traverser) (err error) {
// Grab dependency references
var auxID uint64
err = func() (err error) {
for fieldLabel, ref := range n.References {
auxID = safeParentID(tree, n, ref)
if auxID == 0 {
continue
}
err = n.Resource.SetValue(fieldLabel, 0, auxID)
if err != nil {
return
}
}
return
}()
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("failed to set dependency references for %s %v", n.ResourceType, n.Identifiers.Slice))
return
}
// Flush to the DB
if !n.Evaluated.Skip {
err = store.UpsertAutomationTrigger(ctx, s, n.Resource.(*types.Trigger))
if err != nil {
err = errors.Wrap(err, "failed to upsert Trigger")
return
}
}
// Handle resources nested under it
//
// @todo how can we remove the OmitPlaceholderNodes call the same way we did for
// the root function calls?
err = func() (err error) {
for rt, nn := range envoyx.NodesByResourceType(tree.Children(n)...) {
nn = envoyx.OmitPlaceholderNodes(nn...)
switch rt {
}
}
return
}()
if err != nil {
err = errors.Wrap(err, "failed to encode nested resources")
return
}
return
}
// matchupTriggers returns an index with indicates what resources already exist
func (e StoreEncoder) matchupTriggers(ctx context.Context, s store.Storer, uu map[int]types.Trigger, scopes envoyx.NodeSet, nn envoyx.NodeSet) (err error) {
// @todo might need to do it smarter then this.
// Most resources won't really be that vast so this should be acceptable for now.
aa, _, err := store.SearchAutomationTriggers(ctx, s, types.TriggerFilter{})
if err != nil {
return
}
idMap := make(map[uint64]*types.Trigger, len(aa))
strMap := make(map[string]*types.Trigger, len(aa))
for _, a := range aa {
idMap[a.ID] = a
}
var aux *types.Trigger
var ok bool
for i, n := range nn {
for _, idf := range n.Identifiers.Slice {
if id, err := strconv.ParseUint(idf, 10, 64); err == nil {
aux, ok = idMap[id]
if ok {
uu[i] = *aux
// When any identifier matches we can end it
break
}
}
aux, ok = strMap[idf]
if ok {
uu[i] = *aux
// When any identifier matches we can end it
break
}
}
}
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utility functions
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e *StoreEncoder) grabStorer(p envoyx.EncodeParams) (s store.Storer, err error) {
auxs, ok := p.Params[paramsKeyStorer]
if !ok {
err = errors.Errorf("store encoder expects a store conforming to store.Storer interface")
return
}
s, ok = auxs.(store.Storer)
if !ok {
err = errors.Errorf("store encoder expects a store conforming to store.Storer interface")
return
}
return
}
func (e *StoreEncoder) runEvals(ctx context.Context, existing bool, n *envoyx.Node) (err error) {
// Skip if
if n.Config.SkipIfEval == nil {
return
}
aux, err := expr.EmptyVars().Cast(map[string]any{
"missing": !existing,
})
if err != nil {
return
}
n.Evaluated.Skip, err = n.Config.SkipIfEval.Test(ctx, aux.(*expr.Vars))
return
}
func (e StoreEncoder) getScopeNodes(ctx context.Context, s store.Storer, nn envoyx.NodeSet) (scopes envoyx.NodeSet, err error) {
// Get all requested scopes
scopes = make(envoyx.NodeSet, len(nn))
// @note skipping scope logic since it's currently only supported within
// Compose resources.
return
}
func safeParentID(tt envoyx.Traverser, n *envoyx.Node, ref envoyx.Ref) (out uint64) {
rn := tt.ParentForRef(n, ref)
if rn == nil {
return
}
return rn.Resource.GetID()
}

View File

@@ -0,0 +1,36 @@
package envoy
import (
"context"
"time"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/store"
)
func (e StoreEncoder) encode(ctx context.Context, p envoyx.EncodeParams, s store.Storer, rt string, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) {
return
}
func (e StoreEncoder) setWorkflowDefaults(res *types.Workflow) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateWorkflow(res *types.Workflow) (err error) {
return
}
func (e StoreEncoder) setTriggerDefaults(res *types.Trigger) (err error) {
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
return
}
func (e StoreEncoder) validateTrigger(res *types.Trigger) (err error) {
return
}

1037
server/automation/envoy/yaml_decode.gen.go generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
package envoy
import (
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"gopkg.in/yaml.v3"
)
func (d *auxYamlDoc) unmarshalTriggersExtendedNode(dctx documentContext, n *yaml.Node, meta ...*yaml.Node) (out envoyx.NodeSet, err error) {
return d.unmarshalTriggerNode(dctx, n, meta...)
}
func (d *auxYamlDoc) unmarshalYAML(k string, n *yaml.Node) (out envoyx.NodeSet, err error) { return }

357
server/automation/envoy/yaml_encode.gen.go generated Normal file
View File

@@ -0,0 +1,357 @@
package envoy
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"context"
"io"
"time"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
type (
// YamlEncoder is responsible for encoding Corteza resources into
// a YAML supported format
YamlEncoder struct{}
)
const (
paramsKeyWriter = "writer"
)
// Encode encodes the given Corteza resources into some YAML supported format
//
// Encoding should not do any additional processing apart from matching with
// dependencies and runtime validation
//
// Preparation runs validation, default value initialization, matching with
// already existing instances, ...
//
// The prepare function receives a set of nodes grouped by the resource type.
// This enables some batching optimization and simplifications when it comes to
// matching with existing resources.
//
// Prepare does not receive any placeholder nodes which are used solely
// for dependency resolution.
func (e YamlEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tt envoyx.Traverser) (err error) {
var (
out *yaml.Node
aux *yaml.Node
)
_ = aux
w, err := e.getWriter(p)
if err != nil {
return
}
switch rt {
case types.WorkflowResourceType:
aux, err = e.encodeWorkflows(ctx, p, nodes, tt)
if err != nil {
return
}
// Root level resources are always encoded as a map
out, err = y7s.AddMap(out, "workflow", aux)
if err != nil {
return
}
case types.TriggerResourceType:
aux, err = e.encodeTriggers(ctx, p, nodes, tt)
if err != nil {
return
}
// Root level resources are always encoded as a map
out, err = y7s.AddMap(out, "trigger", aux)
if err != nil {
return
}
default:
out, err = e.encode(ctx, out, p, rt, nodes, tt)
if err != nil {
return
}
}
// Don't output nil values since that will produce broken yaml docs
if out == nil {
return
}
return yaml.NewEncoder(w).Encode(out)
} // // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource workflow
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) encodeWorkflows(ctx context.Context, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
var aux *yaml.Node
for _, n := range nodes {
aux, err = e.encodeWorkflow(ctx, p, n, tt)
if err != nil {
return
}
out, err = y7s.AddSeq(out, aux)
if err != nil {
return
}
}
return
}
// encodeWorkflow focuses on the specific resource invoked by the Encode method
func (e YamlEncoder) encodeWorkflow(ctx context.Context, p envoyx.EncodeParams, node *envoyx.Node, tt envoyx.Traverser) (out *yaml.Node, err error) {
res := node.Resource.(*types.Workflow)
// Pre-compute some map values so we can omit error checking when encoding yaml nodes
auxCreatedAt, err := e.encodeTimestamp(p, res.CreatedAt)
if err != nil {
return
}
auxCreatedBy, err := e.encodeRef(p, res.CreatedBy, "CreatedBy", node, tt)
if err != nil {
return
}
auxDeletedAt, err := e.encodeTimestampNil(p, res.DeletedAt)
if err != nil {
return
}
auxDeletedBy, err := e.encodeRef(p, res.DeletedBy, "DeletedBy", node, tt)
if err != nil {
return
}
auxOwnedBy, err := e.encodeRef(p, res.OwnedBy, "OwnedBy", node, tt)
if err != nil {
return
}
auxRunAs, err := e.encodeRef(p, res.RunAs, "RunAs", node, tt)
if err != nil {
return
}
auxUpdatedAt, err := e.encodeTimestampNil(p, res.UpdatedAt)
if err != nil {
return
}
auxUpdatedBy, err := e.encodeRef(p, res.UpdatedBy, "UpdatedBy", node, tt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"createdAt", auxCreatedAt,
"createdBy", auxCreatedBy,
"deletedAt", auxDeletedAt,
"deletedBy", auxDeletedBy,
"enabled", res.Enabled,
"handle", res.Handle,
"id", res.ID,
"keepSessions", res.KeepSessions,
"meta", res.Meta,
"ownedBy", auxOwnedBy,
"paths", res.Paths,
"runAs", auxRunAs,
"scope", res.Scope,
"steps", res.Steps,
"trace", res.Trace,
"updatedAt", auxUpdatedAt,
"updatedBy", auxUpdatedBy,
)
if err != nil {
return
}
// Handle nested resources
var aux *yaml.Node
_ = aux
aux, err = e.encodeTriggers(ctx, p, tt.ChildrenForResourceType(node, types.TriggerResourceType), tt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"trigger", aux,
)
if err != nil {
return
}
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource trigger
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) encodeTriggers(ctx context.Context, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
var aux *yaml.Node
for _, n := range nodes {
aux, err = e.encodeTrigger(ctx, p, n, tt)
if err != nil {
return
}
out, err = y7s.AddSeq(out, aux)
if err != nil {
return
}
}
return
}
// encodeTrigger focuses on the specific resource invoked by the Encode method
func (e YamlEncoder) encodeTrigger(ctx context.Context, p envoyx.EncodeParams, node *envoyx.Node, tt envoyx.Traverser) (out *yaml.Node, err error) {
res := node.Resource.(*types.Trigger)
// Pre-compute some map values so we can omit error checking when encoding yaml nodes
auxCreatedAt, err := e.encodeTimestamp(p, res.CreatedAt)
if err != nil {
return
}
auxCreatedBy, err := e.encodeRef(p, res.CreatedBy, "CreatedBy", node, tt)
if err != nil {
return
}
auxDeletedAt, err := e.encodeTimestampNil(p, res.DeletedAt)
if err != nil {
return
}
auxDeletedBy, err := e.encodeRef(p, res.DeletedBy, "DeletedBy", node, tt)
if err != nil {
return
}
auxOwnedBy, err := e.encodeRef(p, res.OwnedBy, "OwnedBy", node, tt)
if err != nil {
return
}
auxUpdatedAt, err := e.encodeTimestampNil(p, res.UpdatedAt)
if err != nil {
return
}
auxUpdatedBy, err := e.encodeRef(p, res.UpdatedBy, "UpdatedBy", node, tt)
if err != nil {
return
}
auxWorkflowID, err := e.encodeRef(p, res.WorkflowID, "WorkflowID", node, tt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"constraints", res.Constraints,
"createdAt", auxCreatedAt,
"createdBy", auxCreatedBy,
"deletedAt", auxDeletedAt,
"deletedBy", auxDeletedBy,
"enabled", res.Enabled,
"eventType", res.EventType,
"id", res.ID,
"input", res.Input,
"meta", res.Meta,
"ownedBy", auxOwnedBy,
"resourceType", res.ResourceType,
"stepID", res.StepID,
"updatedAt", auxUpdatedAt,
"updatedBy", auxUpdatedBy,
"workflowID", auxWorkflowID,
)
if err != nil {
return
}
// Handle nested resources
var aux *yaml.Node
_ = aux
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Encoding utils
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) encodeTimestamp(p envoyx.EncodeParams, t time.Time) (any, error) {
if t.IsZero() {
return nil, nil
}
tz := p.Encoder.PreferredTimezone
if tz != "" {
tzL, err := time.LoadLocation(tz)
if err != nil {
return nil, err
}
t = t.In(tzL)
}
ly := p.Encoder.PreferredTimeLayout
if ly == "" {
ly = time.RFC3339
}
return t.Format(ly), nil
}
func (e YamlEncoder) encodeTimestampNil(p envoyx.EncodeParams, t *time.Time) (any, error) {
if t == nil {
return nil, nil
}
return e.encodeTimestamp(p, *t)
}
func (e YamlEncoder) encodeRef(p envoyx.EncodeParams, id uint64, field string, node *envoyx.Node, tt envoyx.Traverser) (any, error) {
parent := tt.ParentForRef(node, node.References[field])
// @todo should we panic instead?
// for now gracefully fallback to the ID
if parent == nil {
return id, nil
}
return parent.Identifiers.FriendlyIdentifier(), nil
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utility functions
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) getWriter(p envoyx.EncodeParams) (out io.Writer, err error) {
aux, ok := p.Params[paramsKeyWriter]
if ok {
out, ok = aux.(io.Writer)
if ok {
return
}
}
// @todo consider adding support for managing files from a location
err = errors.Errorf("YAML encoder expects a writer conforming to io.Writer interface")
return
}
func safeParentIdentifier(tt envoyx.Traverser, n *envoyx.Node, ref envoyx.Ref) (out string) {
aux := tt.ParentForRef(n, ref)
if aux == nil {
return ref.Identifiers.FriendlyIdentifier()
}
return aux.Identifiers.FriendlyIdentifier()
}

View File

@@ -0,0 +1,12 @@
package envoy
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"gopkg.in/yaml.v3"
)
func (e YamlEncoder) encode(ctx context.Context, base *yaml.Node, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
return
}

26
server/automation/model/init.gen.go generated Normal file
View File

@@ -0,0 +1,26 @@
package model
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/dal"
)
type (
modelReplacer interface {
ReplaceModel(ctx context.Context, model *dal.Model) (err error)
}
)
var (
models []*dal.Model
)
func Models() dal.ModelSet {
return models
}

555
server/automation/model/models.gen.go generated Normal file
View File

@@ -0,0 +1,555 @@
package model
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
)
var Session = &dal.Model{
Ident: "automation_sessions",
ResourceType: types.SessionResourceType,
Attributes: dal.AttributeSet{
&dal.Attribute{
Ident: "ID",
Type: &dal.TypeID{},
Store: &dal.CodecAlias{Ident: "id"},
},
&dal.Attribute{
Ident: "WorkflowID", Sortable: true,
Type: &dal.TypeRef{
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::automation:workflow",
},
},
Store: &dal.CodecAlias{Ident: "rel_workflow"},
},
&dal.Attribute{
Ident: "Status", Sortable: true,
Type: &dal.TypeNumber{HasDefault: true,
DefaultValue: 0,
Precision: -1, Scale: -1, Meta: map[string]interface{}{"rdbms:type": "integer"},
},
Store: &dal.CodecAlias{Ident: "status"},
},
&dal.Attribute{
Ident: "EventType", Sortable: true,
Type: &dal.TypeText{Length: 32},
Store: &dal.CodecAlias{Ident: "event_type"},
},
&dal.Attribute{
Ident: "ResourceType", Sortable: true,
Type: &dal.TypeText{Length: 64},
Store: &dal.CodecAlias{Ident: "resource_type"},
},
&dal.Attribute{
Ident: "Input",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "input"},
},
&dal.Attribute{
Ident: "Output",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "output"},
},
&dal.Attribute{
Ident: "Stacktrace",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "stacktrace"},
},
&dal.Attribute{
Ident: "CreatedBy",
Type: &dal.TypeRef{HasDefault: true,
DefaultValue: 0,
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:user",
},
},
Store: &dal.CodecAlias{Ident: "created_by"},
},
&dal.Attribute{
Ident: "CreatedAt", Sortable: true,
Type: &dal.TypeTimestamp{
DefaultCurrentTimestamp: true, Timezone: true, Precision: -1,
},
Store: &dal.CodecAlias{Ident: "created_at"},
},
&dal.Attribute{
Ident: "PurgeAt", Sortable: true,
Type: &dal.TypeTimestamp{Nullable: true, Timezone: true, Precision: -1},
Store: &dal.CodecAlias{Ident: "purge_at"},
},
&dal.Attribute{
Ident: "SuspendedAt", Sortable: true,
Type: &dal.TypeTimestamp{Nullable: true, Timezone: true, Precision: -1},
Store: &dal.CodecAlias{Ident: "suspended_at"},
},
&dal.Attribute{
Ident: "CompletedAt", Sortable: true,
Type: &dal.TypeTimestamp{Nullable: true, Timezone: true, Precision: -1},
Store: &dal.CodecAlias{Ident: "completed_at"},
},
&dal.Attribute{
Ident: "Error",
Type: &dal.TypeText{},
Store: &dal.CodecAlias{Ident: "error"},
},
},
Indexes: dal.IndexSet{
&dal.Index{
Ident: "automation_sessions_completedAt",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "CompletedAt",
},
},
},
&dal.Index{
Ident: "automation_sessions_createdAt",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "CreatedAt",
},
},
},
&dal.Index{
Ident: "automation_sessions_eventType",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "EventType",
},
},
},
&dal.Index{
Ident: "PRIMARY",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "ID",
},
},
},
&dal.Index{
Ident: "automation_sessions_resourceType",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "ResourceType",
},
},
},
&dal.Index{
Ident: "automation_sessions_status",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "Status",
},
},
},
&dal.Index{
Ident: "automation_sessions_suspendedAt",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "SuspendedAt",
},
},
},
},
}
var Trigger = &dal.Model{
Ident: "automation_triggers",
ResourceType: types.TriggerResourceType,
Attributes: dal.AttributeSet{
&dal.Attribute{
Ident: "ID",
Type: &dal.TypeID{},
Store: &dal.CodecAlias{Ident: "id"},
},
&dal.Attribute{
Ident: "WorkflowID", Sortable: true,
Type: &dal.TypeRef{
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::automation:workflow",
},
},
Store: &dal.CodecAlias{Ident: "rel_workflow"},
},
&dal.Attribute{
Ident: "StepID",
Type: &dal.TypeID{},
Store: &dal.CodecAlias{Ident: "rel_step"},
},
&dal.Attribute{
Ident: "Enabled", Sortable: true,
Type: &dal.TypeBoolean{HasDefault: true,
DefaultValue: true,
},
Store: &dal.CodecAlias{Ident: "enabled"},
},
&dal.Attribute{
Ident: "Meta",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "meta"},
},
&dal.Attribute{
Ident: "ResourceType", Sortable: true,
Type: &dal.TypeText{Length: 64},
Store: &dal.CodecAlias{Ident: "resource_type"},
},
&dal.Attribute{
Ident: "EventType", Sortable: true,
Type: &dal.TypeText{},
Store: &dal.CodecAlias{Ident: "event_type"},
},
&dal.Attribute{
Ident: "Constraints",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "constraints"},
},
&dal.Attribute{
Ident: "Input",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "input"},
},
&dal.Attribute{
Ident: "OwnedBy",
Type: &dal.TypeRef{HasDefault: true,
DefaultValue: 0,
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:user",
},
},
Store: &dal.CodecAlias{Ident: "owned_by"},
},
&dal.Attribute{
Ident: "CreatedAt", Sortable: true,
Type: &dal.TypeTimestamp{
DefaultCurrentTimestamp: true, Timezone: true, Precision: -1,
},
Store: &dal.CodecAlias{Ident: "created_at"},
},
&dal.Attribute{
Ident: "UpdatedAt", Sortable: true,
Type: &dal.TypeTimestamp{Nullable: true, Timezone: true, Precision: -1},
Store: &dal.CodecAlias{Ident: "updated_at"},
},
&dal.Attribute{
Ident: "DeletedAt", Sortable: true,
Type: &dal.TypeTimestamp{Nullable: true, Timezone: true, Precision: -1},
Store: &dal.CodecAlias{Ident: "deleted_at"},
},
&dal.Attribute{
Ident: "CreatedBy",
Type: &dal.TypeRef{HasDefault: true,
DefaultValue: 0,
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:user",
},
},
Store: &dal.CodecAlias{Ident: "created_by"},
},
&dal.Attribute{
Ident: "UpdatedBy",
Type: &dal.TypeRef{HasDefault: true,
DefaultValue: 0,
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:user",
},
},
Store: &dal.CodecAlias{Ident: "updated_by"},
},
&dal.Attribute{
Ident: "DeletedBy",
Type: &dal.TypeRef{HasDefault: true,
DefaultValue: 0,
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:user",
},
},
Store: &dal.CodecAlias{Ident: "deleted_by"},
},
},
Indexes: dal.IndexSet{
&dal.Index{
Ident: "PRIMARY",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "ID",
},
},
},
},
}
var Workflow = &dal.Model{
Ident: "automation_workflows",
ResourceType: types.WorkflowResourceType,
Attributes: dal.AttributeSet{
&dal.Attribute{
Ident: "ID",
Type: &dal.TypeID{},
Store: &dal.CodecAlias{Ident: "id"},
},
&dal.Attribute{
Ident: "Handle",
Type: &dal.TypeText{Length: 64},
Store: &dal.CodecAlias{Ident: "handle"},
},
&dal.Attribute{
Ident: "Meta",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "meta"},
},
&dal.Attribute{
Ident: "Enabled", Sortable: true,
Type: &dal.TypeBoolean{HasDefault: true,
DefaultValue: true,
},
Store: &dal.CodecAlias{Ident: "enabled"},
},
&dal.Attribute{
Ident: "Trace",
Type: &dal.TypeBoolean{HasDefault: true,
DefaultValue: false,
},
Store: &dal.CodecAlias{Ident: "trace"},
},
&dal.Attribute{
Ident: "KeepSessions",
Type: &dal.TypeNumber{HasDefault: true,
DefaultValue: 0,
Precision: -1, Scale: -1, Meta: map[string]interface{}{"rdbms:type": "integer"},
},
Store: &dal.CodecAlias{Ident: "keep_sessions"},
},
&dal.Attribute{
Ident: "Scope",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "scope"},
},
&dal.Attribute{
Ident: "Steps",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "steps"},
},
&dal.Attribute{
Ident: "Paths",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "paths"},
},
&dal.Attribute{
Ident: "Issues",
Type: &dal.TypeJSON{
DefaultValue: "{}",
},
Store: &dal.CodecAlias{Ident: "issues"},
},
&dal.Attribute{
Ident: "RunAs",
Type: &dal.TypeRef{HasDefault: true,
DefaultValue: 0,
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:user",
},
},
Store: &dal.CodecAlias{Ident: "run_as"},
},
&dal.Attribute{
Ident: "OwnedBy",
Type: &dal.TypeRef{HasDefault: true,
DefaultValue: 0,
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:user",
},
},
Store: &dal.CodecAlias{Ident: "owned_by"},
},
&dal.Attribute{
Ident: "CreatedAt", Sortable: true,
Type: &dal.TypeTimestamp{
DefaultCurrentTimestamp: true, Timezone: true, Precision: -1,
},
Store: &dal.CodecAlias{Ident: "created_at"},
},
&dal.Attribute{
Ident: "UpdatedAt", Sortable: true,
Type: &dal.TypeTimestamp{Nullable: true, Timezone: true, Precision: -1},
Store: &dal.CodecAlias{Ident: "updated_at"},
},
&dal.Attribute{
Ident: "DeletedAt", Sortable: true,
Type: &dal.TypeTimestamp{Nullable: true, Timezone: true, Precision: -1},
Store: &dal.CodecAlias{Ident: "deleted_at"},
},
&dal.Attribute{
Ident: "CreatedBy",
Type: &dal.TypeRef{HasDefault: true,
DefaultValue: 0,
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:user",
},
},
Store: &dal.CodecAlias{Ident: "created_by"},
},
&dal.Attribute{
Ident: "UpdatedBy",
Type: &dal.TypeRef{HasDefault: true,
DefaultValue: 0,
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:user",
},
},
Store: &dal.CodecAlias{Ident: "updated_by"},
},
&dal.Attribute{
Ident: "DeletedBy",
Type: &dal.TypeRef{HasDefault: true,
DefaultValue: 0,
RefAttribute: "id",
RefModel: &dal.ModelRef{
ResourceType: "corteza::system:user",
},
},
Store: &dal.CodecAlias{Ident: "deleted_by"},
},
},
Indexes: dal.IndexSet{
&dal.Index{
Ident: "PRIMARY",
Type: "BTREE",
Fields: []*dal.IndexField{
{
AttributeIdent: "ID",
},
},
},
},
}
func init() {
models = append(
models,
Session,
Trigger,
Workflow,
)
}

365
server/automation/rest.yaml Normal file
View File

@@ -0,0 +1,365 @@
---
# This is direct conversion from old api/*/spec.json files
# using https://www.json2yaml.com/
#
# Next step: swagger.
endpoints:
- title: Workflows
path: "/workflows"
entrypoint: workflow
authentication: []
imports:
- github.com/cortezaproject/corteza/server/pkg/expr
- github.com/cortezaproject/corteza/server/automation/types
- github.com/cortezaproject/corteza/server/pkg/label
- time
- labelTypes github.com/cortezaproject/corteza/server/pkg/label/types
apis:
- name: list
method: GET
title: List workflows
path: "/"
parameters:
get:
- { name: workflowID, type: "[]string", title: "Filter by workflow ID" }
- { name: query, type: "string", title: "Filter workflows" }
- { name: deleted, type: "uint", title: "Exclude (0, default), include (1) or return only (2) deleted workflows" }
- { name: disabled, type: "uint", title: "Exclude (0, default), include (1) or return only (2) disabled workflows" }
- { name: subWorkflow, type: "uint", title: "Exclude (0, default), include (1) or return only (2) sub workflows" }
- { name: labels, type: "map[string]labelTypes.LabelValue", title: "Labels", parser: "label.ParseStrings" }
- { name: limit, type: "uint", title: "Limit" }
- { name: incTotal, type: "bool", title: "Include total rows counter" }
- { name: pageCursor, type: "string", title: "Page cursor" }
- { name: sort, type: "string", title: "Sort items" }
- name: create
method: POST
title: Create workflow
path: "/"
parameters:
post:
- { name: handle, type: string, title: "Workflow name" }
- { name: labels, type: "map[string]labelTypes.LabelValue", title: "Labels", parser: "label.ParseStrings" }
- { name: meta, type: "*types.WorkflowMeta", title: "Workflow meta data", parser: "types.ParseWorkflowMeta" }
- { name: enabled, type: bool, title: "Is workflow enabled" }
- { name: trace, type: bool, title: "Trace workflow execution" }
- { name: keepSessions, type: int, title: "Keep old workflow sessions" }
- { name: scope, type: "*expr.Vars", title: "Workflow meta data", parser: "types.ParseWorkflowVariables" }
- { name: steps, type: "types.WorkflowStepSet", title: "Workflow steps definition", parser: "types.ParseWorkflowStepSet" }
- { name: paths, type: "types.WorkflowPathSet", title: "Workflow step paths definition", parser: "types.ParseWorkflowPathSet" }
- { name: runAs, type: uint64, required: true, title: "Is workflow enabled" }
- { name: ownedBy, type: uint64, required: true, title: "Owner of the workflow" }
- name: update
method: PUT
title: Update triger details
path: "/{workflowID}"
parameters:
path: [ { name: workflowID, type: uint64, required: true, title: "Workflow ID" } ]
post:
- { name: handle, type: string, title: "Workflow name" }
- { name: labels, type: "map[string]labelTypes.LabelValue", title: "Labels", parser: "label.ParseStrings" }
- { name: meta, type: "*types.WorkflowMeta", title: "Workflow meta data", parser: "types.ParseWorkflowMeta" }
- { name: enabled, type: bool, title: "Is workflow enabled" }
- { name: trace, type: bool, title: "Trace workflow execution" }
- { name: keepSessions, type: int, title: "Keep old workflow sessions" }
- { name: scope, type: "*expr.Vars", title: "Workflow meta data", parser: "types.ParseWorkflowVariables" }
- { name: steps, type: "types.WorkflowStepSet", title: "Workflow steps definition", parser: "types.ParseWorkflowStepSet" }
- { name: paths, type: "types.WorkflowPathSet", title: "Workflow step paths definition", parser: "types.ParseWorkflowPathSet" }
- { name: runAs, type: uint64, required: true, title: "Is workflow enabled" }
- { name: ownedBy, type: uint64, required: true, title: "Owner of the workflow" }
- { type: "*time.Time", name: updatedAt, required: false, title: Last update (or creation) date }
- name: read
method: GET
title: Read workflow details
path: "/{workflowID}"
parameters: { path: [ { name: workflowID, type: uint64, required: true, title: "Workflow ID" } ] }
- name: delete
method: DELETE
title: Remove workflow
path: "/{workflowID}"
parameters: { path: [ { name: workflowID, type: uint64, required: true, title: "Workflow ID" } ] }
- name: undelete
method: POST
title: Undelete workflow
path: "/{workflowID}/undelete"
parameters: { path: [ { name: workflowID, type: uint64, required: true, title: "Workflow ID" } ] }
- name: test
method: POST
title: Test workflow details
path: "/{workflowID}/test"
parameters:
path: [ { name: workflowID, type: uint64, required: true, title: "Workflow ID" } ]
post:
- { name: scope, type: "*expr.Vars", title: "Workflow meta data", parser: "types.ParseWorkflowVariables" }
- { name: runAs, type: bool, required: true, title: "Is workflow enabled" }
- name: exec
method: POST
title: Executes workflow on a specific step (must be orphan step and connected to 'onManual' trigger)
path: "/{workflowID}/exec"
parameters:
path: [ { name: workflowID, type: uint64, required: true, title: "Workflow ID" } ]
post:
- { name: stepID, type: uint64, required: true, title: "Step ID" }
- { name: input, type: "*expr.Vars", title: "Input", parser: "types.ParseWorkflowVariables" }
- { name: trace, type: bool, title: "Trace workflow execution" }
- { name: wait, type: bool, title: "Wait for workflow to complete" }
- { name: async, type: bool, title: "Execute step and return immediately" }
- title: Triggers
path: "/triggers"
entrypoint: trigger
authentication: []
imports:
- github.com/cortezaproject/corteza/server/automation/types
- github.com/cortezaproject/corteza/server/pkg/expr
- github.com/cortezaproject/corteza/server/pkg/label
- time
- labelTypes github.com/cortezaproject/corteza/server/pkg/label/types
apis:
- name: list
method: GET
title: List triggers
path: "/"
parameters:
get:
- { name: triggerID, type: "[]string", title: "Filter by trigger ID" }
- { name: workflowID, type: "[]string", title: "Filter by workflow ID" }
- { name: deleted, type: "uint", title: "Exclude (0, default), include (1) or return only (2) deleted triggers" }
- { name: disabled, type: "uint", title: "Exclude (0, default), include (1) or return only (2) disabled triggers" }
- { name: eventType, type: "string", title: "Filter triggers by event type" }
- { name: resourceType, type: "string", title: "Filter triggers by resource type" }
- { name: query, type: "string", title: "Filter workflows," }
- { name: labels, type: "map[string]labelTypes.LabelValue", title: "Labels", parser: "label.ParseStrings" }
- { name: limit, type: "uint", title: "Limit" }
- { name: pageCursor, type: "string", title: "Page cursor" }
- { name: sort, type: "string", title: "Sort items" }
- name: create
method: POST
title: Create trigger
path: "/"
parameters:
post:
- { name: eventType, type: string, required: true, title: "Event type" }
- { name: resourceType, type: string, required: true, title: "Resource type" }
- { name: enabled, type: bool, title: "Is trigger enabled" }
- { name: workflowID, type: uint64, required: true, title: "Workflow to be triggered" }
- { name: workflowStepID, type: uint64, required: true, title: "Start workflow in a specific step" }
- { name: input, type: "*expr.Vars", title: "Workflow meta data", parser: "types.ParseWorkflowVariables" }
- { name: labels, type: "map[string]labelTypes.LabelValue", title: "Labels", parser: "label.ParseStrings" }
- { name: meta, type: "*types.TriggerMeta", title: "Trigger meta data", parser: "types.ParseTriggerMeta" }
- { name: constraints, type: "types.TriggerConstraintSet", title: "Workflow steps definition", parser: "types.ParseTriggerConstraintSet" }
- { name: ownedBy, type: uint64, required: true, title: "Owner of the trigger" }
- name: update
method: PUT
title: Update trigger details
path: "/{triggerID}"
parameters:
path: [ { name: triggerID, type: uint64, required: true, title: "Trigger ID" } ]
post:
- { name: eventType, type: string, required: true, title: "Event type" }
- { name: resourceType, type: string, required: true, title: "Resource type" }
- { name: enabled, type: bool, title: "Is trigger enabled" }
- { name: workflowID, type: uint64, required: true, title: "Workflow to be triggered" }
- { name: workflowStepID, type: uint64, required: true, title: "Start workflow in a specific step" }
- { name: input, type: "*expr.Vars", title: "Workflow meta data", parser: "types.ParseWorkflowVariables" }
- { name: labels, type: "map[string]labelTypes.LabelValue", title: "Labels", parser: "label.ParseStrings" }
- { name: meta, type: "*types.TriggerMeta", title: "Trigger meta data", parser: "types.ParseTriggerMeta" }
- { name: constraints, type: "types.TriggerConstraintSet", title: "Workflow steps definition", parser: "types.ParseTriggerConstraintSet" }
- { name: ownedBy, type: uint64, required: true, title: "Owner of the trigger" }
- { type: "*time.Time", name: updatedAt, required: false, title: Last update (or creation) date }
- name: read
method: GET
title: Read trigger details
path: "/{triggerID}"
parameters: { path: [ { name: triggerID, type: uint64, required: true, title: "Trigger ID" } ] }
- name: delete
method: DELETE
title: Remove trigger
path: "/{triggerID}"
parameters: { path: [ { name: triggerID, type: uint64, required: true, title: "Trigger ID" } ] }
- name: undelete
method: POST
title: Undelete trigger
path: "/{triggerID}/undelete"
parameters: { path: [ { name: triggerID, type: uint64, required: true, title: "Trigger ID" } ] }
- title: Sessions
path: "/sessions"
entrypoint: session
authentication: []
imports:
- github.com/cortezaproject/corteza/server/pkg/expr
- github.com/cortezaproject/corteza/server/automation/types
apis:
- name: list
method: GET
title: List sessions
path: "/"
parameters:
get:
- { name: sessionID, type: "[]string", title: "Filter by session ID" }
- { name: workflowID, type: "[]string", title: "Filter by workflow ID" }
- { name: createdBy, type: "[]string", title: "Filter by creators ID" }
- { name: completed, type: "uint", title: "Exclude (0, default), include (1) or return only (2) completed sessions" }
- { name: status, type: "[]uint", title: "Filter by status: started (0), prompted (1), suspended (2), failed (3) and completed (4)" }
- { name: eventType, type: "string", title: "Filter event type" }
- { name: resourceType, type: "string", title: "Filter resource type" }
- { name: limit, type: "uint", title: "Limit" }
- { name: incTotal, type: "bool", title: "Include total rows counter" }
- { name: pageCursor, type: "string", title: "Page cursor" }
- { name: sort, type: "string", title: "Sort items" }
- name: read
method: GET
title: Read session details
path: "/{sessionID}"
parameters: { path: [ { name: sessionID, type: uint64, required: true, title: "Session ID" } ] }
- name: cancel
method: POST
title: Cancel session
path: "/{sessionID}/cancel"
parameters: { path: [ { name: sessionID, type: uint64, required: true, title: "Session ID" } ] }
# not yet implemented
#
# - name: delete
# method: DELETE
# title: Remove session
# path: "/{sessionID}"
# parameters: { path: [ { name: sessionID, type: uint64, required: true, title: "Session ID" } ] }
- name: listPrompts
method: GET
title: Returns pending prompts from all sessions
path: "/prompts"
- name: resumeState
method: POST
title: Resume session
path: "/{sessionID}/state/{stateID}"
parameters:
path:
- { name: sessionID, type: uint64, required: true, title: "Session ID" }
- { name: stateID, type: uint64, required: true, title: "State ID" }
post:
- { name: input, type: "*expr.Vars", title: "Prompt variables", parser: "types.ParseWorkflowVariables" }
# not yet implemented
#
# - name: deleteState
# method: DELETE
# title: Cancel session's state
# path: "/{sessionID}/state/{stateID}"
# parameters:
# path:
# - { name: sessionID, type: uint64, required: true, title: "Session ID" }
# - { name: stateID, type: uint64, required: true, title: "State ID" }
- title: Functions
path: "/functions"
entrypoint: function
authentication: []
apis:
- name: list
method: GET
title: Available workflow functions
path: "/"
- title: Types
path: "/types"
entrypoint: type
authentication: []
apis:
- name: list
method: GET
title: Available workflow types
path: "/"
- title: Event types
path: "/event-types"
entrypoint: eventTypes
authentication: []
apis:
- name: list
method: GET
title: Available workflow types
path: "/"
- title: Permissions
entrypoint: permissions
path: "/permissions"
authentication:
- Client ID
- Session ID
imports:
- github.com/cortezaproject/corteza/server/pkg/rbac
apis:
- name: list
path: "/"
method: GET
title: Retrieve defined permissions
- name: effective
path: "/effective"
method: GET
title: Effective rules for current user
parameters:
get:
- name: resource
type: string
required: false
title: Show only rules for a specific resource
- name: trace
path: "/trace"
method: GET
title: Evaluate rules for given user/role combo
parameters:
get:
- name: resource
type: "[]string"
required: false
title: Show only rules for a specific resource
- name: userID
type: uint64
required: false
- name: roleID
type: "[]uint64"
required: false
- name: read
path: "/{roleID}/rules"
method: GET
title: Retrieve role permissions
parameters:
path:
- name: roleID
type: uint64
required: true
title: Role ID
get:
- name: resource
type: "[]string"
required: false
title: Show only rules for a specific resource
- name: delete
path: "/{roleID}/rules"
method: DELETE
title: Remove all defined role permissions
parameters:
path:
- name: roleID
type: uint64
required: true
title: Role ID
- name: update
path: "/{roleID}/rules"
method: PATCH
title: Update permission settings
parameters:
path:
- name: roleID
type: uint64
required: true
title: Role ID
post:
- name: rules
type: rbac.RuleSet
required: true
title: List of permission rules to set

4255
server/automation/rest/eventTypes.gen.go generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/automation/service"
)
type (
EventTypes struct {
reg interface {
Types() []string
}
}
eventTypePayload struct {
Set []eventTypeDef `json:"set"`
}
eventTypeDef struct {
ResourceType string `json:"resourceType"`
EventType string `json:"eventType"`
Properties []eventTypePropertyDef `json:"properties"`
Constraints []eventTypeConstraintDef `json:"constraints"`
}
eventTypePropertyDef struct {
Name string `json:"name"`
Type string `json:"type"`
Immutable bool `json:"immutable"`
}
eventTypeConstraintDef struct {
Name string `json:"name"`
}
)
func (EventTypes) New() *EventTypes {
ctrl := &EventTypes{reg: service.Registry()}
return ctrl
}
func (ctrl EventTypes) List(_ context.Context, _ *request.EventTypesList) (interface{}, error) {
return eventTypePayload{Set: getEventTypeDefinitions()}, nil
}

View File

@@ -0,0 +1,29 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/automation/service"
"github.com/cortezaproject/corteza/server/automation/types"
)
type (
Function struct {
reg interface {
Functions() []*types.Function
}
}
functionSetPayload struct {
Set []*types.Function `json:"set"`
}
)
func (Function) New() *Function {
ctrl := &Function{reg: service.Registry()}
return ctrl
}
func (ctrl Function) List(_ context.Context, _ *request.FunctionList) (interface{}, error) {
return functionSetPayload{Set: ctrl.reg.Functions()}, nil
}

View File

@@ -0,0 +1,57 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
EventTypesAPI interface {
List(context.Context, *request.EventTypesList) (interface{}, error)
}
// HTTP API interface
EventTypes struct {
List func(http.ResponseWriter, *http.Request)
}
)
func NewEventTypes(h EventTypesAPI) *EventTypes {
return &EventTypes{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewEventTypesList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h EventTypes) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/event-types/", h.List)
})
}

View File

@@ -0,0 +1,57 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
FunctionAPI interface {
List(context.Context, *request.FunctionList) (interface{}, error)
}
// HTTP API interface
Function struct {
List func(http.ResponseWriter, *http.Request)
}
)
func NewFunction(h FunctionAPI) *Function {
return &Function{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewFunctionList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Function) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/functions/", h.List)
})
}

View File

@@ -0,0 +1,152 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
PermissionsAPI interface {
List(context.Context, *request.PermissionsList) (interface{}, error)
Effective(context.Context, *request.PermissionsEffective) (interface{}, error)
Trace(context.Context, *request.PermissionsTrace) (interface{}, error)
Read(context.Context, *request.PermissionsRead) (interface{}, error)
Delete(context.Context, *request.PermissionsDelete) (interface{}, error)
Update(context.Context, *request.PermissionsUpdate) (interface{}, error)
}
// HTTP API interface
Permissions struct {
List func(http.ResponseWriter, *http.Request)
Effective func(http.ResponseWriter, *http.Request)
Trace func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
}
)
func NewPermissions(h PermissionsAPI) *Permissions {
return &Permissions{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPermissionsList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Effective: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPermissionsEffective()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Effective(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Trace: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPermissionsTrace()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Trace(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPermissionsRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPermissionsDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewPermissionsUpdate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Permissions) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/permissions/", h.List)
r.Get("/permissions/effective", h.Effective)
r.Get("/permissions/trace", h.Trace)
r.Get("/permissions/{roleID}/rules", h.Read)
r.Delete("/permissions/{roleID}/rules", h.Delete)
r.Patch("/permissions/{roleID}/rules", h.Update)
})
}

View File

@@ -0,0 +1,133 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
SessionAPI interface {
List(context.Context, *request.SessionList) (interface{}, error)
Read(context.Context, *request.SessionRead) (interface{}, error)
Cancel(context.Context, *request.SessionCancel) (interface{}, error)
ListPrompts(context.Context, *request.SessionListPrompts) (interface{}, error)
ResumeState(context.Context, *request.SessionResumeState) (interface{}, error)
}
// HTTP API interface
Session struct {
List func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Cancel func(http.ResponseWriter, *http.Request)
ListPrompts func(http.ResponseWriter, *http.Request)
ResumeState func(http.ResponseWriter, *http.Request)
}
)
func NewSession(h SessionAPI) *Session {
return &Session{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSessionList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSessionRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Cancel: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSessionCancel()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Cancel(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ListPrompts: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSessionListPrompts()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ListPrompts(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
ResumeState: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSessionResumeState()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.ResumeState(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Session) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/sessions/", h.List)
r.Get("/sessions/{sessionID}", h.Read)
r.Post("/sessions/{sessionID}/cancel", h.Cancel)
r.Get("/sessions/prompts", h.ListPrompts)
r.Post("/sessions/{sessionID}/state/{stateID}", h.ResumeState)
})
}

View File

@@ -0,0 +1,152 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
TriggerAPI interface {
List(context.Context, *request.TriggerList) (interface{}, error)
Create(context.Context, *request.TriggerCreate) (interface{}, error)
Update(context.Context, *request.TriggerUpdate) (interface{}, error)
Read(context.Context, *request.TriggerRead) (interface{}, error)
Delete(context.Context, *request.TriggerDelete) (interface{}, error)
Undelete(context.Context, *request.TriggerUndelete) (interface{}, error)
}
// HTTP API interface
Trigger struct {
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Undelete func(http.ResponseWriter, *http.Request)
}
)
func NewTrigger(h TriggerAPI) *Trigger {
return &Trigger{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTriggerList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTriggerCreate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Create(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTriggerUpdate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTriggerRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTriggerDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Undelete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTriggerUndelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Undelete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Trigger) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/triggers/", h.List)
r.Post("/triggers/", h.Create)
r.Put("/triggers/{triggerID}", h.Update)
r.Get("/triggers/{triggerID}", h.Read)
r.Delete("/triggers/{triggerID}", h.Delete)
r.Post("/triggers/{triggerID}/undelete", h.Undelete)
})
}

View File

@@ -0,0 +1,57 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
TypeAPI interface {
List(context.Context, *request.TypeList) (interface{}, error)
}
// HTTP API interface
Type struct {
List func(http.ResponseWriter, *http.Request)
}
)
func NewType(h TypeAPI) *Type {
return &Type{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTypeList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Type) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/types/", h.List)
})
}

View File

@@ -0,0 +1,190 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/go-chi/chi/v5"
"net/http"
)
type (
// Internal API interface
WorkflowAPI interface {
List(context.Context, *request.WorkflowList) (interface{}, error)
Create(context.Context, *request.WorkflowCreate) (interface{}, error)
Update(context.Context, *request.WorkflowUpdate) (interface{}, error)
Read(context.Context, *request.WorkflowRead) (interface{}, error)
Delete(context.Context, *request.WorkflowDelete) (interface{}, error)
Undelete(context.Context, *request.WorkflowUndelete) (interface{}, error)
Test(context.Context, *request.WorkflowTest) (interface{}, error)
Exec(context.Context, *request.WorkflowExec) (interface{}, error)
}
// HTTP API interface
Workflow struct {
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Undelete func(http.ResponseWriter, *http.Request)
Test func(http.ResponseWriter, *http.Request)
Exec func(http.ResponseWriter, *http.Request)
}
)
func NewWorkflow(h WorkflowAPI) *Workflow {
return &Workflow{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWorkflowList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWorkflowCreate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Create(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWorkflowUpdate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWorkflowRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWorkflowDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Undelete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWorkflowUndelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Undelete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Test: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWorkflowTest()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Test(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Exec: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWorkflowExec()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Exec(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Workflow) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/workflows/", h.List)
r.Post("/workflows/", h.Create)
r.Put("/workflows/{workflowID}", h.Update)
r.Get("/workflows/{workflowID}", h.Read)
r.Delete("/workflows/{workflowID}", h.Delete)
r.Post("/workflows/{workflowID}/undelete", h.Undelete)
r.Post("/workflows/{workflowID}/test", h.Test)
r.Post("/workflows/{workflowID}/exec", h.Exec)
})
}

View File

@@ -0,0 +1,69 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/automation/service"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/rbac"
)
type (
Permissions struct {
ac permissionsAccessController
}
permissionsAccessController interface {
Effective(context.Context, ...rbac.Resource) rbac.EffectiveSet
Trace(context.Context, uint64, []uint64, ...string) ([]*rbac.Trace, error)
List() []map[string]string
FindRulesByRoleID(context.Context, uint64) (rbac.RuleSet, error)
FindRules(ctx context.Context, roleID uint64, rr ...string) (rbac.RuleSet, error)
Grant(ctx context.Context, rr ...*rbac.Rule) error
}
)
func (Permissions) New() *Permissions {
return &Permissions{
ac: service.DefaultAccessControl,
}
}
func (ctrl Permissions) Effective(ctx context.Context, r *request.PermissionsEffective) (interface{}, error) {
return ctrl.ac.Effective(ctx, types.Component{}), nil
}
func (ctrl Permissions) Trace(ctx context.Context, r *request.PermissionsTrace) (interface{}, error) {
return ctrl.ac.Trace(ctx, r.UserID, r.RoleID, r.Resource...)
}
func (ctrl Permissions) List(ctx context.Context, r *request.PermissionsList) (interface{}, error) {
return ctrl.ac.List(), nil
}
func (ctrl Permissions) Read(ctx context.Context, r *request.PermissionsRead) (interface{}, error) {
return ctrl.ac.FindRules(ctx, r.RoleID, r.Resource...)
}
func (ctrl Permissions) Delete(ctx context.Context, r *request.PermissionsDelete) (interface{}, error) {
rr, err := ctrl.ac.FindRulesByRoleID(ctx, r.RoleID)
if err != nil {
return nil, err
}
for _, r := range rr {
r.Access = rbac.Inherit
}
return api.OK(), ctrl.ac.Grant(ctx, rr...)
}
func (ctrl Permissions) Update(ctx context.Context, r *request.PermissionsUpdate) (interface{}, error) {
for _, rule := range r.Rules {
// Make sure everything is properly set
rule.RoleID = r.RoleID
}
return api.OK(), ctrl.ac.Grant(ctx, r.Rules...)
}

View File

@@ -0,0 +1,54 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
EventTypesList struct {
}
)
// NewEventTypesList request
func NewEventTypesList() *EventTypesList {
return &EventTypesList{}
}
// Auditable returns all auditable/loggable parameters
func (r EventTypesList) Auditable() map[string]interface{} {
return map[string]interface{}{}
}
// Fill processes request and fills internal variables
func (r *EventTypesList) Fill(req *http.Request) (err error) {
return err
}

View File

@@ -0,0 +1,54 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
FunctionList struct {
}
)
// NewFunctionList request
func NewFunctionList() *FunctionList {
return &FunctionList{}
}
// Auditable returns all auditable/loggable parameters
func (r FunctionList) Auditable() map[string]interface{} {
return map[string]interface{}{}
}
// Fill processes request and fills internal variables
func (r *FunctionList) Fill(req *http.Request) (err error) {
return err
}

View File

@@ -0,0 +1,384 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
PermissionsList struct {
}
PermissionsEffective struct {
// Resource GET parameter
//
// Show only rules for a specific resource
Resource string
}
PermissionsTrace struct {
// Resource GET parameter
//
// Show only rules for a specific resource
Resource []string
// UserID GET parameter
//
//
UserID uint64 `json:",string"`
// RoleID GET parameter
//
//
RoleID []uint64
}
PermissionsRead struct {
// RoleID PATH parameter
//
// Role ID
RoleID uint64 `json:",string"`
// Resource GET parameter
//
// Show only rules for a specific resource
Resource []string
}
PermissionsDelete struct {
// RoleID PATH parameter
//
// Role ID
RoleID uint64 `json:",string"`
}
PermissionsUpdate struct {
// RoleID PATH parameter
//
// Role ID
RoleID uint64 `json:",string"`
// Rules POST parameter
//
// List of permission rules to set
Rules rbac.RuleSet
}
)
// NewPermissionsList request
func NewPermissionsList() *PermissionsList {
return &PermissionsList{}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsList) Auditable() map[string]interface{} {
return map[string]interface{}{}
}
// Fill processes request and fills internal variables
func (r *PermissionsList) Fill(req *http.Request) (err error) {
return err
}
// NewPermissionsEffective request
func NewPermissionsEffective() *PermissionsEffective {
return &PermissionsEffective{}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsEffective) Auditable() map[string]interface{} {
return map[string]interface{}{
"resource": r.Resource,
}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsEffective) GetResource() string {
return r.Resource
}
// Fill processes request and fills internal variables
func (r *PermissionsEffective) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["resource"]; ok && len(val) > 0 {
r.Resource, err = val[0], nil
if err != nil {
return err
}
}
}
return err
}
// NewPermissionsTrace request
func NewPermissionsTrace() *PermissionsTrace {
return &PermissionsTrace{}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsTrace) Auditable() map[string]interface{} {
return map[string]interface{}{
"resource": r.Resource,
"userID": r.UserID,
"roleID": r.RoleID,
}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsTrace) GetResource() []string {
return r.Resource
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsTrace) GetUserID() uint64 {
return r.UserID
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsTrace) GetRoleID() []uint64 {
return r.RoleID
}
// Fill processes request and fills internal variables
func (r *PermissionsTrace) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["resource[]"]; ok {
r.Resource, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["resource"]; ok {
r.Resource, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["userID"]; ok && len(val) > 0 {
r.UserID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["roleID[]"]; ok {
r.RoleID, err = payload.ParseUint64s(val), nil
if err != nil {
return err
}
} else if val, ok := tmp["roleID"]; ok {
r.RoleID, err = payload.ParseUint64s(val), nil
if err != nil {
return err
}
}
}
return err
}
// NewPermissionsRead request
func NewPermissionsRead() *PermissionsRead {
return &PermissionsRead{}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsRead) Auditable() map[string]interface{} {
return map[string]interface{}{
"roleID": r.RoleID,
"resource": r.Resource,
}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsRead) GetRoleID() uint64 {
return r.RoleID
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsRead) GetResource() []string {
return r.Resource
}
// Fill processes request and fills internal variables
func (r *PermissionsRead) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["resource[]"]; ok {
r.Resource, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["resource"]; ok {
r.Resource, err = val, nil
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "roleID")
r.RoleID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewPermissionsDelete request
func NewPermissionsDelete() *PermissionsDelete {
return &PermissionsDelete{}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsDelete) Auditable() map[string]interface{} {
return map[string]interface{}{
"roleID": r.RoleID,
}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsDelete) GetRoleID() uint64 {
return r.RoleID
}
// Fill processes request and fills internal variables
func (r *PermissionsDelete) Fill(req *http.Request) (err error) {
{
var val string
// path params
val = chi.URLParam(req, "roleID")
r.RoleID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewPermissionsUpdate request
func NewPermissionsUpdate() *PermissionsUpdate {
return &PermissionsUpdate{}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsUpdate) Auditable() map[string]interface{} {
return map[string]interface{}{
"roleID": r.RoleID,
"rules": r.Rules,
}
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsUpdate) GetRoleID() uint64 {
return r.RoleID
}
// Auditable returns all auditable/loggable parameters
func (r PermissionsUpdate) GetRules() rbac.RuleSet {
return r.Rules
}
// Fill processes request and fills internal variables
func (r *PermissionsUpdate) Fill(req *http.Request) (err error) {
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// Caching 32MB to memory, the rest to disk
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
return err
} else if err == nil {
// Multipart params
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
//if val, ok := req.Form["rules[]"]; ok && len(val) > 0 {
// r.Rules, err = rbac.RuleSet(val), nil
// if err != nil {
// return err
// }
//}
}
{
var val string
// path params
val = chi.URLParam(req, "roleID")
r.RoleID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}

View File

@@ -0,0 +1,494 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
SessionList struct {
// SessionID GET parameter
//
// Filter by session ID
SessionID []string
// WorkflowID GET parameter
//
// Filter by workflow ID
WorkflowID []string
// CreatedBy GET parameter
//
// Filter by creators ID
CreatedBy []string
// Completed GET parameter
//
// Exclude (0, default), include (1) or return only (2) completed sessions
Completed uint
// Status GET parameter
//
// Filter by status: started (0), prompted (1), suspended (2), failed (3) and completed (4)
Status []uint
// EventType GET parameter
//
// Filter event type
EventType string
// ResourceType GET parameter
//
// Filter resource type
ResourceType string
// Limit GET parameter
//
// Limit
Limit uint
// IncTotal GET parameter
//
// Include total rows counter
IncTotal bool
// PageCursor GET parameter
//
// Page cursor
PageCursor string
// Sort GET parameter
//
// Sort items
Sort string
}
SessionRead struct {
// SessionID PATH parameter
//
// Session ID
SessionID uint64 `json:",string"`
}
SessionCancel struct {
// SessionID PATH parameter
//
// Session ID
SessionID uint64 `json:",string"`
}
SessionListPrompts struct {
}
SessionResumeState struct {
// SessionID PATH parameter
//
// Session ID
SessionID uint64 `json:",string"`
// StateID PATH parameter
//
// State ID
StateID uint64 `json:",string"`
// Input POST parameter
//
// Prompt variables
Input *expr.Vars
}
)
// NewSessionList request
func NewSessionList() *SessionList {
return &SessionList{}
}
// Auditable returns all auditable/loggable parameters
func (r SessionList) Auditable() map[string]interface{} {
return map[string]interface{}{
"sessionID": r.SessionID,
"workflowID": r.WorkflowID,
"createdBy": r.CreatedBy,
"completed": r.Completed,
"status": r.Status,
"eventType": r.EventType,
"resourceType": r.ResourceType,
"limit": r.Limit,
"incTotal": r.IncTotal,
"pageCursor": r.PageCursor,
"sort": r.Sort,
}
}
// Auditable returns all auditable/loggable parameters
func (r SessionList) GetSessionID() []string {
return r.SessionID
}
// Auditable returns all auditable/loggable parameters
func (r SessionList) GetWorkflowID() []string {
return r.WorkflowID
}
// Auditable returns all auditable/loggable parameters
func (r SessionList) GetCreatedBy() []string {
return r.CreatedBy
}
// Auditable returns all auditable/loggable parameters
func (r SessionList) GetCompleted() uint {
return r.Completed
}
// Auditable returns all auditable/loggable parameters
func (r SessionList) GetStatus() []uint {
return r.Status
}
// Auditable returns all auditable/loggable parameters
func (r SessionList) GetEventType() string {
return r.EventType
}
// Auditable returns all auditable/loggable parameters
func (r SessionList) GetResourceType() string {
return r.ResourceType
}
// Auditable returns all auditable/loggable parameters
func (r SessionList) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r SessionList) GetIncTotal() bool {
return r.IncTotal
}
// Auditable returns all auditable/loggable parameters
func (r SessionList) GetPageCursor() string {
return r.PageCursor
}
// Auditable returns all auditable/loggable parameters
func (r SessionList) GetSort() string {
return r.Sort
}
// Fill processes request and fills internal variables
func (r *SessionList) Fill(req *http.Request) (err error) {
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["sessionID[]"]; ok {
r.SessionID, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["sessionID"]; ok {
r.SessionID, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["workflowID[]"]; ok {
r.WorkflowID, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["workflowID"]; ok {
r.WorkflowID, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["createdBy[]"]; ok {
r.CreatedBy, err = val, nil
if err != nil {
return err
}
} else if val, ok := tmp["createdBy"]; ok {
r.CreatedBy, err = val, nil
if err != nil {
return err
}
}
if val, ok := tmp["completed"]; ok && len(val) > 0 {
r.Completed, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["status[]"]; ok {
r.Status, err = payload.ParseUints(val), nil
if err != nil {
return err
}
} else if val, ok := tmp["status"]; ok {
r.Status, err = payload.ParseUints(val), nil
if err != nil {
return err
}
}
if val, ok := tmp["eventType"]; ok && len(val) > 0 {
r.EventType, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["resourceType"]; ok && len(val) > 0 {
r.ResourceType, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["limit"]; ok && len(val) > 0 {
r.Limit, err = payload.ParseUint(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["incTotal"]; ok && len(val) > 0 {
r.IncTotal, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["sort"]; ok && len(val) > 0 {
r.Sort, err = val[0], nil
if err != nil {
return err
}
}
}
return err
}
// NewSessionRead request
func NewSessionRead() *SessionRead {
return &SessionRead{}
}
// Auditable returns all auditable/loggable parameters
func (r SessionRead) Auditable() map[string]interface{} {
return map[string]interface{}{
"sessionID": r.SessionID,
}
}
// Auditable returns all auditable/loggable parameters
func (r SessionRead) GetSessionID() uint64 {
return r.SessionID
}
// Fill processes request and fills internal variables
func (r *SessionRead) Fill(req *http.Request) (err error) {
{
var val string
// path params
val = chi.URLParam(req, "sessionID")
r.SessionID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewSessionCancel request
func NewSessionCancel() *SessionCancel {
return &SessionCancel{}
}
// Auditable returns all auditable/loggable parameters
func (r SessionCancel) Auditable() map[string]interface{} {
return map[string]interface{}{
"sessionID": r.SessionID,
}
}
// Auditable returns all auditable/loggable parameters
func (r SessionCancel) GetSessionID() uint64 {
return r.SessionID
}
// Fill processes request and fills internal variables
func (r *SessionCancel) Fill(req *http.Request) (err error) {
{
var val string
// path params
val = chi.URLParam(req, "sessionID")
r.SessionID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewSessionListPrompts request
func NewSessionListPrompts() *SessionListPrompts {
return &SessionListPrompts{}
}
// Auditable returns all auditable/loggable parameters
func (r SessionListPrompts) Auditable() map[string]interface{} {
return map[string]interface{}{}
}
// Fill processes request and fills internal variables
func (r *SessionListPrompts) Fill(req *http.Request) (err error) {
return err
}
// NewSessionResumeState request
func NewSessionResumeState() *SessionResumeState {
return &SessionResumeState{}
}
// Auditable returns all auditable/loggable parameters
func (r SessionResumeState) Auditable() map[string]interface{} {
return map[string]interface{}{
"sessionID": r.SessionID,
"stateID": r.StateID,
"input": r.Input,
}
}
// Auditable returns all auditable/loggable parameters
func (r SessionResumeState) GetSessionID() uint64 {
return r.SessionID
}
// Auditable returns all auditable/loggable parameters
func (r SessionResumeState) GetStateID() uint64 {
return r.StateID
}
// Auditable returns all auditable/loggable parameters
func (r SessionResumeState) GetInput() *expr.Vars {
return r.Input
}
// Fill processes request and fills internal variables
func (r *SessionResumeState) Fill(req *http.Request) (err error) {
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// Caching 32MB to memory, the rest to disk
if err = req.ParseMultipartForm(32 << 20); err != nil && err != http.ErrNotMultipart {
return err
} else if err == nil {
// Multipart params
if val, ok := req.MultipartForm.Value["input[]"]; ok {
r.Input, err = types.ParseWorkflowVariables(val)
if err != nil {
return err
}
} else if val, ok := req.MultipartForm.Value["input"]; ok {
r.Input, err = types.ParseWorkflowVariables(val)
if err != nil {
return err
}
}
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
if val, ok := req.Form["input[]"]; ok {
r.Input, err = types.ParseWorkflowVariables(val)
if err != nil {
return err
}
} else if val, ok := req.Form["input"]; ok {
r.Input, err = types.ParseWorkflowVariables(val)
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "sessionID")
r.SessionID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "stateID")
r.StateID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/payload"
"github.com/go-chi/chi/v5"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
_ = strings.ToLower
_ = io.EOF
_ = fmt.Errorf
_ = json.NewEncoder
)
type (
// Internal API interface
TypeList struct {
}
)
// NewTypeList request
func NewTypeList() *TypeList {
return &TypeList{}
}
// Auditable returns all auditable/loggable parameters
func (r TypeList) Auditable() map[string]interface{} {
return map[string]interface{}{}
}
// Fill processes request and fills internal variables
func (r *TypeList) Fill(req *http.Request) (err error) {
return err
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
package rest
import (
"github.com/go-chi/chi/v5"
"github.com/cortezaproject/corteza/server/automation/rest/handlers"
"github.com/cortezaproject/corteza/server/pkg/auth"
)
func MountRoutes() func(r chi.Router) {
return func(r chi.Router) {
// Protect all _private_ routes
r.Group(func(r chi.Router) {
r.Use(auth.HttpTokenValidator("api"))
handlers.NewWorkflow(Workflow{}.New()).MountRoutes(r)
handlers.NewTrigger(Trigger{}.New()).MountRoutes(r)
handlers.NewSession(Session{}.New()).MountRoutes(r)
handlers.NewFunction(Function{}.New()).MountRoutes(r)
handlers.NewType(Type{}.New()).MountRoutes(r)
handlers.NewPermissions(Permissions{}.New()).MountRoutes(r)
handlers.NewEventTypes(EventTypes{}.New()).MountRoutes(r)
})
}
}

View File

@@ -0,0 +1,161 @@
package rest
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/automation/service"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/automation"
cmpService "github.com/cortezaproject/corteza/server/compose/service"
cmpTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
type (
Session struct {
svc sessionService
// cross-link with compose service to load module on resolved records
svcModule interface {
FindByID(ctx context.Context, namespaceID, moduleID uint64) (*cmpTypes.Module, error)
}
}
sessionService interface {
Search(ctx context.Context, filter types.SessionFilter) (types.SessionSet, types.SessionFilter, error)
LookupByID(ctx context.Context, sessionID uint64) (*types.Session, error)
Resume(sessionID, stateID uint64, i auth.Identifiable, input *expr.Vars) error
PendingPrompts(context.Context) []*wfexec.PendingPrompt
Cancel(context.Context, uint64) error
}
sessionSetPayload struct {
Filter types.SessionFilter `json:"filter"`
Set []*sessionSetItem `json:"set"`
}
sessionSetItem struct {
*types.Session
// Make sure stacktrace is not included in the list
Stacktrace *struct{} `json:"stacktrace,omitempty"`
// Make sure output values are not included in the list
Output *struct{} `json:"output,omitempty"`
// Make sure input values are not included in the list
Input *struct{} `json:"input,omitempty"`
}
)
func (Session) New() *Session {
ctrl := &Session{}
ctrl.svc = service.DefaultSession
ctrl.svcModule = cmpService.DefaultModule
return ctrl
}
func (ctrl Session) List(ctx context.Context, r *request.SessionList) (interface{}, error) {
var (
err error
f = types.SessionFilter{
WorkflowID: r.WorkflowID,
SessionID: r.SessionID,
CreatedBy: r.CreatedBy,
EventType: r.EventType,
ResourceType: r.ResourceType,
Completed: filter.State(r.Completed),
Status: r.Status,
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
// fixes issue with sorting of status column and pagination
// need to improve on cursor for this
if f.Paging.PageCursor != nil {
for _, status := range r.Status {
f.Paging.PageCursor.Set("status", status, false)
}
}
f.IncTotal = r.IncTotal
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.svc.Search(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl Session) Read(ctx context.Context, r *request.SessionRead) (interface{}, error) {
return ctrl.svc.LookupByID(ctx, r.SessionID)
}
func (ctrl Session) Cancel(ctx context.Context, r *request.SessionCancel) (interface{}, error) {
return true, ctrl.svc.Cancel(ctx, r.SessionID)
}
func (ctrl Session) ListPrompts(ctx context.Context, r *request.SessionListPrompts) (interface{}, error) {
return struct {
Set []*wfexec.PendingPrompt `json:"set"`
}{
Set: ctrl.svc.PendingPrompts(ctx),
}, nil
}
func (ctrl Session) ResumeState(ctx context.Context, r *request.SessionResumeState) (_ interface{}, err error) {
if r.Input != nil {
if err = r.Input.ResolveTypes(service.Registry().Type); err != nil {
return nil, err
}
}
// Now when all types are resolved we have to load modules and link them to records
//
// Very naive approach for now.
//
// @note copied from https://github.com/cortezaproject/corteza/blob/2023.9.x/server/automation/rest/workflow.go#L189
// copied to reduce the need for some dependency; should be good enough for now
r.Input.Each(func(k string, v expr.TypedValue) error {
switch c := v.(type) {
case *automation.ComposeRecord:
rec := c.GetValue()
if rec == nil {
return nil
}
mod, err := ctrl.svcModule.FindByID(ctx, rec.NamespaceID, rec.ModuleID)
if err != nil {
return fmt.Errorf("failed to resolve ComposeRecord type: %w", err)
}
c.GetValue().SetModule(mod)
}
return nil
})
return api.OK(), ctrl.svc.Resume(r.SessionID, r.StateID, auth.GetIdentityFromContext(ctx), r.Input)
}
func (ctrl Session) makeFilterPayload(ctx context.Context, ss types.SessionSet, f types.SessionFilter, err error) (*sessionSetPayload, error) {
if err != nil {
return nil, err
}
out := &sessionSetPayload{Filter: f, Set: make([]*sessionSetItem, len(ss))}
for i, s := range ss {
s.Stacktrace = nil
out.Set[i] = &sessionSetItem{Session: s}
}
return out, nil
}

View File

@@ -0,0 +1,121 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/automation/service"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
Trigger struct {
svc interface {
Search(ctx context.Context, filter types.TriggerFilter) (types.TriggerSet, types.TriggerFilter, error)
LookupByID(ctx context.Context, triggerID uint64) (*types.Trigger, error)
Create(ctx context.Context, new *types.Trigger) (*types.Trigger, error)
Update(ctx context.Context, upd *types.Trigger) (*types.Trigger, error)
DeleteByID(ctx context.Context, triggerID uint64) error
UndeleteByID(ctx context.Context, triggerID uint64) error
}
}
triggerSetPayload struct {
Filter types.TriggerFilter `json:"filter"`
Set types.TriggerSet `json:"set"`
}
)
func (Trigger) New() *Trigger {
ctrl := &Trigger{}
ctrl.svc = service.DefaultTrigger
return ctrl
}
func (ctrl Trigger) List(ctx context.Context, r *request.TriggerList) (interface{}, error) {
var (
err error
f = types.TriggerFilter{
WorkflowID: r.WorkflowID,
TriggerID: r.TriggerID,
EventType: r.EventType,
ResourceType: r.ResourceType,
Labels: r.Labels,
Deleted: filter.State(r.Deleted),
Disabled: filter.State(r.Disabled),
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.svc.Search(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl Trigger) Create(ctx context.Context, r *request.TriggerCreate) (interface{}, error) {
trigger := &types.Trigger{
Enabled: r.Enabled,
WorkflowID: r.WorkflowID,
StepID: r.WorkflowStepID,
ResourceType: r.ResourceType,
EventType: r.EventType,
Constraints: r.Constraints,
Input: r.Input,
Labels: r.Labels,
OwnedBy: r.OwnedBy,
Meta: r.Meta,
}
return ctrl.svc.Create(ctx, trigger)
}
func (ctrl Trigger) Update(ctx context.Context, r *request.TriggerUpdate) (interface{}, error) {
trigger := &types.Trigger{
ID: r.TriggerID,
Enabled: r.Enabled,
WorkflowID: r.WorkflowID,
StepID: r.WorkflowStepID,
ResourceType: r.ResourceType,
EventType: r.EventType,
Constraints: r.Constraints,
Input: r.Input,
Labels: r.Labels,
OwnedBy: r.OwnedBy,
Meta: r.Meta,
UpdatedAt: r.UpdatedAt,
}
return ctrl.svc.Update(ctx, trigger)
}
func (ctrl Trigger) Read(ctx context.Context, r *request.TriggerRead) (interface{}, error) {
return ctrl.svc.LookupByID(ctx, r.TriggerID)
}
func (ctrl Trigger) Delete(ctx context.Context, r *request.TriggerDelete) (interface{}, error) {
return api.OK(), ctrl.svc.DeleteByID(ctx, r.TriggerID)
}
func (ctrl Trigger) Undelete(ctx context.Context, r *request.TriggerUndelete) (interface{}, error) {
return api.OK(), ctrl.svc.UndeleteByID(ctx, r.TriggerID)
}
func (ctrl Trigger) makeFilterPayload(ctx context.Context, uu types.TriggerSet, f types.TriggerFilter, err error) (*triggerSetPayload, error) {
if err != nil {
return nil, err
}
if len(uu) == 0 {
uu = make([]*types.Trigger, 0)
}
return &triggerSetPayload{Filter: f, Set: uu}, nil
}

View File

@@ -0,0 +1,28 @@
package rest
import (
"context"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/automation/service"
)
type (
Type struct {
reg interface {
Types() []string
}
}
typeSetPayload struct {
Set []string `json:"set"`
}
)
func (Type) New() *Type {
ctrl := &Type{reg: service.Registry()}
return ctrl
}
func (ctrl Type) List(_ context.Context, _ *request.TypeList) (interface{}, error) {
return typeSetPayload{Set: ctrl.reg.Types()}, nil
}

View File

@@ -0,0 +1,249 @@
package rest
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/automation/rest/request"
"github.com/cortezaproject/corteza/server/automation/service"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/automation"
cmpService "github.com/cortezaproject/corteza/server/compose/service"
cmpTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/api"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
Workflow struct {
svc interface {
Search(ctx context.Context, filter types.WorkflowFilter) (types.WorkflowSet, types.WorkflowFilter, error)
LookupByID(ctx context.Context, workflowID uint64) (*types.Workflow, error)
Create(ctx context.Context, new *types.Workflow) (*types.Workflow, error)
Update(ctx context.Context, upd *types.Workflow) (*types.Workflow, error)
DeleteByID(ctx context.Context, workflowID uint64) error
UndeleteByID(ctx context.Context, workflowID uint64) error
Exec(ctx context.Context, workflowID uint64, p types.WorkflowExecParams) (*expr.Vars, uint64, types.Stacktrace, error)
}
// cross-link with compose service to load module on resolved records
svcModule interface {
FindByID(ctx context.Context, namespaceID, moduleID uint64) (*cmpTypes.Module, error)
}
ac workflowAccessControl
}
workflowAccessControl interface {
CanGrant(context.Context) bool
CanUpdateWorkflow(context.Context, *types.Workflow) bool
CanDeleteWorkflow(context.Context, *types.Workflow) bool
CanUndeleteWorkflow(context.Context, *types.Workflow) bool
CanExecuteWorkflow(context.Context, *types.Workflow) bool
CanManageTriggersOnWorkflow(context.Context, *types.Workflow) bool
CanManageSessionsOnWorkflow(context.Context, *types.Workflow) bool
}
workflowPayload struct {
*types.Workflow
CanGrant bool `json:"canGrant"`
CanUpdateWorkflow bool `json:"canUpdateWorkflow"`
CanDeleteWorkflow bool `json:"canDeleteWorkflow"`
CanUndeleteWorkflow bool `json:"canUndeleteWorkflow"`
CanExecuteWorkflow bool `json:"canExecuteWorkflow"`
CanManageWorkflowTriggers bool `json:"canManageWorkflowTriggers"`
CanManageWorkflowSessions bool `json:"canManageWorkflowSessions"`
}
workflowSetPayload struct {
Filter types.WorkflowFilter `json:"filter"`
Set []*workflowPayload `json:"set"`
}
workflowExecPayload struct {
Results *expr.Vars `json:"results"`
Trace types.Stacktrace `json:"trace,omitempty"`
SessionID uint64 `json:"sessionID,string,omitempty"`
Error string `json:"error,omitempty"`
}
)
func (Workflow) New() *Workflow {
ctrl := &Workflow{}
ctrl.svc = service.DefaultWorkflow
ctrl.svcModule = cmpService.DefaultModule
ctrl.ac = service.DefaultAccessControl
return ctrl
}
func (ctrl Workflow) List(ctx context.Context, r *request.WorkflowList) (interface{}, error) {
var (
err error
f = types.WorkflowFilter{
WorkflowID: r.WorkflowID,
Query: r.Query,
Labels: r.Labels,
Deleted: filter.State(r.Deleted),
Disabled: filter.State(r.Disabled),
SubWorkflow: filter.State(r.SubWorkflow),
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
f.IncTotal = r.IncTotal
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.svc.Search(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl Workflow) Create(ctx context.Context, r *request.WorkflowCreate) (interface{}, error) {
workflow := &types.Workflow{
Handle: r.Handle,
Labels: r.Labels,
Meta: r.Meta,
Enabled: r.Enabled,
Trace: r.Trace,
KeepSessions: r.KeepSessions,
Scope: r.Scope,
Steps: r.Steps,
Paths: r.Paths,
RunAs: r.RunAs,
OwnedBy: r.OwnedBy,
}
wf, err := ctrl.svc.Create(ctx, workflow)
return ctrl.makePayload(ctx, wf, err)
}
func (ctrl Workflow) Update(ctx context.Context, r *request.WorkflowUpdate) (interface{}, error) {
workflow := &types.Workflow{
ID: r.WorkflowID,
Handle: r.Handle,
Labels: r.Labels,
Meta: r.Meta,
Enabled: r.Enabled,
Trace: r.Trace,
KeepSessions: r.KeepSessions,
Scope: r.Scope,
Steps: r.Steps,
Paths: r.Paths,
RunAs: r.RunAs,
OwnedBy: r.OwnedBy,
UpdatedAt: r.UpdatedAt,
}
wf, err := ctrl.svc.Update(ctx, workflow)
return ctrl.makePayload(ctx, wf, err)
}
func (ctrl Workflow) Read(ctx context.Context, r *request.WorkflowRead) (interface{}, error) {
wf, err := ctrl.svc.LookupByID(ctx, r.WorkflowID)
return ctrl.makePayload(ctx, wf, err)
}
func (ctrl Workflow) Test(ctx context.Context, r *request.WorkflowTest) (interface{}, error) {
return nil, fmt.Errorf("not implemented")
}
func (ctrl Workflow) Delete(ctx context.Context, r *request.WorkflowDelete) (interface{}, error) {
return api.OK(), ctrl.svc.DeleteByID(ctx, r.WorkflowID)
}
func (ctrl Workflow) Undelete(ctx context.Context, r *request.WorkflowUndelete) (interface{}, error) {
return api.OK(), ctrl.svc.UndeleteByID(ctx, r.WorkflowID)
}
func (ctrl Workflow) Exec(ctx context.Context, r *request.WorkflowExec) (interface{}, error) {
var (
wep = &workflowExecPayload{}
err error
execParams = types.WorkflowExecParams{
StepID: r.StepID,
Trace: r.Trace,
Input: r.Input,
Async: r.Async,
Wait: r.Wait,
}
)
if execParams.Input != nil {
if err = execParams.Input.ResolveTypes(service.Registry().Type); err != nil {
return nil, err
}
}
// Now when all types are resolved we have to load modules and link them to records
//
// Very naive approach for now.
execParams.Input.Each(func(k string, v expr.TypedValue) error {
switch c := v.(type) {
case *automation.ComposeRecord:
rec := c.GetValue()
if rec == nil {
return nil
}
mod, err := ctrl.svcModule.FindByID(ctx, rec.NamespaceID, rec.ModuleID)
if err != nil {
return fmt.Errorf("failed to resolve ComposeRecord type: %w", err)
}
c.GetValue().SetModule(mod)
}
return nil
})
wep.Results, wep.SessionID, wep.Trace, err = ctrl.svc.Exec(ctx, r.WorkflowID, execParams)
if err != nil && wep.Trace != nil && r.Trace {
// in case of an error & trace enabled (and stacktrace present)
// we'll suppress the error
wep.Error = err.Error()
return wep, nil
}
return wep, err
}
func (ctrl Workflow) makeFilterPayload(ctx context.Context, set types.WorkflowSet, f types.WorkflowFilter, err error) (*workflowSetPayload, error) {
if err != nil {
return nil, err
}
wfsp := &workflowSetPayload{Filter: f, Set: make([]*workflowPayload, len(set))}
for i, wf := range set {
wfsp.Set[i], _ = ctrl.makePayload(ctx, wf, nil)
}
return wfsp, nil
}
func (ctrl Workflow) makePayload(ctx context.Context, wf *types.Workflow, err error) (*workflowPayload, error) {
if err != nil {
return nil, err
}
return &workflowPayload{
Workflow: wf,
CanGrant: ctrl.ac.CanGrant(ctx),
CanUpdateWorkflow: ctrl.ac.CanUpdateWorkflow(ctx, wf),
CanDeleteWorkflow: ctrl.ac.CanDeleteWorkflow(ctx, wf),
CanUndeleteWorkflow: ctrl.ac.CanUndeleteWorkflow(ctx, wf),
CanExecuteWorkflow: ctrl.ac.CanExecuteWorkflow(ctx, wf),
CanManageWorkflowTriggers: ctrl.ac.CanManageTriggersOnWorkflow(ctx, wf),
CanManageWorkflowSessions: ctrl.ac.CanManageSessionsOnWorkflow(ctx, wf),
}, nil
}

View 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
}

View 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
}

View 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"

View 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 &registry{
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
}

View 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
}

View 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
}

View 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
}

View 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"

View 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"])
}

View 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
}

View 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
}

View 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"

View 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")
}

View 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,
)
}
}
}

File diff suppressed because it is too large Load Diff

View 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"

View 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
}

View 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
}

View 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)
})
}

View File

@@ -0,0 +1,118 @@
package automation
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
session: {
features: {
labels: false
}
model: {
// length for the lang is now a bit shorter
// Reason for that is supported index length in MySQL
ident: "automation_sessions"
attributes: {
id: schema.IdField
workflow_id: {
sortable: true,
ident: "workflowID",
goType: "uint64",
storeIdent: "rel_workflow"
dal: { type: "Ref", refModelResType: "corteza::automation:workflow" }
}
status: {
sortable: true,
goType: "types.SessionStatus"
dal: { type: "Number", default: 0, meta: { "rdbms:type": "integer" } }
omitSetter: true
omitGetter: true
}
event_type: {
sortable: true,
goType: "string"
dal: { length: 32 }
}
resource_type: {
sortable: true,
goType: "string"
dal: { length: 64 }
}
input: {
goType: "*expr.Vars"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
output: {
goType: "*expr.Vars"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
stacktrace: {
goType: "types.Stacktrace"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
created_by: schema.AttributeUserRef
created_at: schema.SortableTimestampNowField
purge_at: schema.SortableTimestampNilField
suspended_at: schema.SortableTimestampNilField
completed_at: schema.SortableTimestampNilField
error: {
dal: {}
}
}
indexes: {
"primary": { attribute: "id" }
"completed_at": { attribute: "completed_at" }
"created_at": { attribute: "created_at" }
"event_type": { attribute: "event_type" }
"resource_type": { attribute: "resource_type" }
"status": { attribute: "status" }
"suspended_at": { attribute: "suspended_at" }
"resource_type": { attribute: "resource_type" }
}
}
envoy: {
omit: true
}
filter: {
struct: {
session_id: { goType: "[]string", storeIdent: "id", ident: "sessionID" }
completed: { schema.SortableTimestampNilField, storeIdent: "completed_at" }
created_by: { goType: "[]string" }
status: { goType: "[]uint" }
workflow_id: { goType: "[]string", storeIdent: "rel_workflow", ident: "workflowID" }
event_type: { goType: "string" }
resource_type: { goType: "string" }
}
byValue: ["status", "session_id", "workflow_id", "event_type", "resource_type", "created_by"]
byNilState: ["completed"]
}
store: {
ident: "automationSession"
api: {
lookups: [
{
fields: ["id"]
description: """
searches for session by ID
It returns session even if deleted
"""
}
]
}
}
}

View File

@@ -0,0 +1,129 @@
package automation
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
)
trigger: {
model: {
ident: "automation_triggers"
attributes: {
id: schema.IdField
workflow_id: {
sortable: true,
ident: "workflowID",
goType: "uint64",
storeIdent: "rel_workflow"
dal: { type: "Ref", refModelResType: "corteza::automation:workflow" }
}
step_id: {
ident: "stepID",
goType: "uint64",
storeIdent: "rel_step"
dal: { type: "ID" }
envoy: {
yaml: {
identKeyAlias: ["stepID", "step_id"]
}
}
}
enabled: {
sortable: true,
goType: "bool"
dal: { type: "Boolean", default: true }
}
meta: {
goType: "*types.TriggerMeta"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
resource_type: {
sortable: true,
goType: "string"
dal: { length: 64 }
envoy: {
yaml: {
identKeyAlias: ["resourceType", "resource_type"]
}
}
}
event_type: {
sortable: true,
goType: "string"
dal: {}
envoy: {
yaml: {
identKeyAlias: ["eventType", "event_type"]
}
}
}
constraints: {
goType: "types.TriggerConstraintSet"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
input: {
goType: "*expr.Vars"
dal: { type: "JSON", defaultEmptyObject: true }
omitSetter: true
omitGetter: true
}
owned_by: schema.AttributeUserRef
created_at: schema.SortableTimestampNowField
updated_at: schema.SortableTimestampNilField
deleted_at: schema.SortableTimestampNilField
created_by: schema.AttributeUserRef
updated_by: schema.AttributeUserRef
deleted_by: schema.AttributeUserRef
}
indexes: {
"primary": { attribute: "id" }
}
}
envoy: {
yaml: {
supportMappedInput: false
identKeyAlias: []
}
store: {
customFilterBuilder: true
}
}
filter: {
struct: {
deleted: { goType: "filter.State", storeIdent: "deleted_at" }
disabled: { goType: "filter.State", storeIdent: "enabled" }
trigger_id: { goType: "[]uint64", ident: "triggerID", storeIdent: "id" }
workflow_id: { goType: "[]uint64", ident: "workflowID", storeIdent: "rel_workflow" }
event_type: { goType: "string" }
resource_type: { goType: "string" }
}
byValue: ["trigger_id", "workflow_id", "event_type", "resource_type"]
byNilState: ["deleted"]
byFalseState: ["disabled"]
}
store: {
ident: "automationTrigger"
api: {
lookups: [
{
fields: ["id"]
description: """
searches for trigger by ID
It returns trigger even if deleted
"""
}
]
}
}
}

View File

@@ -0,0 +1,55 @@
package types
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"go.uber.org/zap"
)
type (
debugStep struct {
wfexec.StepIdentifier
log *zap.Logger
}
)
// DebugStep creates a step that logs entire contents of the scope
func DebugStep(log *zap.Logger) *debugStep {
return &debugStep{
log: log,
}
}
// Executes debug step
func (s debugStep) Exec(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
var (
fields = make([]zap.Field, 0)
// We want to be sure to actually get the logger that will print out debug info
// so we need to use logger directly from workflow and not the one we (could)
// get from context by wfexec.
log = s.log.With(
logger.Uint64("sessionID", r.SessionID),
logger.Uint64("stateID", r.StateID),
logger.Uint64("stepID", s.ID()),
)
)
if r.Scope == nil {
fields = append(fields, zap.Int("scopeKeyCount", 0))
} else {
fields = append(fields, zap.Int("scopeKeyCount", r.Scope.Len()))
_ = r.Scope.Each(func(k string, v expr.TypedValue) error {
fields = append(fields, zap.Any(k, v.Get()))
fields = append(fields, zap.Any(k+"Type", v.Type()))
return nil
})
}
log.Debug("debug", fields...)
return &expr.Vars{}, nil
}

View File

@@ -0,0 +1,62 @@
package types
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"time"
)
type (
delayStep struct {
wfexec.StepIdentifier
args ExprSet
now func() time.Time
}
)
// DelayStep creates a step that logs entire contents of the scope
func DelayStep(args ExprSet) *delayStep {
return &delayStep{
now: func() time.Time { return time.Now() },
args: args,
}
}
// Executes delay step
func (s delayStep) Exec(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
// session will set "resumed" on input when step is resumed
if !r.Input.Has("resumed") {
// not yet resumed
var until = s.now()
var (
result, err = s.args.Eval(ctx, r.Scope)
)
if err != nil {
return nil, err
}
const (
absArgName = "timestamp"
relArgName = "offset"
)
switch {
case result.Has(absArgName):
abs, _ := expr.NewDateTime(expr.Must(result.Select(absArgName)))
return wfexec.Delay(abs.GetValue().Add(0)), nil
case result.Has(relArgName):
rel, _ := expr.NewDuration(expr.Must(result.Select(relArgName)))
return wfexec.Delay(until.Add(rel.GetValue())), nil
}
return nil, errors.InvalidData("failed to execute delay step")
}
return wfexec.Resume(), nil
}

View File

@@ -0,0 +1,24 @@
package types
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
type (
errorHandlerStep struct {
wfexec.StepIdentifier
handler wfexec.Step
results *expr.Vars
}
)
func ErrorHandlerStep(h wfexec.Step, rr *expr.Vars) *errorHandlerStep {
return &errorHandlerStep{handler: h, results: rr}
}
// Exec errorHandler step
func (h errorHandlerStep) Exec(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
return wfexec.ErrorHandler(h.handler, h.results), nil
}

View File

@@ -0,0 +1,248 @@
package types
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
)
type (
// Used for expression steps, arguments/results mapping and for input validation
Expr struct {
// Variable name to set results of the expression to
Target string `json:"target"`
// Source of the value / name of the variable from scope; if set value is copied target
//
// Takes precedence to value
//
Source string `json:"source,omitempty"`
// Expression to evaluate over the input variables; results will be set to target
//
Expr string `json:"expr,omitempty"`
// Raw value to be set to target
//
// If expression is set and fails, evaluation defaults to value and suppresses the error
Value interface{} `json:"value,omitempty"`
eval expr.Evaluable
// Expected type of the input value
Type string `json:"type,omitempty"`
typ expr.Type
// Set of tests that can be run before input is evaluated and result copied to scope
Tests TestSet `json:"tests,omitempty"`
}
ExprSet []*Expr
// WorkflowStepExpression is created from WorkflowStep with kind=expressions
expressionsStep struct {
wfexec.StepIdentifier
Set ExprSet
}
)
func NewExpr(target, typ, expr string) (e *Expr, err error) {
return &Expr{Expr: expr, Target: target, Type: typ}, nil
}
func NewTypedExpr(target string, expr string, typ expr.Type) *Expr {
return &Expr{Expr: expr, Target: target, typ: typ}
}
func (e *Expr) SetType(fn func(string) (expr.Type, error)) error {
if typ, err := fn(e.Type); err != nil {
return err
} else {
e.typ = typ
return nil
}
}
func (e Expr) GetExpr() string { return e.Expr }
func (e *Expr) SetEval(eval expr.Evaluable) { e.eval = eval }
func (e Expr) Eval(ctx context.Context, scope *expr.Vars) (interface{}, error) {
return e.eval.Eval(ctx, scope)
}
func (e Expr) Test(ctx context.Context, scope *expr.Vars) (bool, error) {
return e.eval.Test(ctx, scope)
}
func (set ExprSet) GetByTarget(t string) *Expr {
for _, e := range set {
if e.Target == t {
return e
}
}
return nil
}
func (set ExprSet) Validate(ctx context.Context, in *expr.Vars) (TestSet, error) {
var (
out TestSet
vv TestSet
err error
// Copy/create scope
scope = (&expr.Vars{}).MustMerge(in)
)
for _, e := range set {
vv, err = e.Tests.Validate(ctx, scope)
if err != nil {
return nil, err
}
out = append(out, vv...)
}
return out, nil
}
// Eval on expression set (ExprSet) evaluates all expressions in the set and returns new scope with all set targets
func (set ExprSet) Eval(ctx context.Context, in *expr.Vars) (*expr.Vars, error) {
var (
err error
// Copy input to scope
scope = (&expr.Vars{}).MustMerge(in)
// Prepare output scope
out, _ = expr.NewVars(nil)
// Untyped evaluation result
value interface{}
knownType = func(p expr.Type) bool {
return p != nil && p.Type() != expr.Any{}.Type() && p.Type() != expr.Unresolved{}.Type()
}
)
for _, e := range set {
value = e.Value
if e.typ == nil {
return nil, errors.Internal("type for target %q not initialized", e.Target)
}
err = func() (err error) {
if len(e.Source) > 0 {
// can copy from existing variable
if !scope.Has(e.Source) {
return errors.NotFound("variable %q does not exist", e.Source)
}
value, err = expr.Select(scope, e.Source)
return
}
if len(e.Expr) > 0 {
if e.eval == nil {
// no expression set, fallback to default value
return errors.Internal("expression language for target %q not initialized", e.Target)
} else if value, err = e.eval.Eval(ctx, scope); err != nil {
return errors.Internal("expression %q failed: %s", e.Expr, err.Error()).Wrap(err)
}
}
return
}()
if err != nil && e.Value == nil {
return nil, err
}
typedValue, is := value.(expr.TypedValue)
if !is {
// value to be assigned (evaled, copied..) is not typed!
// try to figure out what we can do
if !knownType(e.typ) {
// Expression does not have type set
if out.Has(e.Target) {
t, _ := out.Select(e.Target)
typedValue, err = t.Cast(value)
if err != nil {
return nil, fmt.Errorf("cannot cast value %T to %s: %w", value, e.typ.Type(), err)
}
} else {
typedValue, err = expr.Typify(value)
if err != nil {
return nil, fmt.Errorf("cannot cast value %T to %s: %w", value, e.typ.Type(), err)
}
}
} else if typedValue, err = e.typ.Cast(value); err != nil {
return nil, fmt.Errorf("cannot cast value %T to %s (target %s): %w", value, e.typ.Type(), e.Target, err)
}
}
if !knownType(e.typ) && !knownType(typedValue) && typedValue.Type() != e.typ.Type() {
// Both, expression & value have type set;
// check if it's the same type or return an error
return nil, fmt.Errorf("cannot set to %q (type %s) value of type %s", e.Target, e.typ.Type(), typedValue.Type())
}
if e.typ != nil {
// @note handling special case for when we're dealing with arrays but in reality
// we're expecting specific types (function results).
// If we're dealing with an array but the expression doesn't want an array,
// make it want an array.
typ := e.typ
array := &expr.Array{}
if typedValue.Type() == array.Type() && typ.Type() != array.Type() {
typ = array
}
if !knownType(typedValue) {
// Expression has fixed type but value does not
// cast the value of evaluation to type of the expressicason
if typedValue, err = typ.Cast(value); err != nil {
return nil, err
}
} else if typ.Type() != typedValue.Type() && typ.Type() != (expr.Any{}).Type() {
//
if typedValue, err = typ.Cast(value); err != nil {
return nil, err
}
}
}
// Set result of the expression to scope
//
// Set() fn handles multi-level path (eg "base.level1.level2")
// that can set result of the expression deep into scope's value
if err = expr.Assign(scope, e.Target, typedValue); err != nil {
return nil, err
}
// Take base of the path (1st part) and
// copy value of it to output scope
//
// This ensures us that the entire variable
// from the original scope will be present in the output
scope.Copy(out, expr.PathBase(e.Target))
}
return out, nil
}
func ExpressionsStep(ee ...*Expr) *expressionsStep {
return &expressionsStep{Set: ee}
}
func (s expressionsStep) Exec(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
result, err := s.Set.Eval(ctx, r.Scope.MustMerge(r.Input))
if err != nil {
return nil, err
}
return result, nil
}

View File

@@ -0,0 +1,175 @@
package types
import (
"context"
"testing"
"time"
. "github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/stretchr/testify/require"
)
func TestExprSet_Eval(t *testing.T) {
var (
ctx = context.Background()
tt = time.Date(1999, 9, 9, 9, 9, 9, 9, time.UTC)
cc = []struct {
name string
set ExprSet
input map[string]interface{}
output map[string]interface{}
err string
}{
{
name: "empty",
set: ExprSet{},
output: make(map[string]interface{}),
},
{
name: "constant assignment",
set: ExprSet{&Expr{Target: "foo", Expr: `format("%s", bar)`, typ: &String{}}},
input: map[string]interface{}{"bar": Must(NewDateTime(tt))},
output: map[string]interface{}{"foo": Must(NewString("1999-09-09 09:09:09.000000009 +0000 UTC"))},
},
{
name: "constant assignment nil datetime",
set: ExprSet{&Expr{Target: "foo", Expr: `format("%s", bar)`, typ: &String{}}},
input: map[string]interface{}{"bar": Must(NewDateTime(nil))},
output: map[string]interface{}{"foo": Must(NewString("<nil>"))},
},
{
name: "vars with path",
set: ExprSet{&Expr{Target: "l1.l2", Expr: `"bar"`}},
input: map[string]interface{}{"l1": map[string]interface{}{}},
output: map[string]interface{}{"l1": map[string]interface{}{"l2": Must(Typify("bar"))}},
},
{
name: "copy vars with same types",
set: ExprSet{
&Expr{Target: "aa", Value: "vv", typ: &String{}},
&Expr{Target: "bb", Source: "aa", typ: &String{}},
},
output: map[string]interface{}{
"aa": Must(NewString("vv")),
"bb": Must(NewString("vv")),
},
},
{
name: "copy var with type",
set: ExprSet{
&Expr{Target: "aa", Value: "should be always String", typ: &String{}},
&Expr{Target: "bb", Source: "aa"},
},
output: map[string]interface{}{
"aa": Must(NewString("should be always String")),
"bb": Must(NewString("should be always String")),
},
},
{
name: "copy var to target with type",
set: ExprSet{
&Expr{Target: "aa", Value: "42", typ: &String{}},
&Expr{Target: "bb", Source: "aa", typ: &Integer{}},
},
output: map[string]interface{}{
"aa": Must(NewString("42")),
"bb": Must(NewInteger(42)),
},
},
{
name: "assign into incompatible",
set: ExprSet{
&Expr{Target: "aa", Value: "foo", typ: &String{}},
&Expr{Target: "bb", Source: "aa", typ: &Integer{}},
},
err: "unable to cast \"foo\" of type string to int64",
},
{
name: "deep set into generated type",
set: ExprSet{
&Expr{Target: "a", typ: &KV{}},
&Expr{Target: "a.b", Value: "c", typ: &String{}},
},
output: map[string]interface{}{
"a": Must(NewKV(map[string]string{
"b": "c",
})),
},
},
{
name: "slice push",
set: ExprSet{
&Expr{Target: "arr", typ: &Array{}, Expr: `[]`},
&Expr{Target: "arr", typ: &Array{}, Expr: `push(arr, "foo")`},
},
output: map[string]interface{}{
"arr": Must(NewArray([]string{
"foo",
})),
},
},
{
name: "slice push w/o type",
set: ExprSet{
&Expr{Target: "arr", typ: &Array{}, Expr: `[]`},
&Expr{Target: "arr", typ: &Any{}, Expr: `push(arr, "foo")`},
},
output: map[string]interface{}{
"arr": Must(NewArray([]string{
"foo",
})),
},
},
{
name: "slice create & push w/o type",
set: ExprSet{
&Expr{Target: "arr", typ: &Array{}, Expr: `push([], "foo")`},
},
output: map[string]interface{}{
"arr": Must(NewArray([]TypedValue{
Must(Typify("foo")),
})),
},
},
{
name: "vars with nested path",
set: ExprSet{&Expr{Target: "t1.t11", typ: &String{}, Expr: `e1.e11`}},
input: map[string]interface{}{"t1": map[string]interface{}{}, "e1": map[string]interface{}{"e11": Must(Typify("bar"))}},
output: map[string]interface{}{"t1": map[string]interface{}{"t11": Must(Typify("bar"))}},
},
}
)
for _, c := range cc {
t.Run(c.name, func(t *testing.T) {
var (
req = require.New(t)
)
for _, e := range c.set {
if e.Expr != "" {
req.NoError(NewGvalParser().ParseEvaluators(e))
}
if e.typ == nil {
e.typ = Any{}
}
}
var (
aux, _ = NewVars(c.input)
output, err = c.set.Eval(ctx, aux)
)
if c.err == "" {
req.NoError(err)
} else {
req.Error(err, c.err)
return
}
req.Equal(Must(Typify(c.output)), output)
})
}
}

View File

@@ -0,0 +1,199 @@
package types
import (
"context"
"fmt"
"time"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"go.uber.org/zap"
)
type (
FunctionHandler func(ctx context.Context, in *expr.Vars) (*expr.Vars, error)
IteratorHandler func(ctx context.Context, in *expr.Vars) (wfexec.IteratorHandler, error)
// workflow functions are defined in the core code
Function struct {
Ref string `json:"ref,omitempty"`
Kind string `json:"kind,omitempty"`
Meta *FunctionMeta `json:"meta,omitempty"`
Parameters ParamSet `json:"parameters,omitempty"`
Results ParamSet `json:"results,omitempty"`
Handler FunctionHandler `json:"-"`
Iterator IteratorHandler `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
Disabled bool `json:"disabled,omitempty"`
}
FunctionMeta struct {
Short string `json:"short,omitempty"`
Description string `json:"description,omitempty"`
Visual map[string]interface{} `json:"visual,omitempty"`
}
functionStep struct {
wfexec.StepIdentifier
def *Function
arguments ExprSet
results ExprSet
}
iteratorStep struct {
wfexec.StepIdentifier
def *Function
arguments ExprSet
results ExprSet
next wfexec.Step
exit wfexec.Step
}
)
const (
FunctionKindFunction = "function"
FunctionKindIterator = "iterator"
)
// FunctionStep initializes new function step with function definition and configured arguments and results
func FunctionStep(def *Function, arguments, results ExprSet) (*functionStep, error) {
if def.Kind != FunctionKindFunction {
return nil, fmt.Errorf("expecting function kind")
}
return &functionStep{def: def, arguments: arguments, results: results}, nil
}
// Exec executes function step
//
// Configured arguments are evaluated with the step's scope and input and passed
// to the function
//
// Configured results are evaluated with the results from the function and final scope is then returned
func (f functionStep) Exec(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
var (
started = time.Now()
args, results *expr.Vars
err error
log = logger.ContextValue(ctx, zap.NewNop()).With(
zap.String("functionRef", f.def.Ref),
zap.String("functionKind", "function"),
)
)
defer func() {
log := log.With(zap.Duration("execTime", time.Now().Sub(started)))
if err == nil {
log.Debug("executed")
} else {
log.Warn("executed with errors", zap.Error(err))
}
}()
ctx = logger.ContextWithValue(ctx, log)
if len(f.arguments) > 0 {
// Arguments defined, get values from scope and use them when calling
// function/handler
args, err = f.arguments.Eval(ctx, r.Scope.MustMerge(r.Input))
if err != nil {
return nil, err
}
}
results, err = f.def.Handler(ctx, args)
if err != nil {
return nil, err
}
if len(f.results) == 0 {
// No results defined, nothing to return
return expr.NewVars(nil)
}
results, err = f.results.Eval(ctx, results)
if err != nil {
return nil, err
}
return results, nil
}
// IteratorStep initializes new iterator step with function (iterator) and other parameters
//
// Pointers to next and exit steps are are given and then passed on to the iterator
func IteratorStep(def *Function, arguments, results ExprSet, next, exit wfexec.Step) (*iteratorStep, error) {
if def.Kind != FunctionKindIterator {
return nil, fmt.Errorf("expecting iterator kind")
}
return &iteratorStep{
def: def,
arguments: arguments,
results: results,
next: next,
exit: exit,
}, nil
}
// Exec configures and returns generic iterator
//
// This fn is executed only once, before 1st iteration. From there on, session executor uses Iterator to
// iterate over all items, elements or content that iterator serves it through Next() fn
//
// See also EvalResults
func (f *iteratorStep) Exec(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
var (
started = time.Now()
args *expr.Vars
err error
ih wfexec.IteratorHandler
log = logger.ContextValue(ctx, zap.NewNop()).With(
zap.String("functionRef", f.def.Ref),
zap.String("functionKind", "iterator"),
)
)
defer func() {
log := log.With(zap.Duration("execTime", time.Now().Sub(started)))
if err == nil {
log.Debug("executed")
} else {
log.Warn("executed with errors", zap.Error(err))
}
}()
if len(f.arguments) > 0 {
// Arguments defined, get values from scope and use them when calling
// iterator/handler
args, err = f.arguments.Eval(ctx, r.Scope.MustMerge(r.Input))
if err != nil {
return nil, err
}
}
if ih, err = f.def.Iterator(ctx, args); err != nil {
return nil, err
}
return wfexec.GenericIterator(f, f.next, f.exit, ih), nil
}
// EvalResults is called from iterator's Next() fn.
//
// It prepares scope for each iteration by evaluating results from the iterator function
func (f *iteratorStep) EvalResults(ctx context.Context, results *expr.Vars) (*expr.Vars, error) {
if results.Len() == 0 || len(f.results) == 0 {
// No results or result expressions defined, nothing to return
return &expr.Vars{}, nil
}
return f.results.Eval(ctx, results)
}

Some files were not shown because too many files have changed in this diff Show More