Files
higress/plugins/wasm-go/extensions/api-workflow/workflow/workflow.go
2024-10-09 19:52:16 +08:00

326 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package workflow
import (
"fmt"
"strings"
"api-workflow/utils"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
const (
TaskTypeHTTP string = "http"
TaskStart string = "start"
TaskEnd string = "end"
TaskContinue string = "continue"
UseContextFlag string = "||"
AllFlag string = "@all"
)
type PluginConfig struct {
// @Title zh-CN 工作流
// @Description zh-CN 工作流的具体描述
Workflow Workflow `json:"workflow" yaml:"workflow"`
// @Title zh-CN 环境变量
// @Description zh-CN 用来定义整个工作流的环境变量
Env Env `json:"env" yaml:"env"`
}
type Env struct {
// @Title zh-CN 超时时间
// @Description zh-CN 用来定义工作流的超时时间,单位是毫秒
Timeout uint32 `json:"timeout" yaml:"timeout"`
// @Title zh-CN 最大迭代深度
// @Description zh-CN 用来定义工作流最大的迭代深度默认是100
MaxDepth uint32 `json:"max_depth" yaml:"max_depth"`
}
type Workflow struct {
// @Title zh-CN 边的列表
// @Description zh-CN 边的列表
Edges []Edge `json:"edges" yaml:"edges"`
// @Title zh-CN 节点的列表
// @Description zh-CN 节点的列表
Nodes map[string]Node `json:"nodes" yaml:"nodes"`
// @Title zh-CN 工作流的状态
// @Description zh-CN 工作流的执行状态用于记录node之间的相互依赖和执行情况
WorkflowExecStatus map[string]int `json:"-" yaml:"-"`
}
type Edge struct {
// @Title zh-CN 上一步节点
// @Description zh-CN 上一步节点必须是定义node的name或者初始化工作流的start
Source string `json:"source" yaml:"source"`
// @Title zh-CN 当前执行的节点
// @Description zh-CN 当前执行节点必须是定义的node的name或者结束工作流的关键字 end continue
Target string `json:"target" yaml:"target"`
// @Title zh-CN 执行操作
// @Description zh-CN 执行单元,里面实时封装需要的数据
Task *Task
// @Title zh-CN 判断表达式
// @Description zh-CN 是否执行下一步的判断条件
Conditional string `json:"conditional" yaml:"conditional"`
}
type Task struct {
Cluster wrapper.Cluster `json:"-" yaml:"-"`
ServicePath string `json:"service_path" yaml:"service_path"`
ServicePort int64 `json:"service_port" yaml:"service_port"`
ServiceKey string `json:"service_key" yaml:"service_key"`
Body []byte `json:"-" yaml:"-"`
Headers [][2]string `json:"headers" yaml:"headers"`
Method string `json:"method" yaml:"method"`
TaskType string `json:"task_type" yaml:"task_type"`
}
type Node struct {
// @Title zh-CN 节点名称
// @Description zh-CN 节点名称全局唯一
Name string `json:"name" yaml:"name"`
// @Title zh-CN 服务名称
// @Description zh-CN 带服务类型的完整名称,例如 my.dns or foo.static
ServiceName string `json:"service_name" yaml:"service_name"`
// @Title zh-CN 服务端口
// @Description zh-CN static类型默认是80
ServicePort int64 `json:"service_port" yaml:"service_port"`
// @Title zh-CN 服务域名
// @Description zh-CN 服务域名,例如 dashscope.aliyuncs.com
ServiceDomain string `json:"service_domain" yaml:"service_domain"`
// @Title zh-CN http访问路径
// @Description zh-CN http访问路径默认是 /
ServicePath string `json:"service_path" yaml:"service_path"`
// @Title zh-CN http 方法
// @Description zh-CN http方法支持所有可用方法 GETPOST等
ServiceMethod string `json:"service_method" yaml:"service_method"`
// @Title zh-CN http 请求头文件
// @Description zh-CN 请求头文件
ServiceHeaders []ServiceHeader `json:"service_headers" yaml:"service_headers"`
// @Title zh-CN http 请求body模板
// @Description zh-CN 请求body模板用来构造请求
ServiceBodyTmpl string `json:"service_body_tmpl" yaml:"service_body_tmpl"`
// @Title zh-CN http 请求body模板替换键值对
// @Description zh-CN 请求body模板替换键值对用来构造请求。to表示填充的位置from表示数据从哪里
// 标识表达式基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串
ServiceBodyReplaceKeys []BodyReplaceKeyPair `json:"service_body_replace_keys" yaml:"service_body_replace_keys"`
}
type BodyReplaceKeyPair struct {
// @Title zh-CN from表示数据从哪里
// @Description zh-CN from表示数据从哪里
// 标识表达式基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串
From string `json:"from" yaml:"from"`
// @Title zh-CN to表示填充的位置
// @Description zh-CN to表示填充的位置
// 标识表达式基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串
To string `json:"to" yaml:"to"`
}
type ServiceHeader struct {
Key string `json:"key" yaml:"key"`
Value string `json:"value" yaml:"value"`
}
func (w *Edge) IsEnd() bool {
if w.Target == TaskEnd {
return true
}
return false
}
func (w *Edge) IsContinue() bool {
if w.Target == TaskContinue {
return true
}
return false
}
func (e *Edge) IsPass(ctx wrapper.HttpContext) (bool, error) {
// 执行判断Conditional
if e.Conditional != "" {
var err error
// 获取模板里的表达式
e.Conditional, err = e.WrapperDataByTmplStr(e.Conditional, ctx)
if err != nil {
return false, fmt.Errorf("workflow WrapperDateByTmplStr %s failed: %v", e.Conditional, err)
}
ok, err := e.ExecConditional()
if err != nil {
return false, fmt.Errorf("wl exec conditional %s failed: %v", e.Conditional, err)
}
return !ok, nil
}
return false, nil
}
func (w *Edge) WrapperTask(config PluginConfig, ctx wrapper.HttpContext) error {
// 判断 node 是否存在
node, isTool := config.Workflow.Nodes[w.Target]
if isTool {
w.Task.TaskType = TaskTypeHTTP
} else {
return fmt.Errorf("do not find target :%s", w.Target)
}
switch w.Task.TaskType {
default:
return fmt.Errorf("unknown node type :%s", w.Task.TaskType)
case TaskTypeHTTP:
err := w.wrapperNodeTask(node, ctx)
if err != nil {
return err
}
}
return nil
}
func (w *Edge) wrapperBody(requestBodyTemplate string, keyPairs []BodyReplaceKeyPair, ctx wrapper.HttpContext) error {
requestBody, err := w.WrapperDataByTmplStrAndKeys(requestBodyTemplate, keyPairs, ctx)
if err != nil {
return fmt.Errorf("wrapper date by tmpl str is %s ,find err: %v", requestBodyTemplate, err)
}
w.Task.Body = requestBody
return nil
}
func (w *Edge) wrapperNodeTask(node Node, ctx wrapper.HttpContext) error {
// 封装cluster
w.Task.Cluster = wrapper.FQDNCluster{
Host: node.ServiceDomain,
FQDN: node.ServiceName,
Port: node.ServicePort,
}
// 封装请求body
err := w.wrapperBody(node.ServiceBodyTmpl, node.ServiceBodyReplaceKeys, ctx)
if err != nil {
return fmt.Errorf("wrapper body parse failed: %v", err)
}
// 封装请求Method path headers
w.Task.Method = node.ServiceMethod
w.Task.ServicePath = node.ServicePath
w.Task.Headers = make([][2]string, 0)
if len(node.ServiceHeaders) > 0 {
for _, header := range node.ServiceHeaders {
w.Task.Headers = append(w.Task.Headers, [2]string{header.Key, header.Value})
}
}
return nil
}
// 利用模板和替换键值对构造请求,使用`||`分隔str1代表使用node是执行结果。tr2代表如何取数据使用gjson的表达式`@all`代表全都要
func (w *Edge) WrapperDataByTmplStrAndKeys(tmpl string, keyPairs []BodyReplaceKeyPair, ctx wrapper.HttpContext) ([]byte, error) {
var err error
// 不需要替换 node.service_body_replace_keys 为空
if len(keyPairs) == 0 {
return []byte(tmpl), nil
}
for _, keyPair := range keyPairs {
jsonPath := keyPair.From
target := keyPair.To
var contextValueRaw []byte
// 获取上下文数据
if strings.Contains(jsonPath, UseContextFlag) {
pathStr := strings.Split(jsonPath, UseContextFlag)
if len(pathStr) == 2 {
contextKey := pathStr[0]
contextBody := ctx.GetContext(contextKey)
if contextValue, ok := contextBody.([]byte); ok {
contextValueRaw = contextValue
jsonPath = pathStr[1]
} else {
return nil, fmt.Errorf("context value is not []byte,key is %s", contextKey)
}
}
}
// 执行封装 `@all`代表全都要
requestBody := gjson.ParseBytes(contextValueRaw)
if jsonPath == AllFlag {
tmpl, err = sjson.SetRaw(tmpl, target, requestBody.Raw)
if err != nil {
return nil, fmt.Errorf("wrapper body parse failed: %v", err)
}
continue
}
requestBodyJson := requestBody.Get(jsonPath)
if requestBodyJson.Exists() {
tmpl, err = sjson.SetRaw(tmpl, target, requestBodyJson.Raw)
if err != nil {
return nil, fmt.Errorf("wrapper body parse failed: %v", err)
}
} else {
return nil, fmt.Errorf("wrapper body parse failed: not exists %s", jsonPath)
}
}
return []byte(tmpl), nil
}
// 变量使用`{{str1||str2}}`包裹,使用`||`分隔str1代表使用node是执行结果。tr2代表如何取数据使用gjson的表达式`@all`代表全都要
func (w *Edge) WrapperDataByTmplStr(tmpl string, ctx wrapper.HttpContext) (string, error) {
var body []byte
// 获取模板里的表达式
TmplKeyAndPath := utils.ParseTmplStr(tmpl)
if len(TmplKeyAndPath) == 0 {
return tmpl, nil
}
// 解析表达式 { "{{str1||str2}}":"str1||str2" }
for k, path := range TmplKeyAndPath {
// 变量使用`{{str1||str2}}`包裹,使用`||`分隔str1代表使用前面命名为name的数据()。
if strings.Contains(path, UseContextFlag) {
pathStr := strings.Split(path, UseContextFlag)
if len(pathStr) == 2 {
contextKey := pathStr[0]
contextBody := ctx.GetContext(contextKey)
if contextValue, ok := contextBody.([]byte); ok {
body = contextValue
path = pathStr[1]
} else {
return tmpl, fmt.Errorf("context value is not []byte,key is %s", contextKey)
}
}
// 执行封装 `@all`代表全都要
requestBody := gjson.ParseBytes(body)
if path == AllFlag {
tmpl = strings.Replace(tmpl, k, utils.TrimQuote(requestBody.Raw), -1)
continue
}
requestBodyJson := requestBody.Get(path)
if requestBodyJson.Exists() {
tmpl = utils.ReplacedStr(tmpl, map[string]string{k: utils.TrimQuote(requestBodyJson.Raw)})
} else {
return tmpl, fmt.Errorf("use path {{%s}} get value is not exists,json is:%s", path, requestBody.Raw)
}
} else {
return "", fmt.Errorf("tmpl parse find error: || is not exists %s", path)
}
}
return tmpl, nil
}
func (w *Edge) ExecConditional() (bool, error) {
ConditionalResult, err := utils.ExecConditionalStr(w.Conditional)
if err != nil {
return false, fmt.Errorf("exec conditional failed: %v", err)
}
return ConditionalResult, nil
}