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