mirror of
https://github.com/alibaba/higress.git
synced 2026-03-08 02:30:56 +08:00
[frontend-gray] 重构业务逻辑,对于微前端和多版本支持更加友好 (#2011)
This commit is contained in:
11
plugins/wasm-go/extensions/frontend-gray/Makefile
Normal file
11
plugins/wasm-go/extensions/frontend-gray/Makefile
Normal file
@@ -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
|
||||
@@ -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中注入全局信息,比如`<script>window.global = {...}</script>` |
|
||||
|
||||
|
||||
@@ -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信息,比如`<link rel="stylesheet" href="https://cdn.example.com/styles.css">` |
|
||||
| `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:
|
||||
- <script>console.log('Header')</script>
|
||||
|
||||
@@ -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<string, string> | 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., `<script>window.global = {...}</script>`). |
|
||||
|
||||
`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<string, string> | Optional | - | Homepage rewrite rules. Key: route path, Value: target file. Example: `/app1` → `/mfe/app1/{version}/index.html`. |
|
||||
| `fileRouting` | map<string, string> | 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 `<head>`. |
|
||||
| `body` | object | Optional | - | Inject elements into `<body>`. |
|
||||
|
||||
#### `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`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
"<script>console.log('Header')</script>"
|
||||
],
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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("<script>var %s = {\n%s:%s,\n %s \n}\n</script>", 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(`<script>
|
||||
!function(){var o,e,n="@@X_GRAY_KEY",t=document.cookie.split("; ").filter(function(o){return 0===o.indexOf(n+"=")});try{"undefined"!=typeof localStorage&&null!==localStorage&&(o=localStorage.getItem(n),e=0<t.length?decodeURIComponent(t[0].split("=")[1]):null,o)&&o.indexOf("=")<0&&e&&e!==o&&(document.cookie=n+"="+encodeURIComponent(o)+"; path=/;",window.location.reload())}catch(o){}}();
|
||||
</script>
|
||||
`, "@@X_GRAY_KEY", grayConfig.LocalStorageGrayKey)
|
||||
newBody = strings.ReplaceAll(newBody, "<body>", "<body>\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
|
||||
|
||||
@@ -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(`<script>
|
||||
!function(){var o,e,n="@@X_GRAY_KEY",t=document.cookie.split("; ").filter(function(o){return 0===o.indexOf(n+"=")});try{"undefined"!=typeof localStorage&&null!==localStorage&&(o=localStorage.getItem(n),e=0<t.length?decodeURIComponent(t[0].split("=")[1]):null,o)&&o.indexOf("=")<0&&e&&e!==o&&(document.cookie=n+"="+encodeURIComponent(o)+"; path=/;",window.location.reload())}catch(o){}}();
|
||||
</script>
|
||||
`, "@@X_GRAY_KEY", localStorageGrayKey)
|
||||
newHtml = strings.ReplaceAll(newHtml, "<body>", "<body>\n"+localStr)
|
||||
}
|
||||
return newHtml
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user