From 0f298d0df1f66a54b187c84752c513cc48d9c16f Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Tue, 6 Aug 2024 11:48:22 +0800 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20frontend-gray=20pl?= =?UTF-8?q?ugin=20support=20cdn=20type=20deploy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 🎸 frontend-gray plugin support cdn type deploy --- .../extensions/frontend-gray/README.md | 86 ++++++-- .../extensions/frontend-gray/config/config.go | 79 ++++--- .../extensions/frontend-gray/envoy.yaml | 20 +- .../wasm-go/extensions/frontend-gray/go.mod | 4 +- .../wasm-go/extensions/frontend-gray/go.sum | 15 +- .../wasm-go/extensions/frontend-gray/main.go | 175 +++++++++++---- .../extensions/frontend-gray/util/utils.go | 204 ++++++++++++++++-- .../frontend-gray/util/utils_test.go | 55 ++++- 8 files changed, 513 insertions(+), 125 deletions(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md index 283785807c..396f3c5ec6 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README.md +++ b/plugins/wasm-go/extensions/frontend-gray/README.md @@ -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,17 @@ | `grayTagKey` | string | 非必填 | - | 用户分类打标的标签key值,来自Cookie | | `grayTagValue` | array of string | 非必填 | - | 用户分类打标的标签value值,来自Cookie | +`rewrite`字段配置说明: +> `index`首页重写和`file`文件重写,本质都是前缀匹配,比如`/app1`: `/mfe/app1/{version}/index.html`代表/app1为前缀的请求,路由到`/mfe/app1/{version}/index.html`页面上,其中`{version}`代表版本号,在运行过程中会被`baseDeployment.version`或者`grayDeployments[].version`动态替换。 + + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +|------------|--------------|------|-----|------------------------------| +| `host` | string | 非必填 | - | host地址,如果是OSS则设置为 VPC 内网访问地址 | +| `notFound` | string | 非必填 | - | 404 页面配置 | +| `index` | object | 非必填 | - | 首页重写配置 | +| `file` | object | 非必填 | - | 文件重写配置 | + `baseDeployment`字段配置说明: | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | @@ -28,11 +40,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 +113,51 @@ cookie存在`appInfo`的JSON数据,其中包含`userId`字段为当前的唯 - cookie中`userid`等于`00000002`或者`00000003` - cookie中`level`等于`level3`或者`level5`的用户 -否则使用`version: base`版本 \ No newline at end of file +否则使用`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 + notFound: /mfe/app1/dev/404.html + index: + /app1: '/mfe/app1/{version}/index.html' + /: '/mfe/app1/{version}/index.html', + file: + /: '/mfe/app1/{version}' + /app1/: '/mfe/app1/{version}' +baseDeployment: + version: base +grayDeployments: + - name: beta-user + version: gray + enabled: true +``` + +`{version}`会在运行过程中动态替换为真正的版本 + +#### 首页配置 +访问 `/app1`, `/app123`,`/app1/index.html`, `/app1/xxx`, `/xxxx` 都会路由到'/mfe/app1/{version}/index.html' + +#### 文件配置 +下面文件映射均生效 +- `/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` + diff --git a/plugins/wasm-go/extensions/frontend-gray/config/config.go b/plugins/wasm-go/extensions/frontend-gray/config/config.go index ce87b816d2..ad1878dbf4 100644 --- a/plugins/wasm-go/extensions/frontend-gray/config/config.go +++ b/plugins/wasm-go/extensions/frontend-gray/config/config.go @@ -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{} +func convertToStringList(results []gjson.Result) []string { + interfaces := make([]string, 0) 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) - } + interfaces = append(interfaces, result.Value().(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.notFound").String(), + Index: convertToStringMap(json.Get("rewrite.index")), + File: convertToStringMap(json.Get("rewrite.file")), + } - // 解析 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(), }) } } diff --git a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml index 63c6ec0d2f..4af5cf333b 100644 --- a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml +++ b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml @@ -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,24 @@ static_resources: ] } ], + "rewrite": { + "host": "frontend-gray-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com", + "notFound": "/mfe/app1/dev/404.html", + "index": { + "/app1": "/mfe/app1/{version}/index.html" + }, + "file": { + "/": "/mfe/app1/{version}" + } + }, "baseDeployment": { - "version": "base" + "version": "dev" }, "grayDeployments": [ { "name": "beta-user", - "version": "gray", + "version": "0.0.1", + "backendVersion": "beta", "enabled": true } ] @@ -98,5 +108,5 @@ static_resources: - endpoint: address: socket_address: - address: httpbin.org + address: frontend-gray-cn-shanghai.oss-cn-shanghai.aliyuncs.com port_value: 80 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/frontend-gray/go.mod b/plugins/wasm-go/extensions/frontend-gray/go.mod index 835da044fb..574fdc4e96 100644 --- a/plugins/wasm-go/extensions/frontend-gray/go.mod +++ b/plugins/wasm-go/extensions/frontend-gray/go.mod @@ -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 ) diff --git a/plugins/wasm-go/extensions/frontend-gray/go.sum b/plugins/wasm-go/extensions/frontend-gray/go.sum index 11f68bcbfb..a143935749 100644 --- a/plugins/wasm-go/extensions/frontend-gray/go.sum +++ b/plugins/wasm-go/extensions/frontend-gray/go.sum @@ -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= diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index 5d4ecaed42..2375bd2e10 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -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,8 @@ func main() { "frontend-gray", wrapper.ParseConfigBy(parseConfig), wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), + wrapper.ProcessResponseHeadersBy(onHttpResponseHeader), + wrapper.ProcessResponseBodyBy(onHttpResponseBody), ) } @@ -24,55 +30,142 @@ 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 - } +func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action { + if !util.IsGreyEnabled(grayConfig) { + return types.ActionContinue + } + + cookies, _ := proxywasm.GetHttpRequestHeader("cookie") + path, _ := proxywasm.GetHttpRequestHeader(":path") + accept, _ := proxywasm.GetHttpRequestHeader("accept") + fetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode") + + isIndex := util.CheckReqesutIsIndex(fetchMode, accept, 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) } - // 第二:校验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("rewrite path: %s %s %v", path, frontendVersion, rewritePath) + proxywasm.ReplaceHttpRequestHeader(":path", rewritePath) + } + + return types.ActionContinue +} + +func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action { + if !util.IsGreyEnabled(grayConfig) { + return types.ActionContinue + } + 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)) + return types.ActionContinue + } else { + ctx.DontReadResponseBody() } } + log.Errorf("error status: %s, error message: %v", status, err) + return types.ActionContinue } - log.Infof("x-mse-tag: %s, grayKeyValue: %s", grayConfig.BaseDeployment.Version, grayKeyValue) - return nil + + if strings.HasPrefix(contentType, "text/html") { + ctx.SetContext(config.IsHTML, true) + // 不会进去Streaming 的Body处理 + ctx.BufferResponseBody() + // 删除content-length,可能要修改Response返回值 + proxywasm.RemoveHttpResponseHeader("Content-Length") + + // 删除Content-Disposition,避免自动下载文件 + proxywasm.RemoveHttpResponseHeader("Content-Disposition") + + // 添加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 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 onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte, log wrapper.Log) types.Action { + if !util.IsGreyEnabled(grayConfig) { + return types.ActionContinue } - // 如果有子key, 尝试从子key中获取值 - if grayConfig.GraySubKey != "" { - subKeyValue := util.GetBySubKey(grayKeyValue, grayConfig.GraySubKey) - if subKeyValue != "" { - grayKeyValue = subKeyValue - } + backendVersion := ctx.GetContext(config.XMseTag) + isHtml := ctx.GetContext(config.IsHTML) + isIndex := ctx.GetContext(config.IsIndex) + notFound := ctx.GetContext(config.NotFound) + if isIndex != nil && isIndex.(bool) && notFound != nil && notFound.(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 } - grayDeployment := FilterGrayRule(&grayConfig, grayKeyValue, log) - if grayDeployment != nil { - proxywasm.AddHttpRequestHeader("x-mse-tag", grayDeployment.Version) + + // 以text/html 开头,将 cookie转到cookie + if isHtml != nil && isHtml.(bool) && backendVersion != nil && backendVersion.(string) != "" { + newText := strings.ReplaceAll(string(body), "", ` + `) + if err := proxywasm.ReplaceHttpResponseBody([]byte(newText)); err != nil { + return types.ActionContinue + } } return types.ActionContinue } diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go index 9faa960823..c1b245246c 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -1,7 +1,11 @@ package util import ( + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" "net/url" + "path" + "path/filepath" + "sort" "strings" "github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config" @@ -9,32 +13,46 @@ import ( "github.com/tidwall/gjson" ) -// GetValueByCookie 根据 cookieStr 和 cookieName 获取 cookie 值 -func GetValueByCookie(cookieStr string, cookieName string) string { - if cookieStr == "" { - return "" +func IsGreyEnabled(grayConfig config.GrayConfig) bool { + if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" { + return true } - 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 + + grayDeployments := grayConfig.GrayDeployments + if grayDeployments == nil || len(grayDeployments) == 0 { + return false + } + hasEnabled := false + for _, grayDeployment := range grayDeployments { + if grayDeployment.Enabled { + hasEnabled = true } } - if !found { + if hasEnabled { + return true + } + return false +} + +// ExtractCookieValueByKey 根据 cookie 和 key 获取 cookie 值 +func ExtractCookieValueByKey(cookie string, key string) string { + if cookie == "" { return "" } - return foundCookieValue + 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 + } + } + 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 +61,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 +94,89 @@ func GetRule(rules []*config.GrayRule, name string) *config.GrayRule { return nil } -func GetBySubKey(grayInfoStr string, graySubKey string) string { +// 检查是否是页面 +func CheckReqesutIsIndex(fetchMode string, accept string, p string) bool { + // 如果存在text/html,则一定是页面 + if strings.Contains(accept, "text/html") { + return true + } + // 使用 fetch/xhr 方式 + if fetchMode == "cors" { + return false + } + + indexSuffixes := []string{ + ".html", + ".htm", + ".jsp", + ".php", + ".asp", + ".aspx", + ".erb", + ".ejs", + ".twig", + } + + for _, suffix := range indexSuffixes { + if strings.HasSuffix(p, suffix) { + return true + } + } + // 如果不是文件,没有后缀,则认为是页面 + // 但是不准确 + if path.Ext(p) != "" { + return false + } + + return false +} + +// 首页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 +192,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 +} diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go index 13e9367944..4ab7154923 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go @@ -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,62 @@ 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 { + path, output string + }{ + {"/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.path + t.Run(testName, func(t *testing.T) { + 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 { - 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"}, + {"/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.userInfoStr + testName := test.path t.Run(testName, func(t *testing.T) { - output := GetBySubKey(test.userInfoStr, test.grayJsonKey) + output := PrefixFileRewrite(testName, "v1.0.0", matchRules) assert.Equal(t, test.output, output) }) } From 67a09e8725703f2a28ec429a449cb156882d8a24 Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Tue, 6 Aug 2024 21:25:39 +0800 Subject: [PATCH 02/12] =?UTF-8?q?fix:=20=F0=9F=90=9B=20frontend-gray=20add?= =?UTF-8?q?=20ProcessStreamingResponseBody?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/frontend-gray/envoy.yaml | 6 ++++-- .../wasm-go/extensions/frontend-gray/main.go | 12 +++++++++++- .../extensions/frontend-gray/util/utils.go | 19 ++++++------------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml index 4af5cf333b..3b39b7ae19 100644 --- a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml +++ b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml @@ -73,10 +73,12 @@ static_resources: "host": "frontend-gray-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com", "notFound": "/mfe/app1/dev/404.html", "index": { - "/app1": "/mfe/app1/{version}/index.html" + "/app1": "/mfe/app1/{version}/index.html", + "/": "/mfe/app1/{version}/index.html" }, "file": { - "/": "/mfe/app1/{version}" + "/": "/mfe/app1/{version}", + "/app1": "/mfe/app1/{version}" } }, "baseDeployment": { diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index 2375bd2e10..b04c4f2c61 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -21,6 +21,7 @@ func main() { wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), wrapper.ProcessResponseHeadersBy(onHttpResponseHeader), wrapper.ProcessResponseBodyBy(onHttpResponseBody), + wrapper.ProcessStreamingResponseBodyBy(onStreamingResponseBody), ) } @@ -95,7 +96,6 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, } status, err := proxywasm.GetHttpResponseHeader(":status") contentType, _ := proxywasm.GetHttpResponseHeader("Content-Type") - if err != nil || status != "200" { isIndex := ctx.GetContext(config.IsIndex) if status == "404" { @@ -107,6 +107,7 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, headersMap["content-type"][0] = "text/html" delete(headersMap, "content-length") proxywasm.ReplaceHttpResponseHeaders(util.ReconvertHeaders(headersMap)) + ctx.BufferResponseBody() return types.ActionContinue } else { ctx.DontReadResponseBody() @@ -116,6 +117,11 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, return types.ActionContinue } + if grayConfig.Rewrite.Host != "" { + // 删除Content-Disposition,避免自动下载文件 + proxywasm.RemoveHttpResponseHeader("Content-Disposition") + } + if strings.HasPrefix(contentType, "text/html") { ctx.SetContext(config.IsHTML, true) // 不会进去Streaming 的Body处理 @@ -169,3 +175,7 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b } return types.ActionContinue } + +func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.GrayConfig, chunk []byte, isLastChunk bool, log wrapper.Log) []byte { + return chunk +} diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go index c1b245246c..93ef626187 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -1,13 +1,14 @@ package util import ( - "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" "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" @@ -96,11 +97,7 @@ func GetRule(rules []*config.GrayRule, name string) *config.GrayRule { // 检查是否是页面 func CheckReqesutIsIndex(fetchMode string, accept string, p string) bool { - // 如果存在text/html,则一定是页面 - if strings.Contains(accept, "text/html") { - return true - } - // 使用 fetch/xhr 方式 + // fetch/xhr 请求,认为不是页面 if fetchMode == "cors" { return false } @@ -117,18 +114,14 @@ func CheckReqesutIsIndex(fetchMode string, accept string, p string) bool { ".twig", } + // 如果后缀存在html等,则返回true for _, suffix := range indexSuffixes { if strings.HasSuffix(p, suffix) { return true } } - // 如果不是文件,没有后缀,则认为是页面 - // 但是不准确 - if path.Ext(p) != "" { - return false - } - - return false + // 如果存在后缀,如果不存在,则返回true + return path.Ext(p) == "" } // 首页Rewrite From cea1812c8a71c3c8d8f1780e8c4e955b1df24a67 Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Fri, 9 Aug 2024 18:42:59 +0800 Subject: [PATCH 03/12] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BC=96=E5=86=99=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/frontend-gray/README.md | 22 +++++----- .../extensions/frontend-gray/config/config.go | 12 +++--- .../extensions/frontend-gray/envoy.yaml | 6 +-- .../wasm-go/extensions/frontend-gray/main.go | 26 +++++------ .../extensions/frontend-gray/util/utils.go | 43 +++++++------------ 5 files changed, 48 insertions(+), 61 deletions(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md index 396f3c5ec6..384d6a526f 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README.md +++ b/plugins/wasm-go/extensions/frontend-gray/README.md @@ -8,7 +8,7 @@ | `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 前端部署的重写配置 | +| `rewrite` | object | 必填 | - | 重写配置,一般用于OSS/CDN前端部署的重写配置 | | `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 | | `grayDeployments` | array of object | 非必填 | - | 配置Gray灰度的生效规则,以及生效版本 | @@ -22,15 +22,17 @@ | `grayTagValue` | array of string | 非必填 | - | 用户分类打标的标签value值,来自Cookie | `rewrite`字段配置说明: -> `index`首页重写和`file`文件重写,本质都是前缀匹配,比如`/app1`: `/mfe/app1/{version}/index.html`代表/app1为前缀的请求,路由到`/mfe/app1/{version}/index.html`页面上,其中`{version}`代表版本号,在运行过程中会被`baseDeployment.version`或者`grayDeployments[].version`动态替换。 +> `indexRouting`首页重写和`fileRouting`文件重写,本质都是前缀匹配,比如`/app1`: `/mfe/app1/{version}/index.html`代表/app1为前缀的请求,路由到`/mfe/app1/{version}/index.html`页面上,其中`{version}`代表版本号,在运行过程中会被`baseDeployment.version`或者`grayDeployments[].version`动态替换。 + +> `{version}` 作为保留字段,在灰度过程中进行动态替换前端版本。 | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | |------------|--------------|------|-----|------------------------------| | `host` | string | 非必填 | - | host地址,如果是OSS则设置为 VPC 内网访问地址 | -| `notFound` | string | 非必填 | - | 404 页面配置 | -| `index` | object | 非必填 | - | 首页重写配置 | -| `file` | object | 非必填 | - | 文件重写配置 | +| `notFoundUri` | string | 非必填 | - | 404 页面配置 | +| `indexRouting` | object | 非必填 | - | 首页重写配置 | +| `fileRouting` | object | 非必填 | - | 文件重写配置 | `baseDeployment`字段配置说明: @@ -134,11 +136,11 @@ rules: - level5 rewrite: host: frontend-gray.oss-cn-shanghai-internal.aliyuncs.com - notFound: /mfe/app1/dev/404.html - index: + notFoundUri: /mfe/app1/dev/404.html + indexRouting: /app1: '/mfe/app1/{version}/index.html' /: '/mfe/app1/{version}/index.html', - file: + fileRouting: /: '/mfe/app1/{version}' /app1/: '/mfe/app1/{version}' baseDeployment: @@ -151,10 +153,10 @@ grayDeployments: `{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` diff --git a/plugins/wasm-go/extensions/frontend-gray/config/config.go b/plugins/wasm-go/extensions/frontend-gray/config/config.go index ad1878dbf4..efadd07b14 100644 --- a/plugins/wasm-go/extensions/frontend-gray/config/config.go +++ b/plugins/wasm-go/extensions/frontend-gray/config/config.go @@ -51,9 +51,9 @@ type GrayConfig struct { } func convertToStringList(results []gjson.Result) []string { - interfaces := make([]string, 0) - for _, result := range results { - interfaces = append(interfaces, result.Value().(string)) + interfaces := make([]string, len(results)) // 预分配切片容量 + for i, result := range results { + interfaces[i] = result.String() // 使用 String() 方法直接获取字符串 } return interfaces } @@ -85,9 +85,9 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { } grayConfig.Rewrite = &Rewrite{ Host: json.Get("rewrite.host").String(), - NotFound: json.Get("rewrite.notFound").String(), - Index: convertToStringMap(json.Get("rewrite.index")), - File: convertToStringMap(json.Get("rewrite.file")), + NotFound: json.Get("rewrite.notFoundUri").String(), + Index: convertToStringMap(json.Get("rewrite.indexRouting")), + File: convertToStringMap(json.Get("rewrite.fileRouting")), } // 解析 deployment diff --git a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml index 3b39b7ae19..ec7620bf06 100644 --- a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml +++ b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml @@ -71,12 +71,12 @@ static_resources: ], "rewrite": { "host": "frontend-gray-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com", - "notFound": "/mfe/app1/dev/404.html", - "index": { + "notFoundUri": "/mfe/app1/dev/404.html", + "indexRouting": { "/app1": "/mfe/app1/{version}/index.html", "/": "/mfe/app1/{version}/index.html" }, - "file": { + "fileRouting": { "/": "/mfe/app1/{version}", "/app1": "/mfe/app1/{version}" } diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index b04c4f2c61..17e03922b4 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -32,7 +32,7 @@ func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.L } func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action { - if !util.IsGreyEnabled(grayConfig) { + if !util.IsGrayEnabled(grayConfig) { return types.ActionContinue } @@ -41,7 +41,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, accept, _ := proxywasm.GetHttpRequestHeader("accept") fetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode") - isIndex := util.CheckReqesutIsIndex(fetchMode, accept, path) + isIndex := util.IsIndexRequest(fetchMode, accept, path) hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0 grayKeyValue := util.GetGrayKey(util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey), grayConfig.GraySubKey) @@ -91,7 +91,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, } func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action { - if !util.IsGreyEnabled(grayConfig) { + if !util.IsGrayEnabled(grayConfig) { return types.ActionContinue } status, err := proxywasm.GetHttpResponseHeader(":status") @@ -117,20 +117,16 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, return types.ActionContinue } - if grayConfig.Rewrite.Host != "" { - // 删除Content-Disposition,避免自动下载文件 - proxywasm.RemoveHttpResponseHeader("Content-Disposition") - } + // 删除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() - // 删除content-length,可能要修改Response返回值 - proxywasm.RemoveHttpResponseHeader("Content-Length") - - // 删除Content-Disposition,避免自动下载文件 - proxywasm.RemoveHttpResponseHeader("Content-Disposition") // 添加Cache-Control 头部,禁止缓存 proxywasm.ReplaceHttpRequestHeader("Cache-Control", "no-cache, no-store") @@ -147,14 +143,14 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, } func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte, log wrapper.Log) types.Action { - if !util.IsGreyEnabled(grayConfig) { + if !util.IsGrayEnabled(grayConfig) { return types.ActionContinue } backendVersion := ctx.GetContext(config.XMseTag) isHtml := ctx.GetContext(config.IsHTML) isIndex := ctx.GetContext(config.IsIndex) - notFound := ctx.GetContext(config.NotFound) - if isIndex != nil && isIndex.(bool) && notFound != nil && notFound.(bool) && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" { + 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) diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go index 93ef626187..45d30da677 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -14,24 +14,22 @@ import ( "github.com/tidwall/gjson" ) -func IsGreyEnabled(grayConfig config.GrayConfig) bool { +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 { - return false - } - hasEnabled := false - for _, grayDeployment := range grayDeployments { - if grayDeployment.Enabled { - hasEnabled = true + if grayDeployments != nil && len(grayDeployments) > 0 { + for _, grayDeployment := range grayDeployments { + if grayDeployment.Enabled { + return true + } } } - if hasEnabled { - return true - } + return false } @@ -96,31 +94,22 @@ func GetRule(rules []*config.GrayRule, name string) *config.GrayRule { } // 检查是否是页面 -func CheckReqesutIsIndex(fetchMode string, accept string, p string) bool { - // fetch/xhr 请求,认为不是页面 +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, accept string, p string) bool { if fetchMode == "cors" { return false } - indexSuffixes := []string{ - ".html", - ".htm", - ".jsp", - ".php", - ".asp", - ".aspx", - ".erb", - ".ejs", - ".twig", - } - - // 如果后缀存在html等,则返回true for _, suffix := range indexSuffixes { if strings.HasSuffix(p, suffix) { return true } } - // 如果存在后缀,如果不存在,则返回true + return path.Ext(p) == "" } From 5513f950144a2f760fb3b644214881c20557a25a Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Mon, 12 Aug 2024 10:24:55 +0800 Subject: [PATCH 04/12] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20update=20fro?= =?UTF-8?q?ntend=20gray=20plugin=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/wasm-go/extensions/frontend-gray/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md index 384d6a526f..af92008377 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README.md +++ b/plugins/wasm-go/extensions/frontend-gray/README.md @@ -24,7 +24,7 @@ `rewrite`字段配置说明: > `indexRouting`首页重写和`fileRouting`文件重写,本质都是前缀匹配,比如`/app1`: `/mfe/app1/{version}/index.html`代表/app1为前缀的请求,路由到`/mfe/app1/{version}/index.html`页面上,其中`{version}`代表版本号,在运行过程中会被`baseDeployment.version`或者`grayDeployments[].version`动态替换。 -> `{version}` 作为保留字段,在灰度过程中进行动态替换前端版本。 +> `{version}` 作为保留字段,在执行过程中会被`baseDeployment.version`或者`grayDeployments[].version`动态替换前端版本。 | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | From 3f8ef3072863dceffdefc3eb06baec2bc56c669d Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Wed, 14 Aug 2024 14:20:23 +0800 Subject: [PATCH 05/12] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20IndexRequest=20=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E5=8A=A0=E4=B8=8Atest=20case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/frontend-gray/README.md | 4 ++-- .../wasm-go/extensions/frontend-gray/main.go | 3 +-- .../extensions/frontend-gray/util/utils.go | 12 +++------- .../frontend-gray/util/utils_test.go | 23 +++++++++++++++++++ 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md index af92008377..dce90802c6 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README.md +++ b/plugins/wasm-go/extensions/frontend-gray/README.md @@ -31,8 +31,8 @@ |------------|--------------|------|-----|------------------------------| | `host` | string | 非必填 | - | host地址,如果是OSS则设置为 VPC 内网访问地址 | | `notFoundUri` | string | 非必填 | - | 404 页面配置 | -| `indexRouting` | object | 非必填 | - | 首页重写配置 | -| `fileRouting` | object | 非必填 | - | 文件重写配置 | +| `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`字段配置说明: diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index 17e03922b4..365c200f95 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -38,10 +38,9 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, cookies, _ := proxywasm.GetHttpRequestHeader("cookie") path, _ := proxywasm.GetHttpRequestHeader(":path") - accept, _ := proxywasm.GetHttpRequestHeader("accept") fetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode") - isIndex := util.IsIndexRequest(fetchMode, accept, path) + 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) diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go index 45d30da677..9b0cb52080 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -99,18 +99,12 @@ var indexSuffixes = []string{ } // IsIndexRequest determines if the request is an index request -func IsIndexRequest(fetchMode string, accept string, p string) bool { +func IsIndexRequest(fetchMode string, p string) bool { if fetchMode == "cors" { return false } - - for _, suffix := range indexSuffixes { - if strings.HasSuffix(p, suffix) { - return true - } - } - - return path.Ext(p) == "" + ext := path.Ext(p) + return ext == "" || ContainsValue(indexSuffixes, ext) } // 首页Rewrite diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go index 4ab7154923..147c32311b 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go @@ -79,3 +79,26 @@ func TestPrefixFileRewrite(t *testing.T) { }) } } + +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) + }) + } +} From 1de4f872228ec807a023b3d48d56578fd559e1b1 Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Tue, 10 Sep 2024 00:19:35 +0800 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20=F0=9F=8E=B8=201.=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=8C=89=E7=85=A7=E6=AF=94=E7=8E=87=20weight=E7=81=B0?= =?UTF-8?q?=E5=BA=A6=E8=83=BD=E5=8A=9B=EF=BC=8C2.=20=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E6=B3=A8=E5=85=A5inject=20=E8=84=9A=E6=9C=AC=E5=88=B0=E9=A6=96?= =?UTF-8?q?=E9=A1=B5HTML=E7=9A=84=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/frontend-gray/README.md | 80 +++++++++++- .../extensions/frontend-gray/config/config.go | 63 ++++++--- .../extensions/frontend-gray/envoy.yaml | 25 +++- .../wasm-go/extensions/frontend-gray/main.go | 119 +++++++++++------ .../extensions/frontend-gray/util/utils.go | 121 +++++++++++++----- .../frontend-gray/util/utils_test.go | 20 +++ 6 files changed, 333 insertions(+), 95 deletions(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md index dce90802c6..c851083d37 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README.md +++ b/plugins/wasm-go/extensions/frontend-gray/README.md @@ -6,11 +6,15 @@ | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | |----------------|--------------|----|-----|----------------------------------------------------------------------------------------------------| | `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灰度的生效规则,以及生效版本 | +| `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` | +| `rules` | array of object | 必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 | +| `rewrite` | object | 必填 | - | 重写配置,一般用于OSS/CDN前端部署的重写配置 | +| `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 | +| `grayDeployments` | array of object | 非必填 | - | 配置Gray灰度的生效规则,以及生效版本 | +| `backendGrayTag` | string | 非必填 | `x-mse-tag` | 后端灰度版本Tag,如果配置了,cookie中将携带值为`${backendGrayTag}:${grayDeployments[].backendVersion}` | +| `debugGrayWeight` | boolean | 非必填 | - | 开启安比例灰度的Debug模式,用于观测按比例灰度效果 | +| `injection` | object | 非必填 | - | 往首页HTML中注入全局信息,比如`` | + `rules`字段配置说明: @@ -45,12 +49,28 @@ | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | |--------|--------|------|-----|-------------------------------------------------| | `version` | string | 必填 | - | Gray版本的版本号,如果命中灰度规则,则使用此版本。如果是非CDN部署,在header添加`x-higress-tag` | -| `backendVersion` | string | 必填 | - | 后端灰度版本,会在`XHR/Fetch`请求的header头添加 `x-mse-tag`到后端 | +| `backendVersion` | string | 必填 | - | 后端灰度版本,配合`key`为`${backendGrayTag}`,写入cookie中 | | `name` | string | 必填 | - | 规则名称和`rules[].name`关联, | | `enabled` | boolean | 必填 | - | 是否启动当前灰度规则 | +| `weight` | int | 非必填 | - | 按照比例灰度,比如`50`。注意:灰度规则权重总和不能超过100,如果配置了`weight`,则优先生效 | + +`injection`字段配置说明: + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +|--------|--------|------|-----|-------------------------------------------------| +| `header` | array of string | 非必填 | - | 注入header信息,比如`` | +| `body` | map of string to string | 非必填 | - | 注入Body | + +`injection.body`字段配置说明: +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +|--------|--------|------|-----|-------------------------------------------------| +| `first` | array of string | 非必填 | - | 注入body标签的首部 | +| `after` | array of string | 非必填 | - | 注入body标签的尾部 | + + ## 配置示例 -### 基础配置 +### 基础配置(按用户灰度) ```yml grayKey: userid rules: @@ -83,6 +103,24 @@ cookie中的用户唯一标识为 `userid`,当前灰度规则配置了`beta-us 否则使用`version: base`版本 +### 按比例灰度 +```yml +grayKey: userid +rules: +- name: inner-user + grayKeyValue: + - '00000001' + - '00000005' +baseDeployment: + version: base +grayDeployments: + - name: beta-user + version: gray + enabled: true + weight: 80 +``` +总的灰度规则为100%,其中灰度版本的权重为`80%`,基线版本为`20%`。一旦用户命中了灰度规则,会根据IP固定这个用户的灰度版本(否则会在下次请求时随机选择一个灰度版本)。如果需要观测按比例灰度是否生效,使用`debugGrayWeight`开启Debug模式。 + ### 用户信息存在JSON中 ```yml @@ -163,3 +201,31 @@ grayDeployments: - `/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` + +### 往HTML首页注入代码 +```yml +grayKey: userid +rules: +- name: inner-user + grayKeyValue: + - '00000001' + - '00000005' +baseDeployment: + version: base +grayDeployments: + - name: beta-user + version: gray + enabled: true + weight: 80 +injection: + header: + - + body: + first: + - + - + last: + - + - +``` +通过 `injection`往HTML首页注入代码,可以在`header`标签注入代码,也可以在`body`标签的`first`和`last`位置注入代码。 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/frontend-gray/config/config.go b/plugins/wasm-go/extensions/frontend-gray/config/config.go index efadd07b14..e003887f3e 100644 --- a/plugins/wasm-go/extensions/frontend-gray/config/config.go +++ b/plugins/wasm-go/extensions/frontend-gray/config/config.go @@ -1,16 +1,17 @@ package config import ( + "strings" + "github.com/tidwall/gjson" ) const ( XHigressTag = "x-higress-tag" + XForwardedFor = "x-forwarded-for" XPreHigressTag = "x-pre-higress-tag" - XMseTag = "x-mse-tag" - IsHTML = "is_html" - IsIndex = "is_index" - NotFound = "not_found" + IsIndex = "is-index" + NotFound = "not-found" ) type LogInfo func(format string, args ...interface{}) @@ -22,16 +23,12 @@ type GrayRule struct { GrayTagValue []string } -type BaseDeployment struct { - Name string - Version string -} - -type GrayDeployment struct { +type Deployment struct { Name string Enabled bool Version string BackendVersion string + Weight int } type Rewrite struct { @@ -41,13 +38,27 @@ type Rewrite struct { File map[string]string } +type Injection struct { + Header []string + Body *BodyInjection +} + +type BodyInjection struct { + First []string + Last []string +} + type GrayConfig struct { + TotalGrayWeight int GrayKey string GraySubKey string Rules []*GrayRule Rewrite *Rewrite - BaseDeployment *BaseDeployment - GrayDeployments []*GrayDeployment + BaseDeployment *Deployment + GrayDeployments []*Deployment + DebugGrayWeight bool + BackendGrayTag string + Injection *Injection } func convertToStringList(results []gjson.Result) []string { @@ -71,6 +82,12 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { // 解析 GrayKey grayConfig.GrayKey = json.Get("grayKey").String() grayConfig.GraySubKey = json.Get("graySubKey").String() + grayConfig.DebugGrayWeight = json.Get("debugGrayWeight").Bool() + grayConfig.BackendGrayTag = json.Get("backendGrayTag").String() + + if grayConfig.BackendGrayTag == "" { + grayConfig.BackendGrayTag = "x-mse-tag" + } // 解析 Rules rules := json.Get("rules").Array() @@ -94,16 +111,30 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { baseDeployment := json.Get("baseDeployment") grayDeployments := json.Get("grayDeployments").Array() - grayConfig.BaseDeployment = &BaseDeployment{ + grayConfig.BaseDeployment = &Deployment{ Name: baseDeployment.Get("name").String(), - Version: baseDeployment.Get("version").String(), + Version: strings.Trim(baseDeployment.Get("version").String(), " "), } for _, item := range grayDeployments { - grayConfig.GrayDeployments = append(grayConfig.GrayDeployments, &GrayDeployment{ + if !item.Get("enabled").Bool() { + continue + } + grayWeight := int(item.Get("weight").Int()) + grayConfig.GrayDeployments = append(grayConfig.GrayDeployments, &Deployment{ Name: item.Get("name").String(), Enabled: item.Get("enabled").Bool(), - Version: item.Get("version").String(), + Version: strings.Trim(item.Get("version").String(), " "), BackendVersion: item.Get("backendVersion").String(), + Weight: grayWeight, }) + grayConfig.TotalGrayWeight += grayWeight + } + + grayConfig.Injection = &Injection{ + Header: convertToStringList(json.Get("injection.header").Array()), + Body: &BodyInjection{ + First: convertToStringList(json.Get("injection.body.first").Array()), + Last: convertToStringList(json.Get("injection.body.last").Array()), + }, } } diff --git a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml index ec7620bf06..b77522a004 100644 --- a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml +++ b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml @@ -48,6 +48,7 @@ static_resources: value: | { "grayKey": "userId", + "backendGrayTag": "x-mse-tag", "rules": [ { "name": "inner-user", @@ -71,7 +72,7 @@ static_resources: ], "rewrite": { "host": "frontend-gray-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com", - "notFoundUri": "/mfe/app1/dev/404.html", + "notFoundUri": "/mfe/app1/dev/333.html", "indexRouting": { "/app1": "/mfe/app1/{version}/index.html", "/": "/mfe/app1/{version}/index.html" @@ -88,10 +89,26 @@ static_resources: { "name": "beta-user", "version": "0.0.1", - "backendVersion": "beta", - "enabled": true + "enabled": true, + "weight": 50 } - ] + ], + "debugGrayWeight": true, + "injection": { + "header": [ + "" + ], + "body": { + "first": [ + "", + "" + ], + "last": [ + "", + "" + ] + } + } } - name: envoy.filters.http.router typed_config: diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index 365c200f95..ff874119d9 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -28,6 +28,7 @@ func main() { func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.Log) error { // 解析json 为GrayConfig config.JsonToGrayConfig(json, grayConfig) + log.Infof("Rewrite: %v, GrayDeployments: %v", json.Get("rewrite"), json.Get("grayDeployments")) return nil } @@ -53,22 +54,30 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, // 删除Accept-Encoding,避免压缩, 如果是压缩的内容,后续插件就没法处理了 _ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding") _ = proxywasm.RemoveHttpRequestHeader("Content-Length") + deployment := &config.Deployment{} - grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue, log.Infof) - frontendVersion := util.GetVersion(grayConfig.BaseDeployment.Version, cookies, isIndex) - backendVersion := "" + xPreHigressVersion := util.ExtractCookieValueByKey(cookies, config.XPreHigressTag) + preVersions := strings.Split(xPreHigressVersion, ",") - // 命中灰度规则 - if grayDeployment != nil { - frontendVersion = util.GetVersion(grayDeployment.Version, cookies, isIndex) - backendVersion = grayDeployment.BackendVersion - } + xForwardedFor, _ := proxywasm.GetHttpRequestHeader("X-Forwarded-For") - proxywasm.AddHttpRequestHeader(config.XHigressTag, frontendVersion) + // 如果没有配置比例,则进行灰度规则匹配 + if isIndex { + if grayConfig.TotalGrayWeight > 0 { + deployment = util.FilterGrayWeight(&grayConfig, preVersions, xForwardedFor) + } else { + deployment = util.FilterGrayRule(&grayConfig, grayKeyValue) + } + log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %v", deployment, path, deployment.BackendVersion, xPreHigressVersion) + } else { + deployment = util.GetVersion(grayConfig, deployment, preVersions[0], isIndex) + } + proxywasm.AddHttpRequestHeader(config.XHigressTag, deployment.Version) - ctx.SetContext(config.XPreHigressTag, frontendVersion) - ctx.SetContext(config.XMseTag, backendVersion) + ctx.SetContext(config.XPreHigressTag, deployment.Version) + ctx.SetContext(grayConfig.BackendGrayTag, deployment.BackendVersion) ctx.SetContext(config.IsIndex, isIndex) + ctx.SetContext(config.XForwardedFor, xForwardedFor) rewrite := grayConfig.Rewrite if rewrite.Host != "" { @@ -78,11 +87,11 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, if hasRewrite { rewritePath := path if isIndex { - rewritePath = util.IndexRewrite(path, frontendVersion, grayConfig.Rewrite.Index) + rewritePath = util.IndexRewrite(path, deployment.Version, grayConfig.Rewrite.Index) } else { - rewritePath = util.PrefixFileRewrite(path, frontendVersion, grayConfig.Rewrite.File) + rewritePath = util.PrefixFileRewrite(path, deployment.Version, grayConfig.Rewrite.File) } - log.Infof("rewrite path: %s %s %v", path, frontendVersion, rewritePath) + log.Infof("rewrite path: %s %s %v", path, deployment.Version, rewritePath) proxywasm.ReplaceHttpRequestHeader(":path", rewritePath) } @@ -95,15 +104,32 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, } status, err := proxywasm.GetHttpResponseHeader(":status") contentType, _ := proxywasm.GetHttpResponseHeader("Content-Type") + + // 只有200状态码才进行重写 + if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" { + // 删除Content-Disposition,避免自动下载文件 + proxywasm.RemoveHttpResponseHeader("Content-Disposition") + } + + isIndex := ctx.GetContext(config.IsIndex).(bool) + if err != nil || status != "200" { - isIndex := ctx.GetContext(config.IsIndex) if status == "404" { - if grayConfig.Rewrite.NotFound != "" && isIndex != nil && isIndex.(bool) { + if grayConfig.Rewrite.NotFound != "" && isIndex { ctx.SetContext(config.NotFound, true) responseHeaders, _ := proxywasm.GetHttpResponseHeaders() headersMap := util.ConvertHeaders(responseHeaders) - headersMap[":status"][0] = "200" - headersMap["content-type"][0] = "text/html" + if _, ok := headersMap[":status"]; !ok { + headersMap[":status"] = []string{"200"} // 如果没有初始化,设定默认值 + } else { + headersMap[":status"][0] = "200" // 修改现有值 + } + if _, ok := headersMap["content-type"]; !ok { + headersMap["content-type"] = []string{"text/html"} // 如果没有初始化,设定默认值 + } else { + headersMap["content-type"][0] = "text/html" // 修改现有值 + } + // 删除 content-length 键 delete(headersMap, "content-length") proxywasm.ReplaceHttpResponseHeaders(util.ReconvertHeaders(headersMap)) ctx.BufferResponseBody() @@ -119,24 +145,22 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, // 删除content-length,可能要修改Response返回值 proxywasm.RemoveHttpResponseHeader("Content-Length") - // 删除Content-Disposition,避免自动下载文件 - proxywasm.RemoveHttpResponseHeader("Content-Disposition") - - if strings.HasPrefix(contentType, "text/html") { - ctx.SetContext(config.IsHTML, true) + if strings.HasPrefix(contentType, "text/html") || isIndex { // 不会进去Streaming 的Body处理 ctx.BufferResponseBody() - // 添加Cache-Control 头部,禁止缓存 - proxywasm.ReplaceHttpRequestHeader("Cache-Control", "no-cache, no-store") + proxywasm.ReplaceHttpResponseHeader("Cache-Control", "no-cache, no-store") frontendVersion := ctx.GetContext(config.XPreHigressTag).(string) - backendVersion := ctx.GetContext(config.XMseTag).(string) + xForwardedFor := ctx.GetContext(config.XForwardedFor).(string) // 设置当前的前端版本 - proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Path=/;", config.XPreHigressTag, frontendVersion)) + proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s,%s; Path=/;", config.XPreHigressTag, frontendVersion, util.GetRealIpFromXff(xForwardedFor))) // 设置后端的前端版本 - proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Path=/;", config.XMseTag, backendVersion)) + if util.IsBackendGrayEnabled(grayConfig) { + backendVersion := ctx.GetContext(grayConfig.BackendGrayTag).(string) + proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Path=/;", grayConfig.BackendGrayTag, backendVersion)) + } } return types.ActionContinue } @@ -145,11 +169,10 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b if !util.IsGrayEnabled(grayConfig) { return types.ActionContinue } - backendVersion := ctx.GetContext(config.XMseTag) - isHtml := ctx.GetContext(config.IsHTML) - isIndex := ctx.GetContext(config.IsIndex) + isIndex := ctx.GetContext(config.IsIndex).(bool) + notFoundUri := ctx.GetContext(config.NotFound) - if isIndex != nil && isIndex.(bool) && notFoundUri != nil && notFoundUri.(bool) && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" { + if isIndex && 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) @@ -158,13 +181,31 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b return types.ActionPause } - // 以text/html 开头,将 cookie转到cookie - if isHtml != nil && isHtml.(bool) && backendVersion != nil && backendVersion.(string) != "" { - newText := strings.ReplaceAll(string(body), "", ` - `) - if err := proxywasm.ReplaceHttpResponseBody([]byte(newText)); err != nil { + if isIndex { + // 将原始字节转换为字符串 + newBody := string(body) + + // 收集需要插入的内容 + headerInjection := strings.Join(grayConfig.Injection.Header, "\n") + bodyFirstInjection := strings.Join(grayConfig.Injection.Body.First, "\n") + bodyLastInjection := strings.Join(grayConfig.Injection.Body.Last, "\n") + + // 使用 strings.Builder 来提高性能 + var sb strings.Builder + // 预分配内存,避免多次内存分配 + sb.Grow(len(newBody) + len(headerInjection) + len(bodyFirstInjection) + len(bodyLastInjection)) + sb.WriteString(newBody) + + // 进行替换 + content := sb.String() + content = strings.ReplaceAll(content, "", fmt.Sprintf("%s\n", headerInjection)) + content = strings.ReplaceAll(content, "", fmt.Sprintf("\n%s", bodyFirstInjection)) + content = strings.ReplaceAll(content, "", fmt.Sprintf("%s\n", bodyLastInjection)) + + // 最终结果 + newBody = content + + if err := proxywasm.ReplaceHttpResponseBody([]byte(newBody)); err != nil { return types.ActionContinue } } diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go index 9b0cb52080..e7112c4187 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -1,11 +1,14 @@ package util import ( + "fmt" + "math/rand" "net/url" "path" "path/filepath" "sort" "strings" + "time" "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" @@ -14,22 +17,40 @@ import ( "github.com/tidwall/gjson" ) +func LogInfof(format string, args ...interface{}) { + format = fmt.Sprintf("[%s] %s", "frontend-gray", format) + proxywasm.LogInfof(format, args...) +} + +// 从xff中获取真实的IP +func GetRealIpFromXff(xff string) string { + if xff != "" { + // 通常客户端的真实 IP 是 XFF 头中的第一个 IP + ips := strings.Split(xff, ",") + if len(ips) > 0 { + return strings.TrimSpace(ips[0]) + } + } + return "" +} + 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 len(grayConfig.GrayDeployments) > 0 +} + +// 是否启用后端的灰度(全链路灰度) +func IsBackendGrayEnabled(grayConfig config.GrayConfig) bool { + for _, deployment := range grayConfig.GrayDeployments { + if deployment.BackendVersion != "" { + return true } } - return false } @@ -133,22 +154,25 @@ func PrefixFileRewrite(path, version string, matchRules map[string]string) strin return filepath.Clean(newPath) } -func GetVersion(version string, cookies string, isIndex bool) string { +func GetVersion(grayConfig config.GrayConfig, deployment *config.Deployment, xPreHigressVersion string, isIndex bool) *config.Deployment { if isIndex { - return version + return deployment } - // 来自Cookie中的版本 - cookieVersion := ExtractCookieValueByKey(cookies, config.XPreHigressTag) // cookie 中为空,返回当前版本 - if cookieVersion == "" { - return version + if xPreHigressVersion == "" { + return deployment } // cookie 中和当前版本不相同,返回cookie中值 - if cookieVersion != version { - return cookieVersion + if xPreHigressVersion != deployment.Version { + deployments := append(grayConfig.GrayDeployments, grayConfig.BaseDeployment) + for _, curDeployment := range deployments { + if curDeployment.Version == xPreHigressVersion { + return curDeployment + } + } } - return version + return grayConfig.BaseDeployment } // 从cookie中解析出灰度信息 @@ -181,18 +205,13 @@ func GetGrayKey(grayKeyValue string, graySubKey string) string { } // 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) +func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string) *config.Deployment { + for _, deployment := range grayConfig.GrayDeployments { + grayRule := GetRule(grayConfig.Rules, deployment.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 + return deployment } } // 第二:校验Cookie中的 GrayTagKey @@ -200,11 +219,55 @@ func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, logInfof 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 + return deployment } } } - logInfof("frontendVersion: %s, grayKeyValue: %s", grayConfig.BaseDeployment.Version, grayKeyValue) + return grayConfig.BaseDeployment +} + +func FilterGrayWeight(grayConfig *config.GrayConfig, preVersions []string, xForwardedFor string) *config.Deployment { + deployments := append(grayConfig.GrayDeployments, grayConfig.BaseDeployment) + realIp := GetRealIpFromXff(xForwardedFor) + + LogInfof("DebugGrayWeight enabled: %s, realIp: %s, preVersions: %v", grayConfig.DebugGrayWeight, realIp, preVersions) + // 开启Debug模式,否则无法观测到效果 + if !grayConfig.DebugGrayWeight { + // 如果没有获取到真实IP,则返回不走灰度规则 + if realIp == "" { + return grayConfig.BaseDeployment + } + + // 确保每个用户每次访问的都是走同一版本 + if len(preVersions) > 1 && preVersions[1] != "" && realIp == preVersions[1] { + for _, deployment := range deployments { + if deployment.Version == strings.Trim(preVersions[0], " ") { + return deployment + } + } + } + return grayConfig.BaseDeployment + } + + if grayConfig.TotalGrayWeight == 0 { + return grayConfig.BaseDeployment + } + + totalWeight := 100 + // 如果总权重小于100,则将基础版本也加入到总版本列表中 + if grayConfig.TotalGrayWeight <= totalWeight { + grayConfig.BaseDeployment.Weight = 100 - grayConfig.TotalGrayWeight + } else { + totalWeight = grayConfig.TotalGrayWeight + } + rand.Seed(time.Now().UnixNano()) + randWeight := rand.Intn(totalWeight) + sumWeight := 0 + for _, deployment := range deployments { + sumWeight += deployment.Weight + if randWeight < sumWeight { + return deployment + } + } return nil } diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go index 147c32311b..f9073913bb 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go @@ -3,7 +3,9 @@ package util import ( "testing" + "github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config" "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" ) func TestExtractCookieValueByKey(t *testing.T) { @@ -102,3 +104,21 @@ func TestIsIndexRequest(t *testing.T) { }) } } + +func TestFilterGrayWeight(t *testing.T) { + var tests = []struct { + name string + input string + }{ + {"demo", `{"grayKey":"userId","rules":[{"name":"inner-user","grayKeyValue":["00000001","00000005"]},{"name":"beta-user","grayKeyValue":["noah","00000003"],"grayTagKey":"level","grayTagValue":["level3","level5"]}],"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":"dev"},"grayDeployments":[{"name":"beta-user","version":"0.0.1","backendVersion":"beta","enabled":true,"weight":50}]}`}, + } + for _, test := range tests { + testName := test.name + t.Run(testName, func(t *testing.T) { + grayConfig := &config.GrayConfig{} + config.JsonToGrayConfig(gjson.Parse(test.input), grayConfig) + reslut := FilterGrayWeight(grayConfig) + t.Logf("reslut-----: %v", reslut) + }) + } +} From 4453f9735754d90c231835a81a6196ca9a0faa08 Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Tue, 10 Sep 2024 15:23:20 +0800 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20[frontend-gray]gra?= =?UTF-8?q?yKey=20=E5=8F=AF=E4=BB=A5=E4=BB=8Ecookie=E6=88=96=E8=80=85heade?= =?UTF-8?q?r=E4=B8=AD=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/wasm-go/extensions/frontend-gray/main.go | 5 ++++- plugins/wasm-go/extensions/frontend-gray/util/utils.go | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index ff874119d9..fba253d2a6 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -43,7 +43,10 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, 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) + grayKeyValueByCookie := util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey) + grayKeyValueByHeader, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey) + // 优先从cookie中获取,否则从header中获取 + grayKeyValue := util.GetGrayKey(grayKeyValueByCookie, grayKeyValueByHeader, grayConfig.GraySubKey) // 如果有重写的配置,则进行重写 if hasRewrite { diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go index e7112c4187..4597ce0e3a 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -193,7 +193,12 @@ func getBySubKey(grayInfoStr string, graySubKey string) string { return value.String() } -func GetGrayKey(grayKeyValue string, graySubKey string) string { +func GetGrayKey(grayKeyValueByCookie string, grayKeyValueByHeader string, graySubKey string) string { + grayKeyValue := grayKeyValueByCookie + if grayKeyValueByCookie == "" { + grayKeyValue = grayKeyValueByHeader + } + // 如果有子key, 尝试从子key中获取值 if graySubKey != "" { subKeyValue := getBySubKey(grayKeyValue, graySubKey) From 2d0ddabf1af5a0b3eaf12d1b5deb15754b96a75d Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Tue, 10 Sep 2024 21:32:08 +0800 Subject: [PATCH 08/12] =?UTF-8?q?fix:=20=F0=9F=90=9B=20[frontend-gray]=20?= =?UTF-8?q?=E4=BF=AE=E6=94=B9header=20->=20head=EF=BC=8C=20cookie=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=BB=98=E8=AE=A4=E6=97=B6=E9=97=B4=E4=B8=BA2?= =?UTF-8?q?=E5=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/frontend-gray/README.md | 10 ++++---- .../extensions/frontend-gray/config/config.go | 10 +++++--- .../extensions/frontend-gray/envoy.yaml | 7 +++--- .../wasm-go/extensions/frontend-gray/main.go | 25 ++++++++++--------- .../extensions/frontend-gray/util/utils.go | 5 +++- .../frontend-gray/util/utils_test.go | 4 ++- 6 files changed, 34 insertions(+), 27 deletions(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md index c851083d37..3cf64cdab8 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README.md +++ b/plugins/wasm-go/extensions/frontend-gray/README.md @@ -12,7 +12,7 @@ | `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 | | `grayDeployments` | array of object | 非必填 | - | 配置Gray灰度的生效规则,以及生效版本 | | `backendGrayTag` | string | 非必填 | `x-mse-tag` | 后端灰度版本Tag,如果配置了,cookie中将携带值为`${backendGrayTag}:${grayDeployments[].backendVersion}` | -| `debugGrayWeight` | boolean | 非必填 | - | 开启安比例灰度的Debug模式,用于观测按比例灰度效果 | +| `debugGrayWeight` | boolean | 非必填 | - | 开启按照比例灰度的Debug模式,用于观测按比例灰度效果, | | `injection` | object | 非必填 | - | 往首页HTML中注入全局信息,比如`` | @@ -58,8 +58,8 @@ | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | |--------|--------|------|-----|-------------------------------------------------| -| `header` | array of string | 非必填 | - | 注入header信息,比如`` | -| `body` | map of string to string | 非必填 | - | 注入Body | +| `head` | array of string | 非必填 | - | 注入head信息,比如`` | +| `body` | object | 非必填 | - | 注入Body | `injection.body`字段配置说明: | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | @@ -218,7 +218,7 @@ grayDeployments: enabled: true weight: 80 injection: - header: + head: - body: first: @@ -228,4 +228,4 @@ injection: - - ``` -通过 `injection`往HTML首页注入代码,可以在`header`标签注入代码,也可以在`body`标签的`first`和`last`位置注入代码。 \ No newline at end of file +通过 `injection`往HTML首页注入代码,可以在`head`标签注入代码,也可以在`body`标签的`first`和`last`位置注入代码。 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/frontend-gray/config/config.go b/plugins/wasm-go/extensions/frontend-gray/config/config.go index e003887f3e..3c0735544e 100644 --- a/plugins/wasm-go/extensions/frontend-gray/config/config.go +++ b/plugins/wasm-go/extensions/frontend-gray/config/config.go @@ -11,7 +11,9 @@ const ( XForwardedFor = "x-forwarded-for" XPreHigressTag = "x-pre-higress-tag" IsIndex = "is-index" - NotFound = "not-found" + IsNotFound = "is-not-found" + // 2 days + MaxAgeCookie = "172800" ) type LogInfo func(format string, args ...interface{}) @@ -39,8 +41,8 @@ type Rewrite struct { } type Injection struct { - Header []string - Body *BodyInjection + Head []string + Body *BodyInjection } type BodyInjection struct { @@ -131,7 +133,7 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { } grayConfig.Injection = &Injection{ - Header: convertToStringList(json.Get("injection.header").Array()), + Head: convertToStringList(json.Get("injection.head").Array()), Body: &BodyInjection{ First: convertToStringList(json.Get("injection.body.first").Array()), Last: convertToStringList(json.Get("injection.body.last").Array()), diff --git a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml index b77522a004..8dfccd46af 100644 --- a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml +++ b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml @@ -72,7 +72,7 @@ static_resources: ], "rewrite": { "host": "frontend-gray-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com", - "notFoundUri": "/mfe/app1/dev/333.html", + "notFoundUri": "/mfe/app1/{version}/333.html", "indexRouting": { "/app1": "/mfe/app1/{version}/index.html", "/": "/mfe/app1/{version}/index.html" @@ -89,13 +89,12 @@ static_resources: { "name": "beta-user", "version": "0.0.1", - "enabled": true, - "weight": 50 + "enabled": true } ], "debugGrayWeight": true, "injection": { - "header": [ + "head": [ "" ], "body": { diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index fba253d2a6..cae4306329 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -108,7 +108,6 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, status, err := proxywasm.GetHttpResponseHeader(":status") contentType, _ := proxywasm.GetHttpResponseHeader("Content-Type") - // 只有200状态码才进行重写 if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" { // 删除Content-Disposition,避免自动下载文件 proxywasm.RemoveHttpResponseHeader("Content-Disposition") @@ -119,7 +118,7 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, if err != nil || status != "200" { if status == "404" { if grayConfig.Rewrite.NotFound != "" && isIndex { - ctx.SetContext(config.NotFound, true) + ctx.SetContext(config.IsNotFound, true) responseHeaders, _ := proxywasm.GetHttpResponseHeaders() headersMap := util.ConvertHeaders(responseHeaders) if _, ok := headersMap[":status"]; !ok { @@ -157,12 +156,12 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, frontendVersion := ctx.GetContext(config.XPreHigressTag).(string) xForwardedFor := ctx.GetContext(config.XForwardedFor).(string) - // 设置当前的前端版本 - proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s,%s; Path=/;", config.XPreHigressTag, frontendVersion, util.GetRealIpFromXff(xForwardedFor))) - // 设置后端的前端版本 + // 设置前端的版本 + proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s,%s; Max-Age=%s; Path=/;", config.XPreHigressTag, frontendVersion, util.GetRealIpFromXff(xForwardedFor), config.MaxAgeCookie)) + // 设置后端的版本 if util.IsBackendGrayEnabled(grayConfig) { backendVersion := ctx.GetContext(grayConfig.BackendGrayTag).(string) - proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Path=/;", grayConfig.BackendGrayTag, backendVersion)) + proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%s; Path=/;", grayConfig.BackendGrayTag, backendVersion, config.MaxAgeCookie)) } } return types.ActionContinue @@ -173,11 +172,13 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b return types.ActionContinue } isIndex := ctx.GetContext(config.IsIndex).(bool) + frontendVersion := ctx.GetContext(config.XPreHigressTag).(string) - notFoundUri := ctx.GetContext(config.NotFound) - if isIndex && notFoundUri != nil && notFoundUri.(bool) && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" { + isNotFound := ctx.GetContext(config.IsNotFound) + if isIndex && isNotFound != nil && isNotFound.(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) { + + client.Get(strings.Replace(grayConfig.Rewrite.NotFound, "{version}", frontendVersion, -1), nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { proxywasm.ReplaceHttpResponseBody(responseBody) proxywasm.ResumeHttpResponse() }, 1500) @@ -189,19 +190,19 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b newBody := string(body) // 收集需要插入的内容 - headerInjection := strings.Join(grayConfig.Injection.Header, "\n") + headInjection := strings.Join(grayConfig.Injection.Head, "\n") bodyFirstInjection := strings.Join(grayConfig.Injection.Body.First, "\n") bodyLastInjection := strings.Join(grayConfig.Injection.Body.Last, "\n") // 使用 strings.Builder 来提高性能 var sb strings.Builder // 预分配内存,避免多次内存分配 - sb.Grow(len(newBody) + len(headerInjection) + len(bodyFirstInjection) + len(bodyLastInjection)) + sb.Grow(len(newBody) + len(headInjection) + len(bodyFirstInjection) + len(bodyLastInjection)) sb.WriteString(newBody) // 进行替换 content := sb.String() - content = strings.ReplaceAll(content, "", fmt.Sprintf("%s\n", headerInjection)) + content = strings.ReplaceAll(content, "", fmt.Sprintf("%s\n", headInjection)) content = strings.ReplaceAll(content, "", fmt.Sprintf("\n%s", bodyFirstInjection)) content = strings.ReplaceAll(content, "", fmt.Sprintf("%s\n", bodyLastInjection)) diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go index 4597ce0e3a..852d8a97d7 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -4,6 +4,7 @@ import ( "fmt" "math/rand" "net/url" + "os" "path" "path/filepath" "sort" @@ -19,7 +20,9 @@ import ( func LogInfof(format string, args ...interface{}) { format = fmt.Sprintf("[%s] %s", "frontend-gray", format) - proxywasm.LogInfof(format, args...) + if os.Getenv("TEST_MODE") != "" { + proxywasm.LogInfof(format, args...) + } } // 从xff中获取真实的IP diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go index f9073913bb..004a299cab 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go @@ -1,6 +1,7 @@ package util import ( + "os" "testing" "github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config" @@ -116,8 +117,9 @@ func TestFilterGrayWeight(t *testing.T) { testName := test.name t.Run(testName, func(t *testing.T) { grayConfig := &config.GrayConfig{} + os.Setenv("TEST_MODE", "true") config.JsonToGrayConfig(gjson.Parse(test.input), grayConfig) - reslut := FilterGrayWeight(grayConfig) + reslut := FilterGrayWeight(grayConfig, []string{"base", "1.0.1"}, "192.168.1.1") t.Logf("reslut-----: %v", reslut) }) } From 102f5f67d479ea13e92898f9bcee11c154d1d24c Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Thu, 12 Sep 2024 14:07:07 +0800 Subject: [PATCH 09/12] =?UTF-8?q?chore:=20=F0=9F=A4=96=20=E5=88=A0?= =?UTF-8?q?=E9=99=A4=20debugGrayWeight=E5=AD=97=E6=AE=B5=EF=BC=8C=E5=90=8C?= =?UTF-8?q?=E6=97=B6=E6=8C=89=E7=85=A7=E6=AF=94=E4=BE=8B=E7=81=B0=E5=BA=A6?= =?UTF-8?q?=E4=BC=98=E5=85=88=E6=94=AF=E6=8C=81=20grayKey=20=E7=9A=84?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=B2=98=E6=BB=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/frontend-gray/README.md | 7 ++-- .../extensions/frontend-gray/config/config.go | 4 +- .../extensions/frontend-gray/envoy.yaml | 4 +- .../wasm-go/extensions/frontend-gray/main.go | 16 +++++--- .../extensions/frontend-gray/util/utils.go | 39 +++++++------------ 5 files changed, 31 insertions(+), 39 deletions(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md index 3cf64cdab8..6c36b3a2e9 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README.md +++ b/plugins/wasm-go/extensions/frontend-gray/README.md @@ -12,7 +12,6 @@ | `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 | | `grayDeployments` | array of object | 非必填 | - | 配置Gray灰度的生效规则,以及生效版本 | | `backendGrayTag` | string | 非必填 | `x-mse-tag` | 后端灰度版本Tag,如果配置了,cookie中将携带值为`${backendGrayTag}:${grayDeployments[].backendVersion}` | -| `debugGrayWeight` | boolean | 非必填 | - | 开启按照比例灰度的Debug模式,用于观测按比例灰度效果, | | `injection` | object | 非必填 | - | 往首页HTML中注入全局信息,比如`` | @@ -52,7 +51,9 @@ | `backendVersion` | string | 必填 | - | 后端灰度版本,配合`key`为`${backendGrayTag}`,写入cookie中 | | `name` | string | 必填 | - | 规则名称和`rules[].name`关联, | | `enabled` | boolean | 必填 | - | 是否启动当前灰度规则 | -| `weight` | int | 非必填 | - | 按照比例灰度,比如`50`。注意:灰度规则权重总和不能超过100,如果配置了`weight`,则优先生效 | +| `weight` | int | 非必填 | - | 按照比例灰度,比如`50`。注意:灰度规则权重总和不能超过100,如果同时配置了`grayKey`以及`grayDeployments[0].weight`按照比例灰度优先生效 | +> 为了实现按比例(weight) 进行灰度发布,并确保用户粘滞,我们需要确认客户端的唯一性。如果配置了 grayKey,则将其用作唯一标识;如果未配置 grayKey,则使用客户端的访问 IP 地址作为唯一标识。 + `injection`字段配置说明: @@ -119,7 +120,7 @@ grayDeployments: enabled: true weight: 80 ``` -总的灰度规则为100%,其中灰度版本的权重为`80%`,基线版本为`20%`。一旦用户命中了灰度规则,会根据IP固定这个用户的灰度版本(否则会在下次请求时随机选择一个灰度版本)。如果需要观测按比例灰度是否生效,使用`debugGrayWeight`开启Debug模式。 +总的灰度规则为100%,其中灰度版本的权重为`80%`,基线版本为`20%`。一旦用户命中了灰度规则,会根据IP固定这个用户的灰度版本(否则会在下次请求时随机选择一个灰度版本)。 ### 用户信息存在JSON中 diff --git a/plugins/wasm-go/extensions/frontend-gray/config/config.go b/plugins/wasm-go/extensions/frontend-gray/config/config.go index 3c0735544e..2be58209e4 100644 --- a/plugins/wasm-go/extensions/frontend-gray/config/config.go +++ b/plugins/wasm-go/extensions/frontend-gray/config/config.go @@ -8,7 +8,7 @@ import ( const ( XHigressTag = "x-higress-tag" - XForwardedFor = "x-forwarded-for" + XUniqueClient = "x-unique-client" XPreHigressTag = "x-pre-higress-tag" IsIndex = "is-index" IsNotFound = "is-not-found" @@ -58,7 +58,6 @@ type GrayConfig struct { Rewrite *Rewrite BaseDeployment *Deployment GrayDeployments []*Deployment - DebugGrayWeight bool BackendGrayTag string Injection *Injection } @@ -84,7 +83,6 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { // 解析 GrayKey grayConfig.GrayKey = json.Get("grayKey").String() grayConfig.GraySubKey = json.Get("graySubKey").String() - grayConfig.DebugGrayWeight = json.Get("debugGrayWeight").Bool() grayConfig.BackendGrayTag = json.Get("backendGrayTag").String() if grayConfig.BackendGrayTag == "" { diff --git a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml index 8dfccd46af..3d57170380 100644 --- a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml +++ b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml @@ -89,10 +89,10 @@ static_resources: { "name": "beta-user", "version": "0.0.1", - "enabled": true + "enabled": true, + "weight": 50 } ], - "debugGrayWeight": true, "injection": { "head": [ "" diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index cae4306329..59d2f8d4d7 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -62,12 +62,18 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, xPreHigressVersion := util.ExtractCookieValueByKey(cookies, config.XPreHigressTag) preVersions := strings.Split(xPreHigressVersion, ",") - xForwardedFor, _ := proxywasm.GetHttpRequestHeader("X-Forwarded-For") + // 客户端唯一ID,用于在按照比率灰度时候 客户访问黏贴 + uniqueClientId := grayKeyValue + if uniqueClientId == "" { + xForwardedFor, _ := proxywasm.GetHttpRequestHeader("X-Forwarded-For") + uniqueClientId = util.GetRealIpFromXff(xForwardedFor) + } // 如果没有配置比例,则进行灰度规则匹配 if isIndex { + log.Infof("grayConfig.TotalGrayWeight==== %v", grayConfig.TotalGrayWeight) if grayConfig.TotalGrayWeight > 0 { - deployment = util.FilterGrayWeight(&grayConfig, preVersions, xForwardedFor) + deployment = util.FilterGrayWeight(&grayConfig, preVersions, uniqueClientId) } else { deployment = util.FilterGrayRule(&grayConfig, grayKeyValue) } @@ -80,7 +86,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, ctx.SetContext(config.XPreHigressTag, deployment.Version) ctx.SetContext(grayConfig.BackendGrayTag, deployment.BackendVersion) ctx.SetContext(config.IsIndex, isIndex) - ctx.SetContext(config.XForwardedFor, xForwardedFor) + ctx.SetContext(config.XUniqueClient, uniqueClientId) rewrite := grayConfig.Rewrite if rewrite.Host != "" { @@ -154,10 +160,10 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, proxywasm.ReplaceHttpResponseHeader("Cache-Control", "no-cache, no-store") frontendVersion := ctx.GetContext(config.XPreHigressTag).(string) - xForwardedFor := ctx.GetContext(config.XForwardedFor).(string) + xUniqueClient := ctx.GetContext(config.XUniqueClient).(string) // 设置前端的版本 - proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s,%s; Max-Age=%s; Path=/;", config.XPreHigressTag, frontendVersion, util.GetRealIpFromXff(xForwardedFor), config.MaxAgeCookie)) + proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s,%s; Max-Age=%s; Path=/;", config.XPreHigressTag, frontendVersion, xUniqueClient, config.MaxAgeCookie)) // 设置后端的版本 if util.IsBackendGrayEnabled(grayConfig) { backendVersion := ctx.GetContext(grayConfig.BackendGrayTag).(string) diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go index 852d8a97d7..5e3b1aaec6 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -4,7 +4,6 @@ import ( "fmt" "math/rand" "net/url" - "os" "path" "path/filepath" "sort" @@ -20,9 +19,7 @@ import ( func LogInfof(format string, args ...interface{}) { format = fmt.Sprintf("[%s] %s", "frontend-gray", format) - if os.Getenv("TEST_MODE") != "" { - proxywasm.LogInfof(format, args...) - } + proxywasm.LogInfof(format, args...) } // 从xff中获取真实的IP @@ -234,31 +231,21 @@ func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string) *config. return grayConfig.BaseDeployment } -func FilterGrayWeight(grayConfig *config.GrayConfig, preVersions []string, xForwardedFor string) *config.Deployment { - deployments := append(grayConfig.GrayDeployments, grayConfig.BaseDeployment) - realIp := GetRealIpFromXff(xForwardedFor) - - LogInfof("DebugGrayWeight enabled: %s, realIp: %s, preVersions: %v", grayConfig.DebugGrayWeight, realIp, preVersions) - // 开启Debug模式,否则无法观测到效果 - if !grayConfig.DebugGrayWeight { - // 如果没有获取到真实IP,则返回不走灰度规则 - if realIp == "" { - return grayConfig.BaseDeployment - } - - // 确保每个用户每次访问的都是走同一版本 - if len(preVersions) > 1 && preVersions[1] != "" && realIp == preVersions[1] { - for _, deployment := range deployments { - if deployment.Version == strings.Trim(preVersions[0], " ") { - return deployment - } - } - } +func FilterGrayWeight(grayConfig *config.GrayConfig, preVersions []string, uniqueClientId string) *config.Deployment { + // 如果没有灰度权重,直接返回基础版本 + if grayConfig.TotalGrayWeight == 0 { return grayConfig.BaseDeployment } - if grayConfig.TotalGrayWeight == 0 { - return grayConfig.BaseDeployment + deployments := append(grayConfig.GrayDeployments, grayConfig.BaseDeployment) + LogInfof("uniqueClientId: %s, preVersions: %v", uniqueClientId, preVersions) + // 用户粘滞,确保每个用户每次访问的都是走同一版本 + if len(preVersions) > 1 && preVersions[1] != "" && uniqueClientId == preVersions[1] { + for _, deployment := range deployments { + if deployment.Version == strings.Trim(preVersions[0], " ") { + return deployment + } + } } totalWeight := 100 From 7a2926e119ca82844f0a9b1c9cc5ecdce50e4c41 Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Wed, 18 Sep 2024 21:22:11 +0800 Subject: [PATCH 10/12] =?UTF-8?q?chore:=20=F0=9F=A4=96=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96CR=E4=B8=AD=E6=8F=90=E5=87=BA=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/frontend-gray/README.md | 1 + .../extensions/frontend-gray/config/config.go | 29 +++++++++++-------- .../extensions/frontend-gray/envoy.yaml | 1 + .../wasm-go/extensions/frontend-gray/main.go | 26 ++++++++--------- .../extensions/frontend-gray/util/utils.go | 7 ++--- .../frontend-gray/util/utils_test.go | 10 +++---- 6 files changed, 39 insertions(+), 35 deletions(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md index 6c36b3a2e9..e5100e1466 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README.md +++ b/plugins/wasm-go/extensions/frontend-gray/README.md @@ -7,6 +7,7 @@ |----------------|--------------|----|-----|----------------------------------------------------------------------------------------------------| | `grayKey` | string | 非必填 | - | 用户ID的唯一标识,可以来自Cookie或者Header中,比如 userid,如果没有填写则使用`rules[].grayTagKey`和`rules[].grayTagValue`过滤灰度规则 | | `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` | +| `userStickyMaxAge` | string | 非必填 | 172800 | 用户粘滞的时长:单位为秒,默认为`172800`,2天时间 | | `rules` | array of object | 必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 | | `rewrite` | object | 必填 | - | 重写配置,一般用于OSS/CDN前端部署的重写配置 | | `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 | diff --git a/plugins/wasm-go/extensions/frontend-gray/config/config.go b/plugins/wasm-go/extensions/frontend-gray/config/config.go index 2be58209e4..29bb0f0dfc 100644 --- a/plugins/wasm-go/extensions/frontend-gray/config/config.go +++ b/plugins/wasm-go/extensions/frontend-gray/config/config.go @@ -10,10 +10,8 @@ const ( XHigressTag = "x-higress-tag" XUniqueClient = "x-unique-client" XPreHigressTag = "x-pre-higress-tag" - IsIndex = "is-index" + IsPageRequest = "is-page-request" IsNotFound = "is-not-found" - // 2 days - MaxAgeCookie = "172800" ) type LogInfo func(format string, args ...interface{}) @@ -51,15 +49,16 @@ type BodyInjection struct { } type GrayConfig struct { - TotalGrayWeight int - GrayKey string - GraySubKey string - Rules []*GrayRule - Rewrite *Rewrite - BaseDeployment *Deployment - GrayDeployments []*Deployment - BackendGrayTag string - Injection *Injection + UserStickyMaxAge string + TotalGrayWeight int + GrayKey string + GraySubKey string + Rules []*GrayRule + Rewrite *Rewrite + BaseDeployment *Deployment + GrayDeployments []*Deployment + BackendGrayTag string + Injection *Injection } func convertToStringList(results []gjson.Result) []string { @@ -84,6 +83,12 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { grayConfig.GrayKey = json.Get("grayKey").String() grayConfig.GraySubKey = json.Get("graySubKey").String() grayConfig.BackendGrayTag = json.Get("backendGrayTag").String() + grayConfig.UserStickyMaxAge = json.Get("userStickyMaxAge").String() + + if grayConfig.UserStickyMaxAge == "" { + // 默认值2天 + grayConfig.UserStickyMaxAge = "172800" + } if grayConfig.BackendGrayTag == "" { grayConfig.BackendGrayTag = "x-mse-tag" diff --git a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml index 3d57170380..f454a7f072 100644 --- a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml +++ b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml @@ -49,6 +49,7 @@ static_resources: { "grayKey": "userId", "backendGrayTag": "x-mse-tag", + "userStickyMaxAge": 172800, "rules": [ { "name": "inner-user", diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index 59d2f8d4d7..ac1d5c8378 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -41,7 +41,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, path, _ := proxywasm.GetHttpRequestHeader(":path") fetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode") - isIndex := util.IsIndexRequest(fetchMode, path) + isPageRequest := util.GetIsPageRequest(fetchMode, path) hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0 grayKeyValueByCookie := util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey) grayKeyValueByHeader, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey) @@ -70,7 +70,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, } // 如果没有配置比例,则进行灰度规则匹配 - if isIndex { + if isPageRequest { log.Infof("grayConfig.TotalGrayWeight==== %v", grayConfig.TotalGrayWeight) if grayConfig.TotalGrayWeight > 0 { deployment = util.FilterGrayWeight(&grayConfig, preVersions, uniqueClientId) @@ -79,13 +79,13 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, } log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %v", deployment, path, deployment.BackendVersion, xPreHigressVersion) } else { - deployment = util.GetVersion(grayConfig, deployment, preVersions[0], isIndex) + deployment = util.GetVersion(grayConfig, deployment, preVersions[0], isPageRequest) } proxywasm.AddHttpRequestHeader(config.XHigressTag, deployment.Version) ctx.SetContext(config.XPreHigressTag, deployment.Version) ctx.SetContext(grayConfig.BackendGrayTag, deployment.BackendVersion) - ctx.SetContext(config.IsIndex, isIndex) + ctx.SetContext(config.IsPageRequest, isPageRequest) ctx.SetContext(config.XUniqueClient, uniqueClientId) rewrite := grayConfig.Rewrite @@ -95,7 +95,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, if hasRewrite { rewritePath := path - if isIndex { + if isPageRequest { rewritePath = util.IndexRewrite(path, deployment.Version, grayConfig.Rewrite.Index) } else { rewritePath = util.PrefixFileRewrite(path, deployment.Version, grayConfig.Rewrite.File) @@ -119,11 +119,11 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, proxywasm.RemoveHttpResponseHeader("Content-Disposition") } - isIndex := ctx.GetContext(config.IsIndex).(bool) + isPageRequest := ctx.GetContext(config.IsPageRequest).(bool) if err != nil || status != "200" { if status == "404" { - if grayConfig.Rewrite.NotFound != "" && isIndex { + if grayConfig.Rewrite.NotFound != "" && isPageRequest { ctx.SetContext(config.IsNotFound, true) responseHeaders, _ := proxywasm.GetHttpResponseHeaders() headersMap := util.ConvertHeaders(responseHeaders) @@ -153,7 +153,7 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, // 删除content-length,可能要修改Response返回值 proxywasm.RemoveHttpResponseHeader("Content-Length") - if strings.HasPrefix(contentType, "text/html") || isIndex { + if strings.HasPrefix(contentType, "text/html") || isPageRequest { // 不会进去Streaming 的Body处理 ctx.BufferResponseBody() @@ -163,11 +163,11 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, xUniqueClient := ctx.GetContext(config.XUniqueClient).(string) // 设置前端的版本 - proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s,%s; Max-Age=%s; Path=/;", config.XPreHigressTag, frontendVersion, xUniqueClient, config.MaxAgeCookie)) + proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s,%s; Max-Age=%s; Path=/;", config.XPreHigressTag, frontendVersion, xUniqueClient, grayConfig.UserStickyMaxAge)) // 设置后端的版本 if util.IsBackendGrayEnabled(grayConfig) { backendVersion := ctx.GetContext(grayConfig.BackendGrayTag).(string) - proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%s; Path=/;", grayConfig.BackendGrayTag, backendVersion, config.MaxAgeCookie)) + proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%s; Path=/;", grayConfig.BackendGrayTag, backendVersion, grayConfig.UserStickyMaxAge)) } } return types.ActionContinue @@ -177,11 +177,11 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b if !util.IsGrayEnabled(grayConfig) { return types.ActionContinue } - isIndex := ctx.GetContext(config.IsIndex).(bool) + isPageRequest := ctx.GetContext(config.IsPageRequest).(bool) frontendVersion := ctx.GetContext(config.XPreHigressTag).(string) isNotFound := ctx.GetContext(config.IsNotFound) - if isIndex && isNotFound != nil && isNotFound.(bool) && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" { + if isPageRequest && isNotFound != nil && isNotFound.(bool) && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" { client := wrapper.NewClusterClient(wrapper.RouteCluster{Host: grayConfig.Rewrite.Host}) client.Get(strings.Replace(grayConfig.Rewrite.NotFound, "{version}", frontendVersion, -1), nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { @@ -191,7 +191,7 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b return types.ActionPause } - if isIndex { + if isPageRequest { // 将原始字节转换为字符串 newBody := string(body) diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go index 5e3b1aaec6..ddd7060ca3 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -119,8 +119,7 @@ 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 { +func GetIsPageRequest(fetchMode string, p string) bool { if fetchMode == "cors" { return false } @@ -154,8 +153,8 @@ func PrefixFileRewrite(path, version string, matchRules map[string]string) strin return filepath.Clean(newPath) } -func GetVersion(grayConfig config.GrayConfig, deployment *config.Deployment, xPreHigressVersion string, isIndex bool) *config.Deployment { - if isIndex { +func GetVersion(grayConfig config.GrayConfig, deployment *config.Deployment, xPreHigressVersion string, isPageRequest bool) *config.Deployment { + if isPageRequest { return deployment } // cookie 中为空,返回当前版本 diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go index 004a299cab..c04ce86157 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go @@ -1,7 +1,6 @@ package util import ( - "os" "testing" "github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config" @@ -83,7 +82,7 @@ func TestPrefixFileRewrite(t *testing.T) { } } -func TestIsIndexRequest(t *testing.T) { +func TestGetIsPageRequest(t *testing.T) { var tests = []struct { fetchMode string p string @@ -100,7 +99,7 @@ func TestIsIndexRequest(t *testing.T) { for _, test := range tests { testPath := test.p t.Run(testPath, func(t *testing.T) { - output := IsIndexRequest(test.fetchMode, testPath) + output := GetIsPageRequest(test.fetchMode, testPath) assert.Equal(t, test.output, output) }) } @@ -117,10 +116,9 @@ func TestFilterGrayWeight(t *testing.T) { testName := test.name t.Run(testName, func(t *testing.T) { grayConfig := &config.GrayConfig{} - os.Setenv("TEST_MODE", "true") config.JsonToGrayConfig(gjson.Parse(test.input), grayConfig) - reslut := FilterGrayWeight(grayConfig, []string{"base", "1.0.1"}, "192.168.1.1") - t.Logf("reslut-----: %v", reslut) + result := FilterGrayWeight(grayConfig, []string{"base", "1.0.1"}, "192.168.1.1") + t.Logf("result-----: %v", result) }) } } From 444c6a337d7fe8e8f4e5db5c77420d40ecf4227f Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Sun, 22 Sep 2024 14:34:50 +0800 Subject: [PATCH 11/12] =?UTF-8?q?chore:=20=F0=9F=A4=96=20[frontend-gray]?= =?UTF-8?q?=20=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81=E9=A3=8E=E6=A0=BC?= =?UTF-8?q?=E5=92=8C=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extensions/frontend-gray/config/config.go | 10 +++--- .../wasm-go/extensions/frontend-gray/main.go | 35 +++++++++++-------- .../extensions/frontend-gray/util/utils.go | 25 +++++++++---- .../frontend-gray/util/utils_test.go | 6 ++-- 4 files changed, 48 insertions(+), 28 deletions(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/config/config.go b/plugins/wasm-go/extensions/frontend-gray/config/config.go index 29bb0f0dfc..fd1c26b154 100644 --- a/plugins/wasm-go/extensions/frontend-gray/config/config.go +++ b/plugins/wasm-go/extensions/frontend-gray/config/config.go @@ -7,11 +7,11 @@ import ( ) const ( - XHigressTag = "x-higress-tag" - XUniqueClient = "x-unique-client" - XPreHigressTag = "x-pre-higress-tag" - IsPageRequest = "is-page-request" - IsNotFound = "is-not-found" + XHigressTag = "x-higress-tag" + XUniqueClientId = "x-unique-client" + XPreHigressTag = "x-pre-higress-tag" + IsPageRequest = "is-page-request" + IsNotFound = "is-not-found" ) type LogInfo func(format string, args ...interface{}) diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index ac1d5c8378..e1fc7a8b83 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -41,13 +41,12 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, path, _ := proxywasm.GetHttpRequestHeader(":path") fetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode") - isPageRequest := util.GetIsPageRequest(fetchMode, path) + isPageRequest := util.IsPageRequest(fetchMode, path) hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0 grayKeyValueByCookie := util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey) grayKeyValueByHeader, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey) // 优先从cookie中获取,否则从header中获取 grayKeyValue := util.GetGrayKey(grayKeyValueByCookie, grayKeyValueByHeader, grayConfig.GraySubKey) - // 如果有重写的配置,则进行重写 if hasRewrite { // 禁止重新路由,要在更改Header之前操作,否则会失效 @@ -59,9 +58,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, _ = proxywasm.RemoveHttpRequestHeader("Content-Length") deployment := &config.Deployment{} - xPreHigressVersion := util.ExtractCookieValueByKey(cookies, config.XPreHigressTag) - preVersions := strings.Split(xPreHigressVersion, ",") - + preVersion, preUniqueClientId := util.GetXPreHigressVersion(cookies) // 客户端唯一ID,用于在按照比率灰度时候 客户访问黏贴 uniqueClientId := grayKeyValue if uniqueClientId == "" { @@ -73,20 +70,20 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, if isPageRequest { log.Infof("grayConfig.TotalGrayWeight==== %v", grayConfig.TotalGrayWeight) if grayConfig.TotalGrayWeight > 0 { - deployment = util.FilterGrayWeight(&grayConfig, preVersions, uniqueClientId) + deployment = util.FilterGrayWeight(&grayConfig, preVersion, preUniqueClientId, uniqueClientId) } else { deployment = util.FilterGrayRule(&grayConfig, grayKeyValue) } - log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %v", deployment, path, deployment.BackendVersion, xPreHigressVersion) + log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %s,%s", deployment, path, deployment.BackendVersion, preVersion, preUniqueClientId) } else { - deployment = util.GetVersion(grayConfig, deployment, preVersions[0], isPageRequest) + deployment = util.GetVersion(grayConfig, deployment, preVersion, isPageRequest) } proxywasm.AddHttpRequestHeader(config.XHigressTag, deployment.Version) ctx.SetContext(config.XPreHigressTag, deployment.Version) ctx.SetContext(grayConfig.BackendGrayTag, deployment.BackendVersion) ctx.SetContext(config.IsPageRequest, isPageRequest) - ctx.SetContext(config.XUniqueClient, uniqueClientId) + ctx.SetContext(config.XUniqueClientId, uniqueClientId) rewrite := grayConfig.Rewrite if rewrite.Host != "" { @@ -119,7 +116,10 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, proxywasm.RemoveHttpResponseHeader("Content-Disposition") } - isPageRequest := ctx.GetContext(config.IsPageRequest).(bool) + isPageRequest, ok := ctx.GetContext(config.IsPageRequest).(bool) + if !ok { + isPageRequest = false // 默认值 + } if err != nil || status != "200" { if status == "404" { @@ -160,7 +160,7 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, proxywasm.ReplaceHttpResponseHeader("Cache-Control", "no-cache, no-store") frontendVersion := ctx.GetContext(config.XPreHigressTag).(string) - xUniqueClient := ctx.GetContext(config.XUniqueClient).(string) + xUniqueClient := ctx.GetContext(config.XUniqueClientId).(string) // 设置前端的版本 proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s,%s; Max-Age=%s; Path=/;", config.XPreHigressTag, frontendVersion, xUniqueClient, grayConfig.UserStickyMaxAge)) @@ -177,11 +177,18 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b if !util.IsGrayEnabled(grayConfig) { return types.ActionContinue } - isPageRequest := ctx.GetContext(config.IsPageRequest).(bool) + isPageRequest, ok := ctx.GetContext(config.IsPageRequest).(bool) + if !ok { + isPageRequest = false // 默认值 + } frontendVersion := ctx.GetContext(config.XPreHigressTag).(string) - isNotFound := ctx.GetContext(config.IsNotFound) - if isPageRequest && isNotFound != nil && isNotFound.(bool) && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" { + isNotFound, ok := ctx.GetContext(config.IsNotFound).(bool) + if !ok { + isNotFound = false // 默认值 + } + + if isPageRequest && isNotFound && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" { client := wrapper.NewClusterClient(wrapper.RouteCluster{Host: grayConfig.Rewrite.Host}) client.Get(strings.Replace(grayConfig.Rewrite.NotFound, "{version}", frontendVersion, -1), nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) { diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go index ddd7060ca3..a8c096816e 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -22,6 +22,19 @@ func LogInfof(format string, args ...interface{}) { proxywasm.LogInfof(format, args...) } +func GetXPreHigressVersion(cookies string) (string, string) { + xPreHigressVersion := ExtractCookieValueByKey(cookies, config.XPreHigressTag) + preVersions := strings.Split(xPreHigressVersion, ",") + if len(preVersions) == 0 { + return "", "" + } + if len(preVersions) == 1 { + return preVersions[0], "" + } + + return strings.TrimSpace(preVersions[0]), strings.TrimSpace(preVersions[1]) +} + // 从xff中获取真实的IP func GetRealIpFromXff(xff string) string { if xff != "" { @@ -119,11 +132,11 @@ var indexSuffixes = []string{ ".html", ".htm", ".jsp", ".php", ".asp", ".aspx", ".erb", ".ejs", ".twig", } -func GetIsPageRequest(fetchMode string, p string) bool { +func IsPageRequest(fetchMode string, myPath string) bool { if fetchMode == "cors" { return false } - ext := path.Ext(p) + ext := path.Ext(myPath) return ext == "" || ContainsValue(indexSuffixes, ext) } @@ -230,18 +243,18 @@ func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string) *config. return grayConfig.BaseDeployment } -func FilterGrayWeight(grayConfig *config.GrayConfig, preVersions []string, uniqueClientId string) *config.Deployment { +func FilterGrayWeight(grayConfig *config.GrayConfig, preVersion string, preUniqueClientId string, uniqueClientId string) *config.Deployment { // 如果没有灰度权重,直接返回基础版本 if grayConfig.TotalGrayWeight == 0 { return grayConfig.BaseDeployment } deployments := append(grayConfig.GrayDeployments, grayConfig.BaseDeployment) - LogInfof("uniqueClientId: %s, preVersions: %v", uniqueClientId, preVersions) + LogInfof("preVersion: %s, preUniqueClientId: %s, uniqueClientId: %s", preVersion, preUniqueClientId, uniqueClientId) // 用户粘滞,确保每个用户每次访问的都是走同一版本 - if len(preVersions) > 1 && preVersions[1] != "" && uniqueClientId == preVersions[1] { + if preVersion != "" && uniqueClientId == preUniqueClientId { for _, deployment := range deployments { - if deployment.Version == strings.Trim(preVersions[0], " ") { + if deployment.Version == preVersion { return deployment } } diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go index c04ce86157..7ba014225f 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go @@ -82,7 +82,7 @@ func TestPrefixFileRewrite(t *testing.T) { } } -func TestGetIsPageRequest(t *testing.T) { +func TestIsPageRequest(t *testing.T) { var tests = []struct { fetchMode string p string @@ -99,7 +99,7 @@ func TestGetIsPageRequest(t *testing.T) { for _, test := range tests { testPath := test.p t.Run(testPath, func(t *testing.T) { - output := GetIsPageRequest(test.fetchMode, testPath) + output := IsPageRequest(test.fetchMode, testPath) assert.Equal(t, test.output, output) }) } @@ -117,7 +117,7 @@ func TestFilterGrayWeight(t *testing.T) { t.Run(testName, func(t *testing.T) { grayConfig := &config.GrayConfig{} config.JsonToGrayConfig(gjson.Parse(test.input), grayConfig) - result := FilterGrayWeight(grayConfig, []string{"base", "1.0.1"}, "192.168.1.1") + result := FilterGrayWeight(grayConfig, "base", "1.0.1", "192.168.1.1") t.Logf("result-----: %v", result) }) } From 1de8d967d3643e05bc9270d1b4e84b4a57b71a05 Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Sun, 22 Sep 2024 14:45:27 +0800 Subject: [PATCH 12/12] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20[frontend-gr?= =?UTF-8?q?ay]=20userStickyMaxAge=20=E5=AD=97=E6=AE=B5=E4=B8=BAint=20?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/wasm-go/extensions/frontend-gray/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md index b64539aa61..1711969c90 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README.md +++ b/plugins/wasm-go/extensions/frontend-gray/README.md @@ -18,7 +18,7 @@ description: 前端灰度插件配置参考 |----------------|--------------|----|-----|----------------------------------------------------------------------------------------------------| | `grayKey` | string | 非必填 | - | 用户ID的唯一标识,可以来自Cookie或者Header中,比如 userid,如果没有填写则使用`rules[].grayTagKey`和`rules[].grayTagValue`过滤灰度规则 | | `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` | -| `userStickyMaxAge` | string | 非必填 | 172800 | 用户粘滞的时长:单位为秒,默认为`172800`,2天时间 | +| `userStickyMaxAge` | int | 非必填 | 172800 | 用户粘滞的时长:单位为秒,默认为`172800`,2天时间 | | `rules` | array of object | 必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 | | `rewrite` | object | 必填 | - | 重写配置,一般用于OSS/CDN前端部署的重写配置 | | `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 |