mirror of
https://github.com/alibaba/higress.git
synced 2026-03-02 23:51:11 +08:00
feat: add api-workflow plugin (#1229)
This commit is contained in:
325
plugins/wasm-go/extensions/api-workflow/workflow/workflow.go
Normal file
325
plugins/wasm-go/extensions/api-workflow/workflow/workflow.go
Normal file
@@ -0,0 +1,325 @@
|
||||
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方法,支持所有可用方法 GET,POST等
|
||||
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
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user