mirror of
https://github.com/alibaba/higress.git
synced 2026-02-06 23:21:08 +08:00
feat: 🎸 add frontend gray plugin (#1120)
Co-authored-by: Kent Dong <ch3cho@qq.com>
This commit is contained in:
103
plugins/wasm-go/extensions/frontend-gray/README.md
Normal file
103
plugins/wasm-go/extensions/frontend-gray/README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# frontend-gray 前端灰度插件
|
||||
## 功能说明
|
||||
`frontend-gray`插件实现了前端用户灰度的的功能,通过此插件,不但可以用于业务`A/B实验`,同时通过`可灰度`配合`可监控`,`可回滚`策略保证系统发布运维的稳定性。
|
||||
|
||||
## 配置字段
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------|
|
||||
| `grayKey` | string | 非必填 | - | 用户ID的唯一标识,可以来自Cookie或者Header中,比如 userid,如果没有填写则使用`rules[].grayTagKey`和`rules[].grayTagValue`过滤灰度规则 |
|
||||
| `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` |
|
||||
| `rules` | array of object | 非必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 |
|
||||
| `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 |
|
||||
| `grayDeployments` | array of object | 非必填 | - | 配置Gray灰度的生效规则,以及生效版本 |
|
||||
|
||||
`rules`字段配置说明:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------|
|
||||
| `name` | string | 必填 | - | 规则名称唯一标识,和`deploy.gray[].name`进行关联生效 |
|
||||
| `grayKeyValue` | array of string | 非必填 | - | 用户ID 白名单列表 |
|
||||
| `grayTagKey` | string | 非必填 | - | 用户分类打标的标签key值,来自Cookie |
|
||||
| `grayTagValue` | array of string | 非必填 | - | 用户分类打标的标签value值,来自Cookie |
|
||||
|
||||
`baseDeployment`字段配置说明:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------|
|
||||
| `version` | string | 必填 | - | Base版本的版本号,作为兜底的版本 |
|
||||
|
||||
`grayDeployments`字段配置说明:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------|--------|------|-----|----------------------------|
|
||||
| `version` | string | 必填 | - | Gray版本的版本号,如果命中灰度规则,则使用此版本 |
|
||||
| `name` | string | 必填 | - | 规则名称和`rules[].name`关联, |
|
||||
| `enabled` | boolean | 必填 | - | 是否启动当前灰度规则 |
|
||||
|
||||
## 配置示例
|
||||
### 基础配置
|
||||
```yml
|
||||
grayKey: userid
|
||||
rules:
|
||||
- name: inner-user
|
||||
grayKeyValue:
|
||||
- '00000001'
|
||||
- '00000005'
|
||||
- name: beta-user
|
||||
grayKeyValue:
|
||||
- '00000002'
|
||||
- '00000003'
|
||||
grayTagKey: level
|
||||
grayTagValue:
|
||||
- level3
|
||||
- level5
|
||||
baseDeployment:
|
||||
version: base
|
||||
grayDeployments:
|
||||
- name: beta-user
|
||||
version: gray
|
||||
enabled: true
|
||||
```
|
||||
|
||||
|
||||
cookie中的用户唯一标识为 `userid`,当前灰度规则配置了`beta-user`的规则。
|
||||
|
||||
当满足下面调试的时候,会使用`version: gray`版本
|
||||
- cookie中`userid`等于`00000002`或者`00000003`
|
||||
- cookie中`level`等于`level3`或者`level5`的用户
|
||||
|
||||
否则使用`version: base`版本
|
||||
|
||||
### 用户信息存在JSON中
|
||||
|
||||
```yml
|
||||
grayKey: appInfo
|
||||
graySubKey: userId
|
||||
rules:
|
||||
- name: inner-user
|
||||
grayKeyValue:
|
||||
- '00000001'
|
||||
- '00000005'
|
||||
- name: beta-user
|
||||
grayKeyValue:
|
||||
- '00000002'
|
||||
- '00000003'
|
||||
grayTagKey: level
|
||||
grayTagValue:
|
||||
- level3
|
||||
- level5
|
||||
baseDeployment:
|
||||
version: base
|
||||
grayDeployments:
|
||||
- name: beta-user
|
||||
version: gray
|
||||
enabled: true
|
||||
```
|
||||
|
||||
cookie存在`appInfo`的JSON数据,其中包含`userId`字段为当前的唯一标识
|
||||
当前灰度规则配置了`beta-user`的规则。
|
||||
当满足下面调试的时候,会使用`version: gray`版本
|
||||
- cookie中`userid`等于`00000002`或者`00000003`
|
||||
- cookie中`level`等于`level3`或者`level5`的用户
|
||||
|
||||
否则使用`version: base`版本
|
||||
82
plugins/wasm-go/extensions/frontend-gray/config/config.go
Normal file
82
plugins/wasm-go/extensions/frontend-gray/config/config.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type GrayRule struct {
|
||||
Name string
|
||||
GrayKeyValue []interface{}
|
||||
GrayTagKey string
|
||||
GrayTagValue []interface{}
|
||||
}
|
||||
|
||||
type BaseDeployment struct {
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
type GrayDeployments struct {
|
||||
Name string
|
||||
Version string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type GrayConfig struct {
|
||||
GrayKey string
|
||||
GraySubKey string
|
||||
Rules []*GrayRule
|
||||
BaseDeployment *BaseDeployment
|
||||
GrayDeployments []*GrayDeployments
|
||||
}
|
||||
|
||||
func interfacesFromJSONResult(results []gjson.Result) []interface{} {
|
||||
var interfaces []interface{}
|
||||
for _, result := range results {
|
||||
switch v := result.Value().(type) {
|
||||
case float64:
|
||||
// 当 v 是 float64 时,将其转换为字符串
|
||||
interfaces = append(interfaces, strconv.FormatFloat(v, 'f', -1, 64))
|
||||
default:
|
||||
// 其它类型不改变,直接追加
|
||||
interfaces = append(interfaces, v)
|
||||
}
|
||||
}
|
||||
return interfaces
|
||||
}
|
||||
|
||||
func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
// 解析 GrayKey
|
||||
grayConfig.GrayKey = json.Get("grayKey").String()
|
||||
grayConfig.GraySubKey = json.Get("graySubKey").String()
|
||||
|
||||
// 解析 Rules
|
||||
rules := json.Get("rules").Array()
|
||||
for _, rule := range rules {
|
||||
grayRule := GrayRule{
|
||||
Name: rule.Get("name").String(),
|
||||
GrayKeyValue: interfacesFromJSONResult(rule.Get("grayKeyValue").Array()), // 使用辅助函数将 []gjson.Result 转换为 []interface{}
|
||||
GrayTagKey: rule.Get("grayTagKey").String(),
|
||||
GrayTagValue: interfacesFromJSONResult(rule.Get("grayTagValue").Array()),
|
||||
}
|
||||
grayConfig.Rules = append(grayConfig.Rules, &grayRule)
|
||||
}
|
||||
|
||||
// 解析 deploy
|
||||
baseDeployment := json.Get("baseDeployment")
|
||||
grayDeployments := json.Get("grayDeployments").Array()
|
||||
|
||||
grayConfig.BaseDeployment = &BaseDeployment{
|
||||
Name: baseDeployment.Get("name").String(),
|
||||
Version: baseDeployment.Get("version").String(),
|
||||
}
|
||||
for _, item := range grayDeployments {
|
||||
grayConfig.GrayDeployments = append(grayConfig.GrayDeployments, &GrayDeployments{
|
||||
Name: item.Get("name").String(),
|
||||
Version: item.Get("version").String(),
|
||||
Enabled: item.Get("enabled").Bool(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestJsonToGrayConfig(t *testing.T) {
|
||||
allConfigData := `{"grayKey":"userid","rules":[{"name":"inner-user","grayKeyValue":["00000001","00000005"]},{"name":"beta-user","grayKeyValue":["00000002","00000003"],"grayTagKey":"level","grayTagValue":["level3","level5"]}],"deploy":{"base":{"version":"base"},"gray":[{"name":"beta-user","version":"gray","enabled":true}]}}`
|
||||
var tests = []struct {
|
||||
testName string
|
||||
grayKey string
|
||||
json string
|
||||
}{
|
||||
{"完整的数据", "userid", allConfigData},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testName := test.testName
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
var grayConfig = &GrayConfig{}
|
||||
JsonToGrayConfig(gjson.Parse(test.json), grayConfig)
|
||||
assert.Equal(t, test.grayKey, grayConfig.GrayKey)
|
||||
})
|
||||
}
|
||||
}
|
||||
102
plugins/wasm-go/extensions/frontend-gray/envoy.yaml
Normal file
102
plugins/wasm-go/extensions/frontend-gray/envoy.yaml
Normal file
@@ -0,0 +1,102 @@
|
||||
admin:
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 9901
|
||||
static_resources:
|
||||
listeners:
|
||||
- name: listener_0
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 10000
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.filters.network.http_connection_manager
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||
scheme_header_transformation:
|
||||
scheme_to_overwrite: https
|
||||
stat_prefix: ingress_http
|
||||
route_config:
|
||||
name: local_route
|
||||
virtual_hosts:
|
||||
- name: local_service
|
||||
domains: ["*"]
|
||||
routes:
|
||||
- match:
|
||||
prefix: "/"
|
||||
route:
|
||||
cluster: httpbin
|
||||
http_filters:
|
||||
- name: wasmdemo
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
|
||||
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
|
||||
value:
|
||||
config:
|
||||
name: wasmdemo
|
||||
vm_config:
|
||||
runtime: envoy.wasm.runtime.v8
|
||||
code:
|
||||
local:
|
||||
filename: ./main.wasm
|
||||
configuration:
|
||||
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||
value: |
|
||||
{
|
||||
"grayKey": "UserInfo",
|
||||
"graySubKey": "userCode",
|
||||
"rules": [
|
||||
{
|
||||
"name": "inner-user",
|
||||
"grayKeyValue": [
|
||||
"00000001",
|
||||
"00000005"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "beta-user",
|
||||
"grayKeyValue": [
|
||||
"noah",
|
||||
"00000003"
|
||||
],
|
||||
"grayTagKey": "level",
|
||||
"grayTagValue": [
|
||||
"level3",
|
||||
"level5"
|
||||
]
|
||||
}
|
||||
],
|
||||
"baseDeployment": {
|
||||
"version": "base"
|
||||
},
|
||||
"grayDeployments": [
|
||||
{
|
||||
"name": "beta-user",
|
||||
"version": "gray",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
- name: envoy.filters.http.router
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||
clusters:
|
||||
- name: httpbin
|
||||
connect_timeout: 30s
|
||||
type: LOGICAL_DNS
|
||||
# Comment out the following line to test on v6 networks
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: httpbin
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: httpbin.org
|
||||
port_value: 80
|
||||
24
plugins/wasm-go/extensions/frontend-gray/go.mod
Normal file
24
plugins/wasm-go/extensions/frontend-gray/go.mod
Normal file
@@ -0,0 +1,24 @@
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray
|
||||
|
||||
go 1.18
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20240531060402-2807ddfbb79e
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tidwall/gjson v1.17.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
32
plugins/wasm-go/extensions/frontend-gray/go.sum
Normal file
32
plugins/wasm-go/extensions/frontend-gray/go.sum
Normal file
@@ -0,0 +1,32 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20240531060402-2807ddfbb79e h1:0b2UXrEpotHwWgwvgvkXnyKWuxTXtzfKu6c2YpRV+zw=
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20240531060402-2807ddfbb79e/go.mod h1:10jQXKsYFUF7djs+Oy7t82f4dbie9pISfP9FJwpPLuk=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.5 h1:VOLL3m442IHCSu8mR5AZ4sc6LVT9X0w1hdqDI7oB9jY=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.5/go.mod h1:kr3V9Ntbspj1eSrX8rgjBsdMXkGupYEf+LM72caGPQc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226065437-8f7a0b3c9071 h1:STb5rOHRZOzoiAa+gTz2LFqO1nYj7U/1eIVUJJadU4A=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226065437-8f7a0b3c9071/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
||||
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
78
plugins/wasm-go/extensions/frontend-gray/main.go
Normal file
78
plugins/wasm-go/extensions/frontend-gray/main.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/util"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"frontend-gray",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.Log) error {
|
||||
// 解析json 为GrayConfig
|
||||
config.JsonToGrayConfig(json, grayConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterGrayRule 过滤灰度规则
|
||||
func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, log wrapper.Log) *config.GrayDeployments {
|
||||
for _, grayDeployment := range grayConfig.GrayDeployments {
|
||||
if !grayDeployment.Enabled {
|
||||
// 跳过Enabled=false
|
||||
continue
|
||||
}
|
||||
grayRule := util.GetRule(grayConfig.Rules, grayDeployment.Name)
|
||||
// 首先:先校验用户名单ID
|
||||
if grayRule.GrayKeyValue != nil && len(grayRule.GrayKeyValue) > 0 && grayKeyValue != "" {
|
||||
if util.Contains(grayRule.GrayKeyValue, grayKeyValue) {
|
||||
log.Infof("x-mse-tag: %s, grayKeyValue: %s", grayDeployment.Version, grayKeyValue)
|
||||
return grayDeployment
|
||||
}
|
||||
}
|
||||
// 第二:校验Cookie中的 GrayTagKey
|
||||
if grayRule.GrayTagKey != "" && grayRule.GrayTagValue != nil && len(grayRule.GrayTagValue) > 0 {
|
||||
cookieStr, _ := proxywasm.GetHttpRequestHeader("cookie")
|
||||
grayTagValue := util.GetValueByCookie(cookieStr, grayRule.GrayTagKey)
|
||||
if util.Contains(grayRule.GrayTagValue, grayTagValue) {
|
||||
log.Infof("x-mse-tag: %s, grayTag: %s=%s", grayDeployment.Version, grayRule.GrayTagKey, grayTagValue)
|
||||
return grayDeployment
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Infof("x-mse-tag: %s, grayKeyValue: %s", grayConfig.BaseDeployment.Version, grayKeyValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
|
||||
// 优先从cookie中获取,如果拿不到再从header中获取
|
||||
cookieStr, _ := proxywasm.GetHttpRequestHeader("cookie")
|
||||
grayHeaderKey, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey)
|
||||
grayKeyValue := util.GetValueByCookie(cookieStr, grayConfig.GrayKey)
|
||||
proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
// 优先从Cookie中获取,否则从header中获取
|
||||
if grayKeyValue == "" {
|
||||
grayKeyValue = grayHeaderKey
|
||||
}
|
||||
// 如果有子key, 尝试从子key中获取值
|
||||
if grayConfig.GraySubKey != "" {
|
||||
subKeyValue := util.GetBySubKey(grayKeyValue, grayConfig.GraySubKey)
|
||||
if subKeyValue != "" {
|
||||
grayKeyValue = subKeyValue
|
||||
}
|
||||
}
|
||||
grayDeployment := FilterGrayRule(&grayConfig, grayKeyValue, log)
|
||||
if grayDeployment != nil {
|
||||
proxywasm.AddHttpRequestHeader("x-mse-tag", grayDeployment.Version)
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
70
plugins/wasm-go/extensions/frontend-gray/util/utils.go
Normal file
70
plugins/wasm-go/extensions/frontend-gray/util/utils.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// GetValueByCookie 根据 cookieStr 和 cookieName 获取 cookie 值
|
||||
func GetValueByCookie(cookieStr string, cookieName string) string {
|
||||
if cookieStr == "" {
|
||||
return ""
|
||||
}
|
||||
cookies := strings.Split(cookieStr, ";")
|
||||
curCookieName := cookieName + "="
|
||||
var foundCookieValue string
|
||||
var found bool
|
||||
// 遍历找到 cookie 对并处理
|
||||
for _, cookie := range cookies {
|
||||
cookie = strings.TrimSpace(cookie) // 清理空白符
|
||||
if strings.HasPrefix(cookie, curCookieName) {
|
||||
foundCookieValue = cookie[len(curCookieName):]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return ""
|
||||
}
|
||||
return foundCookieValue
|
||||
}
|
||||
|
||||
// contains 检查切片 slice 中是否含有元素 value。
|
||||
func Contains(slice []interface{}, value string) bool {
|
||||
for _, item := range slice {
|
||||
if item == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetRule(rules []*config.GrayRule, name string) *config.GrayRule {
|
||||
for _, rule := range rules {
|
||||
if rule.Name == name {
|
||||
return rule
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetBySubKey(grayInfoStr string, graySubKey string) string {
|
||||
// 首先对 URL 编码的字符串进行解码
|
||||
jsonStr, err := url.QueryUnescape(grayInfoStr)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// 使用 gjson 从 JSON 字符串中提取 graySubKey 对应的值
|
||||
value := gjson.Get(jsonStr, graySubKey)
|
||||
|
||||
// 检查所提取的值是否存在
|
||||
if !value.Exists() {
|
||||
return ""
|
||||
}
|
||||
// 返回字符串形式的值
|
||||
return value.String()
|
||||
}
|
||||
42
plugins/wasm-go/extensions/frontend-gray/util/utils_test.go
Normal file
42
plugins/wasm-go/extensions/frontend-gray/util/utils_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetValueByCookie(t *testing.T) {
|
||||
var tests = []struct {
|
||||
cookie, cookieKey, output string
|
||||
}{
|
||||
{"", "uid", ""},
|
||||
{`cna=pf_9be76347560439f3b87daede1b485e37; uid=111`, "uid", "111"},
|
||||
{`cna=pf_9be76347560439f3b87daede1b485e37; userid=222`, "userid", "222"},
|
||||
{`uid=333`, "uid", "333"},
|
||||
{`cna=pf_9be76347560439f3b87daede1b485e37;`, "uid", ""},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testName := test.cookie
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
output := GetValueByCookie(test.cookie, test.cookieKey)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeJsonCookie(t *testing.T) {
|
||||
var tests = []struct {
|
||||
userInfoStr, grayJsonKey, output string
|
||||
}{
|
||||
{"{%22password%22:%22$2a$10$YAvYjA6783YeCi44/M395udIZ4Ll2iyKkQCzePaYx5NNG/aIWgICG%22%2C%22username%22:%22%E8%B0%A2%E6%99%AE%E8%80%80%22%2C%22authorities%22:[]%2C%22accountNonExpired%22:true%2C%22accountNonLocked%22:true%2C%22credentialsNonExpired%22:true%2C%22enabledd%22:true%2C%22id%22:838925798835720200%2C%22mobile%22:%22%22%2C%22userCode%22:%22noah%22%2C%22userName%22:%22%E8%B0%A2%E6%99%AE%E8%80%80%22%2C%22orgId%22:10%2C%22ocId%22:87%2C%22userType%22:%22OWN%22%2C%22firstLogin%22:false%2C%22ownOrgId%22:null%2C%22clientCode%22:%22%22%2C%22clientType%22:null%2C%22country%22:%22UAE%22%2C%22isGuide%22:null%2C%22acctId%22:null%2C%22userToken%22:null%2C%22deviceId%22:%223a47fec00a59d140%22%2C%22ocCode%22:%2299990002%22%2C%22secondType%22:%22dtl%22%2C%22vendorCode%22:%2210000001%22%2C%22status%22:%22ACTIVE%22%2C%22isDelete%22:false%2C%22email%22:%22%22%2C%22deleteStatus%22:null%2C%22deleteRequestDate%22:null%2C%22wechatId%22:null%2C%22userMfaInfoDTO%22:{%22checkMfa%22:false%2C%22checkSuccess%22:false%2C%22mobile%22:null%2C%22email%22:null%2C%22wechatId%22:null%2C%22totpSecret%22:null}}",
|
||||
"userCode", "noah"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testName := test.userInfoStr
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
output := GetBySubKey(test.userInfoStr, test.grayJsonKey)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user