forked from alibaba/higress
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
474 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
# user-gray 前端灰度插件 | ||
## 功能说明 | ||
`user-gray`插件实现了前端用户灰度的的功能,通过此插件,不但可以用于业务`A/B实验`,同时通过`可灰度`配合`可监控`,`可回滚`策略保证系统发布运维的稳定性。 | ||
|
||
## 配置字段 | ||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | | ||
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------| | ||
| `uid-key` | string | 必填 | - | 用户ID的唯一标识,可以来自Cookie或者Header中,比如 userid | | ||
| `uid-sub-key` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`uid-sub-key`取值为`userCode` | | ||
| `rules` | array of map | 非必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 | | ||
| `deploy` | map of map | 非必填 | - | 分别配置Base基线和Gary灰度的生效规则,以及生效版本 | | ||
|
||
`rules`字段配置说明: | ||
|
||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | | ||
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------| | ||
| `name` | string | 必填 | - | 规则名称唯一标识,和`deploy.gray[].name`进行关联生效 | | ||
| `uid-value` | array of string | 非必填 | - | 用户ID 白名单列表 | | ||
| `gray-tag-key` | string | 非必填 | - | 用户分类打标的标签key值,来自Cookie | | ||
| `gray-tag-value` | array of string | 非必填 | - | 用户分类打标的标签value值,来自Cookie | | ||
|
||
|
||
`deploy`字段配置说明: | ||
|
||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | | ||
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------| | ||
| `base` | map of string | 必填 | - | 定义Base版本,如果匹配不到灰度版本,默认fallback到当前版本 | | ||
| `gray` | array of string | 非必填 | - | 定义Gray版本,如果匹配到灰度规则,则当前的灰度版本生效 | | ||
|
||
`deploy.base`字段配置说明: | ||
|
||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | | ||
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------| | ||
| `version` | string | 必填 | - | Base版本的版本号,作为兜底的版本 | | ||
|
||
`deploy.gray`字段配置说明: | ||
|
||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | | ||
|--------|--------|------|-----|----------------------------| | ||
| `version` | string | 必填 | - | Gray版本的版本号,如果命中灰度规则,则使用此版本 | | ||
| `name` | string | 必填 | - | 规则名称和`rules[].name`关联, | | ||
| `enable` | boolean | 必填 | - | 是否启动当前灰度规则 | | ||
|
||
## 配置示例 | ||
### 基础配置 | ||
```yml | ||
uid-key: userid | ||
rules: | ||
- name: inner-user | ||
uid-value: | ||
- '00000001' | ||
- '00000005' | ||
- name: beta-user | ||
uid-value: | ||
- '00000002' | ||
- '00000003' | ||
gray-tag-key: level | ||
gray-tag-value: | ||
- level3 | ||
- level5 | ||
deploy: | ||
base: | ||
version: base | ||
gray: | ||
- name: beta-user | ||
version: gray | ||
enable: true | ||
``` | ||
cookie中的用户唯一标识为 `userid`,当前灰度规则配置了`beta-user`的规则。 | ||
|
||
当满足下面调试的时候,会使用`versin: gray`版本 | ||
- cookie中`userid`等于`00000002`或者`00000003` | ||
- cookie中`level`等于`level3`或者`level5`的用户 | ||
|
||
否则使用`versin: base`版本 | ||
|
||
### 用户信息存在JSON中 | ||
|
||
```yml | ||
uid-key: appInfo | ||
uid-sub-key: userId | ||
rules: | ||
- name: inner-user | ||
uid-value: | ||
- '00000001' | ||
- '00000005' | ||
- name: beta-user | ||
uid-value: | ||
- '00000002' | ||
- '00000003' | ||
gray-tag-key: level | ||
gray-tag-value: | ||
- level3 | ||
- level5 | ||
deploy: | ||
base: | ||
version: base | ||
gray: | ||
- name: beta-user | ||
version: gray | ||
enable: true | ||
``` | ||
|
||
cookie存在`appInfo`的JSON数据,其中包含`userId`字段为当前的唯一标识 | ||
当前灰度规则配置了`beta-user`的规则。 | ||
当满足下面调试的时候,会使用`versin: gray`版本 | ||
- cookie中`userid`等于`00000002`或者`00000003` | ||
- cookie中`level`等于`level3`或者`level5`的用户 | ||
|
||
否则使用`versin: base`版本 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package config | ||
|
||
import ( | ||
"strconv" | ||
|
||
"github.com/tidwall/gjson" | ||
) | ||
|
||
type GrayRule struct { | ||
Name string | ||
UidValue []interface{} | ||
GrayTagKey string | ||
GrayTagValue []interface{} | ||
} | ||
|
||
type DeployItem struct { | ||
Name string | ||
Version string | ||
Enable bool | ||
} | ||
|
||
type Deploy struct { | ||
Base *DeployItem | ||
Gray []*DeployItem | ||
} | ||
|
||
type UserGrayConfig struct { | ||
UidKey string | ||
UidSubKey string | ||
Rules []*GrayRule | ||
Deploy *Deploy | ||
} | ||
|
||
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 JsonToUserGrayConfig(json gjson.Result, userGrayConfig *UserGrayConfig) { | ||
// 解析 UidKey | ||
userGrayConfig.UidKey = json.Get("uid-key").String() | ||
userGrayConfig.UidSubKey = json.Get("uid-sub-key").String() | ||
|
||
// 解析 Rules | ||
rules := json.Get("rules").Array() | ||
for _, rule := range rules { | ||
grayRule := GrayRule{ | ||
Name: rule.Get("name").String(), | ||
UidValue: interfacesFromJSONResult(rule.Get("uid-value").Array()), // 使用辅助函数将 []gjson.Result 转换为 []interface{} | ||
GrayTagKey: rule.Get("gray-tag-key").String(), | ||
GrayTagValue: interfacesFromJSONResult(rule.Get("gray-tag-value").Array()), | ||
} | ||
userGrayConfig.Rules = append(userGrayConfig.Rules, &grayRule) | ||
} | ||
|
||
// 解析 deploy | ||
deployJSON := json.Get("deploy") | ||
baseItem := deployJSON.Get("base") | ||
grayItems := deployJSON.Get("gray").Array() | ||
|
||
// 分配内存给 release 对象 | ||
userGrayConfig.Deploy = &Deploy{ | ||
Base: &DeployItem{ | ||
Name: baseItem.Get("name").String(), | ||
Version: baseItem.Get("version").String(), | ||
Enable: baseItem.Get("enable").Bool(), | ||
}, | ||
Gray: []*DeployItem{}, | ||
} | ||
|
||
// 解析 Gray 列表 | ||
for _, item := range grayItems { | ||
DeployItem := &DeployItem{ | ||
Name: item.Get("name").String(), | ||
Version: item.Get("version").String(), | ||
Enable: item.Get("enable").Bool(), | ||
} | ||
userGrayConfig.Deploy.Gray = append(userGrayConfig.Deploy.Gray, DeployItem) | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
plugins/wasm-go/extensions/user-gray/config/config_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package config | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/tidwall/gjson" | ||
) | ||
|
||
func TestJsonToUserGreyConfig(t *testing.T) { | ||
allConfigData := `{"uid-key":"userid","rules":[{"name":"inner-user","uid-value":["00000001","00000005"]},{"name":"beta-user","uid-value":["00000002","00000003"],"gray-tag-key":"level","gray-tag-value":["level3","level5"]}],"deploy":{"base":{"version":"base"},"gray":[{"name":"beta-user","version":"gray","enable":true}]}}` | ||
var tests = []struct { | ||
testName string | ||
uidKey string | ||
json string | ||
}{ | ||
{"完整的数据", "userid", allConfigData}, | ||
} | ||
for _, test := range tests { | ||
testName := test.testName | ||
t.Run(testName, func(t *testing.T) { | ||
var userGrayConfig = &UserGrayConfig{} | ||
JsonToUserGrayConfig(gjson.Parse(test.json), userGrayConfig) | ||
assert.Equal(t, test.uidKey, userGrayConfig.UidKey) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
module user-gray | ||
|
||
go 1.18 | ||
|
||
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package main | ||
|
||
import ( | ||
"user-gray/config" | ||
"user-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( | ||
"user-gray", | ||
wrapper.ParseConfigBy(parseConfig), | ||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), | ||
) | ||
} | ||
|
||
func parseConfig(json gjson.Result, userGrayConfig *config.UserGrayConfig, log wrapper.Log) error { | ||
// 解析json 为UserGrayConfig | ||
config.JsonToUserGrayConfig(json, userGrayConfig) | ||
return nil | ||
} | ||
|
||
// OmitGrayRule 过滤灰度规则 | ||
func OmitGrayRule(userGrayConfig *config.UserGrayConfig, userId string) *config.DeployItem { | ||
for _, grayItem := range userGrayConfig.Deploy.Gray { | ||
if !grayItem.Enable { | ||
// 跳过Enable=false | ||
continue | ||
} | ||
grayRule := util.ContainsRule(userGrayConfig.Rules, grayItem.Name) | ||
// 首先:先校验用户名单ID | ||
if grayRule.UidValue != nil && len(grayRule.UidValue) > 0 && userId != "" { | ||
if util.Contains(grayRule.UidValue, userId) { | ||
return grayItem | ||
} | ||
} | ||
// 第二:校验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) { | ||
return grayItem | ||
} | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.UserGrayConfig, log wrapper.Log) types.Action { | ||
// 优先从cookie中获取,如果拿不到再从header中获取 | ||
cookieStr, _ := proxywasm.GetHttpRequestHeader("cookie") | ||
uidHeaderKey, _ := proxywasm.GetHttpRequestHeader(grayConfig.UidKey) | ||
uidKeyValue := util.GetValueByCookie(cookieStr, grayConfig.UidKey) | ||
proxywasm.RemoveHttpRequestHeader("Accept-Encoding") | ||
// 优先从Cookie中获取,否则从header中获取 | ||
if uidKeyValue == "" { | ||
uidKeyValue = uidHeaderKey | ||
} | ||
// 如果有子key, 尝试从子key中获取值 | ||
if grayConfig.UidSubKey != "" { | ||
uidSubKeyValue := util.GetUidFromSubKey(uidKeyValue, grayConfig.UidSubKey) | ||
if uidSubKeyValue != "" { | ||
uidKeyValue = uidSubKeyValue | ||
} | ||
} | ||
grayDeployItem := OmitGrayRule(&grayConfig, uidKeyValue) | ||
if grayDeployItem != nil { | ||
log.Infof("x-mse-tag: %s, userCode: %s", grayDeployItem.Version, uidKeyValue) | ||
proxywasm.AddHttpRequestHeader("x-mse-tag", grayDeployItem.Version) | ||
} else { | ||
log.Infof("x-mse-tag: %s, userCode: %s", grayConfig.Deploy.Base.Version, uidKeyValue) | ||
} | ||
|
||
return types.ActionContinue | ||
} |
Oops, something went wrong.