Compare commits

...

142 Commits

Author SHA1 Message Date
johnlanni
823527ab94 update higress console to 2.1.0 2025-04-01 23:34:56 +08:00
johnlanni
cb7f6ccd0f Revert "release 2.1.0 (#1998)"
This reverts commit 3c73976130.
2025-04-01 23:33:30 +08:00
澄潭
5107ce5137 fix poll_oneof (#2003) 2025-04-01 23:06:14 +08:00
johnlanni
e6d32aa1cf fix helm README 2025-04-01 23:05:08 +08:00
Jingze
3c73976130 release 2.1.0 (#1998) 2025-04-01 18:45:36 +08:00
澄潭
639956c0b8 release 2.1.0 rc.2 (#1995) 2025-04-01 15:32:33 +08:00
Jingze
a602f7a725 fix: Golang filter supports skipping processing at the body stage. (#1989) 2025-04-01 15:27:38 +08:00
澄潭
7b6e4154f4 update proxy-wasm-cpp-host (#1993) 2025-04-01 14:59:46 +08:00
Xin Luo
12e3f34c0b use custom nacos go sdk for disable log (#1991) 2025-04-01 14:56:55 +08:00
Xin Luo
bdd802f44f feat: support service delete event trigger for tool and some fix (#1987) 2025-04-01 09:43:43 +08:00
littlejian
d58b66df8f feat: Add an optional Redis component to the Higress helm package (#1973) 2025-04-01 09:29:46 +08:00
rinfx
5d99c7d80a rename redis key (#1986) 2025-03-31 22:28:06 +08:00
johnlanni
3428932aca update mcp-server README 2025-03-31 21:51:50 +08:00
澄潭
7ba3f75d41 support rest to mcp (#1988) 2025-03-31 21:41:38 +08:00
Jingze
ae9a06b05c fix: mcp proxy eventData (#1985) 2025-03-31 18:38:52 +08:00
DefNed
9ebe968921 fix: 修复envoy.yaml配置文件中几处问题 (#1983) 2025-03-31 17:06:36 +08:00
Jingze
93e3b086ce fix: fix bug of mcp server proxy (#1981) 2025-03-31 15:40:36 +08:00
小小hao
20dfc3d64f fix inclusionRegexps not working (#1972) 2025-03-30 10:41:01 +08:00
澄潭
492c5d350a Add all in one mcp (#1978) 2025-03-30 00:25:21 +08:00
澄潭
037c71a320 refactor mcp sdk (#1977) 2025-03-29 20:28:10 +08:00
Yiiong
9a07c50f44 docs: 添加Azure OpenAI配置说明 (#1976) 2025-03-29 20:11:48 +08:00
Yiiong
b86e9fc938 feat: add azure embedding to ai-cache (#1975) 2025-03-29 18:08:37 +08:00
Se7en
2014234356 doc: add ai statistics metric doc (#1889) 2025-03-29 16:21:49 +08:00
Jingze
83f69a0186 fix: mcp server config map (#1969)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2025-03-27 18:13:40 +08:00
Jingze
8495d17070 fix: add match list and wasm mcp-server message pub in redis (#1963) 2025-03-27 17:00:32 +08:00
澄潭
6f762b5e4c fix golang filter build (#1966) 2025-03-27 16:43:06 +08:00
澄潭
96e4713703 update default enable path suffix in model mapper&router plugin (#1962) 2025-03-27 14:15:11 +08:00
Xin Luo
d3887835a3 feat: support nacos mcp registry (#1961) 2025-03-27 09:41:22 +08:00
澄潭
1965d107d0 Update README.md 2025-03-27 00:49:19 +08:00
johnlanni
b2f9bf94fa update README 2025-03-27 00:48:24 +08:00
johnlanni
9257077fa3 update mcp readme 2025-03-27 00:26:02 +08:00
zty98751
7e310a3520 update gomod in hgctl 2025-03-26 21:53:28 +08:00
Jingze
663b28fa9b fix: update log info to debug (#1954) 2025-03-26 21:54:06 +08:00
Kent Dong
9fbe331f5f fix: Fetch get-higress.sh from standalone repo (#1945) 2025-03-26 21:53:39 +08:00
zty98751
dd50ac09dc fix cache action in workflow 2025-03-26 21:43:47 +08:00
澄潭
8450a0869b rel 2.1.0-rc.1 (#1959) 2025-03-26 21:42:25 +08:00
澄潭
bd6708552d key auth support multiple credentials (#1956)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2025-03-26 21:05:55 +08:00
Kent Dong
50cfa0bb4b fix: Fix the incorrect image used to build envoy (#1958) 2025-03-26 20:38:40 +08:00
澄潭
ea0143829d Fix log import (#1957) 2025-03-26 20:27:53 +08:00
Jingze
f83e66c23b feat: update Go filter mcp-server (#1950)
Co-authored-by: johnlanni <zty98751@alibaba-inc.com>
2025-03-26 14:31:23 +08:00
Jingze
87fe1aeeb5 feat: add mcpServer in config map (#1953) 2025-03-26 14:30:41 +08:00
mirror
386a208b14 add: add mcp server amap tools (#1951) 2025-03-25 21:20:36 +08:00
澄潭
ee77ffb753 fix ai-search rewrite query when no search result found (#1949) 2025-03-25 14:24:16 +08:00
澄潭
6eeef07621 revert wrapper changes (#1948) 2025-03-25 11:55:14 +08:00
澄潭
8978a4e0e0 fix invalid ai-proxy cluster (#1947)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2025-03-25 11:20:20 +08:00
007gzs
71029d791d add parse_rule_config fail log (#1938) 2025-03-25 10:44:48 +08:00
澄潭
d9f16f7d5e Add remote mcp server sdk (#1946) 2025-03-24 22:11:45 +08:00
Jingze
f5d20b72e0 feat: add config parse in mcp server (#1944) 2025-03-24 17:52:16 +08:00
Kent Dong
9bde0dfb46 chore: Remove redundant get-higress.sh (#1943) 2025-03-24 14:28:45 +08:00
Jingze
f5c1e7f2ec feat: add golang filter and mcp-server (#1942)
Co-authored-by: johnlanni <zty98751@alibaba-inc.com>
2025-03-24 11:07:03 +08:00
澄潭
45fbc8b084 optimize plugin sdk (#1930) 2025-03-22 22:46:37 +08:00
rinfx
1812a6b0a9 add example for extending span attributes (#1936) 2025-03-21 15:39:52 +08:00
rinfx
2640c76760 improve the logic for constructing redis key (#1933) 2025-03-21 14:02:59 +08:00
rinfx
4223b2d666 Fix the error in the embedding interface under the AI proxy Qwen compatible mode. (#1928) 2025-03-21 08:32:00 +08:00
DefNed
dee4786c1c feat: add buffer_limit functions (#1922)
Co-authored-by: 纪卓志 <jizhuozhi.george@gmail.com>
Co-authored-by: 007gzs <007gzs@gmail.com>
2025-03-20 18:07:13 +08:00
Yiiong
e549c79ae4 feat: add xfyun emb to ai-cache (#1921) 2025-03-20 11:05:36 +08:00
小小hao
6742df57df feat: add ratelimit metrics in the ai-token-ratelimit plugin (#1918) 2025-03-19 21:51:56 +08:00
Kent Dong
eef8adf42f fix: Skip reading non-JSON request bodies in ai-proxy (#1914) 2025-03-18 21:23:54 +08:00
007gzs
029c3e75fc optimization parseIP in xff (#1915) 2025-03-18 15:58:24 +08:00
Kent Dong
9fa3a730d5 feat: Support forwarding embedding calls to Ollama in ai-proxy (#1913) 2025-03-18 10:23:34 +08:00
澄潭
9acaed0b43 Update README_EN.md 2025-03-17 17:40:14 +08:00
澄潭
f95264448c Update README.md 2025-03-17 17:39:46 +08:00
澄潭
e0dc9672ac support nil option in NewCommonVmCtx (#1909) 2025-03-17 15:02:22 +08:00
Kent Dong
5de7c2a5ea feat: Support files and batches APIs provided by Azure OpenAI (#1904) 2025-03-17 11:21:05 +08:00
澄潭
9a89665b22 optimize retry&failover logic (#1903) 2025-03-17 11:19:33 +08:00
Jun
4a82d50d80 add variable from secret when applying istio cr (#1877) 2025-03-17 10:59:05 +08:00
澄潭
34b3fc3114 more optimize of ai search plugin (#1896) 2025-03-14 23:24:22 +08:00
澄潭
f09e029a6b fix chunk merge bug in ai-search (#1895) 2025-03-14 21:52:49 +08:00
澄潭
5e7e20ff7e AI-search plugin supports controlling through the web_search_options parameter. (#1893) 2025-03-14 21:52:33 +08:00
007gzs
26bfdd45ff Rust WASM plugin support for matching service and route name prefixes is effective. (#1882) 2025-03-14 20:43:19 +08:00
澄潭
61defc13c6 fix openai embedding path (#1881) 2025-03-12 13:16:33 +08:00
Se7en
19496e5759 feat: support retry on http status code (#1817)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2025-03-11 13:38:02 +08:00
mamba
beb60fcacd bugfix:【frontend-gray插件】针对fetch的请求,强制不缓存 (#1856) 2025-03-11 12:54:40 +08:00
Se7en
01cc7939ae feat: support elasticsearch hybrid search (#1844) 2025-03-11 11:25:58 +08:00
rinfx
5a5af4ecbf support default value (#1873) 2025-03-11 09:32:11 +08:00
澄潭
d172cf4d19 Update README_EN.md 2025-03-10 17:33:13 +08:00
澄潭
58c4ba2021 Update README.md 2025-03-10 17:32:22 +08:00
rinfx
9e2df8f7c7 add redis init status log (#1867)
Co-authored-by: Kent Dong <ch3cho@qq.com>
2025-03-10 17:10:53 +08:00
Yiiong
b897825069 feat: add huggingface embedding to ai-cache (#1864) 2025-03-10 16:59:13 +08:00
yunmaoQu
f45bc9008a feat: add replay protection plugin (#1672)
Co-authored-by: hanxiantao <601803023@qq.com>
2025-03-10 15:11:13 +08:00
Se7en
5536502c15 feat: allow failover to distinguish between different endpoint of the same provider (#1862) 2025-03-10 10:45:59 +08:00
澄潭
a0c334a7cb optimize model router&mapper (#1866) 2025-03-09 23:07:49 +08:00
澄潭
9e6bd6d2cc optimize ai-search references (#1859) 2025-03-07 10:34:49 +08:00
Kent Dong
ab419efda4 fix: Fix the incorrect reasoning content concat logic in ai-proxy (#1842) 2025-03-07 10:33:45 +08:00
Jacky Wu
d4155411ee fix plugin_wrapper.go log level (#1848) 2025-03-06 14:41:47 +08:00
Jacky Wu
d721c235cb chore: load EXTRA_TAGS from plugin .buildrc file to avoid build issue. (#1852) 2025-03-05 12:15:37 +08:00
澄潭
0905cd0fc0 Set the llm-api-key field of the ai-search plugin to optional (#1846) 2025-03-03 20:42:15 +08:00
Kent Dong
188914a16b feat: Support only watching key resources in one namespace (#1821) 2025-03-03 15:40:44 +08:00
rinfx
988e2c1fa7 add plugin start log in sdk (#1831) 2025-03-03 15:37:23 +08:00
Kent Dong
4f1901586a doc: Update the description of timeout config of ai-proxy (#1845) 2025-03-03 15:33:16 +08:00
Xijun Dai
80b58e86e1 feat(helm): add podLabels to gateway && controller (#1792)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
2025-03-03 15:31:28 +08:00
澄潭
ca32e587d3 optimize ai search (#1843) 2025-03-03 09:44:53 +08:00
澄潭
6d2d98f653 Simplify the implementation of ai-search integration with quark and add a tutorial. (#1838) 2025-02-28 18:36:07 +08:00
firebook
2d1d8ac2b1 fix: gateway log config should read from helm\core\values.yaml when deploy with helm (#1834) 2025-02-28 14:14:13 +08:00
Kent Dong
a2b8f9a646 fix: Disable helm-docs action since it's still under development (#1828) 2025-02-28 13:36:44 +08:00
007gzs
5bece9c8ef fix rust_wasm_build (#1824) 2025-02-27 14:15:50 +08:00
Kent Dong
45fdd95a9c feat: Support pushing multi-arch images to a custom image registry (#1815) 2025-02-26 21:15:53 +08:00
Se7en
d3afe345ad fix: remove last failed apiToken from retry apiToken list (#1802) 2025-02-26 21:11:51 +08:00
韩贤涛
90ca903d2e feat: ext-auth plugin: Blacklist and whitelist modes support HTTP request method matching (#1798) 2025-02-26 20:54:52 +08:00
007gzs
2d8a8f26da Ai data masking msg window (#1775) 2025-02-26 20:48:37 +08:00
Se7en
9ea2410388 feat: update ai-token-ratelimit documentation by removing ai-statistics plugin (#1767) 2025-02-26 20:47:37 +08:00
littlejian
9e1792c245 add notes to gateway.rollingMaxUnavailable (#1819) 2025-02-26 20:46:53 +08:00
rinfx
3eda7def89 ai-search support quark (#1811) 2025-02-26 18:42:22 +08:00
澄潭
1787553294 set include_usage by default for all model providers (#1818) 2025-02-26 16:49:16 +08:00
澄潭
f6c48415d1 Add database configuration for plugins that use Redis. (#1814) 2025-02-26 10:52:54 +08:00
MARATRIX Li
e27d3d0971 fix(typo): use the correct bing name for ai-search. (#1807)
Signed-off-by: maratrixx <maratrix@163.com>
2025-02-25 13:37:32 +08:00
Kent Dong
49617c7a98 feat: Unify the SSE processing logic (#1800) 2025-02-25 11:00:18 +08:00
澄潭
53a015d8fe Update arxiv.md 2025-02-24 11:27:55 +08:00
澄潭
e711e9f997 Update full.md 2025-02-24 11:27:33 +08:00
澄潭
8530742472 Update README_EN.md 2025-02-24 11:16:09 +08:00
澄潭
c0c1f5113a Update README.md 2025-02-24 11:15:55 +08:00
澄潭
2e6ddd7e35 Add ai search plugin (#1804) 2025-02-24 11:14:47 +08:00
Kent Dong
2328e19c9d fix: Fix a bug in openaiCustomUrl support (#1790) 2025-02-22 12:12:49 +08:00
Kent Dong
fabc22f218 feat: Support transforming reasoning_content returned by Qwen to OpenAI contract (#1791) 2025-02-21 17:32:02 +08:00
Yiiong
2986e1911d feat: add ollama embedding to ai-cache (#1794) 2025-02-21 15:21:49 +08:00
澄潭
a566f7257d update helm docs (#1782) 2025-02-19 17:48:20 +08:00
澄潭
3dbd1b2731 release 2.0.7 (#1781) 2025-02-19 17:44:08 +08:00
澄潭
7f23980bf5 remove basic-auth useless annotation (#1779) 2025-02-19 15:58:03 +08:00
澄潭
6fb0684c39 fix openai compatiable (#1778) 2025-02-19 15:23:15 +08:00
澄潭
dfac9fa5e6 Update README.md 2025-02-18 14:17:21 +08:00
澄潭
bfd9e3026d Update helm-docs.yaml 2025-02-18 10:00:05 +08:00
澄潭
49aad4152c Supports completions API & support config openai baseUrl through openaiCustomUrl (#1765) 2025-02-18 09:57:48 +08:00
澄潭
94aacf5153 Update helm-docs.yaml
Remove the part that causes the action to fail
2025-02-17 18:59:54 +08:00
littlejian
efcfdbf36e Add translate-readme action to translate English into Chinese (#1711) 2025-02-17 17:34:30 +08:00
澄潭
2dbde1833f ai proxy support passthrough path when api name is unknown (#1754) 2025-02-13 21:22:43 +08:00
mirror
7272eff8b6 update ai-cache extension (#1746) 2025-02-13 19:49:52 +08:00
pepesi
a84a382f1d feature: allow ai-proxy to forward standard AI capabilities that are … (#1704) 2025-02-12 15:23:44 +08:00
韩贤涛
477e44b9f1 e2e: Enhance the e2e testing of the ai-proxy plugin based on the LLM mock server (#1742) 2025-02-11 20:16:03 +08:00
澄潭
512385d225 fix host rewrite in frontend-gray (#1747) 2025-02-08 17:42:29 +08:00
007gzs
b997e6fd26 wasm32-wasi to wasm32-wasip1 (#1716) 2025-02-05 15:35:48 +08:00
韩贤涛
fab3ebb35a ut: add ext-auth unit tests (#1710) 2025-02-05 13:39:10 +08:00
韩贤涛
1431ff9cfe e2e: Enhance the e2e testing of the ai-proxy plugin based on the LLM mock server (#1713) 2025-02-05 10:14:25 +08:00
kai2321
fac2c3e7a3 feat:完善对接dify时返回usage相关信息 (#1715) 2025-02-03 08:35:00 +08:00
韩贤涛
574d1aa36a fix: Path concatenation issue for authentication requests in Envoy authentication mode (#1709) 2025-01-23 15:47:07 +08:00
澄潭
7840167c4a optimize body bufferlimit set in ext-auth plugin (#1707) 2025-01-23 11:52:30 +08:00
韩贤涛
9d8e78dae3 fix: ext-auth crash bugfix (#1705) 2025-01-23 11:29:49 +08:00
Se7en
133a30b8d5 fix: stream response buffer issue (#1703) 2025-01-22 11:28:37 +08:00
kai2321
ce94c6e62d feat:接入dify (#1664) 2025-01-21 16:04:15 +08:00
Xijun Dai
05f251e627 fix gateway env (#1689) 2025-01-21 15:05:14 +08:00
韩贤涛
0259eaddbb feat: Add ext-auth plugin support for authentication blacklists/whitelists (#1694) 2025-01-21 14:28:49 +08:00
Se7en
cfa3baddf8 sync ai-token-ratelimit docs (#1688) 2025-01-19 13:05:25 +08:00
Se7en
b1f625a652 feat: support baidu api key (#1687) 2025-01-19 11:46:29 +08:00
304 changed files with 27558 additions and 3765 deletions

View File

@@ -133,8 +133,13 @@ jobs:
command="
set -e
cd /workspace/plugins/wasm-rust/extensions/${PLUGIN_NAME}
cargo build --target wasm32-wasi --release
cp target/wasm32-wasi/release/*.wasm plugin.wasm
if [ -f ./.prebuild ]; then
echo 'Found .prebuild file, sourcing it...'
. ./.prebuild
fi
rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release
cp target/wasm32-wasip1/release/*.wasm plugin.wasm
tar czvf plugin.tar.gz plugin.wasm
echo ${{ secrets.REGISTRY_PASSWORD }} | oras login -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin ${{ env.IMAGE_REGISTRY_SERVICE }}
oras push ${target_image} ${push_command}

View File

@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.21.5
go-version: 1.22
# There are too many lint errors in current code bases
# uncomment when we decide what lint should be addressed or ignored.
# - run: make lint
@@ -51,7 +51,7 @@ jobs:
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: 1.21.5
go-version: 1.22
- name: Setup Rust
uses: actions-rs/toolchain@v1

View File

@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.21.5
go-version: 1.22
# There are too many lint errors in current code bases
# uncomment when we decide what lint should be addressed or ignored.
# - run: make lint
@@ -26,7 +26,7 @@ jobs:
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: 1.21.5
go-version: 1.22
- name: Setup Golang Caches
uses: actions/cache@v4
@@ -64,7 +64,7 @@ jobs:
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: 1.21.5
go-version: 1.22
- name: Setup Golang Caches
uses: actions/cache@v4
@@ -111,7 +111,7 @@ jobs:
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: 1.21.5
go-version: 1.22
- name: Setup Golang Caches
uses: actions/cache@v4

View File

@@ -1,229 +1,258 @@
name: Build Docker Images and Push to Image Registry
on:
push:
tags:
- "v*.*.*"
workflow_dispatch: ~
jobs:
build-controller-image:
runs-on: ubuntu-latest
environment:
name: image-registry-controller
env:
CONTROLLER_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
CONTROLLER_IMAGE_NAME: ${{ vars.CONTROLLER_IMAGE_NAME || 'higress/higress' }}
steps:
- name: "Checkout ${{ github.ref }}"
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: 1.21.5
- name: Setup Golang Caches
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: ${{ runner.os }}-go
- name: Calculate Docker metadata
id: docker-meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.CONTROLLER_IMAGE_REGISTRY }}/${{ env.CONTROLLER_IMAGE_NAME }}
tags: |
type=sha
type=ref,event=tag
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ env.CONTROLLER_IMAGE_REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build Docker Image and Push
run: |
GOPROXY="https://proxy.golang.org,direct" make docker-buildx-push
BUILT_IMAGE="higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress"
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
for image in ${IMAGES[@]}; do
echo "Image: $image"
docker buildx imagetools create $BUILT_IMAGE:$GITHUB_SHA --tag $image
done
build-pilot-image:
runs-on: ubuntu-latest
environment:
name: image-registry-pilot
env:
PILOT_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
PILOT_IMAGE_NAME: ${{ vars.PILOT_IMAGE_NAME || 'higress/pilot' }}
steps:
- name: "Checkout ${{ github.ref }}"
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: 1.21.5
- name: Setup Golang Caches
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: ${{ runner.os }}-go
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Calculate Docker metadata
id: docker-meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.PILOT_IMAGE_REGISTRY }}/${{ env.PILOT_IMAGE_NAME }}
tags: |
type=sha
type=ref,event=tag
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ env.PILOT_IMAGE_REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build Pilot-Discovery Image and Push
run: |
GOPROXY="https://proxy.golang.org,direct" make build-istio
BUILT_IMAGE="higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/pilot"
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
for image in ${IMAGES[@]}; do
echo "Image: $image"
docker buildx imagetools create $BUILT_IMAGE:$GITHUB_SHA --tag $image
done
build-gateway-image:
runs-on: ubuntu-latest
environment:
name: image-registry-pilot
env:
GATEWAY_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
GATEWAY_IMAGE_NAME: ${{ vars.GATEWAY_IMAGE_NAME || 'higress/gateway' }}
steps:
- name: "Checkout ${{ github.ref }}"
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: 1.21.5
- name: Setup Golang Caches
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: ${{ runner.os }}-go
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Calculate Docker metadata
id: docker-meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.GATEWAY_IMAGE_REGISTRY }}/${{ env.GATEWAY_IMAGE_NAME }}
tags: |
type=sha
type=ref,event=tag
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ env.GATEWAY_IMAGE_REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build Gateway Image and Push
run: |
GOPROXY="https://proxy.golang.org,direct" make build-gateway
BUILT_IMAGE="higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/proxyv2"
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
for image in ${IMAGES[@]}; do
echo "Image: $image"
docker buildx imagetools create $BUILT_IMAGE:$GITHUB_SHA --tag $image
done
name: Build Docker Images and Push to Image Registry
on:
push:
tags:
- "v*.*.*"
workflow_dispatch: ~
jobs:
build-controller-image:
runs-on: ubuntu-latest
environment:
name: image-registry-controller
env:
CONTROLLER_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
CONTROLLER_IMAGE_NAME: ${{ vars.CONTROLLER_IMAGE_NAME || 'higress/higress' }}
steps:
- name: "Checkout ${{ github.ref }}"
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: 1.22
- name: Setup Golang Caches
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: ${{ runner.os }}-go
- name: Calculate Docker metadata
id: docker-meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.CONTROLLER_IMAGE_REGISTRY }}/${{ env.CONTROLLER_IMAGE_NAME }}
tags: |
type=sha
type=ref,event=tag
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ env.CONTROLLER_IMAGE_REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build Docker Image and Push
run: |
BUILT_IMAGE=""
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
for image in ${IMAGES[@]}; do
echo "Image: $image"
if [ "$BUILT_IMAGE" == "" ]; then
GOPROXY="https://proxy.golang.org,direct" IMG_URL="$image" make docker-buildx-push
BUILT_IMAGE="$image"
else
docker buildx imagetools create $BUILT_IMAGE --tag $image
fi
done
build-pilot-image:
runs-on: ubuntu-latest
environment:
name: image-registry-pilot
env:
PILOT_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
PILOT_IMAGE_NAME: ${{ vars.PILOT_IMAGE_NAME || 'higress/pilot' }}
steps:
- name: "Checkout ${{ github.ref }}"
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: 1.22
- name: Setup Golang Caches
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: ${{ runner.os }}-go
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v7.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Calculate Docker metadata
id: docker-meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.PILOT_IMAGE_REGISTRY }}/${{ env.PILOT_IMAGE_NAME }}
tags: |
type=sha
type=ref,event=tag
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ env.PILOT_IMAGE_REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build Pilot-Discovery Image and Push
run: |
BUILT_IMAGE=""
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
for image in ${IMAGES[@]}; do
echo "Image: $image"
if [ "$BUILT_IMAGE" == "" ]; then
TAG=${image#*:}
HUB=${image%:*}
HUB=${HUB%/*}
BUILT_IMAGE="$HUB/pilot:$TAG"
GOPROXY="https://proxy.golang.org,direct" IMG_URL="$BUILT_IMAGE" make build-istio
fi
if [ "$BUILT_IMAGE" != "$image" ]; then
docker buildx imagetools create $BUILT_IMAGE --tag $image
fi
done
build-gateway-image:
runs-on: ubuntu-latest
environment:
name: image-registry-gateway
env:
GATEWAY_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
GATEWAY_IMAGE_NAME: ${{ vars.GATEWAY_IMAGE_NAME || 'higress/gateway' }}
steps:
- name: "Checkout ${{ github.ref }}"
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: 1.22
- name: Setup Golang Caches
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: ${{ runner.os }}-go
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v7.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Calculate Docker metadata
id: docker-meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.GATEWAY_IMAGE_REGISTRY }}/${{ env.GATEWAY_IMAGE_NAME }}
tags: |
type=sha
type=ref,event=tag
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ env.GATEWAY_IMAGE_REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build Gateway Image and Push
run: |
BUILT_IMAGE=""
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
for image in ${IMAGES[@]}; do
echo "Image: $image"
if [ "$BUILT_IMAGE" == "" ]; then
TAG=${image#*:}
HUB=${image%:*}
HUB=${HUB%/*}
BUILT_IMAGE="$HUB/proxyv2:$TAG"
GOPROXY="https://proxy.golang.org,direct" IMG_URL="$BUILT_IMAGE" make build-gateway
fi
if [ "$BUILT_IMAGE" != "$image" ]; then
docker buildx imagetools create $BUILT_IMAGE --tag $image
fi
done

View File

@@ -20,11 +20,11 @@ jobs:
name: Prepare Standalone Package
run: |
mkdir ./artifact
cp ./tools/get-higress.sh ./artifact
LOCAL_RELEASE_URL="https://github.com/higress-group/higress-standalone/releases"
VERSION=$(curl -Ls $LOCAL_RELEASE_URL | grep 'href="/higress-group/higress-standalone/releases/tag/v[0-9]*.[0-9]*.[0-9]*\"' | sed -E 's/.*\/higress-group\/higress-standalone\/releases\/tag\/(v[0-9\.]+)".*/\1/g' | head -1)
DOWNLOAD_URL="https://github.com/higress-group/higress-standalone/archive/refs/tags/${VERSION}.tar.gz"
curl -SsL "$DOWNLOAD_URL" -o "./artifact/higress-${VERSION}.tar.gz"
curl -SsL "https://raw.githubusercontent.com/higress-group/higress-standalone/refs/heads/main/src/get-higress.sh" -o "./artifact/get-higress.sh"
echo -n "$VERSION" > ./artifact/VERSION
echo "Version=$VERSION"
# Step 3

View File

@@ -4,11 +4,15 @@ on:
pull_request:
branches:
- "*"
paths:
- 'helm/**'
workflow_dispatch: ~
push:
branches: [ main ]
paths:
- 'helm/**'
jobs:
helm:
name: Helm Docs
runs-on: ubuntu-latest
@@ -32,4 +36,80 @@ jobs:
echo "Please use helm-docs in your clone, of your fork, of the project, and commit a updated README.md for the chart."
fi
git diff --exit-code
rm -f ./helm-docs
rm -f ./helm-docs
translate-readme:
if: ${{ ! always() }}
needs: helm
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y jq
- name: Translate README.md to Chinese
env:
API_URL: ${{ secrets.HIGRESS_OPENAI_API_URL }}
API_KEY: ${{ secrets.HIGRESS_OPENAI_API_KEY }}
API_MODEL: ${{ secrets.HIGRESS_OPENAI_API_MODEL }}
run: |
cd ./helm/higress
FILE_CONTENT=$(cat README.md)
PAYLOAD=$(jq -n \
--arg model "$API_MODEL" \
--arg content "$FILE_CONTENT" \
'{
model: $model,
messages: [
{"role": "system", "content": "You are a translation assistant that translates English Markdown text to Chinese."},
{"role": "user", "content": $content}
],
temperature: 1.1,
stream: false
}')
RESPONSE=$(curl -s -X POST "$API_URL" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_KEY" \
-d "$PAYLOAD")
echo "response: $RESPONSE"
TRANSLATED_CONTENT=$(echo "$RESPONSE" | jq -r '.choices[0].message.content')
if [ -z "$TRANSLATED_CONTENT" ]; then
echo "Translation failed! Response: $RESPONSE"
exit 1
fi
echo "$TRANSLATED_CONTENT" > README.zh.new.md
echo "Translation completed and saved to README.zh.new.md."
- name: Compare README.zh.md
id: compare
run: |
cd ./helm/higress
NEW_README_ZH="README.zh.new.md"
EXISTING_README_ZH="README.zh.md"
if [ ! -f "$EXISTING_README_ZH" ]; then
echo "Add README.zh.md."
mv "$NEW_README_ZH" "$EXISTING_README_ZH"
echo "updated=true" >> $GITHUB_ENV
exit 0
fi
if ! diff -q "$NEW_README_ZH" "$EXISTING_README_ZH"; then
echo "Files are different. Updating README.zh.md."
mv "$NEW_README_ZH" "$EXISTING_README_ZH"
echo "updated=true" >> $GITHUB_ENV
else
echo "Files are identical. No update needed."
echo "updated=false" >> $GITHUB_ENV
fi

View File

@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.21.5
go-version: 1.22
- name: Build hgctl latest multiarch binaries
run: |
@@ -43,7 +43,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.21.5
go-version: 1.22
- name: Build hgctl latest macos binaries
run: |
@@ -65,7 +65,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.21.5
go-version: 1.22
- name: Build hgctl latest macos binaries
run: |

1
.gitignore vendored
View File

@@ -17,4 +17,3 @@ target/
tools/hack/cluster.conf
envoy/1.20
istio/1.12
Cargo.lock

View File

@@ -144,7 +144,7 @@ docker-buildx-push: clean-env docker.higress-buildx
export PARENT_GIT_TAG:=$(shell cat VERSION)
export PARENT_GIT_REVISION:=$(TAG)
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.0/envoy-symbol-ARCH.tar.gz
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.3/envoy-symbol-ARCH.tar.gz
build-envoy: prebuild
./tools/hack/build-envoy.sh
@@ -159,16 +159,20 @@ build-pilot-local: prebuild
buildx-prepare:
docker buildx inspect multi-arch >/dev/null 2>&1 || docker buildx create --name multi-arch --platform linux/amd64,linux/arm64 --use
build-gateway: prebuild buildx-prepare
build-gateway: prebuild buildx-prepare build-golang-filter
USE_REAL_USER=1 TARGET_ARCH=amd64 DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh init
USE_REAL_USER=1 TARGET_ARCH=arm64 DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh init
DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh docker.buildx
DOCKER_TARGETS="docker.proxyv2" IMG_URL="${IMG_URL}" ./tools/hack/build-istio-image.sh docker.buildx
build-gateway-local: prebuild
build-gateway-local: prebuild build-golang-filter
TARGET_ARCH=${TARGET_ARCH} DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh docker
build-golang-filter:
TARGET_ARCH=amd64 ./tools/hack/build-golang-filters.sh
TARGET_ARCH=arm64 ./tools/hack/build-golang-filters.sh
build-istio: prebuild buildx-prepare
DOCKER_TARGETS="docker.pilot" ./tools/hack/build-istio-image.sh docker.buildx
DOCKER_TARGETS="docker.pilot" IMG_URL="${IMG_URL}" ./tools/hack/build-istio-image.sh docker.buildx
build-istio-local: prebuild
TARGET_ARCH=${TARGET_ARCH} DOCKER_TARGETS="docker.pilot" ./tools/hack/build-istio-image.sh docker
@@ -231,6 +235,8 @@ clean-gateway: clean-istio
rm -rf external/proxy
rm -rf external/go-control-plane
rm -rf external/package/envoy.tar.gz
rm -rf external/package/mcp-server_amd64.so
rm -rf external/package/mcp-server_arm64.so
clean-env:
rm -rf out/

201
README.md
View File

@@ -14,192 +14,135 @@
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
[**官网**](https://higress.cn/) &nbsp; |
&nbsp; [**文档**](https://higress.cn/docs/latest/overview/what-is-higress/) &nbsp; |
&nbsp; [**博客**](https://higress.cn/blog/) &nbsp; |
&nbsp; [**电子书**](https://higress.cn/docs/ebook/wasm14/) &nbsp; |
&nbsp; [**开发指引**](https://higress.cn/docs/latest/dev/architecture/) &nbsp; |
&nbsp; [**AI插件**](https://higress.cn/plugin/) &nbsp;
[**Official Site**](https://higress.io/en-us/) &nbsp; |
&nbsp; [**Docs**](https://higress.io/en-us/docs/overview/what-is-higress) &nbsp; |
&nbsp; [**Blog**](https://higress.io/en-us/blog) &nbsp; |
&nbsp; [**Developer**](https://higress.io/en-us/docs/developers/developers_dev) &nbsp; |
&nbsp; [**Higress in Cloud**](https://www.alibabacloud.com/product/microservices-engine?spm=higress-website.topbar.0.0.0) &nbsp;
<p>
<a href="README_EN.md"> English <a/>| 中文 | <a href="README_JP.md"> 日本語 <a/>
English | <a href="README_ZH.md">中文<a/> | <a href="README_JP.md">日本語<a/>
</p>
Higress is a cloud-native API gateway based on Istio and Envoy, which can be extended with Wasm plugins written in Go/Rust/JS. It provides dozens of ready-to-use general-purpose plugins and an out-of-the-box console (try the [demo here](http://demo.higress.io/)).
Higress 是一款云原生 API 网关,内核基于 Istio 和 Envoy可以用 Go/Rust/JS 等编写 Wasm 插件提供了数十个现成的通用插件以及开箱即用的控制台demo 点[这里](http://demo.higress.io/)
Higress was born within Alibaba to solve the issues of Tengine reload affecting long-connection services and insufficient load balancing capabilities for gRPC/Dubbo.
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。
阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
Higress 基于 AI 网关能力,支撑了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT
![](https://img.alicdn.com/imgextra/i2/O1CN011AbR8023V8R5N0HcA_!!6000000007260-2-tps-1080-606.png)
Alibaba Cloud has built its cloud-native API gateway product based on Higress, providing 99.99% gateway high availability guarantee service capabilities for a large number of enterprise customers.
Higress's AI gateway capabilities support all [mainstream model providers](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider) both domestic and international, as well as self-built DeepSeek models based on vllm/ollama. Within Alibaba Cloud, it supports AI businesses such as Tongyi Qianwen APP, Bailian large model API, and machine learning PAI platform. It also serves leading AIGC enterprises (such as Zero One Infinite) and AI products (such as FastGPT).
## Summary
- [**快速开始**](#快速开始)
- [**功能展示**](#功能展示)
- [**使用场景**](#使用场景)
- [**核心优势**](#核心优势)
- [**社区**](#社区)
- [**Quick Start**](#quick-start)
- [**Feature Showcase**](#feature-showcase)
- [**Use Cases**](#use-cases)
- [**Core Advantages**](#core-advantages)
- [**Community**](#community)
## 快速开始
## Quick Start
Higress 只需 Docker 即可启动,方便个人开发者在本地搭建学习,或者用于搭建简易站点:
Higress can be started with just Docker, making it convenient for individual developers to set up locally for learning or for building simple sites:
```bash
# 创建一个工作目录
# Create a working directory
mkdir higress; cd higress
# 启动 higress,配置文件会写到工作目录下
# Start higress, configuration files will be written to the working directory
docker run -d --rm --name higress-ai -v ${PWD}:/data \
-p 8001:8001 -p 8080:8080 -p 8443:8443 \
higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one:latest
```
监听端口说明如下:
Port descriptions:
- 8001 端口Higress UI 控制台入口
- 8080 端口:网关 HTTP 协议入口
- 8443 端口:网关 HTTPS 协议入口
- Port 8001: Higress UI console entry
- Port 8080: Gateway HTTP protocol entry
- Port 8443: Gateway HTTPS protocol entry
**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内访问受限的影响**
**All Higress Docker images use their own dedicated repository, unaffected by Docker Hub access restrictions in certain regions**
K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start 文档](https://higress.cn/docs/latest/user/quickstart/)
For other installation methods such as Helm deployment under K8s, please refer to the official [Quick Start documentation](https://higress.io/en-us/docs/user/quickstart).
如果您是在云上部署,生产环境推荐使用[企业版](https://higress.io/cloud/),开发测试可以使用下面一键部署社区版:
## Use Cases
[![Deploy on AlibabaCloud ComputeNest](https://service-info-public.oss-cn-hangzhou.aliyuncs.com/computenest.svg)](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Higress社区版)
- **AI Gateway**:
Higress can connect to all LLM model providers both domestic and international using a unified protocol, while also providing rich AI observability, multi-model load balancing/fallback, AI token rate limiting, AI caching, and other capabilities:
## 使用场景
![](https://img.alicdn.com/imgextra/i2/O1CN01izmBNX1jbHT7lP3Yr_!!6000000004566-0-tps-1920-1080.jpg)
- **AI 网关**:
- **MCP Server Hosting**:
Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时具备丰富的 AI 可观测、多模型负载均衡/fallback、AI token 流控、AI 缓存等能力:
Higress, as an Envoy-based API gateway, supports hosting MCP Servers through its plugin mechanism. MCP (Model Context Protocol) is essentially an AI-friendly API that enables AI Agents to more easily call various tools and services. Higress provides unified capabilities for authentication, authorization, rate limiting, and observability for tool calls, simplifying the development and deployment of AI applications.
![](https://img.alicdn.com/imgextra/i1/O1CN01fNnhCp1cV8mYPRFeS_!!6000000003605-0-tps-1080-608.jpg)
![](https://img.alicdn.com/imgextra/i1/O1CN01wv8H4g1mS4MUzC1QC_!!6000000004952-2-tps-1764-597.png)
- **Kubernetes Ingress 网关**:
By hosting MCP Servers with Higress, you can achieve:
- Unified authentication and authorization mechanisms, ensuring the security of AI tool calls
- Fine-grained rate limiting to prevent abuse and resource exhaustion
- Comprehensive audit logs recording all tool call behaviors
- Rich observability for monitoring the performance and health of tool calls
- Simplified deployment and management through Higress's plugin mechanism for quickly adding new MCP Servers
- Dynamic updates without disruption: Thanks to Envoy's friendly handling of long connections and Wasm plugin's dynamic update mechanism, MCP Server logic can be updated on-the-fly without any traffic disruption or connection drops
Higress 可以作为 K8s 集群的 Ingress 入口网关, 并且兼容了大量 K8s Nginx Ingress 的注解,可以从 K8s Nginx Ingress 快速平滑迁移到 Higress。
- **Kubernetes ingress controller**:
Higress can function as a feature-rich ingress controller, which is compatible with many annotations of K8s' nginx ingress controller.
支持 [Gateway API](https://gateway-api.sigs.k8s.io/) 标准,支持用户从 Ingress API 平滑迁移到 Gateway API
相比 ingress-nginx资源开销大幅下降路由变更生效速度有十倍提升
![](https://img.alicdn.com/imgextra/i1/O1CN01bhEtb229eeMNBWmdP_!!6000000008093-2-tps-750-547.png)
![](https://img.alicdn.com/imgextra/i1/O1CN01bqRets1LsBGyitj4S_!!6000000001354-2-tps-887-489.png)
[Gateway API](https://gateway-api.sigs.k8s.io/) support is coming soon and will support smooth migration from Ingress API to Gateway API.
- **微服务网关**:
- **Microservice gateway**:
Higress 可以作为微服务网关, 能够对接多种类型的注册中心发现服务配置路由,例如 Nacos, ZooKeeper, Consul, Eureka 等。
Higress can function as a microservice gateway, which can discovery microservices from various service registries, such as Nacos, ZooKeeper, Consul, Eureka, etc.
并且深度集成了 [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) 等微服务技术栈,基于 Envoy C++ 网关内核的出色性能,相比传统 Java 类微服务网关,可以显著降低资源使用率,减少成本。
![](https://img.alicdn.com/imgextra/i4/O1CN01v4ZbCj1dBjePSMZ17_!!6000000003698-0-tps-1613-926.jpg)
It deeply integrates with [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) and other microservice technology stacks.
- **安全防护网关**:
- **Security gateway**:
Higress 可以作为安全防护网关, 提供 WAF 的能力,并且支持多种认证鉴权策略,例如 key-auth, hmac-auth, jwt-auth, basic-auth, oidc 等。
Higress can be used as a security gateway, supporting WAF and various authentication strategies, such as key-auth, hmac-auth, jwt-auth, basic-auth, oidc, etc.
## 核心优势
- **生产等级**
## Core Advantages
脱胎于阿里巴巴2年多生产验证的内部产品支持每秒请求量达数十万级的大规模场景。
- **Production Grade**
彻底摆脱 Nginx reload 引起的流量抖动,配置变更毫秒级生效且业务无感。对 AI 业务等长连接场景特别友好。
Born from Alibaba's internal product with over 2 years of production validation, supporting large-scale scenarios with hundreds of thousands of requests per second.
- **流式处理**
Completely eliminates traffic jitter caused by Nginx reload, configuration changes take effect in milliseconds and are transparent to business. Especially friendly to long-connection scenarios such as AI businesses.
支持真正的完全流式处理请求/响应 BodyWasm 插件很方便地自定义处理 SSE Server-Sent Events等流式协议的报文。
- **Streaming Processing**
在 AI 业务等大带宽场景下,可以显著降低内存开销。
Supports true complete streaming processing of request/response bodies, Wasm plugins can easily customize the handling of streaming protocols such as SSE (Server-Sent Events).
In high-bandwidth scenarios such as AI businesses, it can significantly reduce memory overhead.
- **便于扩展**
- **Easy to Extend**
提供丰富的官方插件库,涵盖 AI、流量管理、安全防护等常用功能满足90%以上的业务场景需求。
Provides a rich official plugin library covering AI, traffic management, security protection and other common functions, meeting more than 90% of business scenario requirements.
主打 Wasm 插件扩展,通过沙箱隔离确保内存安全,支持多种编程语言,允许插件版本独立升级,实现流量无损热更新网关逻辑。
Focuses on Wasm plugin extensions, ensuring memory safety through sandbox isolation, supporting multiple programming languages, allowing plugin versions to be upgraded independently, and achieving traffic-lossless hot updates of gateway logic.
- **安全易用**
- **Secure and Easy to Use**
基于 Ingress API Gateway API 标准,提供开箱即用的 UI 控制台WAF 防护插件、IP/Cookie CC 防护插件开箱即用。
Based on Ingress API and Gateway API standards, provides out-of-the-box UI console, WAF protection plugin, IP/Cookie CC protection plugin ready to use.
支持对接 Let's Encrypt 自动签发和续签免费证书,并且可以脱离 K8s 部署,一行 Docker 命令即可启动,方便个人开发者使用。
Supports connecting to Let's Encrypt for automatic issuance and renewal of free certificates, and can be deployed outside of K8s, started with a single Docker command, convenient for individual developers to use.
## Community
## 功能展示
[Slack](https://w1689142780-euk177225.slack.com/archives/C05GEL4TGTG): to get invited go [here](https://communityinviter.com/apps/w1689142780-euk177225/higress).
### AI 网关 Demo 展示
### Thanks
[从 OpenAI 到其他大模型30 秒完成迁移
](https://www.bilibili.com/video/BV1dT421a7w7/?spm_id_from=333.788.recommend_more_video.14)
Higress would not be possible without the valuable open-source work of projects in the community. We would like to extend a special thank you to Envoy and Istio.
### Related Repositories
### Higress UI 控制台
- **丰富的可观测**
- Higress Console: https://github.com/higress-group/higress-console
- Higress Standalone: https://github.com/higress-group/higress-standalone
提供开箱即用的可观测Grafana&Prometheus 可以使用内置的也可对接自建的
![](./docs/images/monitor.gif)
- **插件扩展机制**
官方提供了多种插件,用户也可以[开发](./plugins/wasm-go)自己的插件,构建成 docker/oci 镜像后在控制台配置,可以实时变更插件逻辑,对流量完全无损。
![](./docs/images/plugin.gif)
- **多种服务发现**
默认提供 K8s Service 服务发现,通过配置可以对接 Nacos/ZooKeeper 等注册中心实现服务发现,也可以基于静态 IP 或者 DNS 来发现
![](./docs/images/service-source.gif)
- **域名和证书**
可以创建管理 TLS 证书,并配置域名的 HTTP/HTTPS 行为,域名策略里支持对特定域名生效插件
![](./docs/images/domain.gif)
- **丰富的路由能力**
通过上面定义的服务发现机制,发现的服务会出现在服务列表中;创建路由时,选择域名,定义路由匹配机制,再选择目标服务进行路由;路由策略里支持对特定路由生效插件
![](./docs/images/route-service.gif)
## 社区
### 感谢
如果没有 Envoy 和 Istio 的开源工作Higress 就不可能实现,在这里向这两个项目献上最诚挚的敬意。
### 交流群
![image](https://img.alicdn.com/imgextra/i2/O1CN01fZefEP1aPWkzG3A19_!!6000000003322-0-tps-720-405.jpg)
### 技术分享
微信公众号:
![](https://img.alicdn.com/imgextra/i1/O1CN01WnQt0q1tcmqVDU73u_!!6000000005923-0-tps-258-258.jpg)
### 关联仓库
- Higress 控制台https://github.com/higress-group/higress-console
- Higress独立运行版https://github.com/higress-group/higress-standalone
### 贡献者
### Contributors
<a href="https://github.com/alibaba/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
@@ -207,10 +150,10 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
### Star History
[![Star History](https://api.star-history.com/svg?repos=alibaba/higress&type=Date)](https://star-history.com/#alibaba/higress&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=alibaba/higress&type=Date)](https://star-history.com/#alibaba/higress&Date)
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
返回顶部
Back to Top
</a>
</p>

View File

@@ -1,106 +0,0 @@
<a name="readme-top"></a>
<h1 align="center">
<img src="https://img.alicdn.com/imgextra/i2/O1CN01NwxLDd20nxfGBjxmZ_!!6000000006895-2-tps-960-290.png" alt="Higress" width="240" height="72.5">
<br>
Cloud Native API Gateway
</h1>
[![Build Status](https://github.com/alibaba/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/alibaba/higress/actions)
[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[**Official Site**](https://higress.io/en-us/) &nbsp; |
&nbsp; [**Docs**](https://higress.io/en-us/docs/overview/what-is-higress) &nbsp; |
&nbsp; [**Blog**](https://higress.io/en-us/blog) &nbsp; |
&nbsp; [**Developer**](https://higress.io/en-us/docs/developers/developers_dev) &nbsp; |
&nbsp; [**Higress in Cloud**](https://www.alibabacloud.com/product/microservices-engine?spm=higress-website.topbar.0.0.0) &nbsp;
<p>
English | <a href="README.md">中文<a/> | <a href="README_JP.md">日本語<a/>
</p>
Higress is a cloud-native api gateway based on Alibaba's internal gateway practices.
Powered by [Istio](https://github.com/istio/istio) and [Envoy](https://github.com/envoyproxy/envoy), Higress realizes the integration of the triple gateway architecture of traffic gateway, microservice gateway and security gateway, thereby greatly reducing the costs of deployment, operation and maintenance.
<h1 align="center">
<img src="https://img.alicdn.com/imgextra/i1/O1CN01iO9ph825juHbOIg75_!!6000000007563-2-tps-2483-2024.png" alt="Higress Architecture">
</h1>
## Summary
- [**Use Cases**](#use-cases)
- [**Higress Features**](#higress-features)
- [**Quick Start**](https://higress.io/en-us/docs/user/quickstart)
- [**Community**](#community)
- [**Thanks**](#thanks)
## Use Cases
- **Kubernetes ingress controller**:
Higress can function as a feature-rich ingress controller, which is compatible with many annotations of K8s' nginx ingress controller.
[Gateway API](https://gateway-api.sigs.k8s.io/) support is coming soon and will support smooth migration from Ingress API to Gateway API.
- **Microservice gateway**:
Higress can function as a microservice gateway, which can discovery microservices from various service registries, such as Nacos, ZooKeeper, Consul, Eureka, etc.
It deeply integrates with [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) and other microservice technology stacks.
- **Security gateway**:
Higress can be used as a security gateway, supporting WAF and various authentication strategies, such as key-auth, hmac-auth, jwt-auth, basic-auth, oidc, etc.
## Higress Features
- **Easy to use**
Provides one-stop gateway solutions for traffic scheduling, service management, and security protection, support Console, K8s Ingress, and Gateway API configuration methods, and also support HTTP to Dubbo protocol conversion, and easily complete protocol mapping configuration.
- **Easy to expand**
Provides Wasm, Lua, and out-of-process plug-in extension mechanisms, so that multi-language plug-in writing is no longer an obstacle. The granularity of plug-in effectiveness supports not only the global level, domain name level, but also fine-grained routing level
- **Dynamic hot update**
Get rid of the traffic jitter caused by reload at the bottom, the configuration change takes effect in milliseconds and the business is not affected, the Wasm plug-in is hot updated and the traffic is not damaged
- **Smooth upgrade**
Compatible with 80%+ usage scenarios of Nginx Ingress Annotation, and provides more feature-rich annotations, easy to handle Nginx Ingress migration in one step
- **Security**
Provides JWT, OIDC, custom authentication and authentication, deeply integrates open-source web application firewall.
## Community
[Slack](https://w1689142780-euk177225.slack.com/archives/C05GEL4TGTG): to get invited go [here](https://communityinviter.com/apps/w1689142780-euk177225/higress).
### Thanks
Higress would not be possible without the valuable open-source work of projects in the community. We would like to extend a special thank you to Envoy and Istio.
### Related Repositories
- Higress Console: https://github.com/higress-group/higress-console
- Higress Standalone: https://github.com/higress-group/higress-standalone
### Contributors
<a href="https://github.com/alibaba/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
</a>
### Star History
[![Star History Chart](https://api.star-history.com/svg?repos=alibaba/higress&type=Date)](https://star-history.com/#alibaba/higress&Date)
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
↑ Back to Top ↑
</a>
</p>

230
README_ZH.md Normal file
View File

@@ -0,0 +1,230 @@
<a name="readme-top"></a>
<h1 align="center">
<img src="https://img.alicdn.com/imgextra/i2/O1CN01NwxLDd20nxfGBjxmZ_!!6000000006895-2-tps-960-290.png" alt="Higress" width="240" height="72.5">
<br>
AI Gateway
</h1>
<h4 align="center"> AI Native API Gateway </h4>
<div align="center">
[![Build Status](https://github.com/alibaba/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/alibaba/higress/actions)
[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
[**官网**](https://higress.cn/) &nbsp; |
&nbsp; [**文档**](https://higress.cn/docs/latest/overview/what-is-higress/) &nbsp; |
&nbsp; [**博客**](https://higress.cn/blog/) &nbsp; |
&nbsp; [**电子书**](https://higress.cn/docs/ebook/wasm14/) &nbsp; |
&nbsp; [**开发指引**](https://higress.cn/docs/latest/dev/architecture/) &nbsp; |
&nbsp; [**AI插件**](https://higress.cn/plugin/) &nbsp;
<p>
<a href="README.md"> English <a/>| 中文 | <a href="README_JP.md"> 日本語 <a/>
</p>
Higress 是一款云原生 API 网关,内核基于 Istio 和 Envoy可以用 Go/Rust/JS 等编写 Wasm 插件提供了数十个现成的通用插件以及开箱即用的控制台demo 点[这里](http://demo.higress.io/)
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。
阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)和基于 vllm/ollama 等自建的 DeepSeek 模型;在阿里云内部支撑了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT
![](https://img.alicdn.com/imgextra/i2/O1CN011AbR8023V8R5N0HcA_!!6000000007260-2-tps-1080-606.png)
## Summary
- [**快速开始**](#快速开始)
- [**功能展示**](#功能展示)
- [**使用场景**](#使用场景)
- [**核心优势**](#核心优势)
- [**社区**](#社区)
## 快速开始
Higress 只需 Docker 即可启动,方便个人开发者在本地搭建学习,或者用于搭建简易站点:
```bash
# 创建一个工作目录
mkdir higress; cd higress
# 启动 higress配置文件会写到工作目录下
docker run -d --rm --name higress-ai -v ${PWD}:/data \
-p 8001:8001 -p 8080:8080 -p 8443:8443 \
higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one:latest
```
监听端口说明如下:
- 8001 端口Higress UI 控制台入口
- 8080 端口:网关 HTTP 协议入口
- 8443 端口:网关 HTTPS 协议入口
**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内访问受限的影响**
K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start 文档](https://higress.cn/docs/latest/user/quickstart/)。
如果您是在云上部署,生产环境推荐使用[企业版](https://higress.io/cloud/),开发测试可以使用下面一键部署社区版:
[![Deploy on AlibabaCloud ComputeNest](https://service-info-public.oss-cn-hangzhou.aliyuncs.com/computenest.svg)](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Higress社区版)
## 使用场景
- **AI 网关**:
Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时具备丰富的 AI 可观测、多模型负载均衡/fallback、AI token 流控、AI 缓存等能力:
![](https://img.alicdn.com/imgextra/i1/O1CN01fNnhCp1cV8mYPRFeS_!!6000000003605-0-tps-1080-608.jpg)
- **MCP Server 托管**:
Higress 作为基于 Envoy 的 API 网关,支持通过插件方式托管 MCP Server。MCPModel Context Protocol本质是面向 AI 更友好的 API使 AI Agent 能够更容易地调用各种工具和服务。Higress 可以统一处理工具调用的认证/鉴权/限流/观测等能力,简化 AI 应用的开发和部署。
![](https://img.alicdn.com/imgextra/i3/O1CN01K4qPUX1OliZa8KIPw_!!6000000001746-2-tps-1581-615.png)
通过 Higress 托管 MCP Server可以实现
- 统一的认证和鉴权机制,确保 AI 工具调用的安全性
- 精细化的速率限制,防止滥用和资源耗尽
- 完整的审计日志,记录所有工具调用行为
- 丰富的可观测性,监控工具调用的性能和健康状况
- 简化的部署和管理,通过 Higress 插件机制快速添加新的 MCP Server
- 动态更新无损:得益于 Envoy 对长连接保持的友好支持,以及 Wasm 插件的动态更新机制MCP Server 逻辑可以实时更新,且对流量完全无损,不会导致任何连接断开
- **Kubernetes Ingress 网关**:
Higress 可以作为 K8s 集群的 Ingress 入口网关, 并且兼容了大量 K8s Nginx Ingress 的注解,可以从 K8s Nginx Ingress 快速平滑迁移到 Higress。
支持 [Gateway API](https://gateway-api.sigs.k8s.io/) 标准,支持用户从 Ingress API 平滑迁移到 Gateway API。
相比 ingress-nginx资源开销大幅下降路由变更生效速度有十倍提升
![](https://img.alicdn.com/imgextra/i1/O1CN01bhEtb229eeMNBWmdP_!!6000000008093-2-tps-750-547.png)
![](https://img.alicdn.com/imgextra/i1/O1CN01bqRets1LsBGyitj4S_!!6000000001354-2-tps-887-489.png)
- **微服务网关**:
Higress 可以作为微服务网关, 能够对接多种类型的注册中心发现服务配置路由,例如 Nacos, ZooKeeper, Consul, Eureka 等。
并且深度集成了 [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) 等微服务技术栈,基于 Envoy C++ 网关内核的出色性能,相比传统 Java 类微服务网关,可以显著降低资源使用率,减少成本。
![](https://img.alicdn.com/imgextra/i4/O1CN01v4ZbCj1dBjePSMZ17_!!6000000003698-0-tps-1613-926.jpg)
- **安全防护网关**:
Higress 可以作为安全防护网关, 提供 WAF 的能力,并且支持多种认证鉴权策略,例如 key-auth, hmac-auth, jwt-auth, basic-auth, oidc 等。
## 核心优势
- **生产等级**
脱胎于阿里巴巴2年多生产验证的内部产品支持每秒请求量达数十万级的大规模场景。
彻底摆脱 Nginx reload 引起的流量抖动,配置变更毫秒级生效且业务无感。对 AI 业务等长连接场景特别友好。
- **流式处理**
支持真正的完全流式处理请求/响应 BodyWasm 插件很方便地自定义处理 SSE Server-Sent Events等流式协议的报文。
在 AI 业务等大带宽场景下,可以显著降低内存开销。
- **便于扩展**
提供丰富的官方插件库,涵盖 AI、流量管理、安全防护等常用功能满足90%以上的业务场景需求。
主打 Wasm 插件扩展,通过沙箱隔离确保内存安全,支持多种编程语言,允许插件版本独立升级,实现流量无损热更新网关逻辑。
- **安全易用**
基于 Ingress API 和 Gateway API 标准,提供开箱即用的 UI 控制台WAF 防护插件、IP/Cookie CC 防护插件开箱即用。
支持对接 Let's Encrypt 自动签发和续签免费证书,并且可以脱离 K8s 部署,一行 Docker 命令即可启动,方便个人开发者使用。
## 功能展示
### AI 网关 Demo 展示
[从 OpenAI 到其他大模型30 秒完成迁移
](https://www.bilibili.com/video/BV1dT421a7w7/?spm_id_from=333.788.recommend_more_video.14)
### Higress UI 控制台
- **丰富的可观测**
提供开箱即用的可观测Grafana&Prometheus 可以使用内置的也可对接自建的
![](./docs/images/monitor.gif)
- **插件扩展机制**
官方提供了多种插件,用户也可以[开发](./plugins/wasm-go)自己的插件,构建成 docker/oci 镜像后在控制台配置,可以实时变更插件逻辑,对流量完全无损。
![](./docs/images/plugin.gif)
- **多种服务发现**
默认提供 K8s Service 服务发现,通过配置可以对接 Nacos/ZooKeeper 等注册中心实现服务发现,也可以基于静态 IP 或者 DNS 来发现
![](./docs/images/service-source.gif)
- **域名和证书**
可以创建管理 TLS 证书,并配置域名的 HTTP/HTTPS 行为,域名策略里支持对特定域名生效插件
![](./docs/images/domain.gif)
- **丰富的路由能力**
通过上面定义的服务发现机制,发现的服务会出现在服务列表中;创建路由时,选择域名,定义路由匹配机制,再选择目标服务进行路由;路由策略里支持对特定路由生效插件
![](./docs/images/route-service.gif)
## 社区
### 感谢
如果没有 Envoy 和 Istio 的开源工作Higress 就不可能实现,在这里向这两个项目献上最诚挚的敬意。
### 交流群
![image](https://img.alicdn.com/imgextra/i2/O1CN01fZefEP1aPWkzG3A19_!!6000000003322-0-tps-720-405.jpg)
### 技术分享
微信公众号:
![](https://img.alicdn.com/imgextra/i1/O1CN01WnQt0q1tcmqVDU73u_!!6000000005923-0-tps-258-258.jpg)
### 关联仓库
- Higress 控制台https://github.com/higress-group/higress-console
- Higress独立运行版https://github.com/higress-group/higress-standalone
### 贡献者
<a href="https://github.com/alibaba/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
</a>
### Star History
[![Star History](https://api.star-history.com/svg?repos=alibaba/higress&type=Date)](https://star-history.com/#alibaba/higress&Date)
<p align="right" style="font-size: 14px; color: #555; margin-top: 20px;">
<a href="#readme-top" style="text-decoration: none; color: #007bff; font-weight: bold;">
↑ 返回顶部 ↑
</a>
</p>

View File

@@ -1 +1 @@
v2.0.6
v2.1.0-rc.2

View File

@@ -35,6 +35,8 @@ DOCKER_ALL_VARIANTS ?= debug distroless
INCLUDE_UNTAGGED_DEFAULT ?= false
DEFAULT_DISTRIBUTION=debug
HIGRESS_DOCKER_BUILDX_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker buildx create --name higress --node higress0 --platform linux/amd64,linux/arm64 --use && docker buildx build --no-cache --platform linux/amd64,linux/arm64 $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(HUB)/higress:$(TAG)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . --push ); )
HIGRESS_DOCKER_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker build $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(HUB)/higress:$(TAG)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . ); )
IMG ?= higress
IMG_URL ?= $(HUB)/$(IMG):$(TAG)
HIGRESS_DOCKER_BUILDX_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker buildx create --name higress --node higress0 --platform linux/amd64,linux/arm64 --use && docker buildx build --no-cache --platform linux/amd64,linux/arm64 $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(IMG_URL)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . --push ); )
HIGRESS_DOCKER_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker build $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(IMG_URL)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . ); )

6
go.mod
View File

@@ -1,8 +1,6 @@
module github.com/alibaba/higress
go 1.21.0
toolchain go1.22.2
go 1.22.2
replace github.com/spf13/viper => github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c
@@ -23,6 +21,7 @@ require (
github.com/dubbogo/go-zookeeper v1.0.4-0.20211212162352-f9d2183d89d5
github.com/dubbogo/gost v1.13.1
github.com/envoyproxy/go-control-plane v0.11.2-0.20230725211550-11bfe846bcd4
github.com/go-errors/errors v1.4.2
github.com/gogo/protobuf v1.3.2
github.com/golang/protobuf v1.5.3
github.com/google/go-cmp v0.6.0
@@ -99,7 +98,6 @@ require (
github.com/fatih/color v1.15.0 // indirect
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect

View File

@@ -1,13 +1,18 @@
apiVersion: v2
appVersion: 2.0.6
appVersion: 2.1.0-rc.2
description: Helm chart for deploying higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
keywords:
- higress
- gateways
- higress
- gateways
name: higress-core
sources:
- http://github.com/alibaba/higress
- http://github.com/alibaba/higress
dependencies:
- condition: global.enableRedis
name: redis
repository: "file://../redis"
version: 0.0.1
type: application
version: 2.0.6
version: 2.1.0-rc.2

View File

@@ -2,4 +2,4 @@
Installs the core components of cloud-native gateway [Higress](http://higress.io/)
**Note:** It is highly recommended to install the whole package of Higress. Please visit https://higress.io/docs/user/quickstart/ for details.
**Note:** It is highly recommended to install the whole package of Higress. Please visit https://higress.io/docs/user/quickstart/ for details.

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,24 @@
apiVersion: v2
name: redis
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "7.4.0-v3"

View File

@@ -0,0 +1,34 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "redis.name" -}}
{{- .Values.redis.name | default "redis-stack-server" -}}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "redis.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "redis.labels" -}}
helm.sh/chart: {{ include "redis.chart" . }}
{{ include "redis.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "redis.selectorLabels" -}}
app.kubernetes.io/name: {{ include "redis.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

View File

@@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "redis.name" . }}
namespace: {{ .Release.Namespace }}
data:
redis-stack.conf: |
{{- if .Values.redis.password }}
requirepass {{ .Values.redis.password }}
{{- end }}

View File

@@ -0,0 +1,16 @@
{{- if .Values.redis.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "redis.name" . }}
namespace: {{ .Release.Namespace }}
spec:
accessModes:
{{- range .Values.redis.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
storageClassName: {{ .Values.redis.persistence.storageClass }}
resources:
requests:
storage: {{ .Values.redis.persistence.size | quote }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "redis.name" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis.labels" . | nindent 4 }}
spec:
type: {{ .Values.redis.service.type }}
ports:
- port: {{ .Values.redis.service.port }}
targetPort: 6379
protocol: TCP
selector:
{{- include "redis.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,74 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "redis.name" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "redis.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.redis.replicas }}
serviceName: {{ include "redis.name" . }}
selector:
matchLabels:
{{- include "redis.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "redis.selectorLabels" . | nindent 8 }}
spec:
terminationGracePeriodSeconds: 10
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.global.hub }}/{{ .Values.redis.image | default "redis-stack-server" }}:{{ .Values.redis.tag | default .Chart.AppVersion }}"
{{- if .Values.global.imagePullPolicy }}
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
{{- end }}
ports:
- name: http
containerPort: 6379
protocol: TCP
livenessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 15
periodSeconds: 10
resources:
{{- toYaml .Values.redis.resources | nindent 12 }}
volumeMounts:
- name: config
mountPath: /redis-stack.conf
subPath: redis-stack.conf
{{- if .Values.redis.persistence.enabled }}
- name: db
mountPath: /data
{{- end }}
{{- with .Values.redis.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.redis.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.redis.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: config
configMap:
name: {{ include "redis.name" . }}
{{- if .Values.redis.persistence.enabled }}
- name: db
persistentVolumeClaim:
claimName: {{ include "redis.name" . }}
{{- end }}

View File

@@ -0,0 +1,48 @@
# Default values for redis.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
global:
# -- Specify the image registry and pull policy
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
# -- Specify image pull policy if default behavior isn't desired.
# Default behavior: latest images will be Always else IfNotPresent.
imagePullPolicy: ""
# -- Specify the image pull secrets
imagePullSecrets: []
redis:
# -- Specify the name
name: redis-stack-server
# -- Specify the image
image: "redis-stack-server"
# -- Specify the tag
tag: "7.4.0-v3"
# -- Specify the number of replicas
replicas: 1
# -- Specify the password, if not set, no password is used
password: ""
# -- Service parameters
service:
# -- Exporter service type
type: ClusterIP
# -- Exporter service port
port: 6379
# -- Specify the resources
resources: {}
# -- NodeSelector Node labels for Redis
nodeSelector: {}
# -- Tolerations for Redis
tolerations: []
# -- Affinity for Redis
affinity: {}
persistence:
# -- Enable persistence on Redis
enabled: false
# -- If defined, storageClassName: <storageClass>
# -- If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner
storageClass: ""
# -- Persistent Volume access modes
accessModes:
- ReadWriteOnce
# -- Persistent Volume size
size: 1Gi

View File

@@ -15,6 +15,9 @@ template:
{{- with .Values.gateway.revision }}
istio.io/rev: {{ . }}
{{- end }}
{{- with .Values.gateway.podLabels }}
{{- toYaml . | nindent 6 }}
{{- end }}
{{- include "gateway.selectorLabels" . | nindent 6 }}
spec:
{{- with .Values.gateway.imagePullSecrets }}
@@ -42,9 +45,9 @@ template:
- router
- --domain
- $(POD_NAMESPACE).svc.cluster.local
- --proxyLogLevel=warning
- --proxyComponentLogLevel=misc:error
- --log_output_level=all:info
- --proxyLogLevel={{- default "warning" .Values.global.proxy.logLevel }}
- --proxyComponentLogLevel={{- default "misc:error" .Values.global.proxy.componentLogLevel }}
- --log_output_level={{- default "default:info" .Values.global.logging.level }}
- --serviceCluster=higress-gateway
securityContext:
{{- if .Values.gateway.containerSecurityContext }}
@@ -128,7 +131,7 @@ template:
- name: ISTIO_META_REQUESTED_NETWORK_VIEW
value: "{{.}}"
{{- end }}
{{- range $key, $val := .Values.env }}
{{- range $key, $val := .Values.gateway.env }}
- name: {{ $key }}
value: {{ $val | quote }}
{{- end }}

View File

@@ -85,7 +85,7 @@
{{- end }}
proxyStatsMatcher:
inclusionRegexps:
- ".*"
{{ toYaml .Values.global.proxy.proxyStatsMatcher.inclusionRegexps | indent 8 }}
{{- end }}
{{/* We take the mesh config above, defined with individual values.yaml, and merge with .Values.meshConfig */}}

View File

@@ -19,6 +19,9 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- with .Values.controller.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- include "controller.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.controller.imagePullSecrets }}
@@ -136,6 +139,10 @@ spec:
periodSeconds: 3
timeoutSeconds: 5
env:
{{- if .Values.global.watchNamespace }}
- name: ISTIO_WATCH_NAMESPACE
value: "{{ .Values.global.watchNamespace }}"
{{- end }}
- name: ENABLE_PUSH_ALL_MCP_CLUSTERS
value: "{{ .Values.global.enablePushAllMCPClusters }}"
- name: PILOT_ENABLE_LDS_CACHE

View File

@@ -3,12 +3,14 @@ global:
enableH3: false
enableIPv6: false
enableProxyProtocol: false
enableLDSCache: true
enableLDSCache: false
enablePushAllMCPClusters: true
liteMetrics: false
xdsMaxRecvMsgSize: "104857600"
defaultUpstreamConcurrencyThreshold: 10000
enableSRDS: true
# -- Whether to enable Redis(redis-stack-server) for Higress, default is false.
enableRedis: false
onDemandRDS: false
hostRDSMergeSubset: false
onlyPushRouteCluster: true
@@ -199,6 +201,11 @@ global:
# -- Controls if sidecar is injected at the front of the container list and blocks the start of the other containers until the proxy is ready
holdApplicationUntilProxyStarts: false
# -- Proxy stats name regexps matcher for inclusion
proxyStatsMatcher:
inclusionRegexps:
- ".*"
proxy_init:
# -- Base name for the proxy_init container, used to configure iptables.
image: proxyv2
@@ -462,6 +469,9 @@ gateway:
prometheus.io/path: "/stats/prometheus"
sidecar.istio.io/inject: "false"
# -- Labels to apply to the pod
podLabels: {}
# -- Define the security context for the pod.
# If unset, this will be automatically set to the minimum privileges required to bind to port 80 and 443.
# On Kubernetes 1.22+, this only requires the `net.ipv4.ip_unprivileged_port_start` sysctl.
@@ -488,6 +498,7 @@ gateway:
externalTrafficPolicy: ""
rollingMaxSurge: 100%
# -- If global.local is true, the default value is 100%, otherwise it is 25%
rollingMaxUnavailable: 25%
resources:
@@ -543,12 +554,12 @@ controller:
labels: {}
probe:
{
httpGet: { path: /ready, port: 8888 },
initialDelaySeconds: 1,
periodSeconds: 3,
timeoutSeconds: 5,
}
httpGet:
path: /ready
port: 8888
initialDelaySeconds: 1
periodSeconds: 3
timeoutSeconds: 5
imagePullSecrets: []
@@ -566,21 +577,26 @@ controller:
podAnnotations: {}
# -- Labels to apply to the pod
podLabels: {}
podSecurityContext:
{}
# fsGroup: 2000
# fsGroup: 2000
ports:
[
{ "name": "http", "protocol": "TCP", "port": 8888, "targetPort": 8888 },
{
"name": "http-solver",
"protocol": "TCP",
"port": 8889,
"targetPort": 8889,
},
{ "name": "grpc", "protocol": "TCP", "port": 15051, "targetPort": 15051 },
]
- name: http
protocol: TCP
port: 8888
targetPort: 8888
- name: http-solver
protocol: TCP
port: 8889
targetPort: 8889
- name: grpc
protocol: TCP
port: 15051
targetPort: 15051
service:
type: ClusterIP
@@ -590,9 +606,9 @@ controller:
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
resources:
requests:
@@ -715,3 +731,40 @@ downstream:
upstream:
idleTimeout: 10
connectionBufferLimits: 10485760
redis:
redis:
name: redis-stack-server
# -- Specify the image
image: "redis-stack-server"
# -- Specify the tag
tag: "7.4.0-v3"
# -- Specify the number of replicas
replicas: 1
# -- Specify the password, if not set, no password is used
password: ""
# -- Service parameters
service:
# -- Exporter service type
type: ClusterIP
# -- Exporter service port
port: 6379
# -- Specify the resources
resources: {}
# -- NodeSelector Node labels for Redis
nodeSelector: {}
# -- Tolerations for Redis
tolerations: []
# -- Affinity for Redis
affinity: {}
persistence:
# -- Enable persistence on Redis, default is false
enabled: false
# -- If defined, storageClassName: <storageClass>
# -- If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner
storageClass: ""
# -- Persistent Volume access modes
accessModes:
- ReadWriteOnce
# -- Persistent Volume size
size: 1Gi

View File

@@ -1,9 +1,9 @@
dependencies:
- name: higress-core
repository: file://../core
version: 2.0.6
version: 2.1.0-rc.2
- name: higress-console
repository: https://higress.io/helm-charts/
version: 2.0.2
digest: sha256:9c84a628df434c4bf23ec10d62ad7ddf4b15957f797b01bbaa492ede33d87003
generated: "2025-01-17T15:10:43.589701962+08:00"
version: 2.1.0
digest: sha256:2ad724f1db0e86c9237a05822e29c7356b3995d4248783b3b0b2723ac628f647
generated: "2025-04-01T23:34:42.769494+08:00"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 2.0.6
appVersion: 2.1.0-rc.2
description: Helm chart for deploying Higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -12,9 +12,9 @@ sources:
dependencies:
- name: higress-core
repository: "file://../core"
version: 2.0.6
version: 2.1.0-rc.2
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 2.0.2
version: 2.1.0
type: application
version: 2.0.6
version: 2.1.0-rc.2

View File

@@ -51,6 +51,7 @@ The command removes all the Kubernetes components associated with the chart and
| controller.name | string | `"higress-controller"` | |
| controller.nodeSelector | object | `{}` | |
| controller.podAnnotations | object | `{}` | |
| controller.podLabels | object | `{}` | Labels to apply to the pod |
| controller.podSecurityContext | object | `{}` | |
| controller.ports[0].name | string | `"http"` | |
| controller.ports[0].port | int | `8888` | |
@@ -115,6 +116,7 @@ The command removes all the Kubernetes components associated with the chart and
| gateway.podAnnotations."prometheus.io/port" | string | `"15020"` | |
| gateway.podAnnotations."prometheus.io/scrape" | string | `"true"` | |
| gateway.podAnnotations."sidecar.istio.io/inject" | string | `"false"` | |
| gateway.podLabels | object | `{}` | Labels to apply to the pod |
| gateway.rbac.enabled | bool | `true` | If enabled, roles will be created to enable accessing certificates from Gateways. This is not needed when using http://gateway-api.org/. |
| gateway.readinessFailureThreshold | int | `30` | The number of successive failed probes before indicating readiness failure. |
| gateway.readinessInitialDelaySeconds | int | `1` | The initial delay for readiness probes in seconds. |
@@ -128,7 +130,7 @@ The command removes all the Kubernetes components associated with the chart and
| gateway.resources.requests.memory | string | `"2048Mi"` | |
| gateway.revision | string | `""` | revision declares which revision this gateway is a part of |
| gateway.rollingMaxSurge | string | `"100%"` | |
| gateway.rollingMaxUnavailable | string | `"25%"` | |
| gateway.rollingMaxUnavailable | string | `"25%"` | If global.local is true, the default value is 100%, otherwise it is 25% |
| gateway.securityContext | string | `nil` | Define the security context for the pod. If unset, this will be automatically set to the minimum privileges required to bind to port 80 and 443. On Kubernetes 1.22+, this only requires the `net.ipv4.ip_unprivileged_port_start` sysctl. |
| gateway.service.annotations | object | `{}` | |
| gateway.service.externalTrafficPolicy | string | `""` | |
@@ -162,9 +164,10 @@ The command removes all the Kubernetes components associated with the chart and
| global.enableH3 | bool | `false` | |
| global.enableIPv6 | bool | `false` | |
| global.enableIstioAPI | bool | `true` | If true, Higress Controller will monitor istio resources as well |
| global.enableLDSCache | bool | `true` | |
| global.enableLDSCache | bool | `false` | |
| global.enableProxyProtocol | bool | `false` | |
| global.enablePushAllMCPClusters | bool | `true` | |
| global.enableRedis | bool | `false` | Whether to enable Redis(redis-stack-server) for Higress, default is false. |
| global.enableSRDS | bool | `true` | |
| global.enableStatus | bool | `true` | If true, Higress Controller will update the status field of Ingress resources. When migrating from Nginx Ingress, in order to avoid status field of Ingress objects being overwritten, this parameter needs to be set to false, so Higress won't write the entry IP to the status field of the corresponding Ingress object. |
| global.externalIstiod | bool | `false` | Configure a remote cluster data plane controlled by an external istiod. When set to true, istiod is not deployed locally and only a subset of the other discovery charts are enabled. |
@@ -209,6 +212,7 @@ The command removes all the Kubernetes components associated with the chart and
| global.proxy.includeOutboundPorts | string | `""` | |
| global.proxy.logLevel | string | `"warning"` | Log level for proxy, applies to gateways and sidecars. Expected values are: trace|debug|info|warning|error|critical|off |
| global.proxy.privileged | bool | `false` | If set to true, istio-proxy container will have privileged securityContext |
| global.proxy.proxyStatsMatcher | object | `{"inclusionRegexps":[".*"]}` | Proxy stats name regexps matcher for inclusion |
| global.proxy.readinessFailureThreshold | int | `30` | The number of successive failed probes before indicating readiness failure. |
| global.proxy.readinessInitialDelaySeconds | int | `1` | The initial delay for readiness probes in seconds. |
| global.proxy.readinessPeriodSeconds | int | `2` | The period between readiness probes. |
@@ -269,6 +273,22 @@ The command removes all the Kubernetes components associated with the chart and
| pilot.serviceAnnotations | object | `{}` | |
| pilot.tag | string | `""` | |
| pilot.traceSampling | float | `1` | |
| redis.redis.affinity | object | `{}` | Affinity for Redis |
| redis.redis.image | string | `"redis-stack-server"` | Specify the image |
| redis.redis.name | string | `"redis-stack-server"` | |
| redis.redis.nodeSelector | object | `{}` | NodeSelector Node labels for Redis |
| redis.redis.password | string | `""` | Specify the password, if not set, no password is used |
| redis.redis.persistence.accessModes | list | `["ReadWriteOnce"]` | Persistent Volume access modes |
| redis.redis.persistence.enabled | bool | `false` | Enable persistence on Redis, default is false |
| redis.redis.persistence.size | string | `"1Gi"` | Persistent Volume size |
| redis.redis.persistence.storageClass | string | `""` | If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner |
| redis.redis.replicas | int | `1` | Specify the number of replicas |
| redis.redis.resources | object | `{}` | Specify the resources |
| redis.redis.service | object | `{"port":6379,"type":"ClusterIP"}` | Service parameters |
| redis.redis.service.port | int | `6379` | Exporter service port |
| redis.redis.service.type | string | `"ClusterIP"` | Exporter service type |
| redis.redis.tag | string | `"7.4.0-v3"` | Specify the tag |
| redis.redis.tolerations | list | `[]` | Tolerations for Redis |
| revision | string | `""` | |
| tracing.enable | bool | `false` | |
| tracing.sampling | int | `100` | |

188
helm/higress/README.zh.md Normal file
View File

@@ -0,0 +1,188 @@
## Higress for Kubernetes
Higress 是基于阿里巴巴内部网关实践构建的云原生 API 网关。
依托 Istio 和 EnvoyHigress 实现了流量网关、微服务网关和安全网关三重架构的融合,从而大幅降低了部署、运维成本。
## 设置仓库信息
```console
helm repo add higress.io https://higress.io/helm-charts
helm repo update
```
## 安装
`higress` 为发布名称安装 chart
```console
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
```
## 卸载
要卸载/删除 higress 部署:
```console
helm delete higress -n higress-system
```
该命令会移除与 chart 相关的所有 Kubernetes 组件,并删除发布。
## 参数
## 值
| 键 | 类型 | 默认值 | 描述 |
|-----|------|---------|-------------|
| clusterName | 字符串 | `""` | |
| controller.affinity | 对象 | `{}` | |
| controller.automaticHttps.email | 字符串 | `""` | |
| controller.automaticHttps.enabled | 布尔值 | `true` | |
| controller.autoscaling.enabled | 布尔值 | `false` | |
| controller.autoscaling.maxReplicas | 整数 | `5` | |
| controller.autoscaling.minReplicas | 整数 | `1` | |
| controller.autoscaling.targetCPUUtilizationPercentage | 整数 | `80` | |
| controller.env | 对象 | `{}` | |
| controller.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
| controller.image | 字符串 | `"higress"` | |
| controller.imagePullSecrets | 列表 | `[]` | |
| controller.labels | 对象 | `{}` | |
| controller.name | 字符串 | `"higress-controller"` | |
| controller.nodeSelector | 对象 | `{}` | |
| controller.podAnnotations | 对象 | `{}` | |
| controller.podSecurityContext | 对象 | `{}` | |
| controller.ports[0].name | 字符串 | `"http"` | |
| controller.ports[0].port | 整数 | `8888` | |
| controller.ports[0].protocol | 字符串 | `"TCP"` | |
| controller.ports[0].targetPort | 整数 | `8888` | |
| controller.ports[1].name | 字符串 | `"http-solver"` | |
| controller.ports[1].port | 整数 | `8889` | |
| controller.ports[1].protocol | 字符串 | `"TCP"` | |
| controller.ports[1].targetPort | 整数 | `8889` | |
| controller.ports[2].name | 字符串 | `"grpc"` | |
| controller.ports[2].port | 整数 | `15051` | |
| controller.ports[2].protocol | 字符串 | `"TCP"` | |
| controller.ports[2].targetPort | 整数 | `15051` | |
| controller.probe.httpGet.path | 字符串 | `"/ready"` | |
| controller.probe.httpGet.port | 整数 | `8888` | |
| controller.probe.initialDelaySeconds | 整数 | `1` | |
| controller.probe.periodSeconds | 整数 | `3` | |
| controller.probe.timeoutSeconds | 整数 | `5` | |
| controller.rbac.create | 布尔值 | `true` | |
| controller.replicas | 整数 | `1` | Higress Controller 的 Pod 数量 |
| controller.resources.limits.cpu | 字符串 | `"1000m"` | |
| controller.resources.limits.memory | 字符串 | `"2048Mi"` | |
| controller.resources.requests.cpu | 字符串 | `"500m"` | |
| controller.resources.requests.memory | 字符串 | `"2048Mi"` | |
| controller.securityContext | 对象 | `{}` | |
| controller.service.type | 字符串 | `"ClusterIP"` | |
| controller.serviceAccount.annotations | 对象 | `{}` | 添加到服务账户的注解 |
| controller.serviceAccount.create | 布尔值 | `true` | 指定是否创建服务账户 |
| controller.serviceAccount.name | 字符串 | `""` | 如果未设置且 create 为 true则使用 fullname 模板生成名称 |
| controller.tag | 字符串 | `""` | |
| controller.tolerations | 列表 | `[]` | |
| downstream | 对象 | `{"connectionBufferLimits":32768,"http2":{"initialConnectionWindowSize":1048576,"initialStreamWindowSize":65535,"maxConcurrentStreams":100},"idleTimeout":180,"maxRequestHeadersKb":60,"routeTimeout":0}` | 下游配置设置 |
| gateway.affinity | 对象 | `{}` | |
| gateway.annotations | 对象 | `{}` | 应用到所有资源的注解 |
| gateway.autoscaling.enabled | 布尔值 | `false` | |
| gateway.autoscaling.maxReplicas | 整数 | `5` | |
| gateway.autoscaling.minReplicas | 整数 | `1` | |
| gateway.autoscaling.targetCPUUtilizationPercentage | 整数 | `80` | |
| gateway.containerSecurityContext | 字符串 | `nil` | |
| gateway.env | 对象 | `{}` | Pod 环境变量 |
| gateway.hostNetwork | 布尔值 | `false` | |
| gateway.httpPort | 整数 | `80` | |
| gateway.httpsPort | 整数 | `443` | |
| gateway.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
| gateway.image | 字符串 | `"gateway"` | |
| gateway.kind | 字符串 | `"Deployment"` | 使用 `DaemonSet``Deployment` |
| gateway.labels | 对象 | `{}` | 应用到所有资源的标签 |
| gateway.metrics.enabled | 布尔值 | `false` | 如果为 true则为网关创建 PodMonitor 或 VMPodScrape |
| gateway.metrics.honorLabels | 布尔值 | `false` | |
| gateway.metrics.interval | 字符串 | `""` | |
| gateway.metrics.metricRelabelConfigs | 列表 | `[]` | 用于 operator.victoriametrics.com/v1beta1.VMPodScrape |
| gateway.metrics.metricRelabelings | 列表 | `[]` | 用于 monitoring.coreos.com/v1.PodMonitor |
| gateway.metrics.provider | 字符串 | `"monitoring.coreos.com"` | CustomResourceDefinition 的提供者组名,可以是 monitoring.coreos.com 或 operator.victoriametrics.com |
| gateway.metrics.rawSpec | 对象 | `{}` | 更多原始的 podMetricsEndpoints 规范 |
| gateway.metrics.relabelConfigs | 列表 | `[]` | |
| gateway.metrics.relabelings | 列表 | `[]` | |
| gateway.metrics.scrapeTimeout | 字符串 | `""` | |
| gateway.name | 字符串 | `"higress-gateway"` | |
| gateway.networkGateway | 字符串 | `""` | 如果指定,网关将作为给定网络的网络网关。 |
| gateway.nodeSelector | 对象 | `{}` | |
| gateway.podAnnotations."prometheus.io/path" | 字符串 | `"/stats/prometheus"` | |
| gateway.podAnnotations."prometheus.io/port" | 字符串 | `"15020"` | |
| gateway.podAnnotations."prometheus.io/scrape" | 字符串 | `"true"` | |
| gateway.podAnnotations."sidecar.istio.io/inject" | 字符串 | `"false"` | |
| gateway.rbac.enabled | 布尔值 | `true` | 如果启用,将创建角色以启用从网关访问证书。当使用 http://gateway-api.org/ 时不需要。 |
| gateway.readinessFailureThreshold | 整数 | `30` | 指示准备失败前的连续失败探测次数。 |
| gateway.readinessInitialDelaySeconds | 整数 | `1` | 准备探测的初始延迟秒数。 |
| gateway.readinessPeriodSeconds | 整数 | `2` | 准备探测之间的间隔。 |
| gateway.readinessSuccessThreshold | 整数 | `1` | 指示准备成功前的连续成功探测次数。 |
| gateway.readinessTimeoutSeconds | 整数 | `3` | 准备探测的超时秒数 |
| gateway.replicas | 整数 | `2` | Higress Gateway 的 Pod 数量 |
| gateway.resources.limits.cpu | 字符串 | `"2000m"` | |
| gateway.resources.limits.memory | 字符串 | `"2048Mi"` | |
| gateway.resources.requests.cpu | 字符串 | `"2000m"` | |
| gateway.resources.requests.memory | 字符串 | `"2048Mi"` | |
| gateway.revision | 字符串 | `""` | 修订声明此网关属于哪个修订 |
| gateway.rollingMaxSurge | 字符串 | `"100%"` | |
| gateway.rollingMaxUnavailable | 字符串 | `"25%"` | |
| gateway.securityContext | 字符串 | `nil` | 定义 Pod 的安全上下文。如果未设置,将自动设置为绑定到端口 80 和 443 所需的最小权限。在 Kubernetes 1.22+ 上,这只需要 `net.ipv4.ip_unprivileged_port_start` 系统调用。 |
| gateway.service.annotations | 对象 | `{}` | |
| gateway.service.externalTrafficPolicy | 字符串 | `""` | |
| gateway.service.loadBalancerClass | 字符串 | `""` | |
| gateway.service.loadBalancerIP | 字符串 | `""` | |
| gateway.service.loadBalancerSourceRanges | 列表 | `[]` | |
| gateway.service.ports[0].name | 字符串 | `"http2"` | |
| gateway.service.ports[0].port | 整数 | `80` | |
| gateway.service.ports[0].protocol | 字符串 | `"TCP"` | |
| gateway.service.ports[0].targetPort | 整数 | `80` | |
| gateway.service.ports[1].name | 字符串 | `"https"` | |
| gateway.service.ports[1].port | 整数 | `443` | |
| gateway.service.ports[1].protocol | 字符串 | `"TCP"` | |
| gateway.service.ports[1].targetPort | 整数 | `443` | |
| gateway.service.type | 字符串 | `"LoadBalancer"` | 服务类型。设置为 "None" 以完全禁用服务 |
| gateway.serviceAccount.annotations | 对象 | `{}` | 添加到服务账户的注解 |
| gateway.serviceAccount.create | 布尔值 | `true` | 如果设置,将创建服务账户。否则,使用默认值 |
| gateway.serviceAccount.name | 字符串 | `""` | 要使用的服务账户名称。如果未设置,则使用发布名称 |
| gateway.tag | 字符串 | `""` | |
| gateway.tolerations | 列表 | `[]` | |
| gateway.unprivilegedPortSupported | 字符串 | `nil` | |
| global.autoscalingv2API | 布尔值 | `true` | 是否使用 autoscaling/v2 模板进行 HPA 设置,仅供内部使用,用户不应配置。 |
| global.caAddress | 字符串 | `""` | 自定义的 CA 地址,用于为集群中的 Pod 检索证书。CSR 客户端(如 Istio Agent 和 ingress gateways可以使用此地址指定 CA 端点。如果未明确设置,则默认为 Istio 发现地址。 |
| global.caName | 字符串 | `""` | 工作负载证书的 CA 名称。例如,当 caName=GkeWorkloadCertificate 时GKE 工作负载证书将用作工作负载的证书。默认值为 "",当 caName="" 时CA 将通过其他机制(如环境变量 CA_PROVIDER配置。 |
| global.configCluster | 布尔值 | `false` | 将远程集群配置为外部 istiod 的配置集群。 |
| global.defaultPodDisruptionBudget | 对象 | `{"enabled":false}` | 为控制平面启用 Pod 中断预算,用于确保 Istio 控制平面组件逐步升级或恢复。 |
| global.defaultResources | 对象 | `{"requests":{"cpu":"10m"}}` | 应用于所有部署的最小请求资源集,以便 Horizontal Pod Autoscaler 能够正常工作(如果设置)。每个组件可以通过在相关部分添加自己的资源块并设置所需的资源值来覆盖这些默认值。 |
| global.defaultUpstreamConcurrencyThreshold | 整数 | `10000` | |
| global.disableAlpnH2 | 布尔值 | `false` | 是否在 ALPN 中禁用 HTTP/2 |
| global.enableGatewayAPI | 布尔值 | `false` | 如果为 trueHigress Controller 还将监控 Gateway API 资源 |
| global.enableH3 | 布尔值 | `false` | |
| global.enableIPv6 | 布尔值 | `false` | |
| global.enableIstioAPI | 布尔值 | `true` | 如果为 trueHigress Controller 还将监控 istio 资源 |
| global.enableLDSCache | 布尔值 | `true` | |
| global.enableProxyProtocol | 布尔值 | `false` | |
| global.enablePushAllMCPClusters | 布尔值 | `true` | |
| global.enableSRDS | 布尔值 | `true` | |
| global.enableStatus | 布尔值 | `true` | 如果为 trueHigress Controller 将更新 Ingress 资源的状态字段。从 Nginx Ingress 迁移时,为了避免 Ingress 对象的状态字段被覆盖,需要将此参数设置为 false以便 Higress 不会将入口 IP 写入相应 Ingress 对象的状态字段。 |
| global.externalIstiod | 布尔值 | `false` | 配置由外部 istiod 控制的远程集群数据平面。当设置为 true 时,本地不部署 istiod仅启用其他发现 chart 的子集。 |
| global.hostRDSMergeSubset | 布尔值 | `false` | |
| global.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | Istio 镜像的默认仓库。发布版本发布到 docker hub 的 'istio' 项目下。来自 prow 的开发构建位于 gcr.io |
| global.imagePullPolicy | 字符串 | `""` | 如果不需要默认行为,则指定镜像拉取策略。默认行为:最新镜像将始终拉取,否则 IfNotPresent。 |
| global.imagePullSecrets | 列表 | `[]` | 所有 ServiceAccount 的 ImagePullSecrets用于引用此 ServiceAccount 的 Pod 拉取任何镜像的同一命名空间中的秘密列表。对于不使用 ServiceAccount 的组件(即 grafana、servicegraph、tracingImagePullSecrets 将添加到相应的 Deployment(StatefulSet) 对象中。对于配置了私有 docker 注册表的任何集群,必须设置。 |
| global.ingressClass | 字符串 | `"higress"` | IngressClass 过滤 higress controller 监听的 ingress 资源。默认的 ingress class 是 higress。有一些特殊情况用于特殊的 ingress class。1. 当 ingress class 设置为 nginx 时higress controller 将监听带有 nginx ingress class 或没有任何 ingress class 的 ingress 资源。2. 当 ingress class 设置为空时higress controller 将监听 k8s 集群中的所有 ingress 资源。 |
| global.istioNamespace | 字符串 | `"istio-system"` | 用于定位 istiod。 |
| global.istiod | 对象 | `{"enableAnalysis":false}` | 默认在主分支中启用以最大化测试。 |
| global.jwtPolicy | 字符串 | `"third-party-jwt"` | 配置验证 JWT 的策略。目前支持两个选项:"third-party-jwt" 和 "first-party-jwt"。 |
| global.kind | 布尔值 | `false` | |
| global.liteMetrics | 布尔值 | `false` | |
| global.local | 布尔值 | `false` | 当部署到本地集群kind 集群)时,将此设置为 true。 |
| global.logAsJson | 布尔值 | `false` | |
| global.logging | 对象 | `{"level":"default:info"}` | 以逗号分隔的每个范围的最小日志级别,格式为 <scope>:<level>,<scope>:<level> 控制平面根据组件不同有不同的范围,但可以配置所有组件的默认日志级别 如果为空,将使用代码中配置的默认范围和级别 |
| global.meshID | 字符串 | `""` | 如果网格管理员未指定值Istio 将使用网格的信任域的值。最佳实践是选择一个合适的信任域值。 |
| global.meshNetworks | 对象 | `{}` | |
| global.mountMtlsCerts | 布尔值 | `false` | 使用用户指定的、挂载的密钥和证书用于 Pilot 和工作负载。 |
| global.multiCluster.clusterName | 字符串 | `""` | 应设置为此安装运行的集群的名称。这是为了正确标记代理的 sidecar 注入所必需的 |
| global.multiCluster.enabled | 布尔值 | `true` | 设置为 true 以通过各自的 ingressgateway 服务连接两个 kubernetes 集群,当每个集群中的 Pod 无法直接相互通信时。

View File

@@ -1,9 +1,8 @@
module github.com/alibaba/higress/hgctl
go 1.21.0
toolchain go1.22.2
go 1.22.2
toolchain go1.23.7
replace github.com/spf13/viper => github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c

View File

@@ -235,7 +235,7 @@ func (s *Server) initConfigController() error {
options.ClusterId = ""
}
ingressConfig := translation.NewIngressTranslation(s.kubeClient, s.xdsServer, ns, options.ClusterId)
ingressConfig := translation.NewIngressTranslation(s.kubeClient, s.xdsServer, ns, options)
ingressConfig.AddLocalCluster(options)
s.configStores = append(s.configStores, ingressConfig)

View File

@@ -151,9 +151,37 @@ type IngressConfig struct {
clusterId cluster.ID
httpsConfigMgr *cert.ConfigMgr
// templateProcessor processes template variables in config
templateProcessor *TemplateProcessor
// secretConfigMgr manages secret dependencies
secretConfigMgr *SecretConfigMgr
}
func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpdater, namespace string, clusterId cluster.ID) *IngressConfig {
// getSecretValue implements the getValue function for secret references
func (m *IngressConfig) getSecretValue(valueType, namespace, name, key string) (string, error) {
if valueType != "secret" {
return "", fmt.Errorf("unsupported value type: %s", valueType)
}
m.mutex.RLock()
defer m.mutex.RUnlock()
for _, controller := range m.remoteIngressControllers {
secret, err := controller.SecretLister().Secrets(namespace).Get(name)
if err == nil {
if value, exists := secret.Data[key]; exists {
return string(value), nil
}
return "", fmt.Errorf("key %s not found in secret %s/%s", key, namespace, name)
}
}
return "", fmt.Errorf("secret %s/%s not found", namespace, name)
}
func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpdater, namespace string, options common.Options) *IngressConfig {
clusterId := options.ClusterId
if clusterId == "Kubernetes" {
clusterId = ""
}
@@ -170,17 +198,24 @@ func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpda
wasmPlugins: make(map[string]*extensions.WasmPlugin),
http2rpcs: make(map[string]*higressv1.Http2Rpc),
}
mcpbridgeController := mcpbridge.NewController(localKubeClient, clusterId)
// Initialize secret config manager
config.secretConfigMgr = NewSecretConfigMgr(xdsUpdater)
// Initialize template processor with value getter function
config.templateProcessor = NewTemplateProcessor(config.getSecretValue, namespace, config.secretConfigMgr)
mcpbridgeController := mcpbridge.NewController(localKubeClient, options)
mcpbridgeController.AddEventHandler(config.AddOrUpdateMcpBridge, config.DeleteMcpBridge)
config.mcpbridgeController = mcpbridgeController
config.mcpbridgeLister = mcpbridgeController.Lister()
wasmPluginController := wasmplugin.NewController(localKubeClient, clusterId)
wasmPluginController := wasmplugin.NewController(localKubeClient, options)
wasmPluginController.AddEventHandler(config.AddOrUpdateWasmPlugin, config.DeleteWasmPlugin)
config.wasmPluginController = wasmPluginController
config.wasmPluginLister = wasmPluginController.Lister()
http2rpcController := http2rpc.NewController(localKubeClient, clusterId)
http2rpcController := http2rpc.NewController(localKubeClient, options)
http2rpcController.AddEventHandler(config.AddOrUpdateHttp2Rpc, config.DeleteHttp2Rpc)
config.http2rpcController = http2rpcController
config.http2rpcLister = http2rpcController.Lister()
@@ -225,8 +260,9 @@ func (m *IngressConfig) RegisterEventHandler(kind config.GroupVersionKind, f ist
}
func (m *IngressConfig) AddLocalCluster(options common.Options) {
secretController := secret.NewController(m.localKubeClient, options.ClusterId)
secretController := secret.NewController(m.localKubeClient, options)
secretController.AddEventHandler(m.ReflectSecretChanges)
secretController.AddEventHandler(m.secretConfigMgr.HandleSecretChange)
var ingressController common.IngressController
v1 := common.V1Available(m.localKubeClient)
@@ -253,10 +289,24 @@ func (m *IngressConfig) List(typ config.GroupVersionKind, namespace string) []co
var configs = make([]config.Config, 0)
if configsFromIngress := m.listFromIngressControllers(typ, namespace); configsFromIngress != nil {
// Process templates for ingress configs
for i := range configsFromIngress {
if err := m.templateProcessor.ProcessConfig(&configsFromIngress[i]); err != nil {
IngressLog.Errorf("Failed to process template for config %s/%s: %v",
configsFromIngress[i].Namespace, configsFromIngress[i].Name, err)
}
}
configs = append(configs, configsFromIngress...)
}
if configsFromGateway := m.listFromGatewayControllers(typ, namespace); configsFromGateway != nil {
// Process templates for gateway configs
for i := range configsFromGateway {
if err := m.templateProcessor.ProcessConfig(&configsFromGateway[i]); err != nil {
IngressLog.Errorf("Failed to process template for config %s/%s: %v",
configsFromGateway[i].Namespace, configsFromGateway[i].Name, err)
}
}
configs = append(configs, configsFromGateway...)
}
@@ -986,7 +1036,6 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
return nil, nil
}
return result, nil
}
func isBoolValueTrue(b *wrappers.BoolValue) bool {

View File

@@ -127,7 +127,14 @@ func TestConvertGatewaysForIngress(t *testing.T) {
}
ingressV1Beta1Controller := controllerv1beta1.NewController(fake, fake, v1Beta1Options, nil)
ingressV1Controller := controllerv1.NewController(fake, fake, v1Options, nil)
m := NewIngressConfig(fake, nil, "wakanda", "gw-123-istio")
options := common.Options{
Enable: true,
ClusterId: "gw-123-istio",
RawClusterId: "gw-123-istio__",
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
}
m := NewIngressConfig(fake, nil, "wakanda", options)
m.remoteIngressControllers = map[cluster.ID]common.IngressController{
"ingress-v1beta1": ingressV1Beta1Controller,
"ingress-v1": ingressV1Controller,

View File

@@ -0,0 +1,119 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"encoding/json"
"fmt"
"regexp"
"strings"
. "github.com/alibaba/higress/pkg/ingress/log"
"google.golang.org/protobuf/proto"
"istio.io/istio/pkg/config"
)
// TemplateProcessor handles template substitution in configs
type TemplateProcessor struct {
// getValue is a function that retrieves values by type, namespace, name and key
getValue func(valueType, namespace, name, key string) (string, error)
namespace string
secretConfigMgr *SecretConfigMgr
}
// NewTemplateProcessor creates a new TemplateProcessor with the given value getter function
func NewTemplateProcessor(getValue func(valueType, namespace, name, key string) (string, error), namespace string, secretConfigMgr *SecretConfigMgr) *TemplateProcessor {
return &TemplateProcessor{
getValue: getValue,
namespace: namespace,
secretConfigMgr: secretConfigMgr,
}
}
// ProcessConfig processes a config and substitutes any template variables
func (p *TemplateProcessor) ProcessConfig(cfg *config.Config) error {
// Convert spec to JSON string to process substitutions
jsonBytes, err := json.Marshal(cfg.Spec)
if err != nil {
return fmt.Errorf("failed to marshal config spec: %v", err)
}
configStr := string(jsonBytes)
// Find all value references in format:
// ${type.name.key} or ${type.namespace/name.key}
valueRegex := regexp.MustCompile(`\$\{([^.}]+)\.(?:([^/]+)/)?([^.}]+)\.([^}]+)\}`)
matches := valueRegex.FindAllStringSubmatch(configStr, -1)
// If there are no value references, return immediately
if len(matches) == 0 {
if p.secretConfigMgr != nil {
if err := p.secretConfigMgr.DeleteConfig(cfg); err != nil {
IngressLog.Errorf("failed to delete secret dependency: %v", err)
}
}
return nil
}
foundSecretSource := false
IngressLog.Infof("start to apply config %s/%s with %d variables", cfg.Namespace, cfg.Name, len(matches))
for _, match := range matches {
valueType := match[1]
var namespace, name, key string
if match[2] != "" {
// Format: ${type.namespace/name.key}
namespace = match[2]
} else {
// Format: ${type.name.key} - use default namespace
namespace = p.namespace
}
name = match[3]
key = match[4]
// Get value using the provided getter function
value, err := p.getValue(valueType, namespace, name, key)
if err != nil {
return fmt.Errorf("failed to get %s value for %s/%s.%s: %v", valueType, namespace, name, key, err)
}
// Add secret dependency if this is a secret reference
if valueType == "secret" && p.secretConfigMgr != nil {
foundSecretSource = true
secretKey := fmt.Sprintf("%s/%s", namespace, name)
if err := p.secretConfigMgr.AddConfig(secretKey, cfg); err != nil {
IngressLog.Errorf("failed to add secret dependency: %v", err)
}
}
// Replace placeholder with actual value
configStr = strings.Replace(configStr, match[0], value, 1)
}
// Create a new instance of the same type as cfg.Spec
newSpec := proto.Clone(cfg.Spec.(proto.Message))
if err := json.Unmarshal([]byte(configStr), newSpec); err != nil {
return fmt.Errorf("failed to unmarshal substituted config: %v", err)
}
cfg.Spec = newSpec
// Delete secret dependency if no secret reference is found
if !foundSecretSource {
if p.secretConfigMgr != nil {
if err := p.secretConfigMgr.DeleteConfig(cfg); err != nil {
IngressLog.Errorf("failed to delete secret dependency: %v", err)
}
}
}
IngressLog.Infof("end to process config %s/%s", cfg.Namespace, cfg.Name)
return nil
}

View File

@@ -0,0 +1,166 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/structpb"
extensions "istio.io/api/extensions/v1alpha1"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
)
func TestTemplateProcessor_ProcessConfig(t *testing.T) {
// Create test values map
values := map[string]string{
"secret.default/test-secret.api_key": "test-api-key",
"secret.default/test-secret.plugin_conf.timeout": "5000",
"secret.default/test-secret.plugin_conf.max_retries": "3",
"secret.higress-system/auth-secret.auth_config.type": "basic",
"secret.higress-system/auth-secret.auth_config.credentials": "base64-encoded",
}
// Mock value getter function
getValue := func(valueType, namespace, name, key string) (string, error) {
fullKey := fmt.Sprintf("%s.%s/%s.%s", valueType, namespace, name, key)
fmt.Printf("Getting value for %s", fullKey)
if value, exists := values[fullKey]; exists {
return value, nil
}
return "", fmt.Errorf("value not found for %s", fullKey)
}
// Create template processor
processor := NewTemplateProcessor(getValue, "higress-system", nil)
tests := []struct {
name string
wasmPlugin *extensions.WasmPlugin
expected *extensions.WasmPlugin
expectError bool
}{
{
name: "simple api key reference",
wasmPlugin: &extensions.WasmPlugin{
PluginName: "test-plugin",
PluginConfig: makeStructValue(t, map[string]interface{}{
"api_key": "${secret.default/test-secret.api_key}",
}),
},
expected: &extensions.WasmPlugin{
PluginName: "test-plugin",
PluginConfig: makeStructValue(t, map[string]interface{}{
"api_key": "test-api-key",
}),
},
expectError: false,
},
{
name: "config with multiple fields",
wasmPlugin: &extensions.WasmPlugin{
PluginName: "test-plugin",
PluginConfig: makeStructValue(t, map[string]interface{}{
"config": map[string]interface{}{
"timeout": "${secret.default/test-secret.plugin_conf.timeout}",
"max_retries": "${secret.default/test-secret.plugin_conf.max_retries}",
},
}),
},
expected: &extensions.WasmPlugin{
PluginName: "test-plugin",
PluginConfig: makeStructValue(t, map[string]interface{}{
"config": map[string]interface{}{
"timeout": "5000",
"max_retries": "3",
},
}),
},
expectError: false,
},
{
name: "auth config with default namespace",
wasmPlugin: &extensions.WasmPlugin{
PluginName: "test-plugin",
PluginConfig: makeStructValue(t, map[string]interface{}{
"auth": map[string]interface{}{
"type": "${secret.auth-secret.auth_config.type}",
"credentials": "${secret.auth-secret.auth_config.credentials}",
},
}),
},
expected: &extensions.WasmPlugin{
PluginName: "test-plugin",
PluginConfig: makeStructValue(t, map[string]interface{}{
"auth": map[string]interface{}{
"type": "basic",
"credentials": "base64-encoded",
},
}),
},
expectError: false,
},
{
name: "non-existent secret",
wasmPlugin: &extensions.WasmPlugin{
PluginName: "test-plugin",
PluginConfig: makeStructValue(t, map[string]interface{}{
"api_key": "${secret.default/non-existent.api_key}",
}),
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.WasmPlugin,
Name: "test-plugin",
Namespace: "default",
},
Spec: tt.wasmPlugin,
}
err := processor.ProcessConfig(cfg)
if tt.expectError {
assert.Error(t, err)
return
}
assert.NoError(t, err)
processedPlugin := cfg.Spec.(*extensions.WasmPlugin)
// Compare plugin name
assert.Equal(t, tt.expected.PluginName, processedPlugin.PluginName)
// Compare plugin configs
if tt.expected.PluginConfig != nil {
assert.NotNil(t, processedPlugin.PluginConfig)
assert.Equal(t, tt.expected.PluginConfig.AsMap(), processedPlugin.PluginConfig.AsMap())
}
})
}
}
// Helper function to create structpb.Struct from map
func makeStructValue(t *testing.T, m map[string]interface{}) *structpb.Struct {
s, err := structpb.NewStruct(m)
assert.NoError(t, err, "Failed to create struct value")
return s
}

View File

@@ -75,10 +75,11 @@ type KIngressConfig struct {
clusterId cluster.ID
}
func NewKIngressConfig(localKubeClient kube.Client, XDSUpdater istiomodel.XDSUpdater, namespace string, clusterId cluster.ID) *KIngressConfig {
func NewKIngressConfig(localKubeClient kube.Client, XDSUpdater istiomodel.XDSUpdater, namespace string, options common.Options) *KIngressConfig {
if localKubeClient.KIngressInformer() == nil {
return nil
}
clusterId := options.ClusterId
if clusterId == "Kubernetes" {
clusterId = ""
}
@@ -114,7 +115,7 @@ func (m *KIngressConfig) RegisterEventHandler(kind config.GroupVersionKind, f is
}
func (m *KIngressConfig) AddLocalCluster(options common.Options) common.KIngressController {
secretController := secret.NewController(m.localKubeClient, options.ClusterId)
secretController := secret.NewController(m.localKubeClient, options)
secretController.AddEventHandler(m.ReflectSecretChanges)
var ingressController common.KIngressController

View File

@@ -118,7 +118,14 @@ func TestConvertGatewaysForKIngress(t *testing.T) {
RawClusterId: "kingress__",
}
kingressV1Controller := kcontrollerv1.NewController(fake, fake, v1Options, nil)
m := NewKIngressConfig(fake, nil, "wakanda", "gw-123-istio")
options := common.Options{
Enable: true,
ClusterId: "gw-123-istio",
RawClusterId: "gw-123-istio__",
GatewayHttpPort: 80,
GatewayHttpsPort: 443,
}
m := NewKIngressConfig(fake, nil, "wakanda", options)
m.remoteIngressControllers = map[cluster.ID]common.KIngressController{
"kingress": kingressV1Controller,
}

View File

@@ -0,0 +1,157 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"fmt"
"sync"
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
istiomodel "istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/kind"
"istio.io/istio/pkg/util/sets"
)
// toConfigKey converts config.Config to istiomodel.ConfigKey
func toConfigKey(cfg *config.Config) (istiomodel.ConfigKey, error) {
return istiomodel.ConfigKey{
Kind: kind.MustFromGVK(cfg.GroupVersionKind),
Name: cfg.Name,
Namespace: cfg.Namespace,
}, nil
}
// SecretConfigMgr maintains the mapping between secrets and configs
type SecretConfigMgr struct {
mutex sync.RWMutex
// configSet tracks all configs that have been added
// key format: namespace/name
configSet sets.Set[string]
// secretToConfigs maps secret key to dependent configs
// key format: namespace/name
secretToConfigs map[string]sets.Set[istiomodel.ConfigKey]
// watchedSecrets tracks which secrets are being watched
watchedSecrets sets.Set[string]
// xdsUpdater is used to push config updates
xdsUpdater istiomodel.XDSUpdater
}
// NewSecretConfigMgr creates a new SecretConfigMgr
func NewSecretConfigMgr(xdsUpdater istiomodel.XDSUpdater) *SecretConfigMgr {
return &SecretConfigMgr{
secretToConfigs: make(map[string]sets.Set[istiomodel.ConfigKey]),
watchedSecrets: sets.New[string](),
configSet: sets.New[string](),
xdsUpdater: xdsUpdater,
}
}
// AddConfig adds a config and its secret dependencies
func (m *SecretConfigMgr) AddConfig(secretKey string, cfg *config.Config) error {
configKey, _ := toConfigKey(cfg)
m.mutex.Lock()
defer m.mutex.Unlock()
configId := fmt.Sprintf("%s/%s", cfg.Namespace, cfg.Name)
m.configSet.Insert(configId)
if configs, exists := m.secretToConfigs[secretKey]; exists {
configs.Insert(configKey)
} else {
m.secretToConfigs[secretKey] = sets.New(configKey)
}
// Add to watched secrets
m.watchedSecrets.Insert(secretKey)
return nil
}
// DeleteConfig removes a config from all secret dependencies
func (m *SecretConfigMgr) DeleteConfig(cfg *config.Config) error {
configKey, _ := toConfigKey(cfg)
m.mutex.Lock()
defer m.mutex.Unlock()
configId := fmt.Sprintf("%s/%s", cfg.Namespace, cfg.Name)
if !m.configSet.Contains(configId) {
return nil
}
removeKeys := make([]string, 0)
// Find and remove the config from all secrets
for secretKey, configs := range m.secretToConfigs {
if configs.Contains(configKey) {
configs.Delete(configKey)
// If no more configs depend on this secret, remove it
if configs.Len() == 0 {
removeKeys = append(removeKeys, secretKey)
}
}
}
// Remove the secrets from the secretToConfigs map
for _, secretKey := range removeKeys {
delete(m.secretToConfigs, secretKey)
m.watchedSecrets.Delete(secretKey)
}
// Remove the config from the config set
m.configSet.Delete(configId)
return nil
}
// GetConfigsForSecret returns all configs that depend on the given secret
func (m *SecretConfigMgr) GetConfigsForSecret(secretKey string) []istiomodel.ConfigKey {
m.mutex.RLock()
defer m.mutex.RUnlock()
if configs, exists := m.secretToConfigs[secretKey]; exists {
return configs.UnsortedList()
}
return nil
}
// IsSecretWatched checks if a secret is being watched
func (m *SecretConfigMgr) IsSecretWatched(secretKey string) bool {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.watchedSecrets.Contains(secretKey)
}
// HandleSecretChange handles secret changes and updates affected configs
func (m *SecretConfigMgr) HandleSecretChange(name util.ClusterNamespacedName) {
secretKey := fmt.Sprintf("%s/%s", name.Namespace, name.Name)
// Check if this secret is being watched
if !m.IsSecretWatched(secretKey) {
return
}
// Get affected configs
configKeys := m.GetConfigsForSecret(secretKey)
if len(configKeys) == 0 {
return
}
IngressLog.Infof("SecretConfigMgr Secret %s changed, updating %d dependent configs and push", secretKey, len(configKeys))
m.xdsUpdater.ConfigUpdate(&istiomodel.PushRequest{
Full: true,
Reason: istiomodel.NewReasonStats(istiomodel.SecretTrigger),
})
}

View File

@@ -0,0 +1,155 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"k8s.io/apimachinery/pkg/types"
"testing"
"github.com/alibaba/higress/pkg/ingress/kube/util"
"github.com/stretchr/testify/assert"
istiomodel "istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/cluster"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
"istio.io/istio/pkg/config/schema/kind"
)
type mockXdsUpdater struct {
lastPushRequest *istiomodel.PushRequest
}
func (m *mockXdsUpdater) EDSUpdate(shard istiomodel.ShardKey, hostname string, namespace string, entry []*istiomodel.IstioEndpoint) {
//TODO implement me
panic("implement me")
}
func (m *mockXdsUpdater) EDSCacheUpdate(shard istiomodel.ShardKey, hostname string, namespace string, entry []*istiomodel.IstioEndpoint) {
//TODO implement me
panic("implement me")
}
func (m *mockXdsUpdater) SvcUpdate(shard istiomodel.ShardKey, hostname string, namespace string, event istiomodel.Event) {
//TODO implement me
panic("implement me")
}
func (m *mockXdsUpdater) ProxyUpdate(clusterID cluster.ID, ip string) {
//TODO implement me
panic("implement me")
}
func (m *mockXdsUpdater) RemoveShard(shardKey istiomodel.ShardKey) {
//TODO implement me
panic("implement me")
}
func (m *mockXdsUpdater) ConfigUpdate(req *istiomodel.PushRequest) {
m.lastPushRequest = req
}
func TestSecretConfigMgr(t *testing.T) {
updater := &mockXdsUpdater{}
mgr := NewSecretConfigMgr(updater)
// Test AddConfig
t.Run("AddConfig", func(t *testing.T) {
wasmPlugin := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.WasmPlugin,
Name: "test-plugin",
Namespace: "default",
},
}
err := mgr.AddConfig("default/test-secret", wasmPlugin)
assert.NoError(t, err)
assert.True(t, mgr.IsSecretWatched("default/test-secret"))
configs := mgr.GetConfigsForSecret("default/test-secret")
assert.Len(t, configs, 1)
assert.Equal(t, kind.WasmPlugin, configs[0].Kind)
assert.Equal(t, "test-plugin", configs[0].Name)
assert.Equal(t, "default", configs[0].Namespace)
})
// Test DeleteConfig
t.Run("DeleteConfig", func(t *testing.T) {
wasmPlugin := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.WasmPlugin,
Name: "test-plugin",
Namespace: "default",
},
}
err := mgr.DeleteConfig(wasmPlugin)
assert.NoError(t, err)
assert.False(t, mgr.IsSecretWatched("default/test-secret"))
assert.Empty(t, mgr.GetConfigsForSecret("default/test-secret"))
})
// Test HandleSecretChange
t.Run("HandleSecretChange", func(t *testing.T) {
// Add a config first
wasmPlugin := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.WasmPlugin,
Name: "test-plugin",
Namespace: "default",
},
}
err := mgr.AddConfig("default/test-secret", wasmPlugin)
assert.NoError(t, err)
// Test secret change
secretName := util.ClusterNamespacedName{
NamespacedName: types.NamespacedName{
Name: "test-secret",
Namespace: "default",
},
}
mgr.HandleSecretChange(secretName)
assert.NotNil(t, updater.lastPushRequest)
assert.True(t, updater.lastPushRequest.Full)
})
// Test full push for secret update
t.Run("FullPushForSecretUpdate", func(t *testing.T) {
// Add a secret config
secretConfig := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.Secret,
Name: "test-secret",
Namespace: "default",
},
}
err := mgr.AddConfig("default/test-secret", secretConfig)
assert.NoError(t, err)
// Update the secret
secretName := util.ClusterNamespacedName{
NamespacedName: types.NamespacedName{
Name: "test-secret",
Namespace: "default",
},
}
mgr.HandleSecretChange(secretName)
assert.NotNil(t, updater.lastPushRequest)
assert.True(t, updater.lastPushRequest.Full)
})
}

View File

@@ -15,12 +15,6 @@
package annotations
import (
"errors"
"sort"
"strings"
corev1 "k8s.io/api/core/v1"
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
)
@@ -57,101 +51,10 @@ func (a auth) Parse(annotations Annotations, config *Ingress, globalContext *Glo
if !needAuthConfig(annotations) {
return nil
}
authConfig := &AuthConfig{
AuthType: defaultAuthType,
}
// Check auth type
authType, err := annotations.ParseStringASAP(authType)
if err != nil {
IngressLog.Errorf("Parse auth type error %v within ingress %/%s", err, config.Namespace, config.Name)
return nil
}
if authType != defaultAuthType {
IngressLog.Errorf("Auth type %s within ingress %/%s is not supported yet.", authType, config.Namespace, config.Name)
return nil
}
secretName, _ := annotations.ParseStringASAP(authSecretAnn)
namespaced := util.SplitNamespacedName(secretName)
if namespaced.Name == "" {
IngressLog.Errorf("Auth secret name within ingress %s/%s is invalid", config.Namespace, config.Name)
return nil
}
if namespaced.Namespace == "" {
namespaced.Namespace = config.Namespace
}
configKey := util.ClusterNamespacedName{
NamespacedName: namespaced,
ClusterId: config.ClusterId,
}
authConfig.AuthSecret = configKey
// Subscribe secret
globalContext.WatchedSecrets.Insert(configKey.String())
secretType := authFileAuthSecretType
if rawSecretType, err := annotations.ParseStringASAP(authSecretTypeAnn); err == nil {
resultAuthSecretType := authSecretType(rawSecretType)
if resultAuthSecretType == authFileAuthSecretType || resultAuthSecretType == authMapAuthSecretType {
secretType = resultAuthSecretType
}
}
authConfig.AuthRealm, _ = annotations.ParseStringASAP(authRealm)
// Process credentials.
secretLister, exist := globalContext.ClusterSecretLister[config.ClusterId]
if !exist {
IngressLog.Errorf("secret lister of cluster %s doesn't exist", config.ClusterId)
return nil
}
authSecret, err := secretLister.Secrets(namespaced.Namespace).Get(namespaced.Name)
if err != nil {
IngressLog.Errorf("Secret %s within ingress %s/%s is not found",
namespaced.String(), config.Namespace, config.Name)
return nil
}
credentials, err := convertCredentials(secretType, authSecret)
if err != nil {
IngressLog.Errorf("Parse auth secret fail, err %v", err)
return nil
}
authConfig.Credentials = credentials
config.Auth = authConfig
IngressLog.Error("The annotation nginx.ingress.kubernetes.io/auth-type is no longer supported after version 2.0.0, please use the higress wasm plugin (e.g., basic-auth) as an alternative.")
return nil
}
func convertCredentials(secretType authSecretType, secret *corev1.Secret) ([]string, error) {
var result []string
switch secretType {
case authFileAuthSecretType:
users, exist := secret.Data[authFileKey]
if !exist {
return nil, errors.New("the auth file type must has auth key in secret data")
}
userList := strings.Split(string(users), "\n")
for _, item := range userList {
if !strings.Contains(item, ":") {
continue
}
result = append(result, item)
}
case authMapAuthSecretType:
for name, password := range secret.Data {
result = append(result, name+":"+string(password))
}
}
sort.SliceStable(result, func(i, j int) bool {
return result[i] < result[j]
})
return result, nil
}
func needAuthConfig(annotations Annotations) bool {
return annotations.HasASAP(authType) &&
annotations.HasASAP(authSecretAnn)

View File

@@ -1,197 +0,0 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package annotations
import (
"context"
"reflect"
"testing"
"time"
"istio.io/istio/pkg/cluster"
"istio.io/istio/pkg/util/sets"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
listerv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"github.com/alibaba/higress/pkg/ingress/kube/util"
)
func TestAuthParse(t *testing.T) {
auth := auth{}
inputCases := []struct {
input map[string]string
secret *v1.Secret
expect *AuthConfig
watchedSecret string
}{
{
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
Namespace: "foo",
},
Data: map[string][]byte{
"auth": []byte("A:a\nB:b"),
},
},
},
{
input: map[string]string{
buildNginxAnnotationKey(authType): "digest",
},
expect: nil,
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
Namespace: "foo",
},
Data: map[string][]byte{
"auth": []byte("A:a\nB:b"),
},
},
},
{
input: map[string]string{
buildNginxAnnotationKey(authType): defaultAuthType,
buildHigressAnnotationKey(authSecretAnn): "foo/bar",
},
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
Namespace: "foo",
},
Data: map[string][]byte{
"auth": []byte("A:a\nB:b"),
},
},
expect: &AuthConfig{
AuthType: defaultAuthType,
AuthSecret: util.ClusterNamespacedName{
NamespacedName: types.NamespacedName{
Namespace: "foo",
Name: "bar",
},
ClusterId: "cluster",
},
Credentials: []string{"A:a", "B:b"},
},
watchedSecret: "cluster/foo/bar",
},
{
input: map[string]string{
buildNginxAnnotationKey(authType): defaultAuthType,
buildHigressAnnotationKey(authSecretAnn): "foo/bar",
buildNginxAnnotationKey(authSecretTypeAnn): string(authMapAuthSecretType),
},
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
Namespace: "foo",
},
Data: map[string][]byte{
"A": []byte("a"),
"B": []byte("b"),
},
},
expect: &AuthConfig{
AuthType: defaultAuthType,
AuthSecret: util.ClusterNamespacedName{
NamespacedName: types.NamespacedName{
Namespace: "foo",
Name: "bar",
},
ClusterId: "cluster",
},
Credentials: []string{"A:a", "B:b"},
},
watchedSecret: "cluster/foo/bar",
},
{
input: map[string]string{
buildNginxAnnotationKey(authType): defaultAuthType,
buildHigressAnnotationKey(authSecretAnn): "bar",
buildNginxAnnotationKey(authSecretTypeAnn): string(authFileAuthSecretType),
},
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
Namespace: "default",
},
Data: map[string][]byte{
"auth": []byte("A:a\nB:b"),
},
},
expect: &AuthConfig{
AuthType: defaultAuthType,
AuthSecret: util.ClusterNamespacedName{
NamespacedName: types.NamespacedName{
Namespace: "default",
Name: "bar",
},
ClusterId: "cluster",
},
Credentials: []string{"A:a", "B:b"},
},
watchedSecret: "cluster/default/bar",
},
}
for _, inputCase := range inputCases {
t.Run("", func(t *testing.T) {
config := &Ingress{
Meta: Meta{
Namespace: "default",
ClusterId: "cluster",
},
}
globalContext, cancel := initGlobalContext(inputCase.secret)
defer cancel()
_ = auth.Parse(inputCase.input, config, globalContext)
if !reflect.DeepEqual(inputCase.expect, config.Auth) {
t.Fatal("Should be equal")
}
if inputCase.watchedSecret != "" {
if !globalContext.WatchedSecrets.Contains(inputCase.watchedSecret) {
t.Fatalf("Should watch secret %s", inputCase.watchedSecret)
}
}
})
}
}
func initGlobalContext(secret *v1.Secret) (*GlobalContext, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
client := fake.NewSimpleClientset(secret)
informerFactory := informers.NewSharedInformerFactory(client, time.Hour)
secretInformer := informerFactory.Core().V1().Secrets()
go secretInformer.Informer().Run(ctx.Done())
cache.WaitForCacheSync(ctx.Done(), secretInformer.Informer().HasSynced)
return &GlobalContext{
WatchedSecrets: sets.New[string](),
ClusterSecretLister: map[cluster.ID]listerv1.SecretLister{
"cluster": secretInformer.Lister(),
},
}, cancel
}

View File

@@ -40,6 +40,7 @@ type HigressConfig struct {
Upstream *Upstream `json:"upstream,omitempty"`
DisableXEnvoyHeaders bool `json:"disableXEnvoyHeaders,omitempty"`
AddXRealIpHeader bool `json:"addXRealIpHeader,omitempty"`
McpServer *McpServer `json:"mcpServer,omitempty"`
}
func NewDefaultHigressConfig() *HigressConfig {
@@ -51,6 +52,7 @@ func NewDefaultHigressConfig() *HigressConfig {
Upstream: globalOption.Upstream,
DisableXEnvoyHeaders: globalOption.DisableXEnvoyHeaders,
AddXRealIpHeader: globalOption.AddXRealIpHeader,
McpServer: NewDefaultMcpServer(),
}
return higressConfig
}

View File

@@ -89,6 +89,9 @@ func NewConfigmapMgr(XDSUpdater model.XDSUpdater, namespace string, higressConfi
globalOptionController := NewGlobalOptionController(namespace)
configmapMgr.AddItemControllers(globalOptionController)
mcpServerController := NewMcpServerController(namespace)
configmapMgr.AddItemControllers(mcpServerController)
configmapMgr.initEventHandlers()
return configmapMgr

View File

@@ -0,0 +1,391 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package configmap
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"sync/atomic"
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
)
// RedisConfig defines the configuration for Redis connection
type RedisConfig struct {
// The address of Redis server in the format of "host:port"
Address string `json:"address,omitempty"`
// The username for Redis authentication
Username string `json:"username,omitempty"`
// The password for Redis authentication
Password string `json:"password,omitempty"`
// The database index to use
DB int `json:"db,omitempty"`
}
// SSEServer defines the configuration for Server-Sent Events (SSE) server
type SSEServer struct {
// The name of the SSE server
Name string `json:"name,omitempty"`
// The path where the SSE server will be mounted, the full path is (PATH + SsePathSuffix)
Path string `json:"path,omitempty"`
// The type of the SSE server
Type string `json:"type,omitempty"`
// Additional Config parameters for the real MCP server implementation
Config map[string]interface{} `json:"config,omitempty"`
}
// MatchRule defines a rule for matching requests
type MatchRule struct {
// Domain pattern, supports wildcards
MatchRuleDomain string `json:"match_rule_domain,omitempty"`
// Path pattern to match
MatchRulePath string `json:"match_rule_path,omitempty"`
// Type of match rule: exact, prefix, suffix, contains, regex
MatchRuleType string `json:"match_rule_type,omitempty"`
}
// McpServer defines the configuration for MCP (Model Context Protocol) server
type McpServer struct {
// Flag to control whether MCP server is enabled
Enable bool `json:"enable,omitempty"`
// Redis Config for MCP server
Redis *RedisConfig `json:"redis,omitempty"`
// The suffix to be appended to SSE paths, default is "/sse"
SsePathSuffix string `json:"sse_path_suffix,omitempty"`
// List of SSE servers Configs
Servers []*SSEServer `json:"servers,omitempty"`
// List of match rules for filtering requests
MatchList []*MatchRule `json:"match_list,omitempty"`
}
func NewDefaultMcpServer() *McpServer {
return &McpServer{
Enable: false,
Servers: make([]*SSEServer, 0),
MatchList: make([]*MatchRule, 0),
}
}
const (
higressMcpServerEnvoyFilterName = "higress-config-mcp-server"
)
func validMcpServer(m *McpServer) error {
if m == nil {
return nil
}
if m.Enable && m.Redis == nil {
return errors.New("redis config cannot be empty when mcp server is enabled")
}
// Validate match rule types
if m.MatchList != nil {
validTypes := map[string]bool{
"exact": true,
"prefix": true,
"suffix": true,
"contains": true,
"regex": true,
}
for _, rule := range m.MatchList {
if rule.MatchRuleType == "" {
return errors.New("match_rule_type cannot be empty, must be one of: exact, prefix, suffix, contains, regex")
}
if !validTypes[rule.MatchRuleType] {
return fmt.Errorf("invalid match_rule_type: %s, must be one of: exact, prefix, suffix, contains, regex", rule.MatchRuleType)
}
}
}
return nil
}
func compareMcpServer(old *McpServer, new *McpServer) (Result, error) {
if old == nil && new == nil {
return ResultNothing, nil
}
if new == nil {
return ResultDelete, nil
}
if !reflect.DeepEqual(old, new) {
return ResultReplace, nil
}
return ResultNothing, nil
}
func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
newMcp := NewDefaultMcpServer()
newMcp.Enable = mcp.Enable
if mcp.Redis != nil {
newMcp.Redis = &RedisConfig{
Address: mcp.Redis.Address,
Username: mcp.Redis.Username,
Password: mcp.Redis.Password,
DB: mcp.Redis.DB,
}
}
newMcp.SsePathSuffix = mcp.SsePathSuffix
if len(mcp.Servers) > 0 {
newMcp.Servers = make([]*SSEServer, len(mcp.Servers))
for i, server := range mcp.Servers {
newServer := &SSEServer{
Name: server.Name,
Path: server.Path,
Type: server.Type,
}
if server.Config != nil {
newServer.Config = make(map[string]interface{})
for k, v := range server.Config {
newServer.Config[k] = v
}
}
newMcp.Servers[i] = newServer
}
}
if len(mcp.MatchList) > 0 {
newMcp.MatchList = make([]*MatchRule, len(mcp.MatchList))
for i, rule := range mcp.MatchList {
newMcp.MatchList[i] = &MatchRule{
MatchRuleDomain: rule.MatchRuleDomain,
MatchRulePath: rule.MatchRulePath,
MatchRuleType: rule.MatchRuleType,
}
}
}
return newMcp, nil
}
type McpServerController struct {
Namespace string
mcpServer atomic.Value
Name string
eventHandler ItemEventHandler
}
func NewMcpServerController(namespace string) *McpServerController {
mcpController := &McpServerController{
Namespace: namespace,
mcpServer: atomic.Value{},
Name: "mcpServer",
}
mcpController.SetMcpServer(NewDefaultMcpServer())
return mcpController
}
func (m *McpServerController) GetName() string {
return m.Name
}
func (m *McpServerController) SetMcpServer(mcp *McpServer) {
m.mcpServer.Store(mcp)
}
func (m *McpServerController) GetMcpServer() *McpServer {
value := m.mcpServer.Load()
if value != nil {
if mcp, ok := value.(*McpServer); ok {
return mcp
}
}
return nil
}
func (m *McpServerController) AddOrUpdateHigressConfig(name util.ClusterNamespacedName, old *HigressConfig, new *HigressConfig) error {
if err := validMcpServer(new.McpServer); err != nil {
IngressLog.Errorf("data:%+v convert to mcp server, error: %+v", new.McpServer, err)
return nil
}
result, _ := compareMcpServer(old.McpServer, new.McpServer)
switch result {
case ResultReplace:
if newMcp, err := deepCopyMcpServer(new.McpServer); err != nil {
IngressLog.Infof("mcp server deepcopy error:%v", err)
} else {
m.SetMcpServer(newMcp)
IngressLog.Infof("AddOrUpdate Higress config mcp server")
m.eventHandler(higressMcpServerEnvoyFilterName)
IngressLog.Infof("send event with filter name:%s", higressMcpServerEnvoyFilterName)
}
case ResultDelete:
m.SetMcpServer(NewDefaultMcpServer())
IngressLog.Infof("Delete Higress config mcp server")
m.eventHandler(higressMcpServerEnvoyFilterName)
IngressLog.Infof("send event with filter name:%s", higressMcpServerEnvoyFilterName)
}
return nil
}
func (m *McpServerController) ValidHigressConfig(higressConfig *HigressConfig) error {
if higressConfig == nil {
return nil
}
if higressConfig.McpServer == nil {
return nil
}
return validMcpServer(higressConfig.McpServer)
}
func (m *McpServerController) RegisterItemEventHandler(eventHandler ItemEventHandler) {
m.eventHandler = eventHandler
}
func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error) {
configs := make([]*config.Config, 0)
mcpServer := m.GetMcpServer()
namespace := m.Namespace
if mcpServer == nil || !mcpServer.Enable {
return configs, nil
}
mcpStruct := m.constructMcpServerStruct(mcpServer)
if mcpStruct == "" {
return configs, nil
}
config := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.EnvoyFilter,
Name: higressMcpServerEnvoyFilterName,
Namespace: namespace,
},
Spec: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
Context: networking.EnvoyFilter_GATEWAY,
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
Name: "envoy.filters.network.http_connection_manager",
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
Name: "envoy.filters.http.cors",
},
},
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_INSERT_AFTER,
Value: util.BuildPatchStruct(mcpStruct),
},
},
},
},
}
configs = append(configs, config)
return configs, nil
}
func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
// Build servers configuration
servers := "[]"
if len(mcp.Servers) > 0 {
serverConfigs := make([]string, len(mcp.Servers))
for i, server := range mcp.Servers {
serverConfig := fmt.Sprintf(`{
"name": "%s",
"path": "%s",
"type": "%s"`,
server.Name, server.Path, server.Type)
if len(server.Config) > 0 {
config, _ := json.Marshal(server.Config)
serverConfig += fmt.Sprintf(`,
"config": %s`, string(config))
}
serverConfig += "}"
serverConfigs[i] = serverConfig
}
servers = fmt.Sprintf("[%s]", strings.Join(serverConfigs, ","))
}
// Build match_list configuration
matchList := "[]"
if len(mcp.MatchList) > 0 {
matchConfigs := make([]string, len(mcp.MatchList))
for i, rule := range mcp.MatchList {
matchConfigs[i] = fmt.Sprintf(`{
"match_rule_domain": "%s",
"match_rule_path": "%s",
"match_rule_type": "%s"
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType)
}
matchList = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
}
// Build complete configuration structure
structFmt := `{
"name": "envoy.filters.http.golang",
"typed_config": {
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
"value": {
"library_id": "mcp-server",
"library_path": "/var/lib/istio/envoy/mcp-server.so",
"plugin_name": "mcp-server",
"plugin_config": {
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
"value": {
"redis": {
"address": "%s",
"username": "%s",
"password": "%s",
"db": %d
},
"sse_path_suffix": "%s",
"match_list": %s,
"servers": %s
}
}
}
}
}`
return fmt.Sprintf(structFmt,
mcp.Redis.Address,
mcp.Redis.Username,
mcp.Redis.Password,
mcp.Redis.DB,
mcp.SsePathSuffix,
matchList,
servers)
}

View File

@@ -0,0 +1,411 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package configmap
import (
"errors"
"testing"
"github.com/alibaba/higress/pkg/ingress/kube/util"
"github.com/stretchr/testify/assert"
)
func Test_validMcpServer(t *testing.T) {
tests := []struct {
name string
mcp *McpServer
wantErr error
}{
{
name: "default",
mcp: &McpServer{
Enable: false,
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
wantErr: nil,
},
{
name: "nil",
mcp: nil,
wantErr: nil,
},
{
name: "enabled but no redis config",
mcp: &McpServer{
Enable: true,
Redis: nil,
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
wantErr: errors.New("redis config cannot be empty when mcp server is enabled"),
},
{
name: "valid config with redis",
mcp: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
Username: "default",
Password: "password",
DB: 0,
},
SsePathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "*",
MatchRuleType: "exact",
},
},
Servers: []*SSEServer{
{
Name: "test-server",
Path: "/test",
Type: "test",
Config: map[string]interface{}{
"key": "value",
},
},
},
},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validMcpServer(tt.mcp)
assert.Equal(t, tt.wantErr, err)
})
}
}
func Test_compareMcpServer(t *testing.T) {
tests := []struct {
name string
old *McpServer
new *McpServer
wantResult Result
wantErr error
}{
{
name: "compare both nil",
old: nil,
new: nil,
wantResult: ResultNothing,
wantErr: nil,
},
{
name: "compare result delete",
old: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
},
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
new: nil,
wantResult: ResultDelete,
wantErr: nil,
},
{
name: "compare result equal",
old: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
},
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
new: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
},
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
wantResult: ResultNothing,
wantErr: nil,
},
{
name: "compare result replace",
old: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
},
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
new: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "redis:6379",
},
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "/test",
MatchRuleType: "exact",
},
},
Servers: []*SSEServer{},
},
wantResult: ResultReplace,
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := compareMcpServer(tt.old, tt.new)
assert.Equal(t, tt.wantResult, result)
assert.Equal(t, tt.wantErr, err)
})
}
}
func Test_deepCopyMcpServer(t *testing.T) {
tests := []struct {
name string
mcp *McpServer
wantMcp *McpServer
wantErr error
}{
{
name: "deep copy with redis only",
mcp: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
Username: "default",
Password: "password",
DB: 0,
},
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
wantMcp: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
Username: "default",
Password: "password",
DB: 0,
},
MatchList: []*MatchRule{},
Servers: []*SSEServer{},
},
wantErr: nil,
},
{
name: "deep copy with full config",
mcp: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
Username: "default",
Password: "password",
DB: 0,
},
SsePathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "*",
MatchRuleType: "exact",
},
},
Servers: []*SSEServer{
{
Name: "test-server",
Path: "/test",
Type: "test",
Config: map[string]interface{}{
"key": "value",
},
},
},
},
wantMcp: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
Username: "default",
Password: "password",
DB: 0,
},
SsePathSuffix: "/sse",
MatchList: []*MatchRule{
{
MatchRuleDomain: "*",
MatchRulePath: "*",
MatchRuleType: "exact",
},
},
Servers: []*SSEServer{
{
Name: "test-server",
Path: "/test",
Type: "test",
Config: map[string]interface{}{
"key": "value",
},
},
},
},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mcp, err := deepCopyMcpServer(tt.mcp)
assert.Equal(t, tt.wantMcp, mcp)
assert.Equal(t, tt.wantErr, err)
})
}
}
func TestMcpServerController_AddOrUpdateHigressConfig(t *testing.T) {
eventPush := "default"
defaultHandler := func(name string) {
eventPush = "push"
}
defaultName := util.ClusterNamespacedName{}
tests := []struct {
name string
old *HigressConfig
new *HigressConfig
wantErr error
wantEventPush string
wantMcp *McpServer
}{
{
name: "default",
old: &HigressConfig{
McpServer: NewDefaultMcpServer(),
},
new: &HigressConfig{
McpServer: NewDefaultMcpServer(),
},
wantErr: nil,
wantEventPush: "default",
wantMcp: NewDefaultMcpServer(),
},
{
name: "replace and push - enable mcp server",
old: &HigressConfig{
McpServer: NewDefaultMcpServer(),
},
new: &HigressConfig{
McpServer: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
Username: "default",
Password: "password",
DB: 0,
},
Servers: []*SSEServer{},
MatchList: []*MatchRule{},
},
},
wantErr: nil,
wantEventPush: "push",
wantMcp: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
Username: "default",
Password: "password",
DB: 0,
},
Servers: []*SSEServer{},
MatchList: []*MatchRule{},
},
},
{
name: "replace and push - update config",
old: &HigressConfig{
McpServer: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
},
Servers: []*SSEServer{},
MatchList: []*MatchRule{},
},
},
new: &HigressConfig{
McpServer: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "redis:6379",
},
Servers: []*SSEServer{},
MatchList: []*MatchRule{},
},
},
wantErr: nil,
wantEventPush: "push",
wantMcp: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "redis:6379",
},
Servers: []*SSEServer{},
MatchList: []*MatchRule{},
},
},
{
name: "delete and push",
old: &HigressConfig{
McpServer: &McpServer{
Enable: true,
Redis: &RedisConfig{
Address: "localhost:6379",
},
Servers: []*SSEServer{},
MatchList: []*MatchRule{},
},
},
new: &HigressConfig{
McpServer: nil,
},
wantErr: nil,
wantEventPush: "push",
wantMcp: NewDefaultMcpServer(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMcpServerController("higress-system")
m.eventHandler = defaultHandler
eventPush = "default"
err := m.AddOrUpdateHigressConfig(defaultName, tt.old, tt.new)
assert.Equal(t, tt.wantEventPush, eventPush)
assert.Equal(t, tt.wantErr, err)
assert.Equal(t, tt.wantMcp, m.GetMcpServer())
})
}
}

View File

@@ -15,21 +15,33 @@
package http2rpc
import (
"istio.io/istio/pkg/cluster"
"time"
"istio.io/istio/pkg/kube/controllers"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/cache"
v1 "github.com/alibaba/higress/client/pkg/apis/networking/v1"
"github.com/alibaba/higress/client/pkg/clientset/versioned"
informersv1 "github.com/alibaba/higress/client/pkg/informers/externalversions/networking/v1"
listersv1 "github.com/alibaba/higress/client/pkg/listers/networking/v1"
"github.com/alibaba/higress/pkg/ingress/kube/common"
"github.com/alibaba/higress/pkg/ingress/kube/controller"
kubeclient "github.com/alibaba/higress/pkg/kube"
)
type Http2RpcController controller.Controller[listersv1.Http2RpcLister]
func NewController(client kubeclient.Client, clusterId cluster.ID) Http2RpcController {
informer := client.HigressInformer().Networking().V1().Http2Rpcs().Informer()
return controller.NewCommonController("http2rpc", client.HigressInformer().Networking().V1().Http2Rpcs().Lister(),
informer, GetHttp2Rpc, clusterId)
func NewController(client kubeclient.Client, options common.Options) Http2RpcController {
var informer cache.SharedIndexInformer
if options.WatchNamespace == "" {
informer = client.HigressInformer().Networking().V1().Http2Rpcs().Informer()
} else {
informer = client.HigressInformer().InformerFor(&v1.Http2Rpc{}, func(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return informersv1.NewHttp2RpcInformer(client, options.WatchNamespace, resyncPeriod, nil)
})
}
return controller.NewCommonController("http2rpc", listersv1.NewHttp2RpcLister(informer.GetIndexer()), informer, GetHttp2Rpc, options.ClusterId)
}
func GetHttp2Rpc(lister listersv1.Http2RpcLister, namespacedName types.NamespacedName) (controllers.Object, error) {

View File

@@ -100,7 +100,7 @@ type controller struct {
// NewController creates a new Kubernetes controller
func NewController(localKubeClient, client kubeclient.Client, options common.Options,
secretController secret.SecretController) common.IngressController {
opts := ktypes.InformerOptions{}
opts := ktypes.InformerOptions{Namespace: options.WatchNamespace}
ingressInformer := util.GetInformerFiltered(client, opts, gvrIngressV1Beta1, &ingress.Ingress{},
func(options metav1.ListOptions) (runtime.Object, error) {
return client.Kube().NetworkingV1beta1().Ingresses(opts.Namespace).List(context.Background(), options)

View File

@@ -54,7 +54,7 @@ func TestIngressControllerApplies(t *testing.T) {
options := common.Options{IngressClass: "mse", ClusterId: ""}
secretController := secret.NewController(localKubeClient, options.ClusterId)
secretController := secret.NewController(localKubeClient, options)
ingressController := NewController(localKubeClient, client, options, secretController)
testcases := map[string]func(*testing.T, common.IngressController){
@@ -253,7 +253,7 @@ func TestIngressControllerConventions(t *testing.T) {
options := common.Options{IngressClass: "mse", ClusterId: "", EnableStatus: true}
secretController := secret.NewController(localKubeClient, options.ClusterId)
secretController := secret.NewController(localKubeClient, options)
ingressController := NewController(localKubeClient, client, options, secretController)
testcases := map[string]func(*testing.T, common.IngressController){
@@ -1142,7 +1142,7 @@ func TestIngressControllerProcessing(t *testing.T) {
options := common.Options{IngressClass: "mse", ClusterId: "", EnableStatus: true}
secretController := secret.NewController(localKubeClient, options.ClusterId)
secretController := secret.NewController(localKubeClient, options)
opts := ktypes.InformerOptions{}
ingressInformer := util.GetInformerFiltered(fakeClient, opts, gvrIngressV1Beta1, &ingress.Ingress{},

View File

@@ -92,7 +92,7 @@ type controller struct {
// NewController creates a new Kubernetes controller
func NewController(localKubeClient, client kubeclient.Client, options common.Options, secretController secret.SecretController) common.IngressController {
opts := ktypes.InformerOptions{}
opts := ktypes.InformerOptions{Namespace: options.WatchNamespace}
ingressInformer := schemakubeclient.GetInformerFilteredFromGVR(client, opts, gvr.Ingress)
ingressLister := networkinglister.NewIngressLister(ingressInformer.Informer.GetIndexer())
serviceInformer := schemakubeclient.GetInformerFilteredFromGVR(client, opts, gvr.Service)

View File

@@ -21,6 +21,7 @@ import (
"sort"
"strings"
"sync"
"time"
"github.com/hashicorp/go-multierror"
networking "istio.io/api/networking/v1alpha3"
@@ -43,7 +44,9 @@ import (
listerv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
ingress "knative.dev/networking/pkg/apis/networking/v1alpha1"
networkingv1alpha1 "knative.dev/networking/pkg/client/listers/networking/v1alpha1"
"knative.dev/networking/pkg/client/clientset/versioned"
informernetworkingv1alpha1 "knative.dev/networking/pkg/client/informers/externalversions/networking/v1alpha1"
listernetworkingv1alpha1 "knative.dev/networking/pkg/client/listers/networking/v1alpha1"
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
"github.com/alibaba/higress/pkg/ingress/kube/common"
@@ -76,7 +79,7 @@ type controller struct {
ingresses map[string]*ingress.Ingress
ingressInformer cache.SharedInformer
ingressLister networkingv1alpha1.IngressLister
ingressLister listernetworkingv1alpha1.IngressLister
serviceInformer informerfactory.StartableInformer
serviceLister listerv1.ServiceLister
secretController secret.SecretController
@@ -86,16 +89,23 @@ type controller struct {
// NewController creates a new Kubernetes controller
func NewController(localKubeClient, client kube.Client, options common.Options,
secretController secret.SecretController) common.KIngressController {
//var namespace string = "default"
ingressInformer := client.KIngressInformer().Networking().V1alpha1().Ingresses()
serviceInformer := schemakubeclient.GetInformerFilteredFromGVR(client, ktypes.InformerOptions{}, gvr.Service)
var ingressInformer cache.SharedIndexInformer
if options.WatchNamespace == "" {
ingressInformer = client.KIngressInformer().Networking().V1alpha1().Ingresses().Informer()
} else {
ingressInformer = client.KIngressInformer().InformerFor(&ingress.Ingress{}, func(c versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return informernetworkingv1alpha1.NewIngressInformer(c, options.WatchNamespace, resyncPeriod, nil)
})
}
ingressLister := listernetworkingv1alpha1.NewIngressLister(ingressInformer.GetIndexer())
serviceInformer := schemakubeclient.GetInformerFilteredFromGVR(client, ktypes.InformerOptions{Namespace: options.WatchNamespace}, gvr.Service)
serviceLister := listerv1.NewServiceLister(serviceInformer.Informer.GetIndexer())
c := &controller{
options: options,
ingresses: make(map[string]*ingress.Ingress),
ingressInformer: ingressInformer.Informer(),
ingressLister: ingressInformer.Lister(),
ingressInformer: ingressInformer,
ingressLister: ingressLister,
serviceInformer: serviceInformer,
serviceLister: serviceLister,
secretController: secretController,

View File

@@ -154,7 +154,7 @@ func TestKIngressControllerConventions(t *testing.T) {
options := common.Options{IngressClass: "mse", ClusterId: "", EnableStatus: true}
secretController := secret.NewController(localKubeClient, options.ClusterId)
secretController := secret.NewController(localKubeClient, options)
ingressController := NewController(localKubeClient, client, options, secretController)
testcases := map[string]func(*testing.T, common.KIngressController){

View File

@@ -15,21 +15,33 @@
package mcpbridge
import (
"istio.io/istio/pkg/cluster"
"time"
"istio.io/istio/pkg/kube/controllers"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/cache"
v1 "github.com/alibaba/higress/client/pkg/apis/networking/v1"
"github.com/alibaba/higress/client/pkg/clientset/versioned"
informersv1 "github.com/alibaba/higress/client/pkg/informers/externalversions/networking/v1"
listersv1 "github.com/alibaba/higress/client/pkg/listers/networking/v1"
"github.com/alibaba/higress/pkg/ingress/kube/common"
"github.com/alibaba/higress/pkg/ingress/kube/controller"
kubeclient "github.com/alibaba/higress/pkg/kube"
)
type McpBridgeController controller.Controller[listersv1.McpBridgeLister]
func NewController(client kubeclient.Client, clusterId cluster.ID) McpBridgeController {
informer := client.HigressInformer().Networking().V1().McpBridges().Informer()
return controller.NewCommonController("mcpbridge", client.HigressInformer().Networking().V1().McpBridges().Lister(),
informer, GetMcpBridge, clusterId)
func NewController(client kubeclient.Client, options common.Options) McpBridgeController {
var informer cache.SharedIndexInformer
if options.WatchNamespace == "" {
informer = client.HigressInformer().Networking().V1().McpBridges().Informer()
} else {
informer = client.HigressInformer().InformerFor(&v1.McpBridge{}, func(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return informersv1.NewMcpBridgeInformer(client, options.WatchNamespace, resyncPeriod, nil)
})
}
return controller.NewCommonController("mcpbridge", listersv1.NewMcpBridgeLister(informer.GetIndexer()), informer, GetMcpBridge, options.ClusterId)
}
func GetMcpBridge(lister listersv1.McpBridgeLister, namespacedName types.NamespacedName) (controllers.Object, error) {

View File

@@ -15,15 +15,14 @@
package secret
import (
"github.com/alibaba/higress/pkg/ingress/kube/common"
"github.com/alibaba/higress/pkg/ingress/kube/controller"
"istio.io/istio/pkg/cluster"
"istio.io/istio/pkg/config/schema/gvr"
schemakubeclient "istio.io/istio/pkg/config/schema/kubeclient"
kubeclient "istio.io/istio/pkg/kube"
"istio.io/istio/pkg/kube/controllers"
ktypes "istio.io/istio/pkg/kube/kubetypes"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/types"
listersv1 "k8s.io/client-go/listers/core/v1"
@@ -31,17 +30,17 @@ import (
type SecretController controller.Controller[listersv1.SecretLister]
func NewController(client kubeclient.Client, clusterId cluster.ID) SecretController {
func NewController(client kubeclient.Client, options common.Options) SecretController {
opts := ktypes.InformerOptions{
Namespace: metav1.NamespaceAll,
Cluster: clusterId,
Namespace: options.WatchNamespace,
Cluster: options.ClusterId,
FieldSelector: fields.AndSelectors(
fields.OneTermNotEqualSelector("type", "helm.sh/release.v1"),
fields.OneTermNotEqualSelector("type", string(v1.SecretTypeServiceAccountToken)),
).String(),
}
informer := schemakubeclient.GetInformerFilteredFromGVR(client, opts, gvr.Secret)
return controller.NewCommonController("secret", listersv1.NewSecretLister(informer.Informer.GetIndexer()), informer.Informer, GetSecret, clusterId)
return controller.NewCommonController("secret", listersv1.NewSecretLister(informer.Informer.GetIndexer()), informer.Informer, GetSecret, options.ClusterId)
}
func GetSecret(lister listersv1.SecretLister, namespacedName types.NamespacedName) (controllers.Object, error) {

View File

@@ -16,6 +16,7 @@ package secret
import (
"context"
"github.com/alibaba/higress/pkg/ingress/kube/common"
"reflect"
"sync"
"testing"
@@ -43,7 +44,7 @@ var period = time.Second
func TestController(t *testing.T) {
client := kubeclient.NewFakeClient()
ctrl := NewController(client, "fake-cluster")
ctrl := NewController(client, common.Options{ClusterId: "fake-cluster"})
stop := make(chan struct{})
t.Cleanup(func() {

View File

@@ -15,21 +15,33 @@
package wasmplugin
import (
"istio.io/istio/pkg/cluster"
"time"
"istio.io/istio/pkg/kube/controllers"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/cache"
v1 "github.com/alibaba/higress/client/pkg/apis/extensions/v1alpha1"
"github.com/alibaba/higress/client/pkg/clientset/versioned"
informersv1 "github.com/alibaba/higress/client/pkg/informers/externalversions/extensions/v1alpha1"
listersv1 "github.com/alibaba/higress/client/pkg/listers/extensions/v1alpha1"
"github.com/alibaba/higress/pkg/ingress/kube/common"
"github.com/alibaba/higress/pkg/ingress/kube/controller"
kubeclient "github.com/alibaba/higress/pkg/kube"
)
type WasmPluginController controller.Controller[listersv1.WasmPluginLister]
func NewController(client kubeclient.Client, clusterId cluster.ID) WasmPluginController {
informer := client.HigressInformer().Extensions().V1alpha1().WasmPlugins().Informer()
return controller.NewCommonController("wasmplugin", client.HigressInformer().Extensions().V1alpha1().WasmPlugins().Lister(),
informer, GetWasmPlugin, clusterId)
func NewController(client kubeclient.Client, options common.Options) WasmPluginController {
var informer cache.SharedIndexInformer
if options.WatchNamespace == "" {
informer = client.HigressInformer().Extensions().V1alpha1().WasmPlugins().Informer()
} else {
informer = client.HigressInformer().InformerFor(&v1.WasmPlugin{}, func(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return informersv1.NewWasmPluginInformer(client, options.WatchNamespace, resyncPeriod, nil)
})
}
return controller.NewCommonController("wasmplugin", listersv1.NewWasmPluginLister(informer.GetIndexer()), informer, GetWasmPlugin, options.ClusterId)
}
func GetWasmPlugin(lister listersv1.WasmPluginLister, namespacedName types.NamespacedName) (controllers.Object, error) {

View File

@@ -19,7 +19,6 @@ import (
"istio.io/istio/pilot/pkg/model"
istiomodel "istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/cluster"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/collection"
"istio.io/istio/pkg/config/schema/gvk"
@@ -45,13 +44,13 @@ type IngressTranslation struct {
higressDomainCache model.IngressDomainCollection
}
func NewIngressTranslation(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpdater, namespace string, clusterId cluster.ID) *IngressTranslation {
if clusterId == "Kubernetes" {
clusterId = ""
func NewIngressTranslation(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpdater, namespace string, options common.Options) *IngressTranslation {
if options.ClusterId == "Kubernetes" {
options.ClusterId = ""
}
Config := &IngressTranslation{
ingressConfig: ingressconfig.NewIngressConfig(localKubeClient, xdsUpdater, namespace, clusterId),
kingressConfig: ingressconfig.NewKIngressConfig(localKubeClient, xdsUpdater, namespace, clusterId),
ingressConfig: ingressconfig.NewIngressConfig(localKubeClient, xdsUpdater, namespace, options),
kingressConfig: ingressconfig.NewKIngressConfig(localKubeClient, xdsUpdater, namespace, options),
}
return Config
}

View File

@@ -0,0 +1,39 @@
FROM golang:1.23-bullseye AS golang-base
ARG GOPROXY
ARG GO_FILTER_NAME
ARG GOARCH
ENV GOFLAGS=-buildvcs=false
ENV GOPROXY=${GOPROXY}
ENV GOARCH=${GOARCH}
ENV CGO_ENABLED=1
# 根据目标架构安装对应的编译工具
RUN if [ "$GOARCH" = "arm64" ]; then \
echo "Installing ARM64 toolchain" && \
apt-get update && \
apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu; \
else \
echo "Installing AMD64 toolchain" && \
apt-get update && \
apt-get install -y gcc binutils; \
fi
WORKDIR /workspace
COPY . .
WORKDIR /workspace/$GO_FILTER_NAME
RUN go mod tidy
RUN if [ "$GOARCH" = "arm64" ]; then \
CC=aarch64-linux-gnu-gcc AS=aarch64-linux-gnu-as go build -o /$GO_FILTER_NAME.so -buildmode=c-shared .; \
else \
go build -o /$GO_FILTER_NAME.so -buildmode=c-shared .; \
fi
FROM scratch AS output
ARG GO_FILTER_NAME
ARG GOARCH
COPY --from=golang-base /${GO_FILTER_NAME}.so ${GO_FILTER_NAME}_${GOARCH}.so

View File

@@ -0,0 +1,12 @@
GO_FILTER_NAME ?= mcp-server
GOPROXY := $(shell go env GOPROXY)
GOARCH ?= amd64
.DEFAULT:
build:
DOCKER_BUILDKIT=1 docker build --build-arg GOPROXY=$(GOPROXY) \
--build-arg GO_FILTER_NAME=${GO_FILTER_NAME} \
--build-arg GOARCH=${GOARCH} \
-t ${GO_FILTER_NAME} \
--output ./${GO_FILTER_NAME} \
.

View File

@@ -0,0 +1,47 @@
# Golang HTTP Filter
[English](./README_en.md) | 简体中文
## 简介
Golang HTTP Filter 允许开发者使用 Go 语言编写自定义的 Envoy Filter。该框架支持在请求和响应流程中执行 Golang 代码,使 Envoy 的扩展开发变得更加简单。最重要的是,使用此框架开发的 Go 插件可以独立于 Envoy 进行编译,这大大提高了开发和部署的灵活性。
> **注意** Golang Filter 需要 Higress 2.1.0 或更高版本才能使用。
## 特性
- 支持在HTTP请求和响应流程中执行 Go 代码
- 支持插件独立编译,无需重新编译 Envoy
- 提供简洁的 API 接口
- 支持请求/响应头部修改
- 支持请求/响应体修改
- 支持同步请求
## 快速开始
请参考 [Envoy Golang HTTP Filter 示例](https://github.com/envoyproxy/examples/tree/main/golang-http) 了解如何开发和运行一个基本的 Golang Filter。
## 配置示例
```yaml
http_filters:
- name: envoy.filters.http.golang
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
library_id: my-go-filter
library_path: "./my-go-filter.so"
plugin_name: my-go-filter
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
your_config_here: value
```
## 快速构建
使用以下命令可以快速构建 golang filter 插件:
```bash
GO_FILTER_NAME=mcp-server make build
```

View File

@@ -0,0 +1,45 @@
# Golang HTTP Filter
English | [简体中文](./README.md)
## Introduction
The Golang HTTP Filter allows developers to write custom Envoy Filters using the Go language. This framework supports executing Golang code during both request and response flows, making it easier to extend Envoy. Most importantly, Go plugins developed using this framework can be compiled independently of Envoy, which greatly enhances development and deployment flexibility.
> **注意** Golang Filter require Higress version 2.1.0 or higher to be used.
## Features
- Support for Golang code execution in both request and response flows
- Independent plugin compilation without rebuilding Envoy
- Simple and clean API interface
- Request/response header modification
- Request/response body modification
- Synchronous request support
## Quick Start
Please refer to [Envoy Golang HTTP Filter Example](https://github.com/envoyproxy/examples/tree/main/golang-http) to learn how to develop and run a basic Golang Filter.
## Configuration Example
```yaml
http_filters:
- name: envoy.filters.http.golang
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
library_id: my-go-filter
library_path: "./my-go-filter.so"
plugin_name: my-go-filter
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
your_config_here: value
```
## Quick Build
Use the following command to quickly build the golang filter plugin:
```bash
GO_FILTER_NAME=mcp-server make build
```

View File

@@ -0,0 +1,65 @@
# MCP Server
[English](./README_en.md) | 简体中文
## 概述
MCP Server 是一个基于 Envoy 的 Golang Filter 插件用于实现服务器端事件SSE和消息通信功能。该插件支持多种数据库类型并使用 Redis 作为消息队列来实现负载均衡的请求通过对应的SSE连接发送。
> **注意**MCP Server需要 Higress 2.1.0 或更高版本才能使用。
## 项目结构
```
mcp-server/
├── config.go # 配置解析相关代码
├── filter.go # 请求处理相关代码
├── internal/ # 内部实现逻辑
├── servers/ # MCP 服务器实现
├── go.mod # Go模块依赖定义
└── go.sum # Go模块依赖校验
```
## MCP Server开发指南
```go
// 在init函数中注册你的服务器
// 参数1: 服务器名称
// 参数2: 配置结构体实例
func init() {
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
}
// 服务器配置结构体
type DemoConfig struct {
helloworld string
}
// 解析配置方法
// 从配置map中解析并验证配置项
func (c *DBConfig) ParseConfig(config map[string]any) error {
helloworld, ok := config["helloworld"].(string)
if !ok { return errors.New("missing helloworld")}
c.helloworld = helloworld
return nil
}
// 创建新的MCP服务器实例
// serverName: 服务器名称
// 返回值: MCP服务器实例和可能的错误
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
mcpServer := internal.NewMCPServer(serverName, Version)
// 添加工具方法到服务器
// mcpServer.AddTool()
// 添加资源到服务器
// mcpServer.AddResource()
return mcpServer, nil
}
```
**Note**:
需要在config.go里面使用下划线导入以执行包的init函数
```go
import (
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
)
```

View File

@@ -0,0 +1,67 @@
# MCP Server
English | [简体中文](./README.md)
## Overview
MCP Server is a Golang Filter plugin based on Envoy, designed to implement Server-Sent Events (SSE) and message communication functionality. This plugin supports various database types and uses Redis as a message queue to enable load-balanced requests to be sent through corresponding SSE connections.
> **Note**: MCP Server requires Higress 2.1.0 or higher version.
## Project Structure
```
mcp-server/
├── config.go # Configuration parsing code
├── filter.go # Request processing code
├── internal/ # Internal implementation logic
├── servers/ # MCP server implementation
├── go.mod # Go module dependency definition
└── go.sum # Go module dependency checksum
```
## MCP Server Development Guide
```go
// Register your server in the init function
// Param 1: Server name
// Param 2: Config struct instance
func init() {
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
}
// Server configuration struct
type DemoConfig struct {
helloworld string
}
// Configuration parsing method
// Parse and validate configuration items from the config map
func (c *DBConfig) ParseConfig(config map[string]any) error {
helloworld, ok := config["helloworld"].(string)
if !ok { return errors.New("missing helloworld")}
c.helloworld = helloworld
return nil
}
// Create a new MCP server instance
// serverName: Server name
// Returns: MCP server instance and possible error
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
mcpServer := internal.NewMCPServer(serverName, Version)
// Add tool methods to server
// mcpServer.AddTool()
// Add resources to server
// mcpServer.AddResource()
return mcpServer, nil
}
```
**Note**:
Need to use underscore import in config.go to execute the package's init function
```go
import (
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
)
```

View File

@@ -0,0 +1,181 @@
package main
import (
"fmt"
xds "github.com/cncf/xds/go/xds/type/v3"
"google.golang.org/protobuf/types/known/anypb"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry/nacos"
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
envoyHttp "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http"
)
const Name = "mcp-server"
const Version = "1.0.0"
const DefaultServerName = "defaultServer"
func init() {
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(Name, filterFactory, &parser{})
}
type config struct {
ssePathSuffix string
redisClient *internal.RedisClient
servers []*internal.SSEServer
defaultServer *internal.SSEServer
matchList []internal.MatchRule
}
func (c *config) Destroy() {
if c.redisClient != nil {
api.LogDebug("Closing Redis client")
c.redisClient.Close()
}
}
type parser struct {
}
// Parse the filter configuration
func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
configStruct := &xds.TypedStruct{}
if err := any.UnmarshalTo(configStruct); err != nil {
return nil, err
}
v := configStruct.Value
conf := &config{
matchList: make([]internal.MatchRule, 0),
servers: make([]*internal.SSEServer, 0),
}
// Parse match_list if exists
if matchList, ok := v.AsMap()["match_list"].([]interface{}); ok {
for _, item := range matchList {
if ruleMap, ok := item.(map[string]interface{}); ok {
rule := internal.MatchRule{}
if domain, ok := ruleMap["match_rule_domain"].(string); ok {
rule.MatchRuleDomain = domain
}
if path, ok := ruleMap["match_rule_path"].(string); ok {
rule.MatchRulePath = path
}
if ruleType, ok := ruleMap["match_rule_type"].(string); ok {
rule.MatchRuleType = internal.RuleType(ruleType)
}
conf.matchList = append(conf.matchList, rule)
}
}
}
redisConfigMap, ok := v.AsMap()["redis"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("redis config is not set")
}
redisConfig, err := internal.ParseRedisConfig(redisConfigMap)
if err != nil {
return nil, fmt.Errorf("failed to parse redis config: %w", err)
}
redisClient, err := internal.NewRedisClient(redisConfig)
if err != nil {
return nil, fmt.Errorf("failed to initialize RedisClient: %w", err)
}
conf.redisClient = redisClient
ssePathSuffix, ok := v.AsMap()["sse_path_suffix"].(string)
if !ok || ssePathSuffix == "" {
return nil, fmt.Errorf("sse path suffix is not set or empty")
}
conf.ssePathSuffix = ssePathSuffix
serverConfigs, ok := v.AsMap()["servers"].([]interface{})
if !ok {
api.LogDebug("No servers are configured")
return conf, nil
}
for _, serverConfig := range serverConfigs {
serverConfigMap, ok := serverConfig.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("server config must be an object")
}
serverType, ok := serverConfigMap["type"].(string)
if !ok {
return nil, fmt.Errorf("server type is not set")
}
serverPath, ok := serverConfigMap["path"].(string)
if !ok {
return nil, fmt.Errorf("server %s path is not set", serverType)
}
serverName, ok := serverConfigMap["name"].(string)
if !ok {
return nil, fmt.Errorf("server %s name is not set", serverType)
}
server := internal.GlobalRegistry.GetServer(serverType)
if server == nil {
return nil, fmt.Errorf("server %s is not registered", serverType)
}
serverConfig, ok := serverConfigMap["config"].(map[string]interface{})
if !ok {
api.LogDebug(fmt.Sprintf("No config provided for server %s", serverType))
}
api.LogDebug(fmt.Sprintf("Server config: %+v", serverConfig))
err = server.ParseConfig(serverConfig)
if err != nil {
return nil, fmt.Errorf("failed to parse server config: %w", err)
}
serverInstance, err := server.NewServer(serverName)
if err != nil {
return nil, fmt.Errorf("failed to initialize DBServer: %w", err)
}
conf.servers = append(conf.servers, internal.NewSSEServer(serverInstance,
internal.WithRedisClient(redisClient),
internal.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, ssePathSuffix)),
internal.WithMessageEndpoint(serverPath)))
api.LogDebug(fmt.Sprintf("Registered MCP Server: %s", serverType))
}
return conf, nil
}
func (p *parser) Merge(parent interface{}, child interface{}) interface{} {
parentConfig := parent.(*config)
childConfig := child.(*config)
newConfig := *parentConfig
if childConfig.redisClient != nil {
newConfig.redisClient = childConfig.redisClient
}
if childConfig.ssePathSuffix != "" {
newConfig.ssePathSuffix = childConfig.ssePathSuffix
}
if childConfig.servers != nil {
newConfig.servers = append(newConfig.servers, childConfig.servers...)
}
if childConfig.defaultServer != nil {
newConfig.defaultServer = childConfig.defaultServer
}
return &newConfig
}
func filterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.StreamFilter {
conf, ok := c.(*config)
if !ok {
panic("unexpected config type")
}
return &filter{
callbacks: callbacks,
config: conf,
stopChan: make(chan struct{}),
}
}
func main() {}

View File

@@ -0,0 +1,215 @@
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
)
// The callbacks in the filter, like `DecodeHeaders`, can be implemented on demand.
// Because api.PassThroughStreamFilter provides a default implementation.
type filter struct {
api.PassThroughStreamFilter
callbacks api.FilterCallbackHandler
path string
config *config
stopChan chan struct{}
req *http.Request
serverName string
message bool
proxyURL *url.URL
skip bool
}
type RequestURL struct {
method string
scheme string
host string
path string
baseURL string
parsedURL *url.URL
}
func NewRequestURL(header api.RequestHeaderMap) *RequestURL {
method, _ := header.Get(":method")
scheme, _ := header.Get(":scheme")
host, _ := header.Get(":authority")
path, _ := header.Get(":path")
baseURL := fmt.Sprintf("%s://%s", scheme, host)
parsedURL, _ := url.Parse(path)
api.LogDebugf("RequestURL: method=%s, scheme=%s, host=%s, path=%s", method, scheme, host, path)
return &RequestURL{method: method, scheme: scheme, host: host, path: path, baseURL: baseURL, parsedURL: parsedURL}
}
// Callbacks which are called in request path
// The endStream is true if the request doesn't have body
func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
url := NewRequestURL(header)
f.path = url.parsedURL.Path
// Check if request matches any rule in match_list
if !internal.IsMatch(f.config.matchList, url.host, f.path) {
f.skip = true
api.LogDebugf("Request does not match any rule in match_list: %s", url.parsedURL.String())
return api.Continue
}
for _, server := range f.config.servers {
if f.path == server.GetSSEEndpoint() {
if url.method != http.MethodGet {
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
} else {
f.serverName = server.GetServerName()
body := "SSE connection create"
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
}
api.LogDebugf("%s SSE connection started", server.GetServerName())
server.SetBaseURL(url.baseURL)
return api.LocalReply
} else if f.path == server.GetMessageEndpoint() {
if url.method != http.MethodPost {
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
}
// Create a new http.Request object
f.req = &http.Request{
Method: url.method,
URL: url.parsedURL,
Header: make(http.Header),
}
api.LogDebugf("Message request: %v", url.parsedURL)
// Copy headers from api.RequestHeaderMap to http.Header
header.Range(func(key, value string) bool {
f.req.Header.Add(key, value)
return true
})
f.message = true
if endStream {
return api.Continue
} else {
return api.StopAndBuffer
}
}
}
if !strings.HasSuffix(url.parsedURL.Path, f.config.ssePathSuffix) {
f.proxyURL = url.parsedURL
return api.Continue
}
if url.method != http.MethodGet {
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
} else {
f.config.defaultServer = internal.NewSSEServer(internal.NewMCPServer(DefaultServerName, Version),
internal.WithSSEEndpoint(f.config.ssePathSuffix),
internal.WithMessageEndpoint(strings.TrimSuffix(url.parsedURL.Path, f.config.ssePathSuffix)),
internal.WithRedisClient(f.config.redisClient))
f.serverName = f.config.defaultServer.GetServerName()
body := "SSE connection create"
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
f.config.defaultServer.SetBaseURL(url.baseURL)
}
return api.LocalReply
}
// DecodeData might be called multiple times during handling the request body.
// The endStream is true when handling the last piece of the body.
func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
if f.skip {
return api.Continue
}
if f.message {
if endStream {
for _, server := range f.config.servers {
if f.path == server.GetMessageEndpoint() {
// Create a response recorder to capture the response
recorder := httptest.NewRecorder()
// Call the handleMessage method of SSEServer with complete body
server.HandleMessage(recorder, f.req, buffer.Bytes())
f.message = false
f.callbacks.DecoderFilterCallbacks().SendLocalReply(recorder.Code, recorder.Body.String(), recorder.Header(), 0, "")
return api.LocalReply
}
}
}
return api.StopAndBuffer
}
return api.Continue
}
// Callbacks which are called in response path
// The endStream is true if the response doesn't have body
func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api.StatusType {
if f.skip {
return api.Continue
}
if f.serverName != "" {
header.Set("Content-Type", "text/event-stream")
header.Set("Cache-Control", "no-cache")
header.Set("Connection", "keep-alive")
header.Set("Access-Control-Allow-Origin", "*")
header.Del("Content-Length")
return api.Continue
}
return api.Continue
}
// EncodeData might be called multiple times during handling the response body.
// The endStream is true when handling the last piece of the body.
func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
if f.skip {
return api.Continue
}
if !endStream {
return api.StopAndBuffer
}
if f.proxyURL != nil {
sessionID := f.proxyURL.Query().Get("sessionId")
if sessionID != "" {
channel := internal.GetSSEChannelName(sessionID)
eventData := fmt.Sprintf("event: message\ndata: %s\n\n", buffer.String())
publishErr := f.config.redisClient.Publish(channel, eventData)
if publishErr != nil {
api.LogErrorf("Failed to publish wasm mcp server message to Redis: %v", publishErr)
}
}
}
if f.serverName != "" {
// handle specific server
for _, server := range f.config.servers {
if f.serverName == server.GetServerName() {
buffer.Reset()
server.HandleSSE(f.callbacks, f.stopChan)
return api.Running
}
}
// handle default server
if f.serverName == f.config.defaultServer.GetServerName() {
buffer.Reset()
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
return api.Running
}
return api.Continue
}
return api.Continue
}
// OnDestroy stops the goroutine
func (f *filter) OnDestroy(reason api.DestroyReason) {
api.LogDebugf("OnDestroy: reason=%v", reason)
if f.serverName != "" && f.stopChan != nil {
select {
case <-f.stopChan:
return
default:
api.LogDebug("Stopping SSE connection")
close(f.stopChan)
}
}
}

View File

@@ -0,0 +1,104 @@
module github.com/alibaba/higress/plugins/golang-filter/mcp-server
go 1.23
require (
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99
github.com/go-redis/redis/v8 v8.11.5
github.com/google/uuid v1.6.0
github.com/mark3labs/mcp-go v0.12.0
google.golang.org/protobuf v1.36.5
gorm.io/driver/clickhouse v0.6.1
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
)
require (
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/darabonba-array v0.1.0 // indirect
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 // indirect
github.com/alibabacloud-go/darabonba-map v0.0.2 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 // indirect
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 // indirect
github.com/alibabacloud-go/darabonba-string v1.0.2 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 // indirect
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/tea v1.2.2 // indirect
github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 // indirect
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 // indirect
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 // indirect
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 // indirect
github.com/aliyun/credentials-go v1.4.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/clbanning/mxj/v2 v2.5.5 // indirect
github.com/deckarep/golang-set v1.7.1 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/grpc v1.59.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
)
require (
cel.dev/expr v0.15.0 // indirect
github.com/ClickHouse/ch-go v0.61.5 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.23.2 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/nacos-group/nacos-sdk-go/v2 v2.2.9
github.com/paulmach/orb v0.11.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
go.opentelemetry.io/otel v1.26.0 // indirect
go.opentelemetry.io/otel/trace v1.26.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/nacos-group/nacos-sdk-go/v2 v2.2.9 => github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-30

View File

@@ -0,0 +1,794 @@
cel.dev/expr v0.15.0 h1:O1jzfJCQBfL5BFoYktaxwIhuttaQPsVWerH9/EEKx0w=
cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4=
github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg=
github.com/ClickHouse/clickhouse-go/v2 v2.23.2 h1:+DAKPMnxLS7pduQZsrJc8OhdLS2L9MfDEJ2TS+hpYDM=
github.com/ClickHouse/clickhouse-go/v2 v2.23.2/go.mod h1:aNap51J1OM3yxQJRgM+AlP/MPkGBCL8A74uQThoQhR0=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 h1:vamGcYQFwXVqR6RWcrVTTqlIXZVsYjaA7pZbx+Xw6zw=
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3/go.mod h1:3rIyughsFDLie1ut9gQJXkWkMg/NfXBCk+OtXnPu3lw=
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA=
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o=
github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 h1:ie/8RxBOfKZWcrbYSJi2Z8uX8TcOlSMwPlEJh83OeOw=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 h1:nJYyoFP+aqGKgPs9JeZgS1rWQ4NndNR0Zfhh161ZltU=
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1/go.mod h1:WzGOmFFTlUzXM03CJnHWMQ85UN6QGpOXZocCjwkiyOg=
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 h1:QeUdR7JF7iNCvO/81EhxEr3wDwxk4YBoYZOq6E0AjHI=
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8/go.mod h1:xP0KIZry6i7oGPF24vhAPr1Q8vLZRcMcxtft5xDKwCU=
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 h1:8S0mtD101RDYa0LXwdoqgN0RxdMmmJYjq8g2mk7/lQ4=
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5/go.mod h1:M19fxYz3gpm0ETnoKweYyYtqrtnVtrpKFpwsghbw+cQ=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/aliyun/credentials-go v1.4.3 h1:N3iHyvHRMyOwY1+0qBLSf3hb5JFiOujVSVuEpgeGttY=
github.com/aliyun/credentials-go v1.4.3/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/envoy v1.32.3 h1:eftH199KwYfyBTtm4reeEzsWTqraACEaTQ6efl31v0I=
github.com/envoyproxy/envoy v1.32.3/go.mod h1:KGS+IUehDX1mSIdqodPTWskKOo7bZMLLy3GHxvOKcJk=
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99 h1:jih/Ieb7BFgVCStgvY5fXQ3mI9ByOt4wfwUF0d7qmqI=
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99/go.mod h1:x7d0dNbE0xGuDBUkBg19VGCgnPQ+lJ2k8lDzDzKExow=
github.com/envoyproxy/envoy v1.33.2 h1:k3ChySbVo4HejvbDRxkgRroUnj6TZZpXPJJ0UGaZkXs=
github.com/envoyproxy/envoy v1.33.2/go.mod h1:faFqv1XeNGX/ph6Zto5Culdcpk4Klxp730Q6XhWarV4=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mark3labs/mcp-go v0.12.0 h1:Pue1Tdwqcz77GHq18uzgmLT3wmeDUxXUSAqSwhGLhVo=
github.com/mark3labs/mcp-go v0.12.0/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nacos-group/nacos-sdk-go/v2 v2.2.9 h1:etzCMnB9EBeSKfaDIOe8zH4HO/8fycpc6s0AmXCrmAw=
github.com/nacos-group/nacos-sdk-go/v2 v2.2.9/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.24.2 h1:J/tulyYK6JwBldPViHJReihxxZ+22FHs0piGjQAvoUE=
github.com/onsi/gomega v1.24.2/go.mod h1:gs3J10IS7Z7r7eXRoNJIrNqU4ToQukCJhFtKrWgHWnk=
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc h1:Ak86L+yDSOzKFa7WM5bf5itSOo1e3Xh8bm5YCMUXIjQ=
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q=
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/clickhouse v0.6.1 h1:t7JMB6sLBXxN8hEO6RdzCbJCwq/jAEVZdwXlmQs1Sd4=
gorm.io/driver/clickhouse v0.6.1/go.mod h1:riMYpJcGZ3sJ/OAZZ1rEP1j/Y0H6cByOAnwz7fo2AyM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -0,0 +1,89 @@
package internal
import (
"regexp"
"strings"
)
// RuleType defines the type of matching rule
type RuleType string
const (
ExactMatch RuleType = "exact"
PrefixMatch RuleType = "prefix"
SuffixMatch RuleType = "suffix"
ContainsMatch RuleType = "contains"
RegexMatch RuleType = "regex"
)
// MatchRule defines the structure for a matching rule
type MatchRule struct {
MatchRuleDomain string `json:"match_rule_domain"` // Domain pattern, supports wildcards
MatchRulePath string `json:"match_rule_path"` // Path pattern to match
MatchRuleType RuleType `json:"match_rule_type"` // Type of match rule
}
// convertWildcardToRegex converts wildcard pattern to regex pattern
func convertWildcardToRegex(pattern string) string {
pattern = regexp.QuoteMeta(pattern)
pattern = "^" + strings.ReplaceAll(pattern, "\\*", ".*") + "$"
return pattern
}
// matchPattern checks if the target matches the pattern based on rule type
func matchPattern(pattern string, target string, ruleType RuleType) bool {
if pattern == "" {
return true
}
switch ruleType {
case ExactMatch:
return pattern == target
case PrefixMatch:
return strings.HasPrefix(target, pattern)
case SuffixMatch:
return strings.HasSuffix(target, pattern)
case ContainsMatch:
return strings.Contains(target, pattern)
case RegexMatch:
matched, err := regexp.MatchString(pattern, target)
if err != nil {
return false
}
return matched
default:
return false
}
}
// matchDomain checks if the domain matches the pattern
func matchDomain(domain string, pattern string) bool {
if pattern == "" || pattern == "*" {
return true
}
// Convert wildcard pattern to regex pattern
regexPattern := convertWildcardToRegex(pattern)
matched, _ := regexp.MatchString(regexPattern, domain)
return matched
}
// matchDomainAndPath checks if both domain and path match the rule
func matchDomainAndPath(domain, path string, rule MatchRule) bool {
return matchDomain(domain, rule.MatchRuleDomain) &&
matchPattern(rule.MatchRulePath, path, rule.MatchRuleType)
}
// IsMatch checks if the request matches any rule in the rule list
// Returns true if no rules are specified
func IsMatch(rules []MatchRule, host, path string) bool {
if len(rules) == 0 {
return true
}
for _, rule := range rules {
if matchDomainAndPath(host, path, rule) {
return true
}
}
return false
}

View File

@@ -0,0 +1,209 @@
package internal
import (
"context"
"fmt"
"time"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"github.com/go-redis/redis/v8"
)
type RedisConfig struct {
Address string
Username string
Password string
DB int
}
func ParseRedisConfig(config map[string]any) (*RedisConfig, error) {
c := &RedisConfig{}
// address is required
addr, ok := config["address"].(string)
if !ok {
return nil, fmt.Errorf("address is required and must be a string")
}
c.Address = addr
// username is optional
if username, ok := config["username"].(string); ok {
c.Username = username
}
// password is optional
if password, ok := config["password"].(string); ok {
c.Password = password
}
// db is optional, default to 0
if db, ok := config["db"].(int); ok {
c.DB = db
}
return c, nil
}
// RedisClient is a struct to handle Redis connections and operations
type RedisClient struct {
client *redis.Client
ctx context.Context
cancel context.CancelFunc
config *RedisConfig
}
// NewRedisClient creates a new RedisClient instance and establishes a connection to the Redis server
func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
client := redis.NewClient(&redis.Options{
Addr: config.Address,
Username: config.Username,
Password: config.Password,
DB: config.DB,
})
// Ping the Redis server to check the connection
pong, err := client.Ping(context.Background()).Result()
if err != nil {
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
}
api.LogDebugf("Connected to Redis: %s", pong)
ctx, cancel := context.WithCancel(context.Background())
redisClient := &RedisClient{
client: client,
ctx: ctx,
cancel: cancel,
config: config,
}
// Start keep-alive check
go redisClient.keepAlive()
return redisClient, nil
}
// keepAlive periodically checks Redis connection and attempts to reconnect if needed
func (r *RedisClient) keepAlive() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-r.ctx.Done():
return
case <-ticker.C:
if err := r.checkConnection(); err != nil {
api.LogErrorf("Redis connection check failed: %v", err)
if err := r.reconnect(); err != nil {
api.LogErrorf("Failed to reconnect to Redis: %v", err)
}
}
}
}
}
// checkConnection verifies if the Redis connection is still alive
func (r *RedisClient) checkConnection() error {
_, err := r.client.Ping(r.ctx).Result()
return err
}
// reconnect attempts to establish a new connection to Redis
func (r *RedisClient) reconnect() error {
// Close the old client
if err := r.client.Close(); err != nil {
api.LogErrorf("Error closing old Redis connection: %v", err)
}
// Create new client
r.client = redis.NewClient(&redis.Options{
Addr: r.config.Address,
Username: r.config.Username,
Password: r.config.Password,
DB: r.config.DB,
})
// Test the new connection
if err := r.checkConnection(); err != nil {
return fmt.Errorf("failed to reconnect to Redis: %w", err)
}
api.LogDebugf("Successfully reconnected to Redis")
return nil
}
// Publish publishes a message to a Redis channel
func (r *RedisClient) Publish(channel string, message string) error {
err := r.client.Publish(r.ctx, channel, message).Err()
if err != nil {
return fmt.Errorf("failed to publish message: %w", err)
}
return nil
}
// Subscribe subscribes to a Redis channel and processes messages
func (r *RedisClient) Subscribe(channel string, stopChan chan struct{}, callback func(message string)) error {
pubsub := r.client.Subscribe(r.ctx, channel)
_, err := pubsub.Receive(r.ctx)
if err != nil {
return fmt.Errorf("failed to subscribe to channel: %w", err)
}
go func() {
defer func() {
pubsub.Close()
api.LogDebugf("Closed subscription to channel %s", channel)
}()
ch := pubsub.Channel()
for {
select {
case <-stopChan:
api.LogDebugf("Stopping subscription to channel %s", channel)
return
case msg, ok := <-ch:
if !ok {
api.LogDebugf("Redis subscription channel closed for %s", channel)
return
}
func() {
defer func() {
if r := recover(); r != nil {
api.LogErrorf("Recovered from panic in callback: %v", r)
}
}()
callback(msg.Payload)
}()
}
}
}()
return nil
}
// Set sets the value of a key in Redis
func (r *RedisClient) Set(key string, value string, expiration time.Duration) error {
err := r.client.Set(r.ctx, key, value, expiration).Err()
if err != nil {
return fmt.Errorf("failed to set key: %w", err)
}
return nil
}
// Get retrieves the value of a key from Redis
func (r *RedisClient) Get(key string) (string, error) {
val, err := r.client.Get(r.ctx, key).Result()
if err == redis.Nil {
return "", fmt.Errorf("key does not exist")
} else if err != nil {
return "", fmt.Errorf("failed to get key: %w", err)
}
return val, nil
}
// Close closes the Redis client and stops the keepalive goroutine
func (r *RedisClient) Close() error {
r.cancel()
return r.client.Close()
}

View File

@@ -0,0 +1,26 @@
package internal
var GlobalRegistry = NewServerRegistry()
type Server interface {
ParseConfig(config map[string]any) error
NewServer(serverName string) (*MCPServer, error)
}
type ServerRegistry struct {
servers map[string]Server
}
func NewServerRegistry() *ServerRegistry {
return &ServerRegistry{
servers: make(map[string]Server),
}
}
func (r *ServerRegistry) RegisterServer(name string, server Server) {
r.servers[name] = server
}
func (r *ServerRegistry) GetServer(name string) Server {
return r.servers[name]
}

View File

@@ -0,0 +1,844 @@
package internal
import (
"context"
"encoding/json"
"fmt"
"regexp"
"sort"
"sync"
"sync/atomic"
"github.com/mark3labs/mcp-go/mcp"
)
// resourceEntry holds both a resource and its handler
type resourceEntry struct {
resource mcp.Resource
handler ResourceHandlerFunc
}
// resourceTemplateEntry holds both a template and its handler
type resourceTemplateEntry struct {
template mcp.ResourceTemplate
handler ResourceTemplateHandlerFunc
}
// ServerOption is a function that configures an MCPServer.
type ServerOption func(*MCPServer)
// ResourceHandlerFunc is a function that returns resource contents.
type ResourceHandlerFunc func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error)
// ResourceTemplateHandlerFunc is a function that returns a resource template.
type ResourceTemplateHandlerFunc func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error)
// PromptHandlerFunc handles prompt requests with given arguments.
type PromptHandlerFunc func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error)
// ToolHandlerFunc handles tool calls with given arguments.
type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
// ServerTool combines a Tool with its ToolHandlerFunc.
type ServerTool struct {
Tool mcp.Tool
Handler ToolHandlerFunc
}
// NotificationContext provides client identification for notifications
type NotificationContext struct {
ClientID string
SessionID string
}
// ServerNotification combines the notification with client context
type ServerNotification struct {
Context NotificationContext
Notification mcp.JSONRPCNotification
}
// NotificationHandlerFunc handles incoming notifications.
type NotificationHandlerFunc func(ctx context.Context, notification mcp.JSONRPCNotification)
// MCPServer implements a Model Control Protocol server that can handle various types of requests
// including resources, prompts, and tools.
type MCPServer struct {
mu sync.RWMutex // Add mutex for protecting shared resources
name string
version string
instructions string
resources map[string]resourceEntry
resourceTemplates map[string]resourceTemplateEntry
prompts map[string]mcp.Prompt
promptHandlers map[string]PromptHandlerFunc
tools map[string]ServerTool
notificationHandlers map[string]NotificationHandlerFunc
capabilities serverCapabilities
notifications chan ServerNotification
clientMu sync.Mutex // Separate mutex for client context
currentClient NotificationContext
initialized atomic.Bool // Use atomic for the initialized flag
}
// serverKey is the context key for storing the server instance
type serverKey struct{}
// ServerFromContext retrieves the MCPServer instance from a context
func ServerFromContext(ctx context.Context) *MCPServer {
if srv, ok := ctx.Value(serverKey{}).(*MCPServer); ok {
return srv
}
return nil
}
// WithContext sets the current client context and returns the provided context
func (s *MCPServer) WithContext(
ctx context.Context,
notifCtx NotificationContext,
) context.Context {
s.clientMu.Lock()
s.currentClient = notifCtx
s.clientMu.Unlock()
return ctx
}
// SendNotificationToClient sends a notification to the current client
func (s *MCPServer) SendNotificationToClient(
method string,
params map[string]interface{},
) error {
if s.notifications == nil {
return fmt.Errorf("notification channel not initialized")
}
s.clientMu.Lock()
clientContext := s.currentClient
s.clientMu.Unlock()
notification := mcp.JSONRPCNotification{
JSONRPC: mcp.JSONRPC_VERSION,
Notification: mcp.Notification{
Method: method,
Params: mcp.NotificationParams{
AdditionalFields: params,
},
},
}
select {
case s.notifications <- ServerNotification{
Context: clientContext,
Notification: notification,
}:
return nil
default:
return fmt.Errorf("notification channel full or blocked")
}
}
// serverCapabilities defines the supported features of the MCP server
type serverCapabilities struct {
tools *toolCapabilities
resources *resourceCapabilities
prompts *promptCapabilities
logging bool
}
// resourceCapabilities defines the supported resource-related features
type resourceCapabilities struct {
subscribe bool
listChanged bool
}
// promptCapabilities defines the supported prompt-related features
type promptCapabilities struct {
listChanged bool
}
// toolCapabilities defines the supported tool-related features
type toolCapabilities struct {
listChanged bool
}
// WithResourceCapabilities configures resource-related server capabilities
func WithResourceCapabilities(subscribe, listChanged bool) ServerOption {
return func(s *MCPServer) {
// Always create a non-nil capability object
s.capabilities.resources = &resourceCapabilities{
subscribe: subscribe,
listChanged: listChanged,
}
}
}
// WithPromptCapabilities configures prompt-related server capabilities
func WithPromptCapabilities(listChanged bool) ServerOption {
return func(s *MCPServer) {
// Always create a non-nil capability object
s.capabilities.prompts = &promptCapabilities{
listChanged: listChanged,
}
}
}
// WithToolCapabilities configures tool-related server capabilities
func WithToolCapabilities(listChanged bool) ServerOption {
return func(s *MCPServer) {
// Always create a non-nil capability object
s.capabilities.tools = &toolCapabilities{
listChanged: listChanged,
}
}
}
// WithLogging enables logging capabilities for the server
func WithLogging() ServerOption {
return func(s *MCPServer) {
s.capabilities.logging = true
}
}
// WithInstructions sets the server instructions for the client returned in the initialize response
func WithInstructions(instructions string) ServerOption {
return func(s *MCPServer) {
s.instructions = instructions
}
}
// NewMCPServer creates a new MCP server instance with the given name, version and options
func NewMCPServer(
name, version string,
opts ...ServerOption,
) *MCPServer {
s := &MCPServer{
resources: make(map[string]resourceEntry),
resourceTemplates: make(map[string]resourceTemplateEntry),
prompts: make(map[string]mcp.Prompt),
promptHandlers: make(map[string]PromptHandlerFunc),
tools: make(map[string]ServerTool),
name: name,
version: version,
notificationHandlers: make(map[string]NotificationHandlerFunc),
notifications: make(chan ServerNotification, 100),
capabilities: serverCapabilities{
tools: nil,
resources: nil,
prompts: nil,
logging: false,
},
}
for _, opt := range opts {
opt(s)
}
return s
}
// HandleMessage processes an incoming JSON-RPC message and returns an appropriate response
func (s *MCPServer) HandleMessage(
ctx context.Context,
message json.RawMessage,
) mcp.JSONRPCMessage {
// Add server to context
ctx = context.WithValue(ctx, serverKey{}, s)
var baseMessage struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
ID interface{} `json:"id,omitempty"`
}
if err := json.Unmarshal(message, &baseMessage); err != nil {
return createErrorResponse(
nil,
mcp.PARSE_ERROR,
"Failed to parse message",
)
}
// Check for valid JSONRPC version
if baseMessage.JSONRPC != mcp.JSONRPC_VERSION {
return createErrorResponse(
baseMessage.ID,
mcp.INVALID_REQUEST,
"Invalid JSON-RPC version",
)
}
if baseMessage.ID == nil {
var notification mcp.JSONRPCNotification
if err := json.Unmarshal(message, &notification); err != nil {
return createErrorResponse(
nil,
mcp.PARSE_ERROR,
"Failed to parse notification",
)
}
s.handleNotification(ctx, notification)
return nil // Return nil for notifications
}
switch baseMessage.Method {
case "initialize":
var request mcp.InitializeRequest
if err := json.Unmarshal(message, &request); err != nil {
return createErrorResponse(
baseMessage.ID,
mcp.INVALID_REQUEST,
"Invalid initialize request",
)
}
return s.handleInitialize(ctx, baseMessage.ID, request)
case "ping":
var request mcp.PingRequest
if err := json.Unmarshal(message, &request); err != nil {
return createErrorResponse(
baseMessage.ID,
mcp.INVALID_REQUEST,
"Invalid ping request",
)
}
return s.handlePing(ctx, baseMessage.ID, request)
case "resources/list":
if s.capabilities.resources == nil {
return createErrorResponse(
baseMessage.ID,
mcp.METHOD_NOT_FOUND,
"Resources not supported",
)
}
var request mcp.ListResourcesRequest
if err := json.Unmarshal(message, &request); err != nil {
return createErrorResponse(
baseMessage.ID,
mcp.INVALID_REQUEST,
"Invalid list resources request",
)
}
return s.handleListResources(ctx, baseMessage.ID, request)
case "resources/templates/list":
if s.capabilities.resources == nil {
return createErrorResponse(
baseMessage.ID,
mcp.METHOD_NOT_FOUND,
"Resources not supported",
)
}
var request mcp.ListResourceTemplatesRequest
if err := json.Unmarshal(message, &request); err != nil {
return createErrorResponse(
baseMessage.ID,
mcp.INVALID_REQUEST,
"Invalid list resource templates request",
)
}
return s.handleListResourceTemplates(ctx, baseMessage.ID, request)
case "resources/read":
if s.capabilities.resources == nil {
return createErrorResponse(
baseMessage.ID,
mcp.METHOD_NOT_FOUND,
"Resources not supported",
)
}
var request mcp.ReadResourceRequest
if err := json.Unmarshal(message, &request); err != nil {
return createErrorResponse(
baseMessage.ID,
mcp.INVALID_REQUEST,
"Invalid read resource request",
)
}
return s.handleReadResource(ctx, baseMessage.ID, request)
case "prompts/list":
if s.capabilities.prompts == nil {
return createErrorResponse(
baseMessage.ID,
mcp.METHOD_NOT_FOUND,
"Prompts not supported",
)
}
var request mcp.ListPromptsRequest
if err := json.Unmarshal(message, &request); err != nil {
return createErrorResponse(
baseMessage.ID,
mcp.INVALID_REQUEST,
"Invalid list prompts request",
)
}
return s.handleListPrompts(ctx, baseMessage.ID, request)
case "prompts/get":
if s.capabilities.prompts == nil {
return createErrorResponse(
baseMessage.ID,
mcp.METHOD_NOT_FOUND,
"Prompts not supported",
)
}
var request mcp.GetPromptRequest
if err := json.Unmarshal(message, &request); err != nil {
return createErrorResponse(
baseMessage.ID,
mcp.INVALID_REQUEST,
"Invalid get prompt request",
)
}
return s.handleGetPrompt(ctx, baseMessage.ID, request)
case "tools/list":
if s.capabilities.tools == nil {
return createErrorResponse(
baseMessage.ID,
mcp.METHOD_NOT_FOUND,
"Tools not supported",
)
}
var request mcp.ListToolsRequest
if err := json.Unmarshal(message, &request); err != nil {
return createErrorResponse(
baseMessage.ID,
mcp.INVALID_REQUEST,
"Invalid list tools request",
)
}
return s.handleListTools(ctx, baseMessage.ID, request)
case "tools/call":
if s.capabilities.tools == nil {
return createErrorResponse(
baseMessage.ID,
mcp.METHOD_NOT_FOUND,
"Tools not supported",
)
}
var request mcp.CallToolRequest
if err := json.Unmarshal(message, &request); err != nil {
return createErrorResponse(
baseMessage.ID,
mcp.INVALID_REQUEST,
"Invalid call tool request",
)
}
return s.handleToolCall(ctx, baseMessage.ID, request)
default:
return createErrorResponse(
baseMessage.ID,
mcp.METHOD_NOT_FOUND,
fmt.Sprintf("Method %s not found", baseMessage.Method),
)
}
}
// AddResource registers a new resource and its handler
func (s *MCPServer) AddResource(
resource mcp.Resource,
handler ResourceHandlerFunc,
) {
if s.capabilities.resources == nil {
s.capabilities.resources = &resourceCapabilities{}
}
s.mu.Lock()
defer s.mu.Unlock()
s.resources[resource.URI] = resourceEntry{
resource: resource,
handler: handler,
}
}
// AddResourceTemplate registers a new resource template and its handler
func (s *MCPServer) AddResourceTemplate(
template mcp.ResourceTemplate,
handler ResourceTemplateHandlerFunc,
) {
if s.capabilities.resources == nil {
s.capabilities.resources = &resourceCapabilities{}
}
s.mu.Lock()
defer s.mu.Unlock()
s.resourceTemplates[template.URITemplate] = resourceTemplateEntry{
template: template,
handler: handler,
}
}
// AddPrompt registers a new prompt handler with the given name
func (s *MCPServer) AddPrompt(prompt mcp.Prompt, handler PromptHandlerFunc) {
if s.capabilities.prompts == nil {
s.capabilities.prompts = &promptCapabilities{}
}
s.mu.Lock()
defer s.mu.Unlock()
s.prompts[prompt.Name] = prompt
s.promptHandlers[prompt.Name] = handler
}
// AddTool registers a new tool and its handler
func (s *MCPServer) AddTool(tool mcp.Tool, handler ToolHandlerFunc) {
s.AddTools(ServerTool{Tool: tool, Handler: handler})
}
// AddTools registers multiple tools at once
func (s *MCPServer) AddTools(tools ...ServerTool) {
if s.capabilities.tools == nil {
s.capabilities.tools = &toolCapabilities{}
}
s.mu.Lock()
for _, entry := range tools {
s.tools[entry.Tool.Name] = entry
}
initialized := s.initialized.Load()
s.mu.Unlock()
// Send notification if server is already initialized
if initialized {
if err := s.SendNotificationToClient("notifications/tools/list_changed", nil); err != nil {
// We can't return the error, but in a future version we could log it
}
}
}
// SetTools replaces all existing tools with the provided list
func (s *MCPServer) SetTools(tools ...ServerTool) {
s.mu.Lock()
s.tools = make(map[string]ServerTool)
s.mu.Unlock()
s.AddTools(tools...)
}
// DeleteTools removes a tool from the server
func (s *MCPServer) DeleteTools(names ...string) {
s.mu.Lock()
for _, name := range names {
delete(s.tools, name)
}
initialized := s.initialized.Load()
s.mu.Unlock()
// Send notification if server is already initialized
if initialized {
if err := s.SendNotificationToClient("notifications/tools/list_changed", nil); err != nil {
// We can't return the error, but in a future version we could log it
}
}
}
// AddNotificationHandler registers a new handler for incoming notifications
func (s *MCPServer) AddNotificationHandler(
method string,
handler NotificationHandlerFunc,
) {
s.mu.Lock()
defer s.mu.Unlock()
s.notificationHandlers[method] = handler
}
func (s *MCPServer) handleInitialize(
ctx context.Context,
id interface{},
request mcp.InitializeRequest,
) mcp.JSONRPCMessage {
capabilities := mcp.ServerCapabilities{}
// Only add resource capabilities if they're configured
if s.capabilities.resources != nil {
capabilities.Resources = &struct {
Subscribe bool `json:"subscribe,omitempty"`
ListChanged bool `json:"listChanged,omitempty"`
}{
Subscribe: s.capabilities.resources.subscribe,
ListChanged: s.capabilities.resources.listChanged,
}
}
// Only add prompt capabilities if they're configured
if s.capabilities.prompts != nil {
capabilities.Prompts = &struct {
ListChanged bool `json:"listChanged,omitempty"`
}{
ListChanged: s.capabilities.prompts.listChanged,
}
}
// Only add tool capabilities if they're configured
if s.capabilities.tools != nil {
capabilities.Tools = &struct {
ListChanged bool `json:"listChanged,omitempty"`
}{
ListChanged: s.capabilities.tools.listChanged,
}
}
if s.capabilities.logging {
capabilities.Logging = &struct{}{}
}
result := mcp.InitializeResult{
ProtocolVersion: request.Params.ProtocolVersion,
ServerInfo: mcp.Implementation{
Name: s.name,
Version: s.version,
},
Capabilities: capabilities,
Instructions: s.instructions,
}
s.initialized.Store(true)
return createResponse(id, result)
}
func (s *MCPServer) handlePing(
ctx context.Context,
id interface{},
request mcp.PingRequest,
) mcp.JSONRPCMessage {
return createResponse(id, mcp.EmptyResult{})
}
func (s *MCPServer) handleListResources(
ctx context.Context,
id interface{},
request mcp.ListResourcesRequest,
) mcp.JSONRPCMessage {
s.mu.RLock()
resources := make([]mcp.Resource, 0, len(s.resources))
for _, entry := range s.resources {
resources = append(resources, entry.resource)
}
s.mu.RUnlock()
result := mcp.ListResourcesResult{
Resources: resources,
}
if request.Params.Cursor != "" {
result.NextCursor = "" // Handle pagination if needed
}
return createResponse(id, result)
}
func (s *MCPServer) handleListResourceTemplates(
ctx context.Context,
id interface{},
request mcp.ListResourceTemplatesRequest,
) mcp.JSONRPCMessage {
s.mu.RLock()
templates := make([]mcp.ResourceTemplate, 0, len(s.resourceTemplates))
for _, entry := range s.resourceTemplates {
templates = append(templates, entry.template)
}
s.mu.RUnlock()
result := mcp.ListResourceTemplatesResult{
ResourceTemplates: templates,
}
if request.Params.Cursor != "" {
result.NextCursor = "" // Handle pagination if needed
}
return createResponse(id, result)
}
func (s *MCPServer) handleReadResource(
ctx context.Context,
id interface{},
request mcp.ReadResourceRequest,
) mcp.JSONRPCMessage {
s.mu.RLock()
// First try direct resource handlers
if entry, ok := s.resources[request.Params.URI]; ok {
handler := entry.handler
s.mu.RUnlock()
contents, err := handler(ctx, request)
if err != nil {
return createErrorResponse(id, mcp.INTERNAL_ERROR, err.Error())
}
return createResponse(id, mcp.ReadResourceResult{Contents: contents})
}
// If no direct handler found, try matching against templates
var matchedHandler ResourceTemplateHandlerFunc
var matched bool
for uriTemplate, entry := range s.resourceTemplates {
if matchesTemplate(request.Params.URI, uriTemplate) {
matchedHandler = entry.handler
matched = true
break
}
}
s.mu.RUnlock()
if matched {
contents, err := matchedHandler(ctx, request)
if err != nil {
return createErrorResponse(id, mcp.INTERNAL_ERROR, err.Error())
}
return createResponse(
id,
mcp.ReadResourceResult{Contents: contents},
)
}
return createErrorResponse(
id,
mcp.INVALID_PARAMS,
fmt.Sprintf(
"No handler found for resource URI: %s",
request.Params.URI,
),
)
}
// matchesTemplate checks if a URI matches a URI template pattern
func matchesTemplate(uri string, template string) bool {
// Convert template into a regex pattern
pattern := template
// Replace {name} with ([^/]+)
pattern = regexp.QuoteMeta(pattern)
pattern = regexp.MustCompile(`\\\{[^}]+\\\}`).
ReplaceAllString(pattern, `([^/]+)`)
pattern = "^" + pattern + "$"
matched, _ := regexp.MatchString(pattern, uri)
return matched
}
func (s *MCPServer) handleListPrompts(
ctx context.Context,
id interface{},
request mcp.ListPromptsRequest,
) mcp.JSONRPCMessage {
s.mu.RLock()
prompts := make([]mcp.Prompt, 0, len(s.prompts))
for _, prompt := range s.prompts {
prompts = append(prompts, prompt)
}
s.mu.RUnlock()
result := mcp.ListPromptsResult{
Prompts: prompts,
}
if request.Params.Cursor != "" {
result.NextCursor = "" // Handle pagination if needed
}
return createResponse(id, result)
}
func (s *MCPServer) handleGetPrompt(
ctx context.Context,
id interface{},
request mcp.GetPromptRequest,
) mcp.JSONRPCMessage {
s.mu.RLock()
handler, ok := s.promptHandlers[request.Params.Name]
s.mu.RUnlock()
if !ok {
return createErrorResponse(
id,
mcp.INVALID_PARAMS,
fmt.Sprintf("Prompt not found: %s", request.Params.Name),
)
}
result, err := handler(ctx, request)
if err != nil {
return createErrorResponse(id, mcp.INTERNAL_ERROR, err.Error())
}
return createResponse(id, result)
}
func (s *MCPServer) handleListTools(
ctx context.Context,
id interface{},
request mcp.ListToolsRequest,
) mcp.JSONRPCMessage {
s.mu.RLock()
tools := make([]mcp.Tool, 0, len(s.tools))
// Get all tool names for consistent ordering
toolNames := make([]string, 0, len(s.tools))
for name := range s.tools {
toolNames = append(toolNames, name)
}
// Sort the tool names for consistent ordering
sort.Strings(toolNames)
// Add tools in sorted order
for _, name := range toolNames {
tools = append(tools, s.tools[name].Tool)
}
s.mu.RUnlock()
result := mcp.ListToolsResult{
Tools: tools,
}
if request.Params.Cursor != "" {
result.NextCursor = "" // Handle pagination if needed
}
return createResponse(id, result)
}
func (s *MCPServer) handleToolCall(
ctx context.Context,
id interface{},
request mcp.CallToolRequest,
) mcp.JSONRPCMessage {
s.mu.RLock()
tool, ok := s.tools[request.Params.Name]
s.mu.RUnlock()
if !ok {
return createErrorResponse(
id,
mcp.INVALID_PARAMS,
fmt.Sprintf("Tool not found: %s", request.Params.Name),
)
}
result, err := tool.Handler(ctx, request)
if err != nil {
return createErrorResponse(id, mcp.INTERNAL_ERROR, err.Error())
}
return createResponse(id, result)
}
func (s *MCPServer) handleNotification(
ctx context.Context,
notification mcp.JSONRPCNotification,
) mcp.JSONRPCMessage {
s.mu.RLock()
handler, ok := s.notificationHandlers[notification.Method]
s.mu.RUnlock()
if ok {
handler(ctx, notification)
}
return nil
}
func createResponse(id interface{}, result interface{}) mcp.JSONRPCMessage {
return mcp.JSONRPCResponse{
JSONRPC: mcp.JSONRPC_VERSION,
ID: id,
Result: result,
}
}
func createErrorResponse(
id interface{},
code int,
message string,
) mcp.JSONRPCMessage {
return mcp.JSONRPCError{
JSONRPC: mcp.JSONRPC_VERSION,
ID: id,
Error: struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}{
Code: code,
Message: message,
},
}
}

View File

@@ -0,0 +1,234 @@
package internal
import (
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"github.com/google/uuid"
"github.com/mark3labs/mcp-go/mcp"
)
// GetSSEChannelName returns the Redis channel name for the given session ID
func GetSSEChannelName(sessionID string) string {
return fmt.Sprintf("mcp-server-sse:%s", sessionID)
}
// SSEServer implements a Server-Sent Events (SSE) based MCP server.
// It provides real-time communication capabilities over HTTP using the SSE protocol.
type SSEServer struct {
server *MCPServer
baseURL string
messageEndpoint string
sseEndpoint string
sessions sync.Map
redisClient *RedisClient // Redis client for pub/sub
}
func (s *SSEServer) SetBaseURL(baseURL string) {
s.baseURL = baseURL
}
func (s *SSEServer) GetMessageEndpoint() string {
return s.messageEndpoint
}
func (s *SSEServer) GetSSEEndpoint() string {
return s.sseEndpoint
}
func (s *SSEServer) GetServerName() string {
return s.server.name
}
// Option defines a function type for configuring SSEServer
type Option func(*SSEServer)
// WithBaseURL sets the base URL for the SSE server
func WithBaseURL(baseURL string) Option {
return func(s *SSEServer) {
s.baseURL = baseURL
}
}
// WithMessageEndpoint sets the message endpoint path
func WithMessageEndpoint(endpoint string) Option {
return func(s *SSEServer) {
s.messageEndpoint = endpoint
}
}
// WithSSEEndpoint sets the SSE endpoint path
func WithSSEEndpoint(endpoint string) Option {
return func(s *SSEServer) {
s.sseEndpoint = endpoint
}
}
func WithRedisClient(redisClient *RedisClient) Option {
return func(s *SSEServer) {
s.redisClient = redisClient
}
}
// NewSSEServer creates a new SSE server instance with the given MCP server and options.
func NewSSEServer(server *MCPServer, opts ...Option) *SSEServer {
s := &SSEServer{
server: server,
sseEndpoint: "/sse",
messageEndpoint: "/message",
}
// Apply all options
for _, opt := range opts {
opt(s)
}
return s
}
// handleSSE handles incoming SSE connection requests.
// It sets up appropriate headers and creates a new session for the client.
func (s *SSEServer) HandleSSE(cb api.FilterCallbackHandler, stopChan chan struct{}) {
sessionID := uuid.New().String()
s.sessions.Store(sessionID, true)
defer s.sessions.Delete(sessionID)
channel := GetSSEChannelName(sessionID)
messageEndpoint := fmt.Sprintf(
"%s%s?sessionId=%s",
s.baseURL,
s.messageEndpoint,
sessionID,
)
// go func() {
// for {
// select {
// case serverNotification := <-s.server.notifications:
// // Only forward notifications meant for this session
// if serverNotification.Context.SessionID == sessionID {
// eventData, err := json.Marshal(serverNotification.Notification)
// if err == nil {
// select {
// case session.eventQueue <- fmt.Sprintf("event: message\ndata: %s\n\n", eventData):
// // Event queued successfully
// case <-session.done:
// return
// }
// }
// }
// case <-session.done:
// return
// case <-r.Context().Done():
// return
// }
// }
// }()
err := s.redisClient.Subscribe(channel, stopChan, func(message string) {
defer cb.EncoderFilterCallbacks().RecoverPanic()
api.LogDebugf("SSE Send message: %s", message)
cb.EncoderFilterCallbacks().InjectData([]byte(message))
})
if err != nil {
api.LogErrorf("Failed to subscribe to Redis channel: %v", err)
}
// Send the initial endpoint event
initialEvent := fmt.Sprintf("event: endpoint\ndata: %s\r\n\r\n", messageEndpoint)
err = s.redisClient.Publish(channel, initialEvent)
if err != nil {
api.LogErrorf("Failed to send initial event: %v", err)
}
// Start health check handler
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-stopChan:
return
case <-ticker.C:
// Send health check message
currentTime := time.Now().Format(time.RFC3339)
healthCheckEvent := fmt.Sprintf(": ping - %s\n\n", currentTime)
if err := s.redisClient.Publish(channel, healthCheckEvent); err != nil {
api.LogErrorf("Failed to send health check: %v", err)
}
}
}
}()
}
// handleMessage processes incoming JSON-RPC messages from clients and sends responses
// back through both the SSE connection and HTTP response.
func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body json.RawMessage) {
if r.Method != http.MethodPost {
s.writeJSONRPCError(w, nil, mcp.INVALID_REQUEST, fmt.Sprintf("Method %s not allowed", r.Method))
return
}
sessionID := r.URL.Query().Get("sessionId")
// support streamable http without sessionId
// if sessionID == "" {
// s.writeJSONRPCError(w, nil, mcp.INVALID_PARAMS, "Missing sessionId")
// return
// }
// Set the client context in the server before handling the message
ctx := s.server.WithContext(r.Context(), NotificationContext{
ClientID: sessionID,
SessionID: sessionID,
})
//TODO check session id
// _, ok := s.sessions.Load(sessionID)
// if !ok {
// s.writeJSONRPCError(w, nil, mcp.INVALID_PARAMS, "Invalid session ID")
// return
// }
// Process message through MCPServer
response := s.server.HandleMessage(ctx, body)
// Only send response if there is one (not for notifications)
if response != nil {
eventData, _ := json.Marshal(response)
if sessionID != "" {
channel := GetSSEChannelName(sessionID)
publishErr := s.redisClient.Publish(channel, fmt.Sprintf("event: message\ndata: %s\n\n", eventData))
if publishErr != nil {
api.LogErrorf("Failed to publish message to Redis: %v", publishErr)
}
}
// Send HTTP response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(response)
} else {
// For notifications, just send 202 Accepted with no body
w.WriteHeader(http.StatusAccepted)
}
}
// writeJSONRPCError writes a JSON-RPC error response with the given error details.
func (s *SSEServer) writeJSONRPCError(
w http.ResponseWriter,
id interface{},
code int,
message string,
) {
response := createErrorResponse(id, code, message)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(response)
}

View File

@@ -0,0 +1,281 @@
package nacos
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"github.com/nacos-group/nacos-sdk-go/v2/clients/config_client"
"github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client"
"github.com/nacos-group/nacos-sdk-go/v2/model"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
)
type NacosMcpRegsitry struct {
serviceMatcher map[string]string
configClient config_client.IConfigClient
namingClient naming_client.INamingClient
toolsDescription map[string]*registry.ToolDescription
toolsRpcContext map[string]*registry.RpcContext
toolChangeEventListeners []registry.ToolChangeEventListener
currentServiceSet map[string]bool
}
const DEFAULT_SERVICE_LIST_MAX_PGSIZXE = 10000
const MCP_TOOL_SUBFIX = "-mcp-tools.json"
func (n *NacosMcpRegsitry) ListToolsDesciption() []*registry.ToolDescription {
if n.toolsDescription == nil {
n.refreshToolsList()
}
result := []*registry.ToolDescription{}
for _, tool := range n.toolsDescription {
result = append(result, tool)
}
return result
}
func (n *NacosMcpRegsitry) GetToolRpcContext(toolName string) (*registry.RpcContext, bool) {
tool, ok := n.toolsRpcContext[toolName]
return tool, ok
}
func (n *NacosMcpRegsitry) RegisterToolChangeEventListener(listener registry.ToolChangeEventListener) {
n.toolChangeEventListeners = append(n.toolChangeEventListeners, listener)
}
func (n *NacosMcpRegsitry) refreshToolsList() bool {
changed := false
for group, serviceMatcher := range n.serviceMatcher {
if n.refreshToolsListForGroup(group, serviceMatcher) {
changed = true
}
}
return changed
}
func (n *NacosMcpRegsitry) refreshToolsListForGroup(group string, serviceMatcher string) bool {
services, err := n.namingClient.GetAllServicesInfo(vo.GetAllServiceInfoParam{
GroupName: group,
PageNo: 1,
PageSize: DEFAULT_SERVICE_LIST_MAX_PGSIZXE,
})
if err != nil {
api.LogError(fmt.Sprintf("Get service list error when refresh tools list for group %s, error %s", group, err))
return false
}
changed := false
serviceList := services.Doms
pattern, err := regexp.Compile(serviceMatcher)
if err != nil {
api.LogErrorf("Match service error for patter %s", serviceMatcher)
return false
}
currentServiceList := map[string]bool{}
for _, service := range serviceList {
if !pattern.MatchString(service) {
continue
}
formatServiceName := getFormatServiceName(group, service)
if _, ok := n.currentServiceSet[formatServiceName]; !ok {
changed = true
n.refreshToolsListForService(group, service)
n.listenToService(group, service)
}
currentServiceList[formatServiceName] = true
}
serviceShouldBeDeleted := []string{}
for serviceName, _ := range n.currentServiceSet {
if !strings.HasPrefix(serviceName, group) {
continue
}
if _, ok := currentServiceList[serviceName]; !ok {
serviceShouldBeDeleted = append(serviceShouldBeDeleted, serviceName)
changed = true
toolsShouldBeDeleted := []string{}
for toolName, _ := range n.toolsDescription {
if strings.HasPrefix(toolName, serviceName) {
toolsShouldBeDeleted = append(toolsShouldBeDeleted, toolName)
}
}
for _, toolName := range toolsShouldBeDeleted {
delete(n.toolsDescription, toolName)
delete(n.toolsRpcContext, toolName)
}
}
}
for _, service := range serviceShouldBeDeleted {
delete(n.currentServiceSet, service)
}
return changed
}
func getFormatServiceName(group string, service string) string {
return fmt.Sprintf("%s_%s", group, service)
}
func (n *NacosMcpRegsitry) refreshToolsListForServiceWithContent(group string, service string, newConfig *string, instances *[]model.Instance) {
if newConfig == nil {
dataId := makeToolsConfigId(service)
content, err := n.configClient.GetConfig(vo.ConfigParam{
DataId: dataId,
Group: group,
})
if err != nil {
api.LogError(fmt.Sprintf("Get tools config for sercice %s:%s error %s", group, service, err))
return
}
newConfig = &content
}
if instances == nil {
instancesFromNacos, err := n.namingClient.SelectInstances(vo.SelectInstancesParam{
ServiceName: service,
GroupName: group,
HealthyOnly: true,
})
if err != nil {
api.LogError(fmt.Sprintf("List instance for sercice %s:%s error %s", group, service, err))
return
}
instances = &instancesFromNacos
}
var applicationDescription registry.McpApplicationDescription
err := json.Unmarshal([]byte(*newConfig), &applicationDescription)
if err != nil {
api.LogError(fmt.Sprintf("Parse tools config for sercice %s:%s error, config is %s, error is %s", group, service, *newConfig, err))
return
}
wrappedInstances := []registry.Instance{}
for _, instance := range *instances {
wrappedInstance := registry.Instance{
Host: instance.Ip,
Port: instance.Port,
Meta: instance.Metadata,
}
wrappedInstances = append(wrappedInstances, wrappedInstance)
}
if n.toolsDescription == nil {
n.toolsDescription = map[string]*registry.ToolDescription{}
}
if n.toolsRpcContext == nil {
n.toolsRpcContext = map[string]*registry.RpcContext{}
}
for _, tool := range applicationDescription.ToolsDescription {
meta := applicationDescription.ToolsMeta[tool.Name]
var cred *registry.CredentialInfo
credentialRef := meta.CredentialRef
if credentialRef != nil {
cred = n.GetCredential(*credentialRef, group)
}
context := registry.RpcContext{
ToolMeta: meta,
Instances: &wrappedInstances,
Protocol: applicationDescription.Protocol,
Credential: cred,
}
tool.Name = makeToolName(group, service, tool.Name)
n.toolsDescription[tool.Name] = tool
n.toolsRpcContext[tool.Name] = &context
}
n.currentServiceSet[getFormatServiceName(group, service)] = true
}
func (n *NacosMcpRegsitry) GetCredential(name string, group string) *registry.CredentialInfo {
dataId := makeCredentialDataId(name)
content, err := n.configClient.GetConfig(vo.ConfigParam{
DataId: dataId,
Group: group,
})
if err != nil {
api.LogError(fmt.Sprintf("Get credentials for %s:%s error %s", group, dataId, err))
return nil
}
var credential registry.CredentialInfo
err = json.Unmarshal([]byte(content), &credential)
if err != nil {
api.LogError(fmt.Sprintf("Parse credentials for %s:%s error %s", group, dataId, err))
return nil
}
return &credential
}
func (n *NacosMcpRegsitry) refreshToolsListForService(group string, service string) {
n.refreshToolsListForServiceWithContent(group, service, nil, nil)
}
func (n *NacosMcpRegsitry) listenToService(group string, service string) {
// config changed, tools description may be changed
err := n.configClient.ListenConfig(vo.ConfigParam{
DataId: makeToolsConfigId(service),
Group: group,
OnChange: func(namespace, group, dataId, data string) {
n.refreshToolsListForServiceWithContent(group, service, &data, nil)
for _, listener := range n.toolChangeEventListeners {
listener.OnToolChanged(n)
}
},
})
if err != nil {
api.LogError(fmt.Sprintf("Listen to service's tool config error %s", err))
}
err = n.namingClient.Subscribe(&vo.SubscribeParam{
ServiceName: service,
GroupName: group,
SubscribeCallback: func(services []model.Instance, err error) {
n.refreshToolsListForServiceWithContent(group, service, nil, &services)
for _, listener := range n.toolChangeEventListeners {
listener.OnToolChanged(n)
}
},
})
if err != nil {
api.LogError(fmt.Sprintf("Listen to service's tool instance list error %s", err))
}
}
func makeToolName(group string, service string, toolName string) string {
return fmt.Sprintf("%s_%s_%s", group, service, toolName)
}
func makeToolsConfigId(service string) string {
return service + MCP_TOOL_SUBFIX
}
func makeCredentialDataId(credentialName string) string {
return credentialName
}

View File

@@ -0,0 +1,174 @@
package nacos
import (
"errors"
"fmt"
"time"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"github.com/mark3labs/mcp-go/mcp"
"github.com/nacos-group/nacos-sdk-go/v2/clients"
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
)
func init() {
internal.GlobalRegistry.RegisterServer("nacos-mcp-registry", &NacosConfig{})
}
type NacosConfig struct {
ServerAddr *string
Ak *string
Sk *string
Namespace *string
RegionId *string
ServiceMatcher *map[string]string
}
type McpServerToolsChangeListener struct {
mcpServer *internal.MCPServer
}
func (l *McpServerToolsChangeListener) OnToolChanged(reg registry.McpServerRegistry) {
resetToolsToMcpServer(l.mcpServer, reg)
}
func CreateNacosMcpRegsitry(config *NacosConfig) (*NacosMcpRegsitry, error) {
sc := []constant.ServerConfig{
*constant.NewServerConfig(*config.ServerAddr, 8848, constant.WithContextPath("/nacos")),
}
//create ClientConfig
cc := *constant.NewClientConfig(
constant.WithTimeoutMs(5000),
constant.WithNotLoadCacheAtStart(true),
constant.WithOpenKMS(true),
constant.WithLogLevel("error"),
)
cc.AppendToStdout = true
cc.DiableLog = true
if config.Namespace != nil {
cc.NamespaceId = *config.Namespace
}
if config.RegionId != nil {
cc.RegionId = *config.RegionId
}
if config.Ak != nil {
cc.AccessKey = *config.Ak
}
if config.Sk != nil {
cc.SecretKey = *config.Sk
}
// create config client
configClient, err := clients.NewConfigClient(
vo.NacosClientParam{
ClientConfig: &cc,
ServerConfigs: sc,
},
)
if err != nil {
return nil, fmt.Errorf("failed to initial nacos config client: %w", err)
}
namingClient, err := clients.NewNamingClient(
vo.NacosClientParam{
ClientConfig: &cc,
ServerConfigs: sc,
},
)
if err != nil {
return nil, fmt.Errorf("failed to initial naming config client: %w", err)
}
return &NacosMcpRegsitry{
configClient: configClient,
namingClient: namingClient,
serviceMatcher: *config.ServiceMatcher,
toolChangeEventListeners: []registry.ToolChangeEventListener{},
currentServiceSet: map[string]bool{},
}, nil
}
func (c *NacosConfig) ParseConfig(config map[string]any) error {
serverAddr, ok := config["serverAddr"].(string)
if !ok {
return errors.New("missing serverAddr")
}
c.ServerAddr = &serverAddr
serviceMatcher, ok := config["serviceMatcher"].(map[string]any)
if !ok {
return errors.New("missing serviceMatcher")
}
matchers := map[string]string{}
for key, value := range serviceMatcher {
matchers[key] = value.(string)
}
c.ServiceMatcher = &matchers
if ak, ok := config["accessKey"].(string); ok {
c.Ak = &ak
}
if sk, ok := config["secretKey"].(string); ok {
c.Sk = &sk
}
if region, ok := config["regionId"].(string); ok {
c.RegionId = &region
}
return nil
}
func (c *NacosConfig) NewServer(serverName string) (*internal.MCPServer, error) {
mcpServer := internal.NewMCPServer(
serverName,
"1.0.0",
)
nacosRegistry, err := CreateNacosMcpRegsitry(c)
if err != nil {
return nil, fmt.Errorf("failed to initialize NacosMcpRegistry: %w", err)
}
listener := McpServerToolsChangeListener{
mcpServer: mcpServer,
}
nacosRegistry.RegisterToolChangeEventListener(&listener)
go func() {
for {
if nacosRegistry.refreshToolsList() {
resetToolsToMcpServer(mcpServer, nacosRegistry)
}
time.Sleep(time.Second * 3)
}
}()
return mcpServer, nil
}
func resetToolsToMcpServer(mcpServer *internal.MCPServer, reg registry.McpServerRegistry) {
wrappedTools := []internal.ServerTool{}
tools := reg.ListToolsDesciption()
for _, tool := range tools {
wrappedTools = append(wrappedTools, internal.ServerTool{
Tool: mcp.NewToolWithRawSchema(tool.Name, tool.Description, tool.InputSchema),
Handler: registry.HandleRegistryToolsCall(reg),
})
}
mcpServer.SetTools(wrappedTools...)
api.LogInfof("Tools reset, new tools list len %d", len(wrappedTools))
}

View File

@@ -0,0 +1,64 @@
package registry
import (
"encoding/json"
"github.com/mark3labs/mcp-go/mcp"
)
type McpApplicationDescription struct {
Protocol string `json:"protocol"`
ToolsDescription []*ToolDescription `json:"tools"`
ToolsMeta map[string]ToolMeta `json:"toolsMeta"`
}
type ToolMeta struct {
InvokeContext map[string]string `json:"invokeContext"`
ParametersMapping map[string]ParameterMapInfo `json:"parametersMapping"`
CredentialRef *string `json:"credentialRef"`
}
type ParameterMapInfo struct {
ParamName string `json:"name"`
BackendName string `json:"backendName"`
ParamType string `json:"type"`
Position string `json:"position"`
}
type ToolDescription struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema json.RawMessage `json:"inputSchema"`
}
type ToolChangeEventListener interface {
OnToolChanged(McpServerRegistry)
}
type McpServerRegistry interface {
ListToolsDesciption() []*ToolDescription
GetToolRpcContext(toolname string) (*RpcContext, bool)
RegisterToolChangeEventListener(listener ToolChangeEventListener)
}
type RpcContext struct {
Instances *[]Instance
ToolMeta ToolMeta
Protocol string
Credential *CredentialInfo
}
type CredentialInfo struct {
CredentialType string `json:"type"`
Credentials map[string]any `json:"credentialsMap"`
}
type Instance struct {
Host string
Port uint64
Meta map[string]string
}
type RemoteCallHandle interface {
HandleToolCall(ctx *RpcContext, parameters map[string]any) (*mcp.CallToolResult, error)
}

View File

@@ -0,0 +1,200 @@
package registry
import (
"context"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"strings"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/mark3labs/mcp-go/mcp"
)
const HTTP_URL_TEMPLATE = "%s://%s:%d%s"
const FIX_QUERY_TOKEN_KEY = "key"
const FIX_QUERY_TOKEN_VALUE = "value"
const PROTOCOL_HTTP = "http"
const PROTOCOL_HTTPS = "https"
const DEFAULT_HTTP_METHOD = "GET"
const DEFAULT_HTTP_PATH = "/"
func getHttpCredentialHandle(name string) (func(*CredentialInfo, *HttpRemoteCallHandle), error) {
if name == "fixed-query-token" {
return FixedQueryToken, nil
}
return nil, fmt.Errorf("Unknown credential type")
}
type CommonRemoteCallHandle struct {
Instance *Instance
}
type HttpRemoteCallHandle struct {
CommonRemoteCallHandle
Protocol string
Headers http.Header
Body *string
Query map[string]string
Path string
Method string
}
// http credentials handles
func FixedQueryToken(cred *CredentialInfo, h *HttpRemoteCallHandle) {
key, _ := cred.Credentials[FIX_QUERY_TOKEN_KEY]
value, _ := cred.Credentials[FIX_QUERY_TOKEN_VALUE]
h.Query[key.(string)] = value.(string)
}
func newHttpRemoteCallHandle(ctx *RpcContext) *HttpRemoteCallHandle {
instance := selectOneInstance(ctx)
method, ok := ctx.ToolMeta.InvokeContext["method"]
if !ok {
method = DEFAULT_HTTP_METHOD
}
path, ok := ctx.ToolMeta.InvokeContext["path"]
if !ok {
path = DEFAULT_HTTP_PATH
}
return &HttpRemoteCallHandle{
CommonRemoteCallHandle: CommonRemoteCallHandle{
Instance: &instance,
},
Protocol: ctx.Protocol,
Headers: http.Header{},
Body: nil,
Query: map[string]string{},
Path: path,
Method: method,
}
}
// http remote handle implementation
func (h *HttpRemoteCallHandle) HandleToolCall(ctx *RpcContext, parameters map[string]any) (*mcp.CallToolResult, error) {
if ctx.Credential != nil {
credentialHandle, err := getHttpCredentialHandle(ctx.Credential.CredentialType)
if err != nil {
return nil, err
}
credentialHandle(ctx.Credential, h)
}
err := h.handleParamMapping(&ctx.ToolMeta.ParametersMapping, parameters)
if err != nil {
return nil, err
}
response, err := h.doHttpCall()
if err != nil {
return nil, err
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
responseType := "text"
if respType, ok := ctx.ToolMeta.InvokeContext["responseType"]; ok {
responseType = respType
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: responseType,
Text: string(body),
},
},
}, nil
}
func (h *HttpRemoteCallHandle) handleParamMapping(mapInfo *map[string]ParameterMapInfo, params map[string]any) error {
paramMapInfo := *mapInfo
for param, value := range params {
if info, ok := paramMapInfo[param]; ok {
if info.Position == "Query" {
h.Query[info.BackendName] = fmt.Sprintf("%s", value)
} else if info.Position == "Header" {
h.Headers[info.BackendName] = []string{fmt.Sprintf("%s", value)}
} else {
return fmt.Errorf("Unsupport position for args %s, pos is %s", param, info.Position)
}
} else {
h.Query[param] = fmt.Sprintf("%s", value)
}
}
return nil
}
func (h *HttpRemoteCallHandle) doHttpCall() (*http.Response, error) {
pathPrefix := fmt.Sprintf(HTTP_URL_TEMPLATE, h.Protocol, h.Instance.Host, h.Instance.Port, h.Path)
queryString := ""
queryGroup := []string{}
for queryKey, queryValue := range h.Query {
queryGroup = append(queryGroup, url.QueryEscape(queryKey)+"="+url.QueryEscape(queryValue))
}
if len(queryGroup) > 0 {
queryString = "?" + strings.Join(queryGroup, "&")
}
fullUrl, err := url.Parse(pathPrefix + queryString)
if err != nil {
return nil, fmt.Errorf("Parse url error , url is %s", pathPrefix+queryString)
}
request := http.Request{
URL: fullUrl,
Method: h.Method,
Header: h.Headers,
}
if h.Body != nil {
request.Body = io.NopCloser(strings.NewReader(*h.Body))
}
return http.DefaultClient.Do(&request)
}
func selectOneInstance(ctx *RpcContext) Instance {
instanceId := 0
instances := *ctx.Instances
if len(instances) != 1 {
instanceId = rand.Intn(len(instances) - 1)
}
return instances[instanceId]
}
func getRemoteCallhandle(ctx *RpcContext) RemoteCallHandle {
if ctx.Protocol == PROTOCOL_HTTP || ctx.Protocol == PROTOCOL_HTTPS {
return newHttpRemoteCallHandle(ctx)
} else {
return nil
}
}
// common remote call process
func CommonRemoteCall(reg McpServerRegistry, toolName string, parameters map[string]any) (*mcp.CallToolResult, error) {
ctx, ok := reg.GetToolRpcContext(toolName)
if !ok {
return nil, fmt.Errorf("Unknown tool %s", toolName)
}
remoteHandle := getRemoteCallhandle(ctx)
if remoteHandle == nil {
return nil, fmt.Errorf("Unknown backend protocol %s", ctx.Protocol)
}
return remoteHandle.HandleToolCall(ctx, parameters)
}
func HandleRegistryToolsCall(reg McpServerRegistry) internal.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
return CommonRemoteCall(reg, request.Params.Name, arguments)
}
}

View File

@@ -0,0 +1,90 @@
package gorm
import (
"fmt"
"gorm.io/driver/clickhouse"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// DBClient is a struct to handle PostgreSQL connections and operations
type DBClient struct {
db *gorm.DB
}
// NewDBClient creates a new DBClient instance and establishes a connection to the PostgreSQL database
func NewDBClient(dsn string, dbType string) (*DBClient, error) {
var db *gorm.DB
var err error
if dbType == "postgres" {
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
} else if dbType == "clickhouse" {
db, err = gorm.Open(clickhouse.Open(dsn), &gorm.Config{})
} else if dbType == "mysql" {
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
} else if dbType == "sqlite" {
db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{})
} else {
return nil, fmt.Errorf("unsupported database type %s", dbType)
}
// Connect to the database
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
return &DBClient{db: db}, nil
}
// ExecuteSQL executes a raw SQL query and returns the result as a slice of maps
func (c *DBClient) ExecuteSQL(query string, args ...interface{}) ([]map[string]interface{}, error) {
rows, err := c.db.Raw(query, args...).Rows()
if err != nil {
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
}
defer rows.Close()
// Get column names
columns, err := rows.Columns()
if err != nil {
return nil, fmt.Errorf("failed to get columns: %w", err)
}
// Prepare a slice to hold the results
var results []map[string]interface{}
// Iterate over the rows
for rows.Next() {
// Create a slice of interface{}'s to represent each column,
// and a second slice to contain pointers to each item in the columns slice.
columnsData := make([]interface{}, len(columns))
columnsPointers := make([]interface{}, len(columns))
for i := range columnsData {
columnsPointers[i] = &columnsData[i]
}
// Scan the result into the column pointers...
if err := rows.Scan(columnsPointers...); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
// Create a map to hold the column name and value
rowMap := make(map[string]interface{})
for i, colName := range columns {
val := columnsData[i]
b, ok := val.([]byte)
if ok {
rowMap[colName] = string(b)
} else {
rowMap[colName] = val
}
}
// Append the map to the results slice
results = append(results, rowMap)
}
return results, nil
}

View File

@@ -0,0 +1,58 @@
package gorm
import (
"errors"
"fmt"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
"github.com/mark3labs/mcp-go/mcp"
)
const Version = "1.0.0"
func init() {
internal.GlobalRegistry.RegisterServer("database", &DBConfig{})
}
type DBConfig struct {
dbType string
dsn string
}
func (c *DBConfig) ParseConfig(config map[string]any) error {
dsn, ok := config["dsn"].(string)
if !ok {
return errors.New("missing dsn")
}
c.dsn = dsn
dbType, ok := config["dbType"].(string)
if !ok {
return errors.New("missing database type")
}
c.dbType = dbType
api.LogDebugf("DBConfig ParseConfig: %+v", config)
return nil
}
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
mcpServer := internal.NewMCPServer(
serverName,
Version,
internal.WithInstructions(fmt.Sprintf("This is a %s database server", c.dbType)),
)
dbClient, err := NewDBClient(c.dsn, c.dbType)
if err != nil {
return nil, fmt.Errorf("failed to initialize DBClient: %w", err)
}
// Add query tool
mcpServer.AddTool(
mcp.NewToolWithRawSchema("query", fmt.Sprintf("Run a read-only SQL query in database %s", c.dbType), GetQueryToolSchema()),
HandleQueryTool(dbClient),
)
return mcpServer, nil
}

View File

@@ -0,0 +1,55 @@
package gorm
import (
"context"
"encoding/json"
"fmt"
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
"github.com/mark3labs/mcp-go/mcp"
)
// HandleQueryTool handles SQL query execution
func HandleQueryTool(dbClient *DBClient) internal.ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
arguments := request.Params.Arguments
message, ok := arguments["sql"].(string)
if !ok {
return nil, fmt.Errorf("invalid message argument")
}
results, err := dbClient.ExecuteSQL(message)
if err != nil {
return nil, fmt.Errorf("failed to execute SQL query: %w", err)
}
jsonData, err := json.Marshal(results)
if err != nil {
return nil, fmt.Errorf("failed to marshal SQL results: %w", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(jsonData),
},
},
}, nil
}
}
// GetQueryToolSchema returns the schema for query tool
func GetQueryToolSchema() json.RawMessage {
return json.RawMessage(`
{
"type": "object",
"properties": {
"sql": {
"type": "string",
"description": "The sql query to execute"
}
}
}
`)
}

View File

@@ -16,6 +16,7 @@
#pragma once
#include <functional>
#include <optional>
#include <string>
#include <unordered_map>
@@ -25,6 +26,8 @@
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_split.h"
#include "common/json_util.h"
#include "http_util.h"
@@ -58,12 +61,15 @@ using ::Wasm::Common::JsonValueAs;
template <typename PluginConfig>
class RouteRuleMatcher {
public:
enum CATEGORY { Route, Host };
enum CATEGORY { Route, RoutePrefix, Host, Service, RouteAndService };
enum MATCH_TYPE { Prefix, Exact, Suffix };
struct RuleConfig {
CATEGORY category;
std::unordered_set<std::string> routes;
std::vector<std::string> route_prefixs;
std::vector<std::pair<MATCH_TYPE, std::string>> hosts;
std::unordered_set<std::string> services;
bool disable = false;
PluginConfig config;
};
struct AuthRuleConfig {
@@ -88,6 +94,8 @@ class RouteRuleMatcher {
return rules;
}
bool globalAuthDisable() { return global_auth_ && !global_auth_.value(); }
FilterHeadersStatus onHeaders(
const std::function<FilterHeadersStatus(const PluginConfig&)> process) {
if (invalid_config_) {
@@ -126,10 +134,10 @@ class RouteRuleMatcher {
LOG_DEBUG("no match config");
return true;
}
if (!config.second && global_auth_ && !global_auth_.value()) {
// No allow set, means no need to check auth if global_auth is false
if (!config.second && globalAuthDisable()) {
// No allow set, means no need to check auth if global auth is disable
LOG_DEBUG(
"no allow set found, and global auth is false, no need to auth");
"no allow set found, and global auth is disable, no need to auth");
return true;
}
return checkPlugin(config.first.value(), config.second);
@@ -154,27 +162,71 @@ class RouteRuleMatcher {
auto request_host = request_host_header->view();
std::string route_name;
getValue({"route_name"}, &route_name);
std::string service_name;
getValue({"cluster_name"}, &service_name);
std::optional<std::reference_wrapper<PluginConfig>> match_config;
int rule_id;
if (global_config_) {
rule_id = 0;
match_config = global_config_.value();
}
bool disable_rule = false;
for (int i = 0; i < rule_config_.size(); ++i) {
auto& rule = rule_config_[i];
if (rule.category == CATEGORY::Host) {
if (hostMatch(rule, request_host)) {
rule_id = i + 1;
match_config = rule.config;
disable_rule = rule.disable;
break;
}
} else if (rule.category == CATEGORY::Route) {
// category == Route
if (rule.routes.find(route_name) != rule.routes.end()) {
rule_id = i + 1;
match_config = rule.config;
disable_rule = rule.disable;
break;
}
} else if (rule.category == CATEGORY::RouteAndService) {
// category == RouteAndService
if (rule.routes.find(route_name) != rule.routes.end()) {
if (serviceMatch(rule, service_name)) {
rule_id = i + 1;
match_config = rule.config;
disable_rule = rule.disable;
break;
}
}
} else if (rule.category == CATEGORY::Service) {
// category == Service
if (serviceMatch(rule, service_name)) {
rule_id = i + 1;
match_config = rule.config;
disable_rule = rule.disable;
break;
}
} else {
// category == RoutePrefix
bool is_matched = false;
for (auto& route_prefix : rule.route_prefixs) {
if (route_name.length() < route_prefix.length() ||
route_name.compare(0, route_prefix.length(), route_prefix) != 0) {
continue;
}
is_matched = true;
rule_id = i + 1;
match_config = rule.config;
disable_rule = rule.disable;
break;
}
if (is_matched) {
break;
}
}
// category == Route
if (rule.routes.find(route_name) != rule.routes.end()) {
rule_id = i + 1;
match_config = rule.config;
break;
}
}
if (disable_rule) {
return std::make_pair(-1, std::nullopt);
}
if (match_config) {
return std::make_pair(rule_id, match_config);
@@ -190,6 +242,8 @@ class RouteRuleMatcher {
auto request_host = request_host_header->view();
std::string route_name;
getValue({"route_name"}, &route_name);
std::string service_name;
getValue({"service_name"}, &service_name);
std::optional<std::reference_wrapper<PluginConfig>> match_config;
std::optional<std::reference_wrapper<std::unordered_set<std::string>>>
allow_set;
@@ -200,33 +254,99 @@ class RouteRuleMatcher {
return std::make_pair(match_config, std::nullopt);
}
bool is_matched = false;
bool disable_rule = false;
for (auto& auth_rule : auth_rule_config_) {
if (auth_rule.rule_config.category == CATEGORY::Host) {
if (hostMatch(auth_rule.rule_config, request_host)) {
LOG_DEBUG(absl::StrFormat("host %s is matched for this request",
request_host));
is_matched = true;
if (auth_rule.has_local_config) {
LOG_DEBUG("has local config");
if (auth_rule.rule_config.disable) {
disable_rule = true;
} else if (auth_rule.has_local_config) {
match_config = auth_rule.rule_config.config;
} else {
LOG_DEBUG("has not local config");
allow_set = auth_rule.allow_set;
}
break;
}
}
// category == Route
if (auth_rule.rule_config.routes.find(route_name) !=
auth_rule.rule_config.routes.end()) {
is_matched = true;
if (auth_rule.has_local_config) {
match_config = auth_rule.rule_config.config;
} else {
allow_set = auth_rule.allow_set;
} else if (auth_rule.rule_config.category == CATEGORY::Route) {
// category == Route
if (auth_rule.rule_config.routes.find(route_name) !=
auth_rule.rule_config.routes.end()) {
LOG_DEBUG(absl::StrFormat("route %s is matched for this request",
route_name));
is_matched = true;
if (auth_rule.rule_config.disable) {
disable_rule = true;
} else if (auth_rule.has_local_config) {
match_config = auth_rule.rule_config.config;
} else {
allow_set = auth_rule.allow_set;
}
break;
}
} else if (auth_rule.rule_config.category == CATEGORY::RouteAndService) {
// category == RouteAndService
if (auth_rule.rule_config.routes.find(route_name) !=
auth_rule.rule_config.routes.end()) {
LOG_DEBUG(absl::StrFormat("route %s is matched for this request",
route_name));
if (serviceMatch(auth_rule.rule_config, service_name)) {
LOG_DEBUG(absl::StrFormat("service %s is matched for this request",
service_name));
is_matched = true;
if (auth_rule.rule_config.disable) {
disable_rule = true;
} else if (auth_rule.has_local_config) {
match_config = auth_rule.rule_config.config;
} else {
allow_set = auth_rule.allow_set;
}
break;
}
}
} else if (auth_rule.rule_config.category == CATEGORY::Service) {
// category == Service
if (serviceMatch(auth_rule.rule_config, service_name)) {
LOG_DEBUG(absl::StrFormat("service %s is matched for this request",
service_name));
is_matched = true;
if (auth_rule.rule_config.disable) {
disable_rule = true;
} else if (auth_rule.has_local_config) {
match_config = auth_rule.rule_config.config;
} else {
allow_set = auth_rule.allow_set;
}
break;
}
} else {
// category == RoutePrefix
for (auto& route_prefix : auth_rule.rule_config.route_prefixs) {
if (route_name.length() < route_prefix.length() ||
route_name.compare(0, route_prefix.length(), route_prefix) != 0) {
continue;
}
LOG_DEBUG(absl::StrFormat(
"route_prefix %s is matched for this request", route_prefix));
is_matched = true;
if (auth_rule.rule_config.disable) {
disable_rule = true;
} else if (auth_rule.has_local_config) {
match_config = auth_rule.rule_config.config;
} else {
allow_set = auth_rule.allow_set;
}
break;
}
if (is_matched) {
break;
}
break;
}
}
return is_matched || (global_auth_ && global_auth_.value())
return !disable_rule &&
(is_matched || (global_auth_ && global_auth_.value()))
? std::make_pair(match_config, allow_set)
: std::make_pair(std::nullopt, std::nullopt);
}
@@ -265,23 +385,48 @@ class RouteRuleMatcher {
LOG_WARN("failed to parse configuration for _match_route_");
return false;
}
if (!parseRoutePrefixMatchConfig(config, rule.route_prefixs)) {
LOG_WARN("failed to parse configuration for _match_route_prefix_");
return false;
}
if (!parseDomainMatchConfig(config, rule.hosts)) {
LOG_WARN("failed to parse configuration for _match_domain_");
return false;
}
auto no_route = rule.routes.empty();
auto no_host = rule.hosts.empty();
if ((no_route && no_host) || (!no_route && !no_host)) {
if (!parseServiceMatchConfig(config, rule.services)) {
LOG_WARN("failed to parse configuration for _match_service_");
return false;
}
auto has_route = !rule.routes.empty();
auto has_route_prefix = !rule.route_prefixs.empty();
auto has_service = !rule.services.empty();
auto has_host = !rule.hosts.empty();
if (has_route + has_route_prefix + has_host + has_service == 0) {
LOG_WARN(
"there is only one of '_match_route_' and '_match_domain_' can "
"there is at least one of '_match_route_', '_match_domain_', "
"'_match_route_prefix_' and '_match_service_' can "
"present in configuration.");
return false;
}
if (!no_route) {
if (has_route) {
rule.category = CATEGORY::Route;
if (has_service) {
rule.category = CATEGORY::RouteAndService;
}
} else if (has_route_prefix) {
rule.category = CATEGORY::RoutePrefix;
} else if (has_service) {
rule.category = CATEGORY::Service;
} else {
rule.category = CATEGORY::Host;
}
auto has_disable = config.find("_disable_");
if (has_disable != config.end()) {
auto disable = JsonValueAs<bool>(has_disable.value());
if (disable.second == Wasm::Common::JsonParserResultDetail::OK) {
rule.disable = disable.first.value();
}
}
rule_config_.push_back(std::move(rule));
}
return true;
@@ -323,8 +468,11 @@ class RouteRuleMatcher {
for (const auto& item : rules.items()) {
AuthRuleConfig auth_rule;
auto config = item.value();
// ignore the '_match_route_' or '_match_domain_' field
auto local_config_size = config.size() - 1;
auto has_allow = config.find("allow");
if (has_allow != config.end()) {
local_config_size -= 1;
LOG_DEBUG("has allow filed");
if (!JsonArrayIterate(config, "allow", [&](const json& allow) -> bool {
auto parse_result = JsonValueAs<std::string>(allow);
@@ -332,10 +480,10 @@ class RouteRuleMatcher {
Wasm::Common::JsonParserResultDetail::OK ||
!parse_result.first) {
LOG_WARN(
"failed to parse 'allow' field in filter configuration.");
"failed to parse 'allow' field in filter "
"configuration.");
return false;
}
LOG_DEBUG(parse_result.first.value());
auth_rule.allow_set.insert(parse_result.first.value());
return true;
})) {
@@ -343,32 +491,61 @@ class RouteRuleMatcher {
return false;
}
}
if (!parsePluginConfig(config, auth_rule.rule_config.config)) {
if (has_allow == config.end()) {
LOG_WARN("parse rule's config failed");
return false;
auto has_disable = config.find("_disable_");
if (has_disable != config.end()) {
local_config_size -= 1;
auto disable = JsonValueAs<bool>(has_disable.value());
if (disable.second == Wasm::Common::JsonParserResultDetail::OK) {
auth_rule.rule_config.disable = disable.first.value();
}
}
if (local_config_size > 0) {
if (!parsePluginConfig(config, auth_rule.rule_config.config)) {
if (has_allow == config.end()) {
LOG_WARN("parse rule's config failed");
return false;
}
} else {
auth_rule.has_local_config = true;
}
} else {
auth_rule.has_local_config = true;
}
if (!parseRouteMatchConfig(config, auth_rule.rule_config.routes)) {
LOG_WARN("failed to parse configuration for _match_route_");
return false;
}
if (!parseRoutePrefixMatchConfig(config,
auth_rule.rule_config.route_prefixs)) {
LOG_WARN("failed to parse configuration for _match_route_prefix_");
return false;
}
if (!parseServiceMatchConfig(config, auth_rule.rule_config.services)) {
LOG_WARN("failed to parse configuration for _match_service_");
return false;
}
if (!parseDomainMatchConfig(config, auth_rule.rule_config.hosts)) {
LOG_WARN("failed to parse configuration for _match_domain_");
return false;
}
auto no_route = auth_rule.rule_config.routes.empty();
auto no_host = auth_rule.rule_config.hosts.empty();
if ((no_route && no_host) || (!no_route && !no_host)) {
auto has_route = !auth_rule.rule_config.routes.empty();
auto has_route_prefix = !auth_rule.rule_config.route_prefixs.empty();
auto has_host = !auth_rule.rule_config.hosts.empty();
auto has_service = !auth_rule.rule_config.services.empty();
if (has_route + has_route_prefix + has_host + has_service == 0) {
LOG_WARN(
"there is only one of '_match_route_' and '_match_domain_' can "
"there is at least one of '_match_route_', '_match_domain_', "
"'_match_route_prefix_' and '_match_service_' can "
"present in configuration.");
return false;
}
if (!no_route) {
if (has_route) {
auth_rule.rule_config.category = CATEGORY::Route;
if (has_service) {
auth_rule.rule_config.category = CATEGORY::RouteAndService;
}
} else if (has_route_prefix) {
auth_rule.rule_config.category = CATEGORY::RoutePrefix;
} else if (has_service) {
auth_rule.rule_config.category = CATEGORY::Service;
} else {
auth_rule.rule_config.category = CATEGORY::Host;
}
@@ -419,6 +596,27 @@ class RouteRuleMatcher {
return false;
}
bool serviceMatch(const RuleConfig& rule, std::string_view request_service) {
if (rule.services.empty()) {
// If no services specified, consider this rule applies to all host.
return true;
}
std::vector<std::string> result = absl::StrSplit(request_service, '|');
if (result.size() != 4) {
return false;
}
std::string port = result[1];
std::string fqdn = result[3];
for (const std::string& service_match : rule.services) {
if (service_match == fqdn || service_match == fqdn + ":" + port) {
return true;
}
}
return false;
}
bool parseRouteMatchConfig(const json& config,
std::unordered_set<std::string>& routes) {
return JsonArrayIterate(
@@ -436,6 +634,23 @@ class RouteRuleMatcher {
});
}
bool parseRoutePrefixMatchConfig(const json& config,
std::vector<std::string>& route_prefixs) {
return JsonArrayIterate(
config, "_match_route_prefix_", [&](const json& route) -> bool {
auto parse_result = JsonValueAs<std::string>(route);
if (parse_result.second != Wasm::Common::JsonParserResultDetail::OK ||
!parse_result.first) {
LOG_WARN(
"failed to parse '_match_route_prefix_' field in filter "
"configuration.");
return false;
}
route_prefixs.emplace_back(parse_result.first.value());
return true;
});
}
bool parseDomainMatchConfig(
const json& config,
std::vector<std::pair<MATCH_TYPE, std::string>>& hosts) {
@@ -475,6 +690,23 @@ class RouteRuleMatcher {
});
}
bool parseServiceMatchConfig(const json& config,
std::unordered_set<std::string>& services) {
return JsonArrayIterate(
config, "_match_service_", [&](const json& service) -> bool {
auto parse_result = JsonValueAs<std::string>(service);
if (parse_result.second != Wasm::Common::JsonParserResultDetail::OK ||
!parse_result.first) {
LOG_WARN(
"failed to parse '_match_service_' field in filter "
"configuration.");
return false;
}
services.insert(parse_result.first.value());
return true;
});
}
bool invalid_config_ = false;
std::optional<bool> global_auth_ = std::nullopt;
std::vector<RuleConfig> rule_config_;

View File

@@ -294,7 +294,12 @@ bool PluginRootContext::checkConsumer(
}
auto key_to_name_iter = rule.key_to_name.find(std::string(ca_key));
if (key_to_name_iter != rule.key_to_name.end()) {
if (allow_set && !allow_set.value().empty()) {
if (allow_set) {
if (allow_set.value().empty()) {
LOG_DEBUG("allow set is empty, nobody is allowed");
deniedUnauthorizedConsumer();
return false;
}
if (allow_set.value().find(key_to_name_iter->second) ==
allow_set.value().end()) {
LOG_DEBUG(absl::StrCat("consumer is not allowed: ",
@@ -435,6 +440,7 @@ FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
auto config = rootCtx->getMatchAuthConfig();
config_ = config.first;
if (!config_) {
LOG_DEBUG("no matched config found");
return FilterHeadersStatus::Continue;
}
allow_set_ = config.second;

View File

@@ -624,6 +624,41 @@ TEST_F(HmacAuthTest, TimestampSecCheck) {
EXPECT_EQ(context_->onRequestBody(0, true), FilterDataStatus::Continue);
}
TEST_F(HmacAuthTest, EmptyAllowSet) {
headers_ = {
{":path", "/Third/Tools/checkSign"},
{":method", "GET"},
{"accept", "application/json"},
{"content-type", "application/json"},
{"x-ca-timestamp", "1646365291734"},
{"x-ca-nonce", "787dd0c2-7bd8-41cd-9c19-62c05ea524a2"},
{"x-ca-key", "appKey"},
{"x-ca-signature-headers", "x-ca-key,x-ca-nonce,x-ca-timestamp"},
{"x-ca-signature", "EdJSFAMOWyXZOpXhevZnjuS0ZafnwnCqaSk5hz+tXo8="},
};
HmacAuthConfigRule rule;
rule.credentials = {{"appKey", "appSecret"}};
// EXPECT_EQ(root_context_->checkPlugin(rule, std::nullopt), true);
std::string configuration = R"(
{
"consumers": [{"key": "appKey", "secret": "appSecret", "name": "consumer"}],
"_rules_": [
{
"_match_route_prefix_":["test"],
"allow":[]
}
]
})";
route_name_ = "test@op1";
config_.set(configuration);
EXPECT_TRUE(root_context_->configure(configuration.size()));
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopAllIterationAndBuffer);
}
} // namespace hmac_auth
} // namespace null_plugin
} // namespace proxy_wasm

View File

@@ -45,15 +45,13 @@ namespace {
const std::string OriginalAuthKey("X-HI-ORIGINAL-AUTH");
void deniedInvalidCredentials(const std::string& realm) {
sendLocalResponse(401, "Request denied by Key Auth check. Invalid API key",
"",
sendLocalResponse(401, "Request denied by Key Auth check. Invalid API key", "",
{{"WWW-Authenticate", absl::StrCat("Key realm=", realm)}});
}
void deniedUnauthorizedConsumer(const std::string& realm) {
sendLocalResponse(
403, "Request denied by Key Auth check. Unauthorized consumer", "",
{{"WWW-Authenticate", absl::StrCat("Basic realm=", realm)}});
sendLocalResponse(403, "Request denied by Key Auth check. Unauthorized consumer", "",
{{"WWW-Authenticate", absl::StrCat("Basic realm=", realm)}});
}
} // namespace
@@ -62,12 +60,16 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
KeyAuthConfigRule& rule) {
if ((configuration.find("consumers") != configuration.end()) &&
(configuration.find("credentials") != configuration.end())) {
LOG_WARN(
"The consumers field and the credentials field cannot appear at the "
"same level");
LOG_WARN("The consumers field and the credentials field cannot appear at the same level");
return false;
}
if (!JsonArrayIterate(
if ((configuration.find("consumers") == configuration.end()) &&
(configuration.find("credentials") == configuration.end())) {
LOG_WARN("No consumers and no credentials");
return false;
}
if (configuration.find("credentials") != configuration.end()) {
if (!JsonArrayIterate(
configuration, "credentials", [&](const json& credentials) -> bool {
auto credential = JsonValueAs<std::string>(credentials);
if (credential.second != Wasm::Common::JsonParserResultDetail::OK) {
@@ -76,34 +78,93 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
rule.credentials.insert(credential.first.value());
return true;
})) {
LOG_WARN("failed to parse configuration for credentials.");
return false;
LOG_WARN("failed to parse configuration for credentials.");
return false;
}
if (!JsonArrayIterate(configuration, "keys", [&](const json& item) -> bool {
auto key = JsonValueAs<std::string>(item);
if (key.second != Wasm::Common::JsonParserResultDetail::OK) {
return false;
}
rule.keys.push_back(key.first.value());
return true;
})) {
LOG_WARN("failed to parse configuration for keys.");
return false;
}
if (rule.keys.empty()) {
LOG_WARN("at least one key has to be configured for a rule.");
return false;
}
rule.keys.push_back(OriginalAuthKey);
auto it = configuration.find("realm");
if (it != configuration.end()) {
auto realm_string = JsonValueAs<std::string>(it.value());
if (realm_string.second != Wasm::Common::JsonParserResultDetail::OK) {
return false;
}
rule.realm = realm_string.first.value();
}
it = configuration.find("in_query");
if (it != configuration.end()) {
auto in_query = JsonValueAs<bool>(it.value());
if (in_query.second != Wasm::Common::JsonParserResultDetail::OK ||
!in_query.first) {
LOG_WARN("failed to parse 'in_query' field in filter configuration.");
return false;
}
rule.in_query = in_query.first.value();
}
it = configuration.find("in_header");
if (it != configuration.end()) {
auto in_header = JsonValueAs<bool>(it.value());
if (in_header.second != Wasm::Common::JsonParserResultDetail::OK ||
!in_header.first) {
LOG_WARN("failed to parse 'in_header' field in filter configuration.");
return false;
}
rule.in_header = in_header.first.value();
}
if (!rule.in_query && !rule.in_header) {
LOG_WARN("at least one of 'in_query' and 'in_header' must set to true");
return false;
}
// LOG_DEBUG(rule.debugString("parse phase, credentials branch"));
}
if (!JsonArrayIterate(
configuration, "consumers", [&](const json& consumer) -> bool {
Consumer c;
auto item = consumer.find("name");
if (item == consumer.end()) {
LOG_WARN("can't find 'name' field in consumer.");
return false;
}
auto name = JsonValueAs<std::string>(item.value());
if (name.second != Wasm::Common::JsonParserResultDetail::OK ||
!name.first) {
return false;
}
c.name = name.first.value();
item = consumer.find("credential");
if (item == consumer.end()) {
LOG_WARN("can't find 'credential' field in consumer.");
return false;
}
if (configuration.find("consumers") != configuration.end()) {
bool need_global_keys = false;
if (!JsonArrayIterate(
configuration, "consumers", [&](const json& consumer) -> bool {
Consumer c;
auto item = consumer.find("name");
if (item == consumer.end()) {
LOG_WARN("can't find 'name' field in consumer.");
return false;
}
auto name = JsonValueAs<std::string>(item.value());
if (name.second != Wasm::Common::JsonParserResultDetail::OK ||
!name.first) {
return false;
}
c.name = name.first.value();
if (consumer.find("credential") != consumer.end() &&
consumer.find("credentials") != consumer.end()) {
LOG_WARN("'credential' and 'credentials' can't appear at the same time.");
return false;
}
if (consumer.find("credential") == consumer.end() &&
consumer.find("credentials") == consumer.end()) {
LOG_WARN("at least one of 'credential' and 'credentials' should be set.");
return false;
}
item = consumer.find("credential");
if (item != consumer.end()) {
auto credential = JsonValueAs<std::string>(item.value());
if (credential.second != Wasm::Common::JsonParserResultDetail::OK ||
!credential.first) {
return false;
}
c.credential = credential.first.value();
c.credentials.insert(credential.first.value());
if (rule.credential_to_name.find(credential.first.value()) !=
rule.credential_to_name.end()) {
LOG_WARN(absl::StrCat("duplicate consumer credential: ",
@@ -113,106 +174,118 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
rule.credentials.insert(credential.first.value());
rule.credential_to_name.emplace(
std::make_pair(credential.first.value(), name.first.value()));
item = consumer.find("keys");
if (item != consumer.end()) {
c.keys = std::vector<std::string>{OriginalAuthKey};
}
item = consumer.find("credentials");
if (item != consumer.end()) {
if (!JsonArrayIterate(
consumer, "keys", [&](const json& key_json) -> bool {
auto key = JsonValueAs<std::string>(key_json);
if (key.second !=
Wasm::Common::JsonParserResultDetail::OK) {
return false;
}
c.keys->push_back(key.first.value());
return true;
})) {
LOG_WARN("failed to parse configuration for consumer keys.");
consumer, "credentials", [&](const json& credential_json) -> bool {
auto credential = JsonValueAs<std::string>(credential_json);
if (credential.second != Wasm::Common::JsonParserResultDetail::OK ||
!credential.first) {
return false;
}
c.credentials.insert(credential.first.value());
if (rule.credential_to_name.find(credential.first.value()) !=
rule.credential_to_name.end()) {
LOG_WARN(absl::StrCat("duplicate consumer credential: ",
credential.first.value()));
return false;
}
rule.credentials.insert(credential.first.value());
rule.credential_to_name.emplace(
std::make_pair(credential.first.value(), name.first.value()));
return true;
})) {
LOG_WARN(absl::StrCat("failed to parse credentials for consumer: ", c.name));
return false;
}
item = consumer.find("in_query");
if (item != consumer.end()) {
auto in_query = JsonValueAs<bool>(item.value());
if (in_query.second !=
Wasm::Common::JsonParserResultDetail::OK ||
!in_query.first) {
LOG_WARN(
"failed to parse 'in_query' field in consumer "
"configuration.");
return false;
}
c.in_query = in_query.first;
}
item = consumer.find("in_header");
if (item != consumer.end()) {
auto in_header = JsonValueAs<bool>(item.value());
if (in_header.second !=
Wasm::Common::JsonParserResultDetail::OK ||
!in_header.first) {
LOG_WARN(
"failed to parse 'in_header' field in consumer "
"configuration.");
return false;
}
c.in_header = in_header.first;
}
}
item = consumer.find("keys");
if (item == consumer.end()) {
LOG_WARN("not found keys configuration for consumer " + c.name + ", will use global configuration to extract keys");
need_global_keys = true;
} else {
c.keys = std::vector<std::string>{OriginalAuthKey};
if (!JsonArrayIterate(
consumer, "keys", [&](const json& key_json) -> bool {
auto key = JsonValueAs<std::string>(key_json);
if (key.second !=
Wasm::Common::JsonParserResultDetail::OK) {
return false;
}
c.keys->push_back(key.first.value());
return true;
})) {
LOG_WARN("failed to parse configuration for consumer keys.");
return false;
}
rule.consumers.push_back(std::move(c));
item = consumer.find("in_query");
if (item != consumer.end()) {
auto in_query = JsonValueAs<bool>(item.value());
if (in_query.second != Wasm::Common::JsonParserResultDetail::OK || !in_query.first) {
LOG_WARN("failed to parse 'in_query' field in consumer configuration.");
return false;
}
c.in_query = in_query.first;
}
item = consumer.find("in_header");
if (item != consumer.end()) {
auto in_header = JsonValueAs<bool>(item.value());
if (in_header.second !=
Wasm::Common::JsonParserResultDetail::OK ||
!in_header.first) {
LOG_WARN(
"failed to parse 'in_header' field in consumer "
"configuration.");
return false;
}
c.in_header = in_header.first;
}
}
rule.consumers.push_back(std::move(c));
return true;
})) {
LOG_WARN("failed to parse configuration for credentials.");
return false;
}
if (need_global_keys) {
if (!JsonArrayIterate(configuration, "keys", [&](const json& item) -> bool {
auto key = JsonValueAs<std::string>(item);
if (key.second != Wasm::Common::JsonParserResultDetail::OK) {
return false;
}
rule.keys.push_back(key.first.value());
return true;
})) {
LOG_WARN("failed to parse configuration for credentials.");
return false;
}
// if (rule.credentials.empty()) {
// LOG_INFO("at least one credential has to be configured for a rule.");
// return false;
// }
if (!JsonArrayIterate(configuration, "keys", [&](const json& item) -> bool {
auto key = JsonValueAs<std::string>(item);
if (key.second != Wasm::Common::JsonParserResultDetail::OK) {
LOG_WARN("failed to parse configuration for keys.");
return false;
}
auto it = configuration.find("in_query");
if (it != configuration.end()) {
auto in_query = JsonValueAs<bool>(it.value());
if (in_query.second != Wasm::Common::JsonParserResultDetail::OK ||
!in_query.first) {
LOG_WARN("failed to parse 'in_query' field in filter configuration.");
return false;
}
rule.keys.push_back(key.first.value());
return true;
})) {
LOG_WARN("failed to parse configuration for keys.");
return false;
}
if (rule.keys.empty()) {
LOG_WARN("at least one key has to be configured for a rule.");
return false;
}
rule.keys.push_back(OriginalAuthKey);
auto it = configuration.find("realm");
if (it != configuration.end()) {
auto realm_string = JsonValueAs<std::string>(it.value());
if (realm_string.second != Wasm::Common::JsonParserResultDetail::OK) {
return false;
rule.in_query = in_query.first.value();
}
it = configuration.find("in_header");
if (it != configuration.end()) {
auto in_header = JsonValueAs<bool>(it.value());
if (in_header.second != Wasm::Common::JsonParserResultDetail::OK ||
!in_header.first) {
LOG_WARN("failed to parse 'in_header' field in filter configuration.");
return false;
}
rule.in_header = in_header.first.value();
}
if (!rule.in_query && !rule.in_header) {
LOG_WARN("at least one of 'in_query' and 'in_header' must set to true");
return false;
}
}
rule.realm = realm_string.first.value();
}
it = configuration.find("in_query");
if (it != configuration.end()) {
auto in_query = JsonValueAs<bool>(it.value());
if (in_query.second != Wasm::Common::JsonParserResultDetail::OK ||
!in_query.first) {
LOG_WARN("failed to parse 'in_query' field in filter configuration.");
return false;
}
rule.in_query = in_query.first.value();
}
it = configuration.find("in_header");
if (it != configuration.end()) {
auto in_header = JsonValueAs<bool>(it.value());
if (in_header.second != Wasm::Common::JsonParserResultDetail::OK ||
!in_header.first) {
LOG_WARN("failed to parse 'in_header' field in filter configuration.");
return false;
}
rule.in_header = in_header.first.value();
}
if (!rule.in_query && !rule.in_header) {
LOG_WARN("at least one of 'in_query' and 'in_header' must set to true");
return false;
// LOG_DEBUG(rule.debugString("parse phase, consumers branch"));
}
return true;
}
@@ -220,6 +293,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
bool PluginRootContext::checkPlugin(
const KeyAuthConfigRule& rule,
const std::optional<std::unordered_set<std::string>>& allow_set) {
// LOG_DEBUG(rule.debugString("check phase"));
if (rule.consumers.empty()) {
for (const auto& key : rule.keys) {
auto credential = extractCredential(rule.in_header, rule.in_query, key);
@@ -263,9 +337,8 @@ bool PluginRootContext::checkPlugin(
continue;
}
if (credential != consumer.credential) {
LOG_DEBUG("credential does not match the consumer's credential: " +
credential);
if (consumer.credentials.find(credential) == consumer.credentials.end()) {
LOG_DEBUG("credential " + credential + " does not match the consumer " + consumer.name);
continue;
}
@@ -277,9 +350,13 @@ bool PluginRootContext::checkPlugin(
auto credential_to_name_iter = rule.credential_to_name.find(credential);
if (credential_to_name_iter != rule.credential_to_name.end()) {
if (allow_set && !allow_set->empty()) {
if (allow_set->find(credential_to_name_iter->second) ==
allow_set->end()) {
if (allow_set) {
if (allow_set->empty()) {
LOG_DEBUG("allow set is empty, nobody is allowed");
deniedUnauthorizedConsumer(rule.realm);
return false;
}
if (allow_set->find(credential_to_name_iter->second) == allow_set->end()) {
deniedUnauthorizedConsumer(rule.realm);
LOG_DEBUG("unauthorized consumer: " +
credential_to_name_iter->second);

View File

@@ -38,10 +38,26 @@ namespace key_auth {
struct Consumer {
std::string name;
std::string credential;
std::unordered_set<std::string> credentials;
std::optional<std::vector<std::string>> keys;
std::optional<bool> in_query = std::nullopt;
std::optional<bool> in_header = std::nullopt;
// std::string debugString() const {
// std::string msg;
// msg += "name: " + name + "\n";
// msg += " keys: \n";
// if (keys.has_value()) {
// for (const auto& item : keys.value()) {
// msg += " - " + item + "\n";
// }
// }
// msg += " credentials: \n";
// for (const auto& item : credentials) {
// msg += " - " + item + "\n";
// }
// return msg;
// }
};
struct KeyAuthConfigRule {
@@ -52,6 +68,30 @@ struct KeyAuthConfigRule {
std::vector<std::string> keys;
bool in_query = true;
bool in_header = true;
// std::string debugString(std::string prompt="") const {
// std::string msg;
// msg += prompt + "\n";
// msg += "realm: " + realm + "\n";
// msg += "keys: \n";
// for (const auto& item : keys) {
// msg += "- " + item + "\n";
// }
// msg += "credentials: \n";
// for (const auto& item : credentials) {
// msg += "- " + item + "\n";
// }
// msg += "credential_to_name: \n";
// for (const auto& item : credential_to_name) {
// msg += "- " + item.first + ": " + item.second + "\n";
// }
// msg += "consumers: \n";
// for (const auto& item : consumers) {
// msg += "- " + item.debugString();
// }
// return msg;
// }
};
// PluginRootContext is the root context for all streams processed by the

View File

@@ -148,6 +148,11 @@ TEST_F(KeyAuthTest, InQuery) {
path_ = "/test?hello=123&apiKey=123&x-api-key=def";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
route_name_ = "pass";
path_ = "/pass?hello=123&apiKey=123";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
}
TEST_F(KeyAuthTest, InQueryWithConsumer) {
@@ -177,6 +182,35 @@ TEST_F(KeyAuthTest, InQueryWithConsumer) {
FilterHeadersStatus::StopIteration);
}
TEST_F(KeyAuthTest, EmptyAllowSet) {
std::string configuration = R"(
{
"consumers" : [{"credential" : "abc", "name" : "consumer1"}],
"keys" : [ "apiKey", "x-api-key" ],
"_rules_" : [ {"_match_route_" : ["test"], "allow" : []}, {"_match_route_prefix_" : ["prefix"], "allow" : []} ]
})";
BufferBase buffer;
buffer.set(configuration);
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
EXPECT_TRUE(root_context_->configure(configuration.size()));
route_name_ = "test";
path_ = "/test?hello=1&apiKey=abc";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
route_name_ = "noauth";
path_ = "/test?hello=1&apiKey=abc";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
route_name_ = "prefix@operation";
path_ = "/test?hello=1";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
}
TEST_F(KeyAuthTest, EmptyConsumer) {
std::string configuration = R"(
{
@@ -301,6 +335,131 @@ TEST_F(KeyAuthTest, ConsumerDifferentKey) {
FilterHeadersStatus::Continue);
}
TEST_F(KeyAuthTest, ConsumerMultiCredentials) {
std::string configuration = R"(
{
"global_auth": false,
"consumers": [
{
"name": "c1",
"credentials":["123","345"],
"keys": ["c1key"],
"in_header": false,
"in_query": true
},
{
"name": "c2",
"credentials":["abc","def"],
"keys": ["c2key"],
"in_header": false,
"in_query": true
}
],
"_rules_": [
{
"_match_route_": ["test"],
"allow": ["c1"]
}
]
})";
BufferBase buffer;
buffer.set(configuration);
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
EXPECT_TRUE(root_context_->configure(configuration.size()));
route_name_ = "test";
path_ = "/test?c1key=123";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
path_ = "/test?c2key=adc";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
}
TEST_F(KeyAuthTest, ConsumerDefaultKey) {
std::string configuration = R"(
{
"global_auth": false,
"consumers": [
{
"name": "c1",
"credentials":["123","345"],
"keys": ["c1key"],
"in_header": false,
"in_query": true
},
{
"name": "c2",
"credentials":["abc","def"]
}
],
"_rules_": [
{
"_match_route_": ["test"],
"allow": ["c2"]
}
],
"keys": ["defaultkey"]
})";
BufferBase buffer;
buffer.set(configuration);
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
EXPECT_TRUE(root_context_->configure(configuration.size()));
route_name_ = "test";
path_ = "/test?c1key=123";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
path_ = "/test?defaultkey=def";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
}
TEST_F(KeyAuthTest, NoGlobalKeySetting) {
std::string configuration = R"(
{
"global_auth": false,
"consumers": [
{
"name": "c1",
"credentials":["123","345"],
"keys": ["c1key"],
"in_header": false,
"in_query": true
},
{
"name": "c2",
"credentials":["abc","def"],
"keys": ["c2key"]
}
],
"_rules_": [
{
"_match_route_": ["test"],
"allow": ["c2"]
}
]
})";
BufferBase buffer;
buffer.set(configuration);
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
EXPECT_TRUE(root_context_->configure(configuration.size()));
route_name_ = "test";
path_ = "/test?c1key=123";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
path_ = "/test?c2key=def";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
}
} // namespace key_auth
} // namespace null_plugin
} // namespace proxy_wasm

View File

@@ -44,8 +44,8 @@ static RegisterContextFactory register_ModelMapper(
namespace {
constexpr std::string_view SetDecoderBufferLimitKey =
"SetRequestBodyBufferLimit";
constexpr std::string_view DefaultMaxBodyBytes = "10485760";
"set_decoder_buffer_limit";
constexpr std::string_view DefaultMaxBodyBytes = "104857600";
} // namespace
@@ -166,6 +166,7 @@ FilterHeadersStatus PluginRootContext::onHeader(
}
removeRequestHeader(Wasm::Common::Http::Header::ContentLength);
setFilterState(SetDecoderBufferLimitKey, DefaultMaxBodyBytes);
LOG_INFO(absl::StrCat("SetRequestBodyBufferLimit: ", DefaultMaxBodyBytes));
return FilterHeadersStatus::StopIteration;
}

View File

@@ -41,7 +41,9 @@ struct ModelMapperConfigRule {
std::map<std::string, std::string> exact_model_mapping_;
std::vector<std::pair<std::string, std::string>> prefix_model_mapping_;
std::string default_model_mapping_;
std::vector<std::string> enable_on_path_suffix_ = {"/v1/chat/completions"};
std::vector<std::string> enable_on_path_suffix_ = {
"/completions", "/embeddings", "/images/generations",
"/audio/speech", "/fine_tuning/jobs", "/moderations"};
};
// PluginRootContext is the root context for all streams processed by the

View File

@@ -1,14 +1,14 @@
# 功能说明
## 功能说明
`model-router`插件实现了基于LLM协议中的model参数路由的功能
# 配置字段
## 配置字段
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
| `modelKey` | string | 选填 | model | 请求body中model参数的位置 |
| `addProviderHeader` | string | 选填 | - | 从model参数中解析出的provider名字放到哪个请求header中 |
| `modelToHeader` | string | 选填 | - | 直接将model参数放到哪个请求header中 |
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效 |
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效,可以配置为 "*" 以匹配所有路径 |
## 运行属性

View File

@@ -1,31 +1,31 @@
## Function Description
The `model-router` plugin implements the function of routing based on the model parameter in the LLM protocol.
## Feature Description
The `model-router` plugin implements routing functionality based on the model parameter in LLM protocols.
## Configuration Fields
| Name | Data Type | Filling Requirement | Default Value | Description |
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
| `modelKey` | string | Optional | model | The location of the model parameter in the request body |
| `addProviderHeader` | string | Optional | - | Which request header to place the provider name parsed from the model parameter |
| `modelToHeader` | string | Optional | - | Which request header to directly place the model parameter |
| `enableOnPathSuffix` | array of string | Optional | ["/v1/chat/completions"] | Only effective for requests with these specific path suffixes |
| Name | Data Type | Requirement | Default Value | Description |
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
| `modelKey` | string | Optional | model | Location of the model parameter in the request body |
| `addProviderHeader` | string | Optional | - | Which request header to add the provider name parsed from the model parameter |
| `modelToHeader` | string | Optional | - | Which request header to directly add the model parameter to |
| `enableOnPathSuffix` | array of string | Optional | ["/v1/chat/completions"] | Only effective for requests with these specific path suffixes, can be configured as "*" to match all paths |
## Runtime Attributes
## Runtime Properties
Plugin execution phase: Authentication phase
Plugin execution priority: 900
## Effect Description
### Routing Based on the model Parameter
### Routing Based on Model Parameter
The following configuration is required:
The following configuration is needed:
```yaml
modelToHeader: x-higress-llm-model
```
The plugin will extract the model parameter from the request and set it in the x-higress-llm-model request header, which can be used for subsequent routing. For example, the original LLM request body:
The plugin extracts the model parameter from the request and sets it to the x-higress-llm-model request header for subsequent routing. For example, the original LLM request body is:
```json
{
@@ -35,7 +35,7 @@ The plugin will extract the model parameter from the request and set it in the x
"stream": false,
"messages": [{
"role": "user",
"content": "What is the GitHub address of the main repository for the higress project"
"content": "What is the GitHub address of the Higress project's main repository?"
}],
"presence_penalty": 0,
"temperature": 0.7,
@@ -43,21 +43,21 @@ The plugin will extract the model parameter from the request and set it in the x
}
```
After processing by this plugin, the following request header (which can be used for route matching) will be added:
After processing by this plugin, the following request header will be added (can be used for route matching):
x-higress-llm-model: qwen-long
### Extracting the provider Field from the model Parameter for Routing
### Extracting Provider Field from Model Parameter for Routing
> Note that this mode requires the client to specify the provider using a `/` separator in the model parameter.
> Note that this mode requires the client to specify the provider in the model parameter using the `/` delimiter
The following configuration is required:
The following configuration is needed:
```yaml
addProviderHeader: x-higress-llm-provider
```
The plugin will extract the provider part (if present) from the model parameter in the request and set it in the x-higress-llm-provider request header, which can be used for subsequent routing, and rewrite the model parameter to the model name part. For example, the original LLM request body:
The plugin extracts the provider part (if any) from the model parameter in the request, sets it to the x-higress-llm-provider request header for subsequent routing, and rewrites the model parameter to only contain the model name part. For example, the original LLM request body is:
```json
{
@@ -67,7 +67,7 @@ The plugin will extract the provider part (if present) from the model parameter
"stream": false,
"messages": [{
"role": "user",
"content": "What is the GitHub address of the main repository for the higress project"
"content": "What is the GitHub address of the Higress project's main repository?"
}],
"presence_penalty": 0,
"temperature": 0.7,
@@ -75,7 +75,7 @@ The plugin will extract the provider part (if present) from the model parameter
}
```
After processing by this plugin, the following request header (which can be used for route matching) will be added:
After processing by this plugin, the following request header will be added (can be used for route matching):
x-higress-llm-provider: dashscope
@@ -89,7 +89,7 @@ The original LLM request body will be changed to:
"stream": false,
"messages": [{
"role": "user",
"content": "What is the GitHub address of the main repository for the higress project"
"content": "What is the GitHub address of the Higress project's main repository?"
}],
"presence_penalty": 0,
"temperature": 0.7,

View File

@@ -44,8 +44,8 @@ static RegisterContextFactory register_ModelRouter(
namespace {
constexpr std::string_view SetDecoderBufferLimitKey =
"SetRequestBodyBufferLimit";
constexpr std::string_view DefaultMaxBodyBytes = "10485760";
"set_decoder_buffer_limit";
constexpr std::string_view DefaultMaxBodyBytes = "104857600";
} // namespace
@@ -137,6 +137,11 @@ FilterHeadersStatus PluginRootContext::onHeader(
}
bool enable = false;
for (const auto& enable_suffix : rule.enable_on_path_suffix_) {
// Support wildcard "*" to enable for all paths
if (enable_suffix == "*") {
enable = true;
break;
}
if (absl::EndsWith({path.c_str(), uri_end}, enable_suffix)) {
enable = true;
break;
@@ -153,6 +158,7 @@ FilterHeadersStatus PluginRootContext::onHeader(
}
removeRequestHeader(Wasm::Common::Http::Header::ContentLength);
setFilterState(SetDecoderBufferLimitKey, DefaultMaxBodyBytes);
LOG_INFO(absl::StrCat("SetRequestBodyBufferLimit: ", DefaultMaxBodyBytes));
return FilterHeadersStatus::StopIteration;
}

View File

@@ -40,7 +40,9 @@ struct ModelRouterConfigRule {
std::string model_key_ = "model";
std::string add_provider_header_;
std::string model_to_header_;
std::vector<std::string> enable_on_path_suffix_ = {"/v1/chat/completions"};
std::vector<std::string> enable_on_path_suffix_ = {
"/completions", "/embeddings", "/images/generations",
"/audio/speech", "/fine_tuning/jobs", "/moderations"};
};
// PluginRootContext is the root context for all streams processed by the

View File

@@ -12,14 +12,14 @@ COMMIT_ID := $(shell git rev-parse --short HEAD 2>/dev/null)
IMAGE_TAG = $(if $(strip $(PLUGIN_VERSION)),${PLUGIN_VERSION},${BUILD_TIME}-${COMMIT_ID})
IMG ?= ${REGISTRY}${PLUGIN_NAME}:${IMAGE_TAG}
GOPROXY := $(shell go env GOPROXY)
EXTRA_TAGS ?=
EXTRA_TAGS := $(shell [ -f extensions/${PLUGIN_NAME}/.buildrc ] && . extensions/${PLUGIN_NAME}/.buildrc && echo $$EXTRA_TAGS || echo "")
.DEFAULT:
build:
DOCKER_BUILDKIT=1 docker build --build-arg PLUGIN_NAME=${PLUGIN_NAME} \
--build-arg BUILDER=${BUILDER} \
--build-arg GOPROXY=$(GOPROXY) \
--build-arg EXTRA_TAGS=$(EXTRA_TAGS) \
--build-arg EXTRA_TAGS=${EXTRA_TAGS} \
-t ${IMG} \
--output extensions/${PLUGIN_NAME} \
.
@@ -30,7 +30,7 @@ build-image:
DOCKER_BUILDKIT=1 docker build --build-arg PLUGIN_NAME=${PLUGIN_NAME} \
--build-arg BUILDER=${BUILDER} \
--build-arg GOPROXY=$(GOPROXY) \
--build-arg EXTRA_TAGS=$(EXTRA_TAGS) \
--build-arg EXTRA_TAGS=${EXTRA_TAGS} \
-t ${IMG} \
.
@echo ""

View File

@@ -9,6 +9,8 @@
使用以下命令可以快速构建 wasm-go 插件:
```bash
# NOTE: 如果你想在构建插件的时候设置额外的构建参数 EXTRA_TAGS
# 请更新 extensions/${PLUGIN_NAME} 插件目录对应的 .buildrc 文件
$ PLUGIN_NAME=request-block make build
```

View File

@@ -7,6 +7,8 @@ This SDK is used to develop the WASM Plugins for Higress in Go.
The wasm-go plugin can be built quickly with the following command:
```bash
# NOTE: if you want to set EXTRA_TAGS for the wasm plugin
# please set them in the .buildrc file under extensions/${PLUGIN_NAME} directory
$ PLUGIN_NAME=request-block make build
```

Some files were not shown because too many files have changed in this diff Show More