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)
+ })
+ }
+}