From d1700009e8e2a3f2a9d0a0a7949eaab326cfe105 Mon Sep 17 00:00:00 2001 From: mamba <371510756@qq.com> Date: Fri, 11 Apr 2025 10:35:18 +0800 Subject: [PATCH] =?UTF-8?q?[frontend-gray]=20=E9=87=8D=E6=9E=84=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E9=80=BB=E8=BE=91=EF=BC=8C=E5=AF=B9=E4=BA=8E=E5=BE=AE?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=92=8C=E5=A4=9A=E7=89=88=E6=9C=AC=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=9B=B4=E5=8A=A0=E5=8F=8B=E5=A5=BD=20(#2011)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wasm-go/extensions/frontend-gray/Makefile | 11 + .../extensions/frontend-gray/README.md | 35 ++- .../extensions/frontend-gray/README_EN.md | 209 ++++++-------- .../extensions/frontend-gray/config/config.go | 135 ++++++--- .../extensions/frontend-gray/envoy.yaml | 40 ++- .../wasm-go/extensions/frontend-gray/go.mod | 1 + .../wasm-go/extensions/frontend-gray/go.sum | 6 +- .../wasm-go/extensions/frontend-gray/main.go | 267 ++++++++---------- .../extensions/frontend-gray/util/utils.go | 246 ++++++++-------- .../frontend-gray/util/utils_test.go | 49 ++-- 10 files changed, 504 insertions(+), 495 deletions(-) create mode 100644 plugins/wasm-go/extensions/frontend-gray/Makefile diff --git a/plugins/wasm-go/extensions/frontend-gray/Makefile b/plugins/wasm-go/extensions/frontend-gray/Makefile new file mode 100644 index 000000000..61e6b1cbd --- /dev/null +++ b/plugins/wasm-go/extensions/frontend-gray/Makefile @@ -0,0 +1,11 @@ +.PHONY: reload + +build: + tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer' ./main.go + +reload: + tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer' ./main.go + ./envoy -c envoy.yaml --concurrency 0 --log-level info --component-log-level wasm:debug + +start: + ./envoy -c envoy.yaml --concurrency 0 --log-level info --component-log-level wasm:debug \ No newline at end of file diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md index 8d21869cf..eea2256fd 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README.md +++ b/plugins/wasm-go/extensions/frontend-gray/README.md @@ -9,8 +9,8 @@ description: 前端灰度插件配置参考 ## 运行属性 -插件执行阶段:`认证阶段` -插件执行优先级:`450` +插件执行阶段:`默认阶段` +插件执行优先级:`1000` ## 配置字段 @@ -19,16 +19,17 @@ description: 前端灰度插件配置参考 | `grayKey` | string | 非必填 | - | 用户ID的唯一标识,可以来自Cookie或者Header中,比如 userid,如果没有填写则使用`rules[].grayTagKey`和`rules[].grayTagValue`过滤灰度规则 | | `localStorageGrayKey` | string | 非必填 | - | 使用JWT鉴权方式,用户ID的唯一标识来自`localStorage`中,如果配置了当前参数,则`grayKey`失效 | | `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` | -| `userStickyMaxAge` | int | 非必填 | 172800 | 用户粘滞的时长:单位为秒,默认为`172800`,2天时间 | -| `includePathPrefixes` | array of strings | 非必填 | - | 强制处理的路径。例如,在 微前端 场景下,XHR 接口如: `/resource/xxx`本质是一个资源请求,需要走插件转发逻辑。 | -| `skippedPathPrefixes` | array of strings | 非必填 | - | 用于排除特定路径,避免当前插件处理这些请求。例如,在 rewrite 场景下,XHR 接口请求 `/api/xxx` 如果经过插件转发逻辑,可能会导致非预期的结果。 | +| `storeMaxAge` | int | 非必填 | 60 * 60 * 24 * 365 | 网关设置Cookie最大存储时长:单位为秒,默认为1年 | +| `indexPaths` | array of strings | 非必填 | - | 强制处理的路径,支持 `Glob` 模式匹配。例如:在 微前端场景下,XHR 接口如: `/resource/**/manifest-main.json`本质是一个资源请求,需要走插件转发逻辑。 | +| `skippedPaths` | array of strings | 非必填 | - | 用于排除特定路径,避免当前插件处理这些请求,支持 `Glob` 模式匹配。例如,在 rewrite 场景下,XHR 接口请求 `/api/**` 如果经过插件转发逻辑,可能会导致非预期的结果。 | | `skippedByHeaders` | map of string to string | 非必填 | - | 用于通过请求头过滤,指定哪些请求不被当前插件 -处理。`skippedPathPrefixes` 的优先级高于当前配置,且页面HTML请求不受本配置的影响。若本配置为空,默认会判断`sec-fetch-mode=cors`以及`upgrade=websocket`两个header头,进行过滤 | +处理。`skippedPaths` 的优先级高于当前配置,且页面HTML请求不受本配置的影响。 | | `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}` | +| `uniqueGrayTag` | string | 非必填 | `x-higress-uid` | 开启按照比例灰度时候,网关会生成一个唯一标识存在`cookie`中,一方面用于session黏贴,另一方面后端也可以使用这个值用于全链路的灰度串联 | | `injection` | object | 非必填 | - | 往首页HTML中注入全局信息,比如`` | @@ -50,7 +51,6 @@ description: 前端灰度插件配置参考 | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | |------------|--------------|------|-----|------------------------------| | `host` | string | 非必填 | - | host地址,如果是OSS则设置为 VPC 内网访问地址 | -| `notFoundUri` | string | 非必填 | - | 404 页面配置 | | `indexRouting` | map of string to string | 非必填 | - | 用于定义首页重写路由规则。每个键 (Key) 表示首页的路由路径,值 (Value) 则指向重定向的目标文件。例如,键为 `/app1` 对应的值为 `/mfe/app1/{version}/index.html`。生效version为`0.0.1`, 访问路径为 `/app1`,则重定向到 `/mfe/app1/0.0.1/index.html`。 | | `fileRouting` | map of string to string | 非必填 | - | 用于定义资源文件重写路由规则。每个键 (Key) 表示资源访问路径,值 (Value) 则指向重定向的目标文件。例如,键为 `/app1/` 对应的值为 `/mfe/app1/{version}`。生效version为`0.0.1`,访问路径为 `/app1/js/a.js`,则重定向到 `/mfe/app1/0.0.1/js/a.js`。 | @@ -59,6 +59,7 @@ description: 前端灰度插件配置参考 | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | |----------------|--------------|------|-----|-----------------------------------------------------------------------------------| | `version` | string | 必填 | - | Base版本的版本号,作为兜底的版本 | +| `backendVersion` | string | 必填 | - | 后端灰度版本,配合`key`为`${backendGrayTag}`,写入cookie中 | | `versionPredicates` | string | 必填 | - | 和`version`含义相同,但是满足多版本的需求:根据不同路由映射不同的`Version`版本。一般用于微前端的场景:一个主应用需要管理多个微应用 | `grayDeployments`字段配置说明: @@ -70,17 +71,28 @@ description: 前端灰度插件配置参考 | `backendVersion` | string | 必填 | - | 后端灰度版本,配合`key`为`${backendGrayTag}`,写入cookie中 | | `name` | string | 必填 | - | 规则名称和`rules[].name`关联 | | `enabled` | boolean | 必填 | - | 是否启动当前灰度规则 | -| `weight` | int | 非必填 | - | 按照比例灰度,比如`50`。注意:灰度规则权重总和不能超过100,如果同时配置了`grayKey`以及`grayDeployments[0].weight`按照比例灰度优先生效 | -> 为了实现按比例(weight) 进行灰度发布,并确保用户粘滞,我们需要确认客户端的唯一性。如果配置了 grayKey,则将其用作唯一标识;如果未配置 grayKey,则使用客户端的访问 IP 地址作为唯一标识。 +| `weight` | int | 非必填 | - | 按照比例灰度,比如50。 | +>按照比例灰度注意下面几点: +> 1. 如果同时配置了`按用户灰度`以及`按比例灰度`,按`比例灰度`优先生效 +> 2. 采用客户端设备标识符的哈希摘要机制实现流量比例控制,其唯一性判定逻辑遵循以下原则:自动生成全局唯一标识符(UUID)作为设备指纹,可以通过`uniqueGrayTag`配置`cookie`的key值,并通过SHA-256哈希算法生成对应灰度判定基准值。 `injection`字段配置说明: | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | |--------|--------|------|-----|-------------------------------------------------| +| `globalConfig` | object | 非必填 | - | 注入到HTML首页的全局变量 | | `head` | array of string | 非必填 | - | 注入head信息,比如`` | | `body` | object | 非必填 | - | 注入Body | +`injection.globalConfig`字段配置说明: +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +|--------|--------|------|-----|-------------------------------------------------| +| `key` | string | 非必填 | HIGRESS_CONSOLE_CONFIG | 注入到window全局变量的key值 | +| `featureKey` | string | 非必填 | FEATURE_STATUS | 关于`rules`相关规则的命中情况,返回实例`{"beta-user":true,"inner-user":false}` | +| `value` | string | 非必填 | - | 自定义的全局变量 | +| `enabled` | boolean | 非必填 | false | 是否开启注入全局变量 | + `injection.body`字段配置说明: | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | |--------|--------|------|-----|-------------------------------------------------| @@ -139,8 +151,7 @@ grayDeployments: enabled: true weight: 80 ``` -总的灰度规则为100%,其中灰度版本的权重为`80%`,基线版本为`20%`。一旦用户命中了灰度规则,会根据IP固定这个用户的灰度版本(否则会在下次请求时随机选择一个灰度版本)。 - +总的灰度规则为100%,其中灰度版本的权重为80%,基线版本为20%。 ### 用户信息存在JSON中 ```yml @@ -218,7 +229,6 @@ rules: - level5 rewrite: host: frontend-gray.oss-cn-shanghai-internal.aliyuncs.com - notFoundUri: /mfe/app1/dev/404.html indexRouting: /app1: '/mfe/app1/{version}/index.html' /: '/mfe/app1/{version}/index.html', @@ -260,7 +270,6 @@ grayDeployments: - name: beta-user version: gray enabled: true - weight: 80 injection: head: - diff --git a/plugins/wasm-go/extensions/frontend-gray/README_EN.md b/plugins/wasm-go/extensions/frontend-gray/README_EN.md index b3a652d44..76c73740d 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README_EN.md +++ b/plugins/wasm-go/extensions/frontend-gray/README_EN.md @@ -1,59 +1,95 @@ --- title: Frontend Gray keywords: [higress, frontend gray] -description: Frontend gray plugin configuration reference ---- -## Function Description -The `frontend-gray` plugin implements the functionality of user gray release on the frontend. Through this plugin, it can be used for business `A/B testing`, while the `gradual release` combined with `monitorable` and `rollback` strategies ensures the stability of system release operations. +description: Frontend Gray Plugin Configuration Reference -## Runtime Attributes -Plugin execution phase: `Authentication Phase` -Plugin execution priority: `450` +## Feature Description +The `frontend-gray` plugin implements frontend user grayscale capabilities. This plugin can be used for business `A/B testing` while ensuring system release stability through `grayscale`, `monitoring`, and `rollback` strategies. + +## Runtime Properties + +Execution Stage: `Default Stage` +Execution Priority: `1000` ## Configuration Fields -| Name | Data Type | Requirements | Default Value | Description | -|-----------------|-------------------|---------------|---------------|-------------------------------------------------------------------------------------------------------------| -| `grayKey` | string | Optional | - | The unique identifier of the user ID, which can be from Cookie or Header, such as userid. If not provided, uses `rules[].grayTagKey` and `rules[].grayTagValue` to filter gray release rules. | -| `graySubKey` | string | Optional | - | User identity information may be output in JSON format, for example: `userInfo:{ userCode:"001" }`, in the current example, `graySubKey` is `userCode`. | -| `rules` | array of object | Required | - | User-defined different gray release rules, adapted to different gray release scenarios. | -| `rewrite` | object | Required | - | Rewrite configuration, generally used for OSS/CDN frontend deployment rewrite configurations. | -| `baseDeployment`| object | Optional | - | Configuration of the Base baseline rules. | -| `grayDeployments` | array of object | Optional | - | Configuration of the effective rules for gray release, as well as the effective versions. | +| Name | Data Type | Required | Default | Description | +|------|-----------|----------|---------|-------------| +| `grayKey` | string | Optional | - | Unique user identifier from Cookie/Header (e.g., userid). If empty, uses `rules[].grayTagKey` and `rules[].grayTagValue` to filter rules. | +| `localStorageGrayKey` | string | Optional | - | When using JWT authentication, user ID comes from `localStorage`. Overrides `grayKey` if configured. | +| `graySubKey` | string | Optional | - | Used when user info is in JSON format (e.g., `userInfo:{ userCode:"001" }`). In this example, `graySubKey` would be `userCode`. | +| `storeMaxAge` | int | Optional | 31536000 | Max cookie storage duration in seconds (default: 1 year). | +| `indexPaths` | string[] | Optional | - | Paths requiring mandatory processing (supports Glob patterns). Example: `/resource/**/manifest-main.json` in micro-frontend scenarios. | +| `skippedPaths` | string[] | Optional | - | Excluded paths (supports Glob patterns). Example: `/api/**` XHR requests in rewrite scenarios. | +| `skippedByHeaders` | map | Optional | - | Filter requests via headers. `skippedPaths` has higher priority. HTML page requests are unaffected. | +| `rules` | object[] | Required | - | User-defined grayscale rules for different scenarios. | +| `rewrite` | object | Required | - | Rewrite configuration for OSS/CDN deployments. | +| `baseDeployment` | object | Optional | - | Baseline configuration. | +| `grayDeployments` | object[] | Optional | - | Gray deployment rules and versions. | +| `backendGrayTag` | string | Optional | `x-mse-tag` | Backend grayscale tag. Cookies will carry `${backendGrayTag}:${grayDeployments[].backendVersion}` if configured. | +| `uniqueGrayTag` | string | Optional | `x-higress-uid` | UUID stored in cookies for percentage-based grayscale session stickiness and backend tracking. | +| `injection` | object | Optional | - | Inject global info into HTML (e.g., ``). | -`rules` field configuration description: -| Name | Data Type | Requirements | Default Value | Description | -|------------------|-------------------|---------------|---------------|--------------------------------------------------------------------------------------------| -| `name` | string | Required | - | Unique identifier for the rule name, associated with `deploy.gray[].name` for effectiveness. | -| `grayKeyValue` | array of string | Optional | - | Whitelist of user IDs. | -| `grayTagKey` | string | Optional | - | Label key for user classification tagging, derived from Cookie. | -| `grayTagValue` | array of string | Optional | - | Label value for user classification tagging, derived from Cookie. | +### `rules` Field +| Name | Data Type | Required | Default | Description | +|------|-----------|----------|---------|-------------| +| `name` | string | Required | - | Unique rule name linked to `grayDeployments[].name`. | +| `grayKeyValue` | string[] | Optional | - | User ID whitelist. | +| `grayTagKey` | string | Optional | - | User tag key from cookies. | +| `grayTagValue` | string[] | Optional | - | User tag values from cookies. | -`rewrite` field configuration description: -> `indexRouting` homepage rewrite and `fileRouting` file rewrite essentially use prefix matching, for example, `/app1`: `/mfe/app1/{version}/index.html` represents requests with the prefix /app1 routed to `/mfe/app1/{version}/index.html` page, where `{version}` represents the version number, which will be dynamically replaced by `baseDeployment.version` or `grayDeployments[].version` during execution. -> `{version}` will be replaced dynamically during execution by the frontend version from `baseDeployment.version` or `grayDeployments[].version`. +### `rewrite` Field +> Both `indexRouting` and `fileRouting` use prefix matching. The `{version}` placeholder will be dynamically replaced by `baseDeployment.version` or `grayDeployments[].version`. -| Name | Data Type | Requirements | Default Value | Description | -|------------------|-------------------|---------------|---------------|---------------------------------------| -| `host` | string | Optional | - | Host address, if OSS set to the VPC internal access address. | -| `notFoundUri` | string | Optional | - | 404 page configuration. | -| `indexRouting` | map of string to string | Optional | - | Defines the homepage rewrite routing rules. Each key represents the homepage routing path, and the value points to the redirect target file. For example, the key `/app1` corresponds to the value `/mfe/app1/{version}/index.html`. If the effective version is `0.0.1`, the access path is `/app1`, it redirects to `/mfe/app1/0.0.1/index.html`. | -| `fileRouting` | map of string to string | Optional | - | Defines resource file rewrite routing rules. Each key represents the resource access path, and the value points to the redirect target file. For example, the key `/app1/` corresponds to the value `/mfe/app1/{version}`. If the effective version is `0.0.1`, the access path is `/app1/js/a.js`, it redirects to `/mfe/app1/0.0.1/js/a.js`. | +| Name | Data Type | Required | Default | Description | +|------|-----------|----------|---------|-------------| +| `host` | string | Optional | - | Host address (use VPC endpoint for OSS). | +| `indexRouting` | map | Optional | - | Homepage rewrite rules. Key: route path, Value: target file. Example: `/app1` → `/mfe/app1/{version}/index.html`. | +| `fileRouting` | map | Optional | - | Resource rewrite rules. Key: resource path, Value: target path. Example: `/app1/` → `/mfe/app1/{version}`. | -`baseDeployment` field configuration description: -| Name | Data Type | Requirements | Default Value | Description | -|------------------|-------------------|---------------|---------------|-------------------------------------------------------------------------------------------| -| `version` | string | Required | - | The version number of the Base version, as a fallback version. | +### `baseDeployment` Field +| Name | Data Type | Required | Default | Description | +|------|-----------|----------|---------|-------------| +| `version` | string | Required | - | Baseline version as fallback. | +| `backendVersion` | string | Required | - | Backend grayscale version written to cookies via `${backendGrayTag}`. | +| `versionPredicates` | string | Required | - | Supports multi-version mapping for micro-frontend scenarios. | -`grayDeployments` field configuration description: -| Name | Data Type | Requirements | Default Value | Description | -|------------------|-------------------|---------------|---------------|----------------------------------------------------------------------------------------------| -| `version` | string | Required | - | Version number of the Gray version, if the gray rules are hit, this version will be used. If it is a non-CDN deployment, add `x-higress-tag` to the header. | -| `backendVersion` | string | Required | - | Gray version for the backend, which will add `x-mse-tag` to the header of `XHR/Fetch` requests. | -| `name` | string | Required | - | Rule name associated with `rules[].name`. | -| `enabled` | boolean | Required | - | Whether to activate the current gray release rule. | +### `grayDeployments` Field +| Name | Data Type | Required | Default | Description | +|------|-----------|----------|---------|-------------| +| `version` | string | Required | - | Gray version used when rules match. Adds `x-higress-tag` header for non-CDN deployments. | +| `versionPredicates` | string | Required | - | Multi-version support for micro-frontends. | +| `backendVersion` | string | Required | - | Backend grayscale version for cookies. | +| `name` | string | Required | - | Linked to `rules[].name`. | +| `enabled` | boolean | Required | - | Enable/disable rule. | +| `weight` | int | Optional | - | Traffic percentage (e.g., 50). | -## Configuration Example -### Basic Configuration +> **Percentage-based Grayscale Notes**: +> 1. Percentage rules override user-based rules when both exist. +> 2. Uses UUID fingerprint hashed via SHA-256 for traffic distribution. + +### `injection` Field +| Name | Data Type | Required | Default | Description | +|------|-----------|----------|---------|-------------| +| `globalConfig` | object | Optional | - | Global variables injected into HTML. | +| `head` | string[] | Optional | - | Inject elements into ``. | +| `body` | object | Optional | - | Inject elements into ``. | + +#### `globalConfig` Sub-field +| Name | Data Type | Required | Default | Description | +|------|-----------|----------|---------|-------------| +| `key` | string | Optional | `HIGRESS_CONSOLE_CONFIG` | Window global variable key. | +| `featureKey` | string | Optional | `FEATURE_STATUS` | Rule hit status (e.g., `{"beta-user":true}`). | +| `value` | string | Optional | - | Custom global value. | +| `enabled` | boolean | Optional | `false` | Enable global injection. | + +#### `body` Sub-field +| Name | Data Type | Required | Default | Description | +|------|-----------|----------|---------|-------------| +| `first` | string[] | Optional | - | Inject at body start. | +| `after` | string[] | Optional | - | Inject at body end. | + +## Configuration Examples +### Basic Configuration (User-based) ```yml grayKey: userid rules: @@ -75,88 +111,3 @@ grayDeployments: - name: beta-user version: gray enabled: true -``` - -The unique identifier of the user in the cookie is `userid`, and the current gray release rule has configured the `beta-user` rule. -When the following conditions are met, the version `version: gray` will be used: -- `userid` in the cookie equals `00000002` or `00000003` -- Users whose `level` in the cookie equals `level3` or `level5` -Otherwise, use version `version: base`. - -### User Information Exists in JSON -```yml -grayKey: appInfo -graySubKey: userId -rules: -- name: inner-user - grayKeyValue: - - '00000001' - - '00000005' -- name: beta-user - grayKeyValue: - - '00000002' - - '00000003' - grayTagKey: level - grayTagValue: - - level3 - - level5 -baseDeployment: - version: base -grayDeployments: - - name: beta-user - version: gray - enabled: true -``` - -The cookie contains JSON data for `appInfo`, which includes the field `userId` as the current unique identifier. -The current gray release rule has configured the `beta-user` rule. -When the following conditions are met, the version `version: gray` will be used: -- `userid` in the cookie equals `00000002` or `00000003` -- Users whose `level` in the cookie equals `level3` or `level5` -Otherwise, use version `version: base`. - -### Rewrite Configuration -> Generally used in CDN deployment scenarios. -```yml -grayKey: userid -rules: -- name: inner-user - grayKeyValue: - - '00000001' - - '00000005' -- name: beta-user - grayKeyValue: - - '00000002' - - '00000003' - grayTagKey: level - grayTagValue: - - level3 - - level5 -rewrite: - host: frontend-gray.oss-cn-shanghai-internal.aliyuncs.com - notFoundUri: /mfe/app1/dev/404.html - indexRouting: - /app1: '/mfe/app1/{version}/index.html' - /: '/mfe/app1/{version}/index.html', - fileRouting: - /: '/mfe/app1/{version}' - /app1/: '/mfe/app1/{version}' -baseDeployment: - version: base -grayDeployments: - - name: beta-user - version: gray - enabled: true -``` - -`{version}` will be dynamically replaced with the actual version during execution. - -#### indexRouting: Homepage Route Configuration -Accessing `/app1`, `/app123`, `/app1/index.html`, `/app1/xxx`, `/xxxx` will route to '/mfe/app1/{version}/index.html'. - -#### fileRouting: File Route Configuration -The following file mappings are effective: -- `/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 43d7225c4..ef0f4ab52 100644 --- a/plugins/wasm-go/extensions/frontend-gray/config/config.go +++ b/plugins/wasm-go/extensions/frontend-gray/config/config.go @@ -1,23 +1,23 @@ package config import ( + "errors" + "path/filepath" + "regexp" + "strconv" "strings" "github.com/tidwall/gjson" ) const ( - XHigressTag = "x-higress-tag" - XUniqueClientId = "x-unique-client" - XPreHigressTag = "x-pre-higress-tag" - IsPageRequest = "is-page-request" - IsNotFound = "is-not-found" - EnabledGray = "enabled-gray" - SecFetchMode = "sec-fetch-mode" + XHigressTag = "x-higress-tag" + PreHigressVersion = "pre-higress-version" + IsHtmlRequest = "is-html-request" + IsIndexRequest = "is-index-request" + EnabledGray = "enabled-gray" ) -type LogInfo func(format string, args ...interface{}) - type GrayRule struct { Name string GrayKeyValue []string @@ -35,15 +35,22 @@ type Deployment struct { } type Rewrite struct { - Host string - NotFound string - Index map[string]string - File map[string]string + Host string + Index map[string]string + File map[string]string } type Injection struct { - Head []string - Body *BodyInjection + GlobalConfig *GlobalConfig + Head []string + Body *BodyInjection +} + +type GlobalConfig struct { + Key string + FeatureKey string + Value string + Enabled bool } type BodyInjection struct { @@ -52,8 +59,7 @@ type BodyInjection struct { } type GrayConfig struct { - UserStickyMaxAge string - TotalGrayWeight int + StoreMaxAge int GrayKey string LocalStorageGrayKey string GraySubKey string @@ -63,10 +69,26 @@ type GrayConfig struct { BaseDeployment *Deployment GrayDeployments []*Deployment BackendGrayTag string + UniqueGrayTag string Injection *Injection - SkippedPathPrefixes []string - IncludePathPrefixes []string + SkippedPaths []string SkippedByHeaders map[string]string + IndexPaths []string + GrayWeight int +} + +func isValidName(s string) bool { + // 定义一个正则表达式,匹配字母、数字和下划线 + re := regexp.MustCompile(`^[a-zA-Z0-9_]+$`) + return re.MatchString(s) +} + +func GetWithDefault(json gjson.Result, path, defaultValue string) string { + res := json.Get(path) + if res.Exists() { + return res.String() + } + return defaultValue } func convertToStringList(results []gjson.Result) []string { @@ -77,6 +99,22 @@ func convertToStringList(results []gjson.Result) []string { return interfaces } +func compatibleConvertToStringList(results []gjson.Result, compatibleResults []gjson.Result) []string { + // 优先使用兼容模式的数据 + if len(compatibleResults) == 0 { + interfaces := make([]string, len(results)) // 预分配切片容量 + for i, result := range results { + interfaces[i] = result.String() // 使用 String() 方法直接获取字符串 + } + return interfaces + } + compatibleInterfaces := make([]string, len(compatibleResults)) // 预分配切片容量 + for i, result := range compatibleResults { + compatibleInterfaces[i] = filepath.Join(result.String(), "**") + } + return compatibleInterfaces +} + func convertToStringMap(result gjson.Result) map[string]string { m := make(map[string]string) result.ForEach(func(key, value gjson.Result) bool { @@ -86,7 +124,7 @@ func convertToStringMap(result gjson.Result) map[string]string { return m } -func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { +func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) error { // 解析 GrayKey grayConfig.LocalStorageGrayKey = json.Get("localStorageGrayKey").String() grayConfig.GrayKey = json.Get("grayKey").String() @@ -94,22 +132,18 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { grayConfig.GrayKey = grayConfig.LocalStorageGrayKey } grayConfig.GraySubKey = json.Get("graySubKey").String() - grayConfig.BackendGrayTag = json.Get("backendGrayTag").String() - grayConfig.UserStickyMaxAge = json.Get("userStickyMaxAge").String() + grayConfig.BackendGrayTag = GetWithDefault(json, "backendGrayTag", "x-mse-tag") + grayConfig.UniqueGrayTag = GetWithDefault(json, "uniqueGrayTag", "x-higress-uid") + grayConfig.StoreMaxAge = 60 * 60 * 24 * 365 // 默认一年 + storeMaxAge, err := strconv.Atoi(GetWithDefault(json, "StoreMaxAge", strconv.Itoa(grayConfig.StoreMaxAge))) + if err != nil { + grayConfig.StoreMaxAge = storeMaxAge + } + grayConfig.Html = json.Get("html").String() - grayConfig.SkippedPathPrefixes = convertToStringList(json.Get("skippedPathPrefixes").Array()) + grayConfig.SkippedPaths = compatibleConvertToStringList(json.Get("skippedPaths").Array(), json.Get("skippedPathPrefixes").Array()) + grayConfig.IndexPaths = compatibleConvertToStringList(json.Get("indexPaths").Array(), json.Get("includePathPrefixes").Array()) grayConfig.SkippedByHeaders = convertToStringMap(json.Get("skippedByHeaders")) - grayConfig.IncludePathPrefixes = convertToStringList(json.Get("includePathPrefixes").Array()) - - if grayConfig.UserStickyMaxAge == "" { - // 默认值2天 - grayConfig.UserStickyMaxAge = "172800" - } - - if grayConfig.BackendGrayTag == "" { - grayConfig.BackendGrayTag = "x-mse-tag" - } - // 解析 Rules rules := json.Get("rules").Array() for _, rule := range rules { @@ -122,10 +156,9 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { grayConfig.Rules = append(grayConfig.Rules, &grayRule) } grayConfig.Rewrite = &Rewrite{ - Host: json.Get("rewrite.host").String(), - NotFound: json.Get("rewrite.notFoundUri").String(), - Index: convertToStringMap(json.Get("rewrite.indexRouting")), - File: convertToStringMap(json.Get("rewrite.fileRouting")), + Host: json.Get("rewrite.host").String(), + Index: convertToStringMap(json.Get("rewrite.indexRouting")), + File: convertToStringMap(json.Get("rewrite.fileRouting")), } // 解析 deployment @@ -134,6 +167,7 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { grayConfig.BaseDeployment = &Deployment{ Name: baseDeployment.Get("name").String(), + BackendVersion: baseDeployment.Get("backendVersion").String(), Version: strings.Trim(baseDeployment.Get("version").String(), " "), VersionPredicates: convertToStringMap(baseDeployment.Get("versionPredicates")), } @@ -141,16 +175,28 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { if !item.Get("enabled").Bool() { continue } - grayWeight := int(item.Get("weight").Int()) + weight := int(item.Get("weight").Int()) grayConfig.GrayDeployments = append(grayConfig.GrayDeployments, &Deployment{ Name: item.Get("name").String(), Enabled: item.Get("enabled").Bool(), Version: strings.Trim(item.Get("version").String(), " "), BackendVersion: item.Get("backendVersion").String(), - Weight: grayWeight, + Weight: weight, VersionPredicates: convertToStringMap(item.Get("versionPredicates")), }) - grayConfig.TotalGrayWeight += grayWeight + if weight > 0 { + grayConfig.GrayWeight = weight + break + } + } + + injectGlobalFeatureKey := GetWithDefault(json, "injection.globalConfig.featureKey", "FEATURE_STATUS") + injectGlobalKey := GetWithDefault(json, "injection.globalConfig.key", "HIGRESS_CONSOLE_CONFIG") + if !isValidName(injectGlobalFeatureKey) { + return errors.New("injection.globalConfig.featureKey is invalid") + } + if !isValidName(injectGlobalKey) { + return errors.New("injection.globalConfig.featureKey is invalid") } grayConfig.Injection = &Injection{ @@ -159,5 +205,12 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) { First: convertToStringList(json.Get("injection.body.first").Array()), Last: convertToStringList(json.Get("injection.body.last").Array()), }, + GlobalConfig: &GlobalConfig{ + FeatureKey: injectGlobalFeatureKey, + Key: injectGlobalKey, + Value: json.Get("injection.globalConfig.value").String(), + Enabled: json.Get("injection.globalConfig.enabled").Bool(), + }, } + return nil } diff --git a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml index bc584d5cc..b576c3249 100644 --- a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml +++ b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml @@ -48,8 +48,8 @@ static_resources: value: | { "grayKey": "userId", - "backendGrayTag": "x-mse-tag", - "userStickyMaxAge": 172800, + "backendGrayTag": "env", + "uniqueGrayTag": "uuid", "rules": [ { "name": "inner-user", @@ -72,30 +72,44 @@ static_resources: } ], "rewrite": { - "host": "frontend-gray-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com", + "host": "apig-oss-integration.oss-cn-hangzhou.aliyuncs.com", "indexRouting": { - "/app1": "/mfe/app1/{version}/index.html", - "/": "/mfe/app1/{version}/index.html" + "/": "/mfe/{version}/index.html" }, "fileRouting": { - "/": "/mfe/app1/{version}", - "/app1": "/mfe/app1/{version}" + "/": "/mfe/{version}", + "/mfe": "/mfe/{version}" } }, - "skippedPathPrefixes": [ - "/api/" + "skippedPaths": [ + "/api/**", + "/v2/**" + ], + "indexPaths": [ + "/mfe/**/mf-manifest-main.json" ], "baseDeployment": { - "version": "dev" + "version": "v1" }, "grayDeployments": [ { + "weight": 90, "name": "beta-user", - "version": "0.0.1", - "enabled": true + "version": "v2", + "enabled": true, + "backendVersion":"gray", + "versionPredicates": { + "/mfe": "v1" + } } ], "injection": { + "globalConfig": { + "key": "HIGRESS_CONSOLE_CONFIG", + "featureKey": "FEATURE_STATUS", + "value": "{CONSOLE_GLOBAL: {'gray':'2.0.15','main':'2.0.15'}}", + "enabled": true + }, "head": [ "" ], @@ -127,5 +141,5 @@ static_resources: - endpoint: address: socket_address: - address: frontend-gray-cn-shanghai.oss-cn-shanghai.aliyuncs.com + address: apig-oss-integration.oss-cn-hangzhou.aliyuncs.com port_value: 80 diff --git a/plugins/wasm-go/extensions/frontend-gray/go.mod b/plugins/wasm-go/extensions/frontend-gray/go.mod index 4900b839d..6bd16198d 100644 --- a/plugins/wasm-go/extensions/frontend-gray/go.mod +++ b/plugins/wasm-go/extensions/frontend-gray/go.mod @@ -6,6 +6,7 @@ replace github.com/alibaba/higress/plugins/wasm-go => ../.. require ( github.com/alibaba/higress/plugins/wasm-go v1.4.3-0.20240727022514-bccfbde62188 + github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/higress-group/proxy-wasm-go-sdk v1.0.0 github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.17.3 diff --git a/plugins/wasm-go/extensions/frontend-gray/go.sum b/plugins/wasm-go/extensions/frontend-gray/go.sum index 8143975d8..974ba746e 100644 --- a/plugins/wasm-go/extensions/frontend-gray/go.sum +++ b/plugins/wasm-go/extensions/frontend-gray/go.sum @@ -1,6 +1,8 @@ -github.com/davecgh/go-spew v1.1.1+incompatible/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1+incompatible/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= @@ -11,9 +13,9 @@ github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKE github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsefv8GU57h9CCLYGJA74/tF5/0= 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+incompatible/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/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= diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index c238b26f4..f561166d5 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -2,13 +2,11 @@ package main import ( "fmt" - "net/http" - "net/url" - "path" "strings" "github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config" "github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/util" + "github.com/alibaba/higress/plugins/wasm-go/pkg/log" "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" @@ -19,44 +17,42 @@ import ( func main() { wrapper.SetCtx( "frontend-gray", - wrapper.ParseConfigBy(parseConfig), - wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), - wrapper.ProcessResponseHeadersBy(onHttpResponseHeader), - wrapper.ProcessResponseBodyBy(onHttpResponseBody), + wrapper.ParseConfig(parseConfig), + wrapper.ProcessRequestHeaders(onHttpRequestHeaders), + wrapper.ProcessResponseHeaders(onHttpResponseHeader), + wrapper.ProcessResponseBody(onHttpResponseBody), ) } -func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.Log) error { +func parseConfig(json gjson.Result, grayConfig *config.GrayConfig) error { // 解析json 为GrayConfig - config.JsonToGrayConfig(json, grayConfig) - log.Infof("Rewrite: %v, GrayDeployments: %v", json.Get("rewrite"), json.Get("grayDeployments")) + if err := config.JsonToGrayConfig(json, grayConfig); err != nil { + log.Errorf("failed to parse config: %v", err) + return err + } + return nil } -func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action { - requestPath, _ := proxywasm.GetHttpRequestHeader(":path") - requestPath = path.Clean(requestPath) - parsedURL, err := url.Parse(requestPath) - if err == nil { - requestPath = parsedURL.Path - } else { - log.Errorf("parse request path %s failed: %v", requestPath, err) - } - enabledGray := util.IsGrayEnabled(grayConfig, requestPath) +func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig) types.Action { + requestPath := util.GetRequestPath() + enabledGray := util.IsGrayEnabled(requestPath, &grayConfig) ctx.SetContext(config.EnabledGray, enabledGray) - secFetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode") - ctx.SetContext(config.SecFetchMode, secFetchMode) + route, _ := util.GetRouteName() if !enabledGray { - log.Infof("gray not enabled") + log.Infof("route: %s, gray not enabled, requestPath: %v", route, requestPath) ctx.DontReadRequestBody() return types.ActionContinue } - cookies, _ := proxywasm.GetHttpRequestHeader("cookie") - isPageRequest := util.IsPageRequest(requestPath) + cookie, _ := proxywasm.GetHttpRequestHeader("cookie") + isHtmlRequest := util.CheckIsHtmlRequest(requestPath) + ctx.SetContext(config.IsHtmlRequest, isHtmlRequest) + isIndexRequest := util.IsIndexRequest(requestPath, grayConfig.IndexPaths) + ctx.SetContext(config.IsIndexRequest, isIndexRequest) hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0 - grayKeyValueByCookie := util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey) + grayKeyValueByCookie := util.GetCookieValue(cookie, grayConfig.GrayKey) grayKeyValueByHeader, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey) // 优先从cookie中获取,否则从header中获取 grayKeyValue := util.GetGrayKey(grayKeyValueByCookie, grayKeyValueByHeader, grayConfig.GraySubKey) @@ -65,93 +61,92 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, // 禁止重新路由,要在更改Header之前操作,否则会失效 ctx.DisableReroute() } + frontendVersion := util.GetCookieValue(cookie, config.XHigressTag) + + if grayConfig.GrayWeight > 0 { + ctx.SetContext(grayConfig.UniqueGrayTag, util.GetGrayWeightUniqueId(cookie, grayConfig.UniqueGrayTag)) + } // 删除Accept-Encoding,避免压缩, 如果是压缩的内容,后续插件就没法处理了 _ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding") _ = proxywasm.RemoveHttpRequestHeader("Content-Length") deployment := &config.Deployment{} - preVersion, preUniqueClientId := util.GetXPreHigressVersion(cookies) - // 客户端唯一ID,用于在按照比率灰度时候 客户访问黏贴 - uniqueClientId := grayKeyValue - if uniqueClientId == "" { - xForwardedFor, _ := proxywasm.GetHttpRequestHeader("X-Forwarded-For") - uniqueClientId = util.GetRealIpFromXff(xForwardedFor) + globalConfig := grayConfig.Injection.GlobalConfig + if globalConfig.Enabled { + conditionRule := util.GetConditionRules(grayConfig.Rules, grayKeyValue, cookie) + trimmedValue := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(globalConfig.Value), "{"), "}") + ctx.SetContext(globalConfig.Key, fmt.Sprintf("", globalConfig.Key, globalConfig.FeatureKey, conditionRule, trimmedValue)) } - // 如果没有配置比例,则进行灰度规则匹配 - if util.IsSupportMultiVersion(grayConfig) { - deployment = util.FilterMultiVersionGrayRule(&grayConfig, grayKeyValue, requestPath) - log.Infof("multi version %v", deployment) - } else { - if isPageRequest { - if grayConfig.TotalGrayWeight > 0 { - log.Infof("grayConfig.TotalGrayWeight: %v", grayConfig.TotalGrayWeight) - deployment = util.FilterGrayWeight(&grayConfig, preVersion, preUniqueClientId, uniqueClientId) - } else { - deployment = util.FilterGrayRule(&grayConfig, grayKeyValue) - } - log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %s,%s", deployment, requestPath, deployment.BackendVersion, preVersion, preUniqueClientId) - } else { - grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue) - deployment = util.GetVersion(grayConfig, grayDeployment, preVersion, isPageRequest) - } - ctx.SetContext(config.XPreHigressTag, deployment.Version) + if isHtmlRequest { + // index首页请求每次都会进度灰度规则判断 + deployment = util.FilterGrayRule(&grayConfig, grayKeyValue, cookie) + log.Infof("route: %s, index html request: %v, backend: %v, xPreHigressVersion: %s", route, requestPath, deployment.BackendVersion, frontendVersion) + ctx.SetContext(config.PreHigressVersion, deployment.Version) ctx.SetContext(grayConfig.BackendGrayTag, deployment.BackendVersion) + } else { + if util.IsSupportMultiVersion(grayConfig) { + deployment = util.FilterMultiVersionGrayRule(&grayConfig, grayKeyValue, cookie, requestPath) + log.Infof("route: %s, multi version %v", route, deployment) + } else { + grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue, cookie) + if isIndexRequest { + deployment = grayDeployment + } else { + deployment = util.GetVersion(grayConfig, grayDeployment, frontendVersion) + } + } } - proxywasm.AddHttpRequestHeader(config.XHigressTag, deployment.Version) - ctx.SetContext(config.IsPageRequest, isPageRequest) - ctx.SetContext(config.XUniqueClientId, uniqueClientId) - rewrite := grayConfig.Rewrite if rewrite.Host != "" { err := proxywasm.ReplaceHttpRequestHeader(":authority", rewrite.Host) if err != nil { - log.Errorf("host rewrite failed: %v", err) + log.Errorf("route: %s, host rewrite failed: %v", route, err) } } if hasRewrite { rewritePath := requestPath - if isPageRequest { + if isHtmlRequest { rewritePath = util.IndexRewrite(requestPath, deployment.Version, grayConfig.Rewrite.Index) } else { rewritePath = util.PrefixFileRewrite(requestPath, deployment.Version, grayConfig.Rewrite.File) } if requestPath != rewritePath { - log.Infof("rewrite path:%s, rewritePath:%s, Version:%v", requestPath, rewritePath, deployment.Version) + log.Infof("route: %s, rewrite path:%s, rewritePath:%s, Version:%v", route, requestPath, rewritePath, deployment.Version) proxywasm.ReplaceHttpRequestHeader(":path", rewritePath) } } - log.Infof("request path:%s, has rewrited:%v, rewrite config:%+v", requestPath, hasRewrite, rewrite) return types.ActionContinue } -func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action { +func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig) types.Action { enabledGray, _ := ctx.GetContext(config.EnabledGray).(bool) if !enabledGray { ctx.DontReadResponseBody() return types.ActionContinue } - secFetchMode, isSecFetchModeOk := ctx.GetContext(config.SecFetchMode).(string) - if isSecFetchModeOk && secFetchMode == "cors" { + isIndexRequest, indexOk := ctx.GetContext(config.IsIndexRequest).(bool) + if indexOk && isIndexRequest { + // 首页请求强制不缓存 proxywasm.ReplaceHttpResponseHeader("cache-control", "no-cache, no-store, max-age=0, must-revalidate") + ctx.DontReadResponseBody() + return types.ActionContinue } - isPageRequest, ok := ctx.GetContext(config.IsPageRequest).(bool) - if !ok { - isPageRequest = false // 默认值 - } + + isHtmlRequest, htmlOk := ctx.GetContext(config.IsHtmlRequest).(bool) // response 不处理非首页的请求 - if !isPageRequest { + if !htmlOk || !isHtmlRequest { ctx.DontReadResponseBody() return types.ActionContinue } else { // 不会进去Streaming 的Body处理 ctx.BufferResponseBody() } - + // 处理HTML的首页 status, err := proxywasm.GetHttpResponseHeader(":status") if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" { // 删除Content-Disposition,避免自动下载文件 @@ -163,117 +158,81 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, // 处理code为 200的情况 if err != nil || status != "200" { - if status == "404" { - if grayConfig.Rewrite.NotFound != "" && isPageRequest { - ctx.SetContext(config.IsNotFound, true) - responseHeaders, _ := proxywasm.GetHttpResponseHeaders() - headersMap := util.ConvertHeaders(responseHeaders) - 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() - return types.ActionContinue - } else { - // 直接返回400 - ctx.DontReadResponseBody() - } + // 如果找不到HTML,但配置了HTML页面 + if status == "404" && grayConfig.Html != "" { + responseHeaders, _ := proxywasm.GetHttpResponseHeaders() + headersMap := util.ConvertHeaders(responseHeaders) + delete(headersMap, "content-length") + headersMap[":status"][0] = "200" + headersMap["content-type"][0] = "text/html" + ctx.BufferResponseBody() + proxywasm.ReplaceHttpResponseHeaders(util.ReconvertHeaders(headersMap)) + } else { + route, _ := util.GetRouteName() + log.Errorf("route: %s, request error code: %s, message: %v", route, status, err) + ctx.DontReadResponseBody() + return types.ActionContinue } - log.Errorf("error status: %s, error message: %v", status, err) - return types.ActionContinue } - cacheControl, _ := proxywasm.GetHttpResponseHeader("cache-control") - if !strings.Contains(cacheControl, "no-cache") { - proxywasm.ReplaceHttpResponseHeader("cache-control", "no-cache, no-store, max-age=0, must-revalidate") + proxywasm.ReplaceHttpResponseHeader("cache-control", "no-cache, no-store, max-age=0, must-revalidate") + + // 前端版本 + frontendVersion, isFrontendVersionOk := ctx.GetContext(config.PreHigressVersion).(string) + if isFrontendVersionOk { + proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Path=/;", config.XHigressTag, frontendVersion, grayConfig.StoreMaxAge)) } - - frontendVersion, isFeVersionOk := ctx.GetContext(config.XPreHigressTag).(string) - xUniqueClient, isUniqClientOk := ctx.GetContext(config.XUniqueClientId).(string) - - // 设置前端的版本 - if isFeVersionOk && isUniqClientOk && frontendVersion != "" { - proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s,%s; Max-Age=%s; Path=/;", config.XPreHigressTag, frontendVersion, xUniqueClient, grayConfig.UserStickyMaxAge)) + // 设置GrayWeight 唯一值 + if grayConfig.GrayWeight > 0 { + uniqueId, isUniqueIdOk := ctx.GetContext(grayConfig.UniqueGrayTag).(string) + if isUniqueIdOk { + proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Path=/;", grayConfig.UniqueGrayTag, uniqueId, grayConfig.StoreMaxAge)) + } } // 设置后端的版本 if util.IsBackendGrayEnabled(grayConfig) { backendVersion, isBackVersionOk := ctx.GetContext(grayConfig.BackendGrayTag).(string) - if isBackVersionOk && backendVersion != "" { - proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%s; Path=/;", grayConfig.BackendGrayTag, backendVersion, grayConfig.UserStickyMaxAge)) + if isBackVersionOk { + if backendVersion == "" { + // 删除后端灰度版本 + proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/;", grayConfig.BackendGrayTag, backendVersion)) + } else { + proxywasm.AddHttpResponseHeader("Set-Cookie", fmt.Sprintf("%s=%s; Max-Age=%d; Path=/;", grayConfig.BackendGrayTag, backendVersion, grayConfig.StoreMaxAge)) + } } } return types.ActionContinue } -func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte, log wrapper.Log) types.Action { +func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte) types.Action { enabledGray, _ := ctx.GetContext(config.EnabledGray).(bool) if !enabledGray { return types.ActionContinue } - isPageRequest, isPageRequestOk := ctx.GetContext(config.IsPageRequest).(bool) - frontendVersion, isFeVersionOk := ctx.GetContext(config.XPreHigressTag).(string) + isHtmlRequest, isHtmlRequestOk := ctx.GetContext(config.IsHtmlRequest).(bool) + frontendVersion, isFeVersionOk := ctx.GetContext(config.PreHigressVersion).(string) // 只处理首页相关请求 - if !isFeVersionOk || !isPageRequestOk || !isPageRequest { + if !isFeVersionOk || !isHtmlRequestOk || !isHtmlRequest { return types.ActionContinue } - - isNotFound, ok := ctx.GetContext(config.IsNotFound).(bool) - if !ok { - isNotFound = false // 默认值 + globalConfig := grayConfig.Injection.GlobalConfig + globalConfigValue, isGobalConfigOk := ctx.GetContext(globalConfig.Key).(string) + if !isGobalConfigOk { + globalConfigValue = "" } - // 检查是否存在自定义 HTML, 如有则省略 rewrite.indexRouting 的内容 + newHtml := string(body) if grayConfig.Html != "" { - log.Debugf("Returning custom HTML from config.") - // 替换响应体为 config.Html 内容 - if err := proxywasm.ReplaceHttpResponseBody([]byte(grayConfig.Html)); err != nil { - log.Errorf("Error replacing response body: %v", err) - return types.ActionContinue - } - - newHtml := util.InjectContent(grayConfig.Html, grayConfig.Injection) - // 替换当前html加载的动态文件版本 - newHtml = strings.ReplaceAll(newHtml, "{version}", frontendVersion) - - // 最终替换响应体 - if err := proxywasm.ReplaceHttpResponseBody([]byte(newHtml)); err != nil { - log.Errorf("Error replacing injected response body: %v", err) - return types.ActionContinue - } - - return types.ActionContinue + newHtml = grayConfig.Html } + newHtml = util.InjectContent(newHtml, grayConfig.Injection, globalConfigValue) + // 替换当前html加载的动态文件版本 + newHtml = strings.ReplaceAll(newHtml, "{version}", frontendVersion) + newHtml = util.FixLocalStorageKey(newHtml, grayConfig.LocalStorageGrayKey) - // 针对404页面处理 - if 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) { - proxywasm.ReplaceHttpResponseBody(responseBody) - proxywasm.ResumeHttpResponse() - }, 1500) - return types.ActionPause - } - - // 处理响应体HTML - newBody := string(body) - newBody = util.InjectContent(newBody, grayConfig.Injection) - if grayConfig.LocalStorageGrayKey != "" { - localStr := strings.ReplaceAll(` - `, "@@X_GRAY_KEY", grayConfig.LocalStorageGrayKey) - newBody = strings.ReplaceAll(newBody, "", "\n"+localStr) - } - if err := proxywasm.ReplaceHttpResponseBody([]byte(newBody)); err != nil { + // 最终替换响应体 + if err := proxywasm.ReplaceHttpResponseBody([]byte(newHtml)); err != nil { + route, _ := util.GetRouteName() + log.Errorf("route: %s, Failed to replace response body: %v", route, err) 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 557bf3746..031ec558b 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -1,15 +1,16 @@ package util import ( - "fmt" - "math/rand" + "encoding/json" + "hash/crc32" "net/url" "path" "path/filepath" "sort" "strings" - "time" + "github.com/bmatcuk/doublestar/v4" + "github.com/google/uuid" "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" "github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config" @@ -17,43 +18,26 @@ import ( "github.com/tidwall/gjson" ) -func LogInfof(format string, args ...interface{}) { - format = fmt.Sprintf("[%s] %s", "frontend-gray", format) - proxywasm.LogInfof(format, args...) +func GetRequestPath() string { + requestPath, _ := proxywasm.GetHttpRequestHeader(":path") + requestPath = path.Clean(requestPath) + parsedURL, err := url.Parse(requestPath) + if err == nil { + requestPath = parsedURL.Path + } else { + return "" + } + return requestPath } -func GetXPreHigressVersion(cookies string) (string, string) { - xPreHigressVersion := ExtractCookieValueByKey(cookies, config.XPreHigressTag) - preVersions := strings.Split(xPreHigressVersion, ",") - if len(preVersions) == 0 { - return "", "" +func GetRouteName() (string, error) { + if raw, err := proxywasm.GetProperty([]string{"route_name"}); err != nil { + return "-", err + } else { + return string(raw), nil } - 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 != "" { - // 通常客户端的真实 IP 是 XFF 头中的第一个 IP - ips := strings.Split(xff, ",") - if len(ips) > 0 { - return strings.TrimSpace(ips[0]) - } - } - return "" -} - -func IsRequestSkippedByHeaders(grayConfig config.GrayConfig) bool { - secFetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode") - upgrade, _ := proxywasm.GetHttpRequestHeader("upgrade") - if len(grayConfig.SkippedByHeaders) == 0 { - // 默认不走插件逻辑的header - return secFetchMode == "cors" || upgrade == "websocket" - } +func IsRequestSkippedByHeaders(grayConfig *config.GrayConfig) bool { for headerKey, headerValue := range grayConfig.SkippedByHeaders { requestHeader, _ := proxywasm.GetHttpRequestHeader(headerKey) if requestHeader == headerValue { @@ -63,34 +47,40 @@ func IsRequestSkippedByHeaders(grayConfig config.GrayConfig) bool { return false } -func IsGrayEnabled(grayConfig config.GrayConfig, requestPath string) bool { - for _, prefix := range grayConfig.IncludePathPrefixes { - if strings.HasPrefix(requestPath, prefix) { +func IsIndexRequest(requestPath string, indexPaths []string) bool { + for _, prefix := range indexPaths { + matchResult, err := doublestar.Match(prefix, requestPath) + if err == nil && matchResult { return true } } + return false +} - // 当前路径中前缀为 SkippedPathPrefixes,则不走插件逻辑 - for _, prefix := range grayConfig.SkippedPathPrefixes { - if strings.HasPrefix(requestPath, prefix) { +func IsGrayEnabled(requestPath string, grayConfig *config.GrayConfig) bool { + if IsIndexRequest(requestPath, grayConfig.IndexPaths) { + return true + } + // 当前路径中前缀为 SkippedPaths,则不走插件逻辑 + for _, prefix := range grayConfig.SkippedPaths { + matchResult, err := doublestar.Match(prefix, requestPath) + if err == nil && matchResult { return false } } // 如果是首页,进入插件逻辑 - if IsPageRequest(requestPath) { + if CheckIsHtmlRequest(requestPath) { + return true + } + // 检查是否存在重写主机 + if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" { return true } // 检查header标识,判断是否需要跳过 if IsRequestSkippedByHeaders(grayConfig) { return false } - - // 检查是否存在重写主机 - if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" { - return true - } - // 检查是否存在灰度版本配置 return len(grayConfig.GrayDeployments) > 0 } @@ -105,8 +95,8 @@ func IsBackendGrayEnabled(grayConfig config.GrayConfig) bool { return false } -// ExtractCookieValueByKey 根据 cookie 和 key 获取 cookie 值 -func ExtractCookieValueByKey(cookie string, key string) string { +// GetCookieValue 根据 cookie 和 key 获取 cookie 值 +func GetCookieValue(cookie string, key string) string { if cookie == "" { return "" } @@ -170,7 +160,7 @@ var indexSuffixes = []string{ ".html", ".htm", ".jsp", ".php", ".asp", ".aspx", ".erb", ".ejs", ".twig", } -func IsPageRequest(requestPath string) bool { +func CheckIsHtmlRequest(requestPath string) bool { if requestPath == "/" || requestPath == "" { return true } @@ -227,10 +217,7 @@ func PrefixFileRewrite(path, version string, matchRules map[string]string) strin return path } -func GetVersion(grayConfig config.GrayConfig, deployment *config.Deployment, xPreHigressVersion string, isPageRequest bool) *config.Deployment { - if isPageRequest { - return deployment - } +func GetVersion(grayConfig config.GrayConfig, deployment *config.Deployment, xPreHigressVersion string) *config.Deployment { // cookie 中为空,返回当前版本 if xPreHigressVersion == "" { return deployment @@ -295,10 +282,75 @@ func IsSupportMultiVersion(grayConfig config.GrayConfig) bool { return false } +func GetConditionRules(rules []*config.GrayRule, grayKeyValue string, cookie string) string { + ruleMaps := map[string]bool{} + for _, grayRule := range rules { + if grayRule.GrayKeyValue != nil && len(grayRule.GrayKeyValue) > 0 && grayKeyValue != "" { + ruleMaps[grayRule.Name] = ContainsValue(grayRule.GrayKeyValue, grayKeyValue) + continue + } else if grayRule.GrayTagKey != "" && grayRule.GrayTagValue != nil && len(grayRule.GrayTagValue) > 0 { + grayTagValue := GetCookieValue(cookie, grayRule.GrayTagKey) + ruleMaps[grayRule.Name] = ContainsValue(grayRule.GrayTagValue, grayTagValue) + continue + } else { + ruleMaps[grayRule.Name] = false + } + } + jsonBytes, err := json.Marshal(ruleMaps) + if err != nil { + return "" + } + return string(jsonBytes) +} + +func GetGrayWeightUniqueId(cookie string, uniqueGrayTag string) string { + uniqueId := GetCookieValue(cookie, uniqueGrayTag) + if uniqueId == "" { + uniqueId = strings.ReplaceAll(uuid.NewString(), "-", "") + } + return uniqueId +} + +// FilterGrayRule 过滤灰度规则 +func FilterGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, cookie string) *config.Deployment { + if grayConfig.GrayWeight > 0 { + uniqueId := GetGrayWeightUniqueId(cookie, grayConfig.UniqueGrayTag) + // 计算哈希后取模 + mod := crc32.ChecksumIEEE([]byte(uniqueId)) % 100 + isGray := mod < uint32(grayConfig.GrayWeight) + if isGray { + for _, deployment := range grayConfig.GrayDeployments { + if deployment.Enabled && deployment.Weight > 0 { + return deployment + } + } + } + return grayConfig.BaseDeployment + } + + 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) { + return deployment + } + } + // 第二:校验Cookie中的 GrayTagKey + if grayRule.GrayTagKey != "" && grayRule.GrayTagValue != nil && len(grayRule.GrayTagValue) > 0 { + grayTagValue := GetCookieValue(cookie, grayRule.GrayTagKey) + if ContainsValue(grayRule.GrayTagValue, grayTagValue) { + return deployment + } + } + } + return grayConfig.BaseDeployment +} + // FilterMultiVersionGrayRule 过滤多版本灰度规则 -func FilterMultiVersionGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, requestPath string) *config.Deployment { +func FilterMultiVersionGrayRule(grayConfig *config.GrayConfig, grayKeyValue string, cookie string, requestPath string) *config.Deployment { // 首先根据灰度键值获取当前部署 - currentDeployment := FilterGrayRule(grayConfig, grayKeyValue) + currentDeployment := FilterGrayRule(grayConfig, grayKeyValue, cookie) // 创建一个新的部署对象,初始化版本为当前部署的版本 deployment := &config.Deployment{ @@ -319,68 +371,13 @@ func FilterMultiVersionGrayRule(grayConfig *config.GrayConfig, grayKeyValue stri return deployment } -// FilterGrayRule 过滤灰度规则 -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) { - return deployment - } - } - // 第二:校验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) { - return deployment - } - } - } - return grayConfig.BaseDeployment -} - -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("preVersion: %s, preUniqueClientId: %s, uniqueClientId: %s", preVersion, preUniqueClientId, uniqueClientId) - // 用户粘滞,确保每个用户每次访问的都是走同一版本 - if preVersion != "" && uniqueClientId == preUniqueClientId { - for _, deployment := range deployments { - if deployment.Version == preVersion { - return deployment - } - } - } - - 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 -} - // InjectContent 用于将内容注入到 HTML 文档的指定位置 -func InjectContent(originalHtml string, injectionConfig *config.Injection) string { - - headInjection := strings.Join(injectionConfig.Head, "\n") +func InjectContent(originalHtml string, injectionConfig *config.Injection, globalConfigValue string) string { + heads := injectionConfig.Head + if globalConfigValue != "" { + heads = append([]string{globalConfigValue}, injectionConfig.Head...) + } + headInjection := strings.Join(heads, "\n") bodyFirstInjection := strings.Join(injectionConfig.Body.First, "\n") bodyLastInjection := strings.Join(injectionConfig.Body.Last, "\n") @@ -401,3 +398,14 @@ func InjectContent(originalHtml string, injectionConfig *config.Injection) strin return modifiedHtml } + +func FixLocalStorageKey(newHtml string, localStorageGrayKey string) string { + if localStorageGrayKey != "" { + localStr := strings.ReplaceAll(` + `, "@@X_GRAY_KEY", localStorageGrayKey) + newHtml = strings.ReplaceAll(newHtml, "", "\n"+localStr) + } + return newHtml +} 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 5fb69bb36..831ef55db 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go @@ -3,12 +3,14 @@ package util import ( "testing" + "github.com/bmatcuk/doublestar/v4" + "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) { +func TestGetCookieValue(t *testing.T) { var tests = []struct { cookie, cookieKey, output string }{ @@ -21,7 +23,7 @@ func TestExtractCookieValueByKey(t *testing.T) { for _, test := range tests { testName := test.cookie t.Run(testName, func(t *testing.T) { - output := ExtractCookieValueByKey(test.cookie, test.cookieKey) + output := GetCookieValue(test.cookie, test.cookieKey) assert.Equal(t, test.output, output) }) } @@ -106,7 +108,7 @@ func TestPrefixFileRewrite(t *testing.T) { } } -func TestIsPageRequest(t *testing.T) { +func TestCheckIsHtmlRequest(t *testing.T) { var tests = []struct { p string output bool @@ -121,30 +123,11 @@ func TestIsPageRequest(t *testing.T) { for _, test := range tests { testPath := test.p t.Run(testPath, func(t *testing.T) { - output := IsPageRequest(testPath) + output := CheckIsHtmlRequest(testPath) assert.Equal(t, test.output, output) }) } } - -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) - result := FilterGrayWeight(grayConfig, "base", "1.0.1", "192.168.1.1") - t.Logf("result-----: %v", result) - }) - } -} - func TestReplaceHtml(t *testing.T) { var tests = []struct { name string @@ -158,8 +141,26 @@ func TestReplaceHtml(t *testing.T) { t.Run(testName, func(t *testing.T) { grayConfig := &config.GrayConfig{} config.JsonToGrayConfig(gjson.Parse(test.input), grayConfig) - result := InjectContent(grayConfig.Html, grayConfig.Injection) + result := InjectContent(grayConfig.Html, grayConfig.Injection, "") t.Logf("result-----: %v", result) }) } } + +func TestIsIndexRequest(t *testing.T) { + var tests = []struct { + name string + input string + output bool + }{ + {"/api/user.json", "/api/**", true}, + {"/api/blade-auth/oauth/captcha", "/api/**", true}, + } + for _, test := range tests { + testName := test.name + t.Run(testName, func(t *testing.T) { + matchResult, _ := doublestar.Match(test.input, testName) + assert.Equal(t, test.output, matchResult) + }) + } +}