mirror of
https://github.com/alibaba/higress.git
synced 2026-03-05 09:00:47 +08:00
feat: 🎸 frontend-gray plugin support cdn type deploy (#1178)
Co-authored-by: Kent Dong <ch3cho@qq.com>
This commit is contained in:
@@ -3,13 +3,14 @@
|
||||
`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灰度的生效规则,以及生效版本 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|----------------|--------------|----|-----|----------------------------------------------------------------------------------------------------|
|
||||
| `grayKey` | string | 非必填 | - | 用户ID的唯一标识,可以来自Cookie或者Header中,比如 userid,如果没有填写则使用`rules[].grayTagKey`和`rules[].grayTagValue`过滤灰度规则 |
|
||||
| `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` |
|
||||
| `rules` | array of object | 必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 |
|
||||
| `rewrite` | object | 必填 | - | 重写配置,一般用于OSS/CDN前端部署的重写配置 |
|
||||
| `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 |
|
||||
| `grayDeployments` | array of object | 非必填 | - | 配置Gray灰度的生效规则,以及生效版本 |
|
||||
|
||||
`rules`字段配置说明:
|
||||
|
||||
@@ -20,6 +21,19 @@
|
||||
| `grayTagKey` | string | 非必填 | - | 用户分类打标的标签key值,来自Cookie |
|
||||
| `grayTagValue` | array of string | 非必填 | - | 用户分类打标的标签value值,来自Cookie |
|
||||
|
||||
`rewrite`字段配置说明:
|
||||
> `indexRouting`首页重写和`fileRouting`文件重写,本质都是前缀匹配,比如`/app1`: `/mfe/app1/{version}/index.html`代表/app1为前缀的请求,路由到`/mfe/app1/{version}/index.html`页面上,其中`{version}`代表版本号,在运行过程中会被`baseDeployment.version`或者`grayDeployments[].version`动态替换。
|
||||
|
||||
> `{version}` 作为保留字段,在执行过程中会被`baseDeployment.version`或者`grayDeployments[].version`动态替换前端版本。
|
||||
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------|--------------|------|-----|------------------------------|
|
||||
| `host` | string | 非必填 | - | host地址,如果是OSS则设置为 VPC 内网访问地址 |
|
||||
| `notFoundUri` | string | 非必填 | - | 404 页面配置 |
|
||||
| `indexRouting` | map of string to string | 非必填 | - | 用于定义首页重写路由规则。每个键 (Key) 表示首页的路由路径,值 (Value) 则指向重定向的目标文件。例如,键为 `/app1` 对应的值为 `/mfe/app1/{version}/index.html`。生效version为`0.0.1`, 访问路径为 `/app1`,则重定向到 `/mfe/app1/0.0.1/index.html`。 |
|
||||
| `fileRouting` | map of string to string | 非必填 | - | 用于定义资源文件重写路由规则。每个键 (Key) 表示资源访问路径,值 (Value) 则指向重定向的目标文件。例如,键为 `/app1/` 对应的值为 `/mfe/app1/{version}`。生效version为`0.0.1`,访问路径为 `/app1/js/a.js`,则重定向到 `/mfe/app1/0.0.1/js/a.js`。 |
|
||||
|
||||
`baseDeployment`字段配置说明:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
@@ -28,11 +42,12 @@
|
||||
|
||||
`grayDeployments`字段配置说明:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------|--------|------|-----|----------------------------|
|
||||
| `version` | string | 必填 | - | Gray版本的版本号,如果命中灰度规则,则使用此版本 |
|
||||
| `name` | string | 必填 | - | 规则名称和`rules[].name`关联, |
|
||||
| `enabled` | boolean | 必填 | - | 是否启动当前灰度规则 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------|--------|------|-----|-------------------------------------------------|
|
||||
| `version` | string | 必填 | - | Gray版本的版本号,如果命中灰度规则,则使用此版本。如果是非CDN部署,在header添加`x-higress-tag` |
|
||||
| `backendVersion` | string | 必填 | - | 后端灰度版本,会在`XHR/Fetch`请求的header头添加 `x-mse-tag`到后端 |
|
||||
| `name` | string | 必填 | - | 规则名称和`rules[].name`关联, |
|
||||
| `enabled` | boolean | 必填 | - | 是否启动当前灰度规则 |
|
||||
|
||||
## 配置示例
|
||||
### 基础配置
|
||||
@@ -100,4 +115,51 @@ cookie存在`appInfo`的JSON数据,其中包含`userId`字段为当前的唯
|
||||
- cookie中`userid`等于`00000002`或者`00000003`
|
||||
- cookie中`level`等于`level3`或者`level5`的用户
|
||||
|
||||
否则使用`version: base`版本
|
||||
否则使用`version: base`版本
|
||||
|
||||
### rewrite重写配置
|
||||
> 一般用于CDN部署场景
|
||||
```yml
|
||||
grayKey: userid
|
||||
rules:
|
||||
- name: inner-user
|
||||
grayKeyValue:
|
||||
- '00000001'
|
||||
- '00000005'
|
||||
- name: beta-user
|
||||
grayKeyValue:
|
||||
- '00000002'
|
||||
- '00000003'
|
||||
grayTagKey: level
|
||||
grayTagValue:
|
||||
- level3
|
||||
- level5
|
||||
rewrite:
|
||||
host: frontend-gray.oss-cn-shanghai-internal.aliyuncs.com
|
||||
notFoundUri: /mfe/app1/dev/404.html
|
||||
indexRouting:
|
||||
/app1: '/mfe/app1/{version}/index.html'
|
||||
/: '/mfe/app1/{version}/index.html',
|
||||
fileRouting:
|
||||
/: '/mfe/app1/{version}'
|
||||
/app1/: '/mfe/app1/{version}'
|
||||
baseDeployment:
|
||||
version: base
|
||||
grayDeployments:
|
||||
- name: beta-user
|
||||
version: gray
|
||||
enabled: true
|
||||
```
|
||||
|
||||
`{version}`会在运行过程中动态替换为真正的版本
|
||||
|
||||
#### indexRouting:首页路由配置
|
||||
访问 `/app1`, `/app123`,`/app1/index.html`, `/app1/xxx`, `/xxxx` 都会路由到'/mfe/app1/{version}/index.html'
|
||||
|
||||
#### fileRouting:文件路由配置
|
||||
下面文件映射均生效
|
||||
- `/js/a.js` => `/mfe/app1/v1.0.0/js/a.js`
|
||||
- `/js/template/a.js` => `/mfe/app1/v1.0.0/js/template/a.js`
|
||||
- `/app1/js/a.js` => `/mfe/app1/v1.0.0/js/a.js`
|
||||
- `/app1/js/template/a.js` => `/mfe/app1/v1.0.0/js/template/a.js`
|
||||
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
XHigressTag = "x-higress-tag"
|
||||
XPreHigressTag = "x-pre-higress-tag"
|
||||
XMseTag = "x-mse-tag"
|
||||
IsHTML = "is_html"
|
||||
IsIndex = "is_index"
|
||||
NotFound = "not_found"
|
||||
)
|
||||
|
||||
type LogInfo func(format string, args ...interface{})
|
||||
|
||||
type GrayRule struct {
|
||||
Name string
|
||||
GrayKeyValue []interface{}
|
||||
GrayKeyValue []string
|
||||
GrayTagKey string
|
||||
GrayTagValue []interface{}
|
||||
GrayTagValue []string
|
||||
}
|
||||
|
||||
type BaseDeployment struct {
|
||||
@@ -18,35 +27,46 @@ type BaseDeployment struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
type GrayDeployments struct {
|
||||
Name string
|
||||
Version string
|
||||
Enabled bool
|
||||
type GrayDeployment struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
Version string
|
||||
BackendVersion string
|
||||
}
|
||||
|
||||
type Rewrite struct {
|
||||
Host string
|
||||
NotFound string
|
||||
Index map[string]string
|
||||
File map[string]string
|
||||
}
|
||||
|
||||
type GrayConfig struct {
|
||||
GrayKey string
|
||||
GraySubKey string
|
||||
Rules []*GrayRule
|
||||
Rewrite *Rewrite
|
||||
BaseDeployment *BaseDeployment
|
||||
GrayDeployments []*GrayDeployments
|
||||
GrayDeployments []*GrayDeployment
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
func convertToStringList(results []gjson.Result) []string {
|
||||
interfaces := make([]string, len(results)) // 预分配切片容量
|
||||
for i, result := range results {
|
||||
interfaces[i] = result.String() // 使用 String() 方法直接获取字符串
|
||||
}
|
||||
return interfaces
|
||||
}
|
||||
|
||||
func convertToStringMap(result gjson.Result) map[string]string {
|
||||
m := make(map[string]string)
|
||||
result.ForEach(func(key, value gjson.Result) bool {
|
||||
m[key.String()] = value.String()
|
||||
return true // keep iterating
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
// 解析 GrayKey
|
||||
grayConfig.GrayKey = json.Get("grayKey").String()
|
||||
@@ -57,14 +77,20 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
for _, rule := range rules {
|
||||
grayRule := GrayRule{
|
||||
Name: rule.Get("name").String(),
|
||||
GrayKeyValue: interfacesFromJSONResult(rule.Get("grayKeyValue").Array()), // 使用辅助函数将 []gjson.Result 转换为 []interface{}
|
||||
GrayKeyValue: convertToStringList(rule.Get("grayKeyValue").Array()),
|
||||
GrayTagKey: rule.Get("grayTagKey").String(),
|
||||
GrayTagValue: interfacesFromJSONResult(rule.Get("grayTagValue").Array()),
|
||||
GrayTagValue: convertToStringList(rule.Get("grayTagValue").Array()),
|
||||
}
|
||||
grayConfig.Rules = append(grayConfig.Rules, &grayRule)
|
||||
}
|
||||
grayConfig.Rewrite = &Rewrite{
|
||||
Host: json.Get("rewrite.host").String(),
|
||||
NotFound: json.Get("rewrite.notFoundUri").String(),
|
||||
Index: convertToStringMap(json.Get("rewrite.indexRouting")),
|
||||
File: convertToStringMap(json.Get("rewrite.fileRouting")),
|
||||
}
|
||||
|
||||
// 解析 deploy
|
||||
// 解析 deployment
|
||||
baseDeployment := json.Get("baseDeployment")
|
||||
grayDeployments := json.Get("grayDeployments").Array()
|
||||
|
||||
@@ -73,10 +99,11 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
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(),
|
||||
grayConfig.GrayDeployments = append(grayConfig.GrayDeployments, &GrayDeployment{
|
||||
Name: item.Get("name").String(),
|
||||
Enabled: item.Get("enabled").Bool(),
|
||||
Version: item.Get("version").String(),
|
||||
BackendVersion: item.Get("backendVersion").String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,7 @@ static_resources:
|
||||
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||
value: |
|
||||
{
|
||||
"grayKey": "UserInfo",
|
||||
"graySubKey": "userCode",
|
||||
"grayKey": "userId",
|
||||
"rules": [
|
||||
{
|
||||
"name": "inner-user",
|
||||
@@ -70,13 +69,26 @@ static_resources:
|
||||
]
|
||||
}
|
||||
],
|
||||
"rewrite": {
|
||||
"host": "frontend-gray-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com",
|
||||
"notFoundUri": "/mfe/app1/dev/404.html",
|
||||
"indexRouting": {
|
||||
"/app1": "/mfe/app1/{version}/index.html",
|
||||
"/": "/mfe/app1/{version}/index.html"
|
||||
},
|
||||
"fileRouting": {
|
||||
"/": "/mfe/app1/{version}",
|
||||
"/app1": "/mfe/app1/{version}"
|
||||
}
|
||||
},
|
||||
"baseDeployment": {
|
||||
"version": "base"
|
||||
"version": "dev"
|
||||
},
|
||||
"grayDeployments": [
|
||||
{
|
||||
"name": "beta-user",
|
||||
"version": "gray",
|
||||
"version": "0.0.1",
|
||||
"backendVersion": "beta",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
@@ -98,5 +110,5 @@ static_resources:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: httpbin.org
|
||||
address: frontend-gray-cn-shanghai.oss-cn-shanghai.aliyuncs.com
|
||||
port_value: 80
|
||||
@@ -5,9 +5,9 @@ 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/alibaba/higress/plugins/wasm-go v1.4.3-0.20240727022514-bccfbde62188
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.17.0
|
||||
)
|
||||
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
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+incompatible/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/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/pmezard/go-difflib v1.0.0+incompatible/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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=
|
||||
@@ -27,7 +23,6 @@ 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=
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/util"
|
||||
|
||||
@@ -15,6 +19,9 @@ func main() {
|
||||
"frontend-gray",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessResponseHeadersBy(onHttpResponseHeader),
|
||||
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
|
||||
wrapper.ProcessStreamingResponseBodyBy(onStreamingResponseBody),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,55 +31,146 @@ func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.L
|
||||
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
|
||||
}
|
||||
}
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
|
||||
if !util.IsGrayEnabled(grayConfig) {
|
||||
return types.ActionContinue
|
||||
}
|
||||
log.Infof("x-mse-tag: %s, grayKeyValue: %s", grayConfig.BaseDeployment.Version, grayKeyValue)
|
||||
return nil
|
||||
|
||||
cookies, _ := proxywasm.GetHttpRequestHeader("cookie")
|
||||
path, _ := proxywasm.GetHttpRequestHeader(":path")
|
||||
fetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode")
|
||||
|
||||
isIndex := util.IsIndexRequest(fetchMode, path)
|
||||
hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0
|
||||
grayKeyValue := util.GetGrayKey(util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey), grayConfig.GraySubKey)
|
||||
|
||||
// 如果有重写的配置,则进行重写
|
||||
if hasRewrite {
|
||||
// 禁止重新路由,要在更改Header之前操作,否则会失效
|
||||
ctx.DisableReroute()
|
||||
}
|
||||
|
||||
// 删除Accept-Encoding,避免压缩, 如果是压缩的内容,后续插件就没法处理了
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
|
||||
grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue, log.Infof)
|
||||
frontendVersion := util.GetVersion(grayConfig.BaseDeployment.Version, cookies, isIndex)
|
||||
backendVersion := ""
|
||||
|
||||
// 命中灰度规则
|
||||
if grayDeployment != nil {
|
||||
frontendVersion = util.GetVersion(grayDeployment.Version, cookies, isIndex)
|
||||
backendVersion = grayDeployment.BackendVersion
|
||||
}
|
||||
|
||||
proxywasm.AddHttpRequestHeader(config.XHigressTag, frontendVersion)
|
||||
|
||||
ctx.SetContext(config.XPreHigressTag, frontendVersion)
|
||||
ctx.SetContext(config.XMseTag, backendVersion)
|
||||
ctx.SetContext(config.IsIndex, isIndex)
|
||||
|
||||
rewrite := grayConfig.Rewrite
|
||||
if rewrite.Host != "" {
|
||||
proxywasm.ReplaceHttpRequestHeader("HOST", rewrite.Host)
|
||||
}
|
||||
|
||||
if hasRewrite {
|
||||
rewritePath := path
|
||||
if isIndex {
|
||||
rewritePath = util.IndexRewrite(path, frontendVersion, grayConfig.Rewrite.Index)
|
||||
} else {
|
||||
rewritePath = util.PrefixFileRewrite(path, frontendVersion, grayConfig.Rewrite.File)
|
||||
}
|
||||
log.Infof("rewrite path: %s %s %v", path, frontendVersion, rewritePath)
|
||||
proxywasm.ReplaceHttpRequestHeader(":path", rewritePath)
|
||||
}
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
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
|
||||
func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
|
||||
if !util.IsGrayEnabled(grayConfig) {
|
||||
return types.ActionContinue
|
||||
}
|
||||
// 如果有子key, 尝试从子key中获取值
|
||||
if grayConfig.GraySubKey != "" {
|
||||
subKeyValue := util.GetBySubKey(grayKeyValue, grayConfig.GraySubKey)
|
||||
if subKeyValue != "" {
|
||||
grayKeyValue = subKeyValue
|
||||
status, err := proxywasm.GetHttpResponseHeader(":status")
|
||||
contentType, _ := proxywasm.GetHttpResponseHeader("Content-Type")
|
||||
if err != nil || status != "200" {
|
||||
isIndex := ctx.GetContext(config.IsIndex)
|
||||
if status == "404" {
|
||||
if grayConfig.Rewrite.NotFound != "" && isIndex != nil && isIndex.(bool) {
|
||||
ctx.SetContext(config.NotFound, true)
|
||||
responseHeaders, _ := proxywasm.GetHttpResponseHeaders()
|
||||
headersMap := util.ConvertHeaders(responseHeaders)
|
||||
headersMap[":status"][0] = "200"
|
||||
headersMap["content-type"][0] = "text/html"
|
||||
delete(headersMap, "content-length")
|
||||
proxywasm.ReplaceHttpResponseHeaders(util.ReconvertHeaders(headersMap))
|
||||
ctx.BufferResponseBody()
|
||||
return types.ActionContinue
|
||||
} else {
|
||||
ctx.DontReadResponseBody()
|
||||
}
|
||||
}
|
||||
log.Errorf("error status: %s, error message: %v", status, err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
grayDeployment := FilterGrayRule(&grayConfig, grayKeyValue, log)
|
||||
if grayDeployment != nil {
|
||||
proxywasm.AddHttpRequestHeader("x-mse-tag", grayDeployment.Version)
|
||||
|
||||
// 删除content-length,可能要修改Response返回值
|
||||
proxywasm.RemoveHttpResponseHeader("Content-Length")
|
||||
|
||||
// 删除Content-Disposition,避免自动下载文件
|
||||
proxywasm.RemoveHttpResponseHeader("Content-Disposition")
|
||||
|
||||
if strings.HasPrefix(contentType, "text/html") {
|
||||
ctx.SetContext(config.IsHTML, true)
|
||||
// 不会进去Streaming 的Body处理
|
||||
ctx.BufferResponseBody()
|
||||
|
||||
// 添加Cache-Control 头部,禁止缓存
|
||||
proxywasm.ReplaceHttpRequestHeader("Cache-Control", "no-cache, no-store")
|
||||
|
||||
frontendVersion := ctx.GetContext(config.XPreHigressTag).(string)
|
||||
backendVersion := ctx.GetContext(config.XMseTag).(string)
|
||||
|
||||
// 设置当前的前端版本
|
||||
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Path=/;", config.XPreHigressTag, frontendVersion))
|
||||
// 设置后端的前端版本
|
||||
proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Path=/;", config.XMseTag, backendVersion))
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte, log wrapper.Log) types.Action {
|
||||
if !util.IsGrayEnabled(grayConfig) {
|
||||
return types.ActionContinue
|
||||
}
|
||||
backendVersion := ctx.GetContext(config.XMseTag)
|
||||
isHtml := ctx.GetContext(config.IsHTML)
|
||||
isIndex := ctx.GetContext(config.IsIndex)
|
||||
notFoundUri := ctx.GetContext(config.NotFound)
|
||||
if isIndex != nil && isIndex.(bool) && notFoundUri != nil && notFoundUri.(bool) && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" {
|
||||
client := wrapper.NewClusterClient(wrapper.RouteCluster{Host: grayConfig.Rewrite.Host})
|
||||
client.Get(grayConfig.Rewrite.NotFound, nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
proxywasm.ReplaceHttpResponseBody(responseBody)
|
||||
proxywasm.ResumeHttpResponse()
|
||||
}, 1500)
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
// 以text/html 开头,将 cookie转到cookie
|
||||
if isHtml != nil && isHtml.(bool) && backendVersion != nil && backendVersion.(string) != "" {
|
||||
newText := strings.ReplaceAll(string(body), "</head>", `<script>
|
||||
!function(e,t){function n(e){var n="; "+t.cookie,r=n.split("; "+e+"=");return 2===r.length?r.pop().split(";").shift():null}var r=n("x-mse-tag");if(!r)return null;var s=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(e,t,n,a,i){return this._XHR=!0,this.addEventListener("readystatechange",function(){1===this.readyState&&r&&this.setRequestHeader("x-mse-tag",r)}),s.apply(this,arguments)};var a=e.fetch;e.fetch=function(e,t){return"undefined"==typeof t&&(t={}),"undefined"==typeof t.headers&&(t.headers={}),r&&(t.headers["x-mse-tag"]=r),a.apply(this,[e,t])}}(window,document);
|
||||
</script>
|
||||
</head>`)
|
||||
if err := proxywasm.ReplaceHttpResponseBody([]byte(newText)); err != nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.GrayConfig, chunk []byte, isLastChunk bool, log wrapper.Log) []byte {
|
||||
return chunk
|
||||
}
|
||||
|
||||
@@ -2,39 +2,56 @@ package util
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
|
||||
"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 == "" {
|
||||
func IsGrayEnabled(grayConfig config.GrayConfig) bool {
|
||||
// 检查是否存在重写主机
|
||||
if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查灰度部署是否为 nil 或空
|
||||
grayDeployments := grayConfig.GrayDeployments
|
||||
if grayDeployments != nil && len(grayDeployments) > 0 {
|
||||
for _, grayDeployment := range grayDeployments {
|
||||
if grayDeployment.Enabled {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ExtractCookieValueByKey 根据 cookie 和 key 获取 cookie 值
|
||||
func ExtractCookieValueByKey(cookie string, key string) string {
|
||||
if cookie == "" {
|
||||
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
|
||||
value := ""
|
||||
pairs := strings.Split(cookie, ";")
|
||||
for _, pair := range pairs {
|
||||
pair = strings.TrimSpace(pair)
|
||||
kv := strings.Split(pair, "=")
|
||||
if kv[0] == key {
|
||||
value = kv[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return ""
|
||||
}
|
||||
return foundCookieValue
|
||||
return value
|
||||
}
|
||||
|
||||
// contains 检查切片 slice 中是否含有元素 value。
|
||||
func Contains(slice []interface{}, value string) bool {
|
||||
func ContainsValue(slice []string, value string) bool {
|
||||
for _, item := range slice {
|
||||
if item == value {
|
||||
return true
|
||||
@@ -43,6 +60,30 @@ func Contains(slice []interface{}, value string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// headers: [][2]string -> map[string][]string
|
||||
func ConvertHeaders(hs [][2]string) map[string][]string {
|
||||
ret := make(map[string][]string)
|
||||
for _, h := range hs {
|
||||
k, v := strings.ToLower(h[0]), h[1]
|
||||
ret[k] = append(ret[k], v)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// headers: map[string][]string -> [][2]string
|
||||
func ReconvertHeaders(hs map[string][]string) [][2]string {
|
||||
var ret [][2]string
|
||||
for k, vs := range hs {
|
||||
for _, v := range vs {
|
||||
ret = append(ret, [2]string{k, v})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(ret, func(i, j int) bool {
|
||||
return ret[i][0] < ret[j][0]
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
func GetRule(rules []*config.GrayRule, name string) *config.GrayRule {
|
||||
for _, rule := range rules {
|
||||
if rule.Name == name {
|
||||
@@ -52,7 +93,66 @@ func GetRule(rules []*config.GrayRule, name string) *config.GrayRule {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetBySubKey(grayInfoStr string, graySubKey string) string {
|
||||
// 检查是否是页面
|
||||
var indexSuffixes = []string{
|
||||
".html", ".htm", ".jsp", ".php", ".asp", ".aspx", ".erb", ".ejs", ".twig",
|
||||
}
|
||||
|
||||
// IsIndexRequest determines if the request is an index request
|
||||
func IsIndexRequest(fetchMode string, p string) bool {
|
||||
if fetchMode == "cors" {
|
||||
return false
|
||||
}
|
||||
ext := path.Ext(p)
|
||||
return ext == "" || ContainsValue(indexSuffixes, ext)
|
||||
}
|
||||
|
||||
// 首页Rewrite
|
||||
func IndexRewrite(path, version string, matchRules map[string]string) string {
|
||||
for prefix, rewrite := range matchRules {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
newPath := strings.Replace(rewrite, "{version}", version, -1)
|
||||
return newPath
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func PrefixFileRewrite(path, version string, matchRules map[string]string) string {
|
||||
var matchedPrefix, replacement string
|
||||
for prefix, template := range matchRules {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
if len(prefix) > len(matchedPrefix) { // 找到更长的前缀
|
||||
matchedPrefix = prefix
|
||||
replacement = strings.Replace(template, "{version}", version, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 将path 中的前缀部分用 replacement 替换掉
|
||||
newPath := strings.Replace(path, matchedPrefix, replacement+"/", 1)
|
||||
return filepath.Clean(newPath)
|
||||
}
|
||||
|
||||
func GetVersion(version string, cookies string, isIndex bool) string {
|
||||
if isIndex {
|
||||
return version
|
||||
}
|
||||
// 来自Cookie中的版本
|
||||
cookieVersion := ExtractCookieValueByKey(cookies, config.XPreHigressTag)
|
||||
// cookie 中为空,返回当前版本
|
||||
if cookieVersion == "" {
|
||||
return version
|
||||
}
|
||||
|
||||
// cookie 中和当前版本不相同,返回cookie中值
|
||||
if cookieVersion != version {
|
||||
return cookieVersion
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// 从cookie中解析出灰度信息
|
||||
func getBySubKey(grayInfoStr string, graySubKey string) string {
|
||||
// 首先对 URL 编码的字符串进行解码
|
||||
jsonStr, err := url.QueryUnescape(grayInfoStr)
|
||||
if err != nil {
|
||||
@@ -68,3 +168,43 @@ func GetBySubKey(grayInfoStr string, graySubKey string) string {
|
||||
// 返回字符串形式的值
|
||||
return value.String()
|
||||
}
|
||||
|
||||
func GetGrayKey(grayKeyValue string, graySubKey string) string {
|
||||
// 如果有子key, 尝试从子key中获取值
|
||||
if graySubKey != "" {
|
||||
subKeyValue := getBySubKey(grayKeyValue, graySubKey)
|
||||
if subKeyValue != "" {
|
||||
grayKeyValue = subKeyValue
|
||||
}
|
||||
}
|
||||
return grayKeyValue
|
||||
}
|
||||
|
||||
// FilterGrayRule 过滤灰度规则
|
||||
func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, logInfof func(format string, args ...interface{})) *config.GrayDeployment {
|
||||
for _, grayDeployment := range grayConfig.GrayDeployments {
|
||||
if !grayDeployment.Enabled {
|
||||
// 跳过Enabled=false
|
||||
continue
|
||||
}
|
||||
grayRule := GetRule(grayConfig.Rules, grayDeployment.Name)
|
||||
// 首先:先校验用户名单ID
|
||||
if grayRule.GrayKeyValue != nil && len(grayRule.GrayKeyValue) > 0 && grayKeyValue != "" {
|
||||
if ContainsValue(grayRule.GrayKeyValue, grayKeyValue) {
|
||||
logInfof("frontendVersion: %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 := ExtractCookieValueByKey(cookieStr, grayRule.GrayTagKey)
|
||||
if ContainsValue(grayRule.GrayTagValue, grayTagValue) {
|
||||
logInfof("frontendVersion: %s, grayTag: %s=%s", grayDeployment.Version, grayRule.GrayTagKey, grayTagValue)
|
||||
return grayDeployment
|
||||
}
|
||||
}
|
||||
}
|
||||
logInfof("frontendVersion: %s, grayKeyValue: %s", grayConfig.BaseDeployment.Version, grayKeyValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetValueByCookie(t *testing.T) {
|
||||
func TestExtractCookieValueByKey(t *testing.T) {
|
||||
var tests = []struct {
|
||||
cookie, cookieKey, output string
|
||||
}{
|
||||
@@ -19,23 +19,85 @@ func TestGetValueByCookie(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
testName := test.cookie
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
output := GetValueByCookie(test.cookie, test.cookieKey)
|
||||
output := ExtractCookieValueByKey(test.cookie, test.cookieKey)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeJsonCookie(t *testing.T) {
|
||||
// 测试首页Rewrite重写
|
||||
func TestIndexRewrite(t *testing.T) {
|
||||
matchRules := map[string]string{
|
||||
"/app1": "/mfe/app1/{version}/index.html",
|
||||
"/": "/mfe/app1/{version}/index.html",
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
userInfoStr, grayJsonKey, output string
|
||||
path, 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"},
|
||||
{"/app1/", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/app123", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/app1/index.html", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/app1/index.jsp", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/app1/xxx", "/mfe/app1/v1.0.0/index.html"},
|
||||
{"/xxxx", "/mfe/app1/v1.0.0/index.html"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testName := test.userInfoStr
|
||||
testName := test.path
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
output := GetBySubKey(test.userInfoStr, test.grayJsonKey)
|
||||
output := IndexRewrite(testName, "v1.0.0", matchRules)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixFileRewrite(t *testing.T) {
|
||||
matchRules := map[string]string{
|
||||
// 前缀匹配
|
||||
"/": "/mfe/app1/{version}",
|
||||
"/app2/": "/mfe/app1/{version}",
|
||||
"/app1/": "/mfe/app1/{version}",
|
||||
"/app1/prefix2": "/mfe/app1/{version}",
|
||||
"/mfe/app1": "/mfe/app1/{version}",
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
path, output string
|
||||
}{
|
||||
{"/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/app2/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/app1/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/app1/prefix2/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/app1/prefix2/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
{"/mfe/app1/js/a.js", "/mfe/app1/v1.0.0/js/a.js"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testName := test.path
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
output := PrefixFileRewrite(testName, "v1.0.0", matchRules)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsIndexRequest(t *testing.T) {
|
||||
var tests = []struct {
|
||||
fetchMode string
|
||||
p string
|
||||
output bool
|
||||
}{
|
||||
{"cors", "/js/a.js", false},
|
||||
{"no-cors", "/js/a.js", false},
|
||||
{"no-cors", "/images/a.png", false},
|
||||
{"no-cors", "/index", true},
|
||||
{"cors", "/inde", false},
|
||||
{"no-cors", "/index.html", true},
|
||||
{"no-cors", "/demo.php", true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
testPath := test.p
|
||||
t.Run(testPath, func(t *testing.T) {
|
||||
output := IsIndexRequest(test.fetchMode, testPath)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user